mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78e5ecdcd7 | ||
|
|
5f589ff909 | ||
|
|
d69d14d6f8 | ||
|
|
d3003dfa5e | ||
|
|
37b7e70490 | ||
|
|
aa934409a7 | ||
|
|
4cc23e6492 | ||
|
|
6849979d60 | ||
|
|
a9d145cab9 | ||
|
|
5a4fcafb5b | ||
|
|
6fdf697bc7 | ||
|
|
ad440edfd8 | ||
|
|
6f5856f06c |
@@ -152,6 +152,44 @@ The methods remain on the common interface so an agent that supports them can ov
|
||||
`sequence_id` remains part of the command contract because `DeviceManager` creates and tracks it as
|
||||
the command ID.
|
||||
|
||||
## Filament and AMS synchronization
|
||||
|
||||
The `OrcaPrinterAgent` bridges OrcaSonar's filament/AMS state. Its capability reply
|
||||
carries two independent signals: `protocol.features.fms` means a material system resolves,
|
||||
and `protocol.features.filament_slots` means the connector owns a filament-slot model that
|
||||
persists without any material hardware. Either one enables filament synchronization, so a
|
||||
printer with slots but no AMS still exposes the slot workflow. The agent also caches
|
||||
`protocol.ams_ops`, the canonical material write operations the resolved drivers implement,
|
||||
and uses it to gate AMS writes; `print.ams_filament_setting` is exempt because it persists
|
||||
connector state and is advertised by `filament_slots` alone. A write whose operation is not
|
||||
declared is refused with `CAP_NOT_AVAILABLE` before it reaches the wire.
|
||||
|
||||
Printer state arrives as the pushed `ams`/`vir_slot` projection inside `push_status`; the
|
||||
Orca agent does not poll the Moonraker `lane_data` namespace, which exists for
|
||||
Moonraker-channel consumers. Slot presence is the user's declaration, not sensed material:
|
||||
`tray_exist_bits` and the presence of a `vir_slot` entry mark a slot as present even with an
|
||||
empty `tray_type`, which is the `is_exists` state the filament UI reads. A full status frame
|
||||
(`msg=0`, or a LAN frame with no `msg`) updates each virtual-tray ID it contains but does not
|
||||
remove IDs already observed during the current connection. An observed ID remains if omitted
|
||||
from a populated `vir_slot`, if `vir_slot` is empty, or if the field is absent. Before any
|
||||
virtual tray has been observed, a full frame with no virtual-tray entries clears the initial
|
||||
placeholder. `MachineObject::reset()` clears retained IDs on reconnect. Delta frames update
|
||||
named entries only. This retention rule is specific to the native Orca agent.
|
||||
|
||||
Writes follow the same edge-translation rule as the rest of the agent. The shared
|
||||
`MachineObject` command builders emit Bambu-shaped `print.ams_*` payloads, and the agent's
|
||||
single send funnel decodes them into OrcaSonar's canonical bodies — `selector` with a flat
|
||||
lane or `ams_id`/`slot_id` coordinates for `ams_change_filament`, coordinates for
|
||||
`ams_filament_setting` — before publishing. The server resolves and rejects with its own
|
||||
`errno` backstop; the agent only withholds operations the device did not declare.
|
||||
|
||||
A filament frame can arrive before the capability reply that decides synchronization mode,
|
||||
for example when the topology bootstraps after connect or Klipper restarts. While a device's
|
||||
capabilities are still unknown, the agent re-requests `get_capabilities` and a full status on
|
||||
a filament frame, throttled per device, and stops once an answer arrives. Dedicated
|
||||
capability state is cleared when a cloud device is deselected, while an independently active
|
||||
LAN session for the same device id keeps its declaration.
|
||||
|
||||
## Device ownership and stale responses
|
||||
|
||||
Printer-agent ownership is represented by `printer_agent_id` on device records and `MachineObject`
|
||||
@@ -233,6 +271,10 @@ inside the Bambu agent and the Orca agent's v1 sink adapter can be removed as a
|
||||
cloud camera contract
|
||||
- [`NetworkAgent`](../../src/slic3r/Utils/NetworkAgent.hpp) — façade and dispatch between active agents
|
||||
- [`NetworkAgentFactory`](../../src/slic3r/Utils/NetworkAgentFactory.hpp) — built-in and Python agent registry
|
||||
- [`OrcaPrinterAgent`](../../src/slic3r/Utils/OrcaPrinterAgent.hpp) — OrcaSonar transport, capability
|
||||
discovery, and Bambu-to-canonical command translation
|
||||
- [`AmsPayload`](../../src/slic3r/Utils/AmsPayload.hpp) — shared filament payload rendering and the
|
||||
per-device AMS capability cache
|
||||
- [`DeviceManager`](../../src/slic3r/GUI/DeviceCore/DevManager.cpp) — device ownership, filtering, and
|
||||
stale-response checks
|
||||
- [`PrinterAgentPluginCapability`](../../src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp)
|
||||
|
||||
@@ -24,7 +24,7 @@ NAMESPACE = "lane_data"
|
||||
|
||||
# Repo-relative paths for the offline generic-map check
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
MOONRAKER_AGENT_CPP = os.path.join(REPO_ROOT, "src", "slic3r", "Utils", "MoonrakerPrinterAgent.cpp")
|
||||
AMS_PAYLOAD_CPP = os.path.join(REPO_ROOT, "src", "slic3r", "Utils", "AmsPayload.cpp")
|
||||
OFL_FILAMENT_DIR = os.path.join(REPO_ROOT, "resources", "profiles", "OrcaFilamentLibrary", "filament")
|
||||
LANE_KEYS = [f"lane{i}" for i in range(1, 9)] # lane1-lane8
|
||||
MATERIALS = ["PLA", "ABS", "PETG", "ASA", "ASA Sparkle", "TPU", ""]
|
||||
@@ -41,16 +41,16 @@ MATERIAL_TEMPS = {
|
||||
}
|
||||
|
||||
def parse_cpp_type_map():
|
||||
"""Extract the normalized-type -> OFL generic family table from MoonrakerPrinterAgent.cpp.
|
||||
"""Extract the normalized-type -> OFL generic family table from AmsPayload.cpp.
|
||||
|
||||
Reads MoonrakerPrinterAgent::map_filament_type_to_generic_id's type_to_ofl_family
|
||||
initializer so the check tracks the C++ normalization without a duplicated list.
|
||||
Reads map_filament_type_to_generic_id's type_to_ofl_family initializer so the
|
||||
check tracks the C++ normalization without a duplicated list.
|
||||
"""
|
||||
with open(MOONRAKER_AGENT_CPP, encoding="utf-8") as f:
|
||||
with open(AMS_PAYLOAD_CPP, encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
m = re.search(r"type_to_ofl_family\s*=\s*\{(.*?)\n\s*\};", src, re.DOTALL)
|
||||
if not m:
|
||||
raise RuntimeError(f"type_to_ofl_family table not found in {MOONRAKER_AGENT_CPP}")
|
||||
raise RuntimeError(f"type_to_ofl_family table not found in {AMS_PAYLOAD_CPP}")
|
||||
pairs = re.findall(r'\{\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\}', m.group(1))
|
||||
if not pairs:
|
||||
raise RuntimeError("type_to_ofl_family table parsed empty")
|
||||
|
||||
@@ -3456,7 +3456,7 @@ size_t PresetCollection::first_visible_idx() const
|
||||
return first_visible;
|
||||
}
|
||||
|
||||
size_t PresetCollection::first_visible_idx_by_type(const std::string& filament_type) const
|
||||
size_t PresetCollection::first_matching_filament_idx(const std::string& filament_type) const
|
||||
{
|
||||
size_t start = m_default_suppressed ? m_num_default_presets : 0;
|
||||
|
||||
@@ -3481,14 +3481,17 @@ size_t PresetCollection::first_visible_idx_by_type(const std::string& filament_t
|
||||
// e.g. "PLA High Speed" -> "PLA"
|
||||
// Dash-separated types like "PA-CF", "PET-CF" are distinct materials, not modifiers.
|
||||
auto sep = filament_type.find(' ');
|
||||
if (sep != std::string::npos) {
|
||||
idx = find_by_type(filament_type.substr(0, sep));
|
||||
if (idx != size_t(-1))
|
||||
return idx;
|
||||
}
|
||||
if (sep != std::string::npos)
|
||||
return find_by_type(filament_type.substr(0, sep));
|
||||
|
||||
return size_t(-1);
|
||||
}
|
||||
|
||||
size_t PresetCollection::first_visible_idx_by_type(const std::string& filament_type) const
|
||||
{
|
||||
size_t idx = first_matching_filament_idx(filament_type);
|
||||
// 3. Any visible preset
|
||||
return first_visible_idx();
|
||||
return idx != size_t(-1) ? idx : first_visible_idx();
|
||||
}
|
||||
|
||||
std::string PresetCollection::filament_id_by_type(const std::string& filament_type) const
|
||||
@@ -3496,6 +3499,15 @@ std::string PresetCollection::filament_id_by_type(const std::string& filament_ty
|
||||
return preset(first_visible_idx_by_type(filament_type)).filament_id;
|
||||
}
|
||||
|
||||
bool PresetCollection::filament_id_by_type(const std::string& filament_type, std::string& out) const
|
||||
{
|
||||
size_t idx = first_matching_filament_idx(filament_type);
|
||||
if (idx == size_t(-1))
|
||||
return false;
|
||||
out = preset(idx).filament_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> PresetCollection::diameters_of_selected_printer()
|
||||
{
|
||||
std::set<std::string> diameters;
|
||||
|
||||
@@ -756,6 +756,11 @@ public:
|
||||
size_t first_visible_idx_by_type(const std::string& filament_type) const;
|
||||
// Return the filament_id of the best-matching visible preset for the given filament type.
|
||||
std::string filament_id_by_type(const std::string& filament_type) const;
|
||||
// As above, but returns false (and leaves `out` untouched) when no preset
|
||||
// matches instead of falling back to first_visible_idx(). The fallback is a
|
||||
// plain first-visible/PLA preset, which would bind an unknown material to an
|
||||
// unrelated default.
|
||||
bool filament_id_by_type(const std::string& filament_type, std::string& out) const;
|
||||
// Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible.
|
||||
// If one of the prefered_alternates is compatible, select it.
|
||||
template<typename PreferedCondition> size_t first_compatible_idx(PreferedCondition prefered_condition) const
|
||||
@@ -889,6 +894,10 @@ protected:
|
||||
void set_custom_preset_alias(Preset &preset);
|
||||
|
||||
private:
|
||||
// Index of the first visible, compatible, system base preset matching
|
||||
// filament_type (exact, then base type), or (size_t)-1 when none matches.
|
||||
size_t first_matching_filament_idx(const std::string& filament_type) const;
|
||||
|
||||
std::string canonical_preset_name(const std::string &name, const PresetOrigin &load_origin = PresetOrigin()) const;
|
||||
|
||||
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
|
||||
|
||||
@@ -4255,9 +4255,19 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
|
||||
return filament_infos;
|
||||
}
|
||||
|
||||
// ORCA TODO: currently, this function assumes the printer name follows the pattern of "<printer_model> <nozzle_diameter>", e.g.
|
||||
// printer_type: "Bambu Lab X2D", nozzle_diameter_str: "0.4 nozzle" => printer_name: "Bambu Lab X2D 0.4 nozzle". If the printer name does
|
||||
// not follow this pattern, the function may not work correctly.
|
||||
std::string PresetBundle::get_printer_model_display_name(const std::string &model_id) const
|
||||
{
|
||||
for (const auto &vendor_entry : vendors) {
|
||||
for (const auto &model : vendor_entry.second.models) {
|
||||
if (model.model_id == model_id)
|
||||
return model.name;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// ORCA TODO: this assumes printer names follow "<printer_model> <nozzle_diameter>", e.g.
|
||||
// "Bambu Lab X2D 0.4 nozzle". Other naming schemes may not resolve correctly.
|
||||
std::set<std::string> PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only)
|
||||
{
|
||||
std::set<std::string> printer_names;
|
||||
|
||||
@@ -391,6 +391,7 @@ public:
|
||||
|
||||
std::vector<std::vector<DynamicPrintConfig>> get_extruder_filament_info() const;
|
||||
|
||||
std::string get_printer_model_display_name(const std::string &model_id) const;
|
||||
std::set<std::string> get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only = true);
|
||||
// Orca: the root filament presets a connected machine can use, resolved with the rule the rest
|
||||
// of the app applies (is_compatible_with_printer): an empty compatible_printers means every
|
||||
|
||||
@@ -758,6 +758,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/OrcaMqttConnection.hpp
|
||||
Utils/OrcaPrinterAgent.cpp
|
||||
Utils/OrcaPrinterAgent.hpp
|
||||
Utils/AmsPayload.cpp
|
||||
Utils/AmsPayload.hpp
|
||||
Utils/OrcaCloudSignalingChannel.cpp
|
||||
Utils/OrcaCloudSignalingChannel.hpp
|
||||
Utils/QidiPrinterAgent.cpp
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/Utils/AmsPayload.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include <algorithm>
|
||||
@@ -146,6 +147,7 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
|
||||
#else
|
||||
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
|
||||
#endif
|
||||
m_comboBox_filament->SetClientData(&m_comboBox_from_printer);
|
||||
|
||||
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
|
||||
|
||||
@@ -431,6 +433,10 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing)
|
||||
m_button_confirm->Show(true);
|
||||
}
|
||||
|
||||
const bool can_edit = !is_printing || obj->is_support_filament_setting_inprinting;
|
||||
m_input_nozzle_min->Enable(m_is_third && can_edit);
|
||||
m_input_nozzle_max->Enable(m_is_third && can_edit);
|
||||
|
||||
if (!m_is_third) {
|
||||
m_tip_readonly->SetLabelText(wxEmptyString);
|
||||
m_tip_readonly->Hide();
|
||||
@@ -453,6 +459,8 @@ void AMSMaterialsSetting::update_filament_editing(bool is_printing)
|
||||
if (m_view_only) { // Orca: view-only (2D laser/cut) — lock every edit control and hide apply/reset
|
||||
m_comboBox_filament->Enable(false);
|
||||
m_comboBox_cali_result->Enable(false);
|
||||
m_input_nozzle_min->Enable(false);
|
||||
m_input_nozzle_max->Enable(false);
|
||||
m_input_k_val->Enable(false);
|
||||
m_input_n_val->Enable(false);
|
||||
m_button_confirm->Hide();
|
||||
@@ -477,10 +485,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
|
||||
m_filament_type = "";
|
||||
long nozzle_temp_min_int = 0;
|
||||
long nozzle_temp_max_int = 0;
|
||||
wxColour color = *wxWHITE;
|
||||
char col_buf[10];
|
||||
sprintf(col_buf, "%02X%02X%02X00", (int)color.Red(), (int)color.Green(), (int)color.Blue());
|
||||
std::string color_str; // reset use empty string
|
||||
std::string color_str; // empty; command_ams_filament_settings maps it to the empty-tray color
|
||||
|
||||
std::string selected_ams_id;
|
||||
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
||||
@@ -497,7 +502,7 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
|
||||
|
||||
if (obj) {
|
||||
if(m_is_third){
|
||||
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int,
|
||||
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, color_str, m_filament_type, nozzle_temp_min_int,
|
||||
nozzle_temp_max_int);
|
||||
}
|
||||
|
||||
@@ -668,7 +673,7 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
|
||||
wxString nozzle_temp_max = m_input_nozzle_max->GetTextCtrl()->GetValue();
|
||||
|
||||
long nozzle_temp_min_int, nozzle_temp_max_int;
|
||||
long nozzle_temp_min_int = 0, nozzle_temp_max_int = 0;
|
||||
nozzle_temp_min.ToLong(&nozzle_temp_min_int);
|
||||
nozzle_temp_max.ToLong(&nozzle_temp_max_int);
|
||||
wxColour color = m_clr_picker->m_colour;
|
||||
@@ -872,12 +877,22 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
|
||||
}
|
||||
|
||||
std::vector<wxColour> ams_colors;
|
||||
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
|
||||
// A fixed-vocabulary printer can only show its own palette; anything else
|
||||
// would offer a color its screen cannot hold.
|
||||
const FilamentMetadataValues metadata = filament_metadata_values(obj->get_dev_id());
|
||||
const bool constrained = metadata.constrained && !metadata.colors.empty();
|
||||
if (constrained) {
|
||||
for (const std::string& color : metadata.colors)
|
||||
ams_colors.push_back(DevAmsTray::decode_color(color));
|
||||
} else {
|
||||
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
|
||||
}
|
||||
|
||||
wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0));
|
||||
wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65));
|
||||
m_color_picker_popup.Position(popup_pos, wxSize(0, 0));
|
||||
m_color_picker_popup.set_ams_colours(ams_colors);
|
||||
m_color_picker_popup.set_constrained(constrained);
|
||||
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
|
||||
m_color_picker_popup.Popup();
|
||||
}
|
||||
@@ -940,6 +955,21 @@ static void _collect_filament_info(const wxString& shown_name,
|
||||
query_filament_types[shown_name] = filament.config.get_filament_type();
|
||||
}
|
||||
|
||||
static std::string sGetFilamentPrinterModel(MachineObject *obj, PresetBundle *preset_bundle)
|
||||
{
|
||||
if (!obj || !preset_bundle)
|
||||
return {};
|
||||
|
||||
const wxString device_model = obj->get_printer_type_display_str();
|
||||
if (!device_model.empty() && device_model != "OrcaSonar Printer" && device_model != _L("Unknown"))
|
||||
return device_model.ToStdString();
|
||||
|
||||
// OrcaSonar's model ID is optional; use the selected profile when it is generic.
|
||||
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("printer_model");
|
||||
const auto *model = dynamic_cast<const ConfigOptionString *>(opt);
|
||||
return model ? model->value : std::string();
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
|
||||
{
|
||||
if (!obj) return;
|
||||
@@ -952,6 +982,8 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
|
||||
m_input_k_val->GetTextCtrl()->SetValue(k);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(n);
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
|
||||
wxArrayString filament_items;
|
||||
wxString bambu_filament_name;
|
||||
@@ -965,9 +997,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
map_filament_items.clear();
|
||||
PresetBundle * preset_bundle = wxGetApp().preset_bundle;
|
||||
std::ostringstream stream;
|
||||
// Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires
|
||||
// only during the brief BBL startup window before firmware reports nozzle info. Without this,
|
||||
// the "0.0" lookup string returns an empty set and the filament dropdown goes blank.
|
||||
// Use the selected profile's nozzle diameter until the connected device reports one.
|
||||
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
|
||||
if (machine_diameter == 0.0f && preset_bundle) {
|
||||
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
|
||||
@@ -975,19 +1005,24 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
}
|
||||
stream << std::fixed << std::setprecision(1) << machine_diameter;
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
const std::string filament_printer_model = sGetFilamentPrinterModel(obj, preset_bundle);
|
||||
|
||||
if (preset_bundle) {
|
||||
if (preset_bundle && !filament_printer_model.empty()) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "system_preset_bundle filament number=" << preset_bundle->filaments.size();
|
||||
for (Preset *filament_it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (!filament_id_set.insert(filament_it->filament_id).second)
|
||||
continue;
|
||||
const std::string alias = preset_bundle->filaments.get_preset_alias(*filament_it, true);
|
||||
if (alias.empty())
|
||||
continue;
|
||||
|
||||
filament_items.push_back(alias);
|
||||
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
|
||||
// A printer with a fixed vocabulary only offers the types it can
|
||||
// hold, so an incompatible preset never reaches the list.
|
||||
if (!filament_material_compatible(obj->get_dev_id(), query_filament_types[alias].ToStdString()))
|
||||
continue;
|
||||
filament_items.push_back(alias);
|
||||
|
||||
FilamentInfos filament_infos;
|
||||
filament_infos.filament_id = filament_it->filament_id;
|
||||
@@ -1045,6 +1080,10 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
else {
|
||||
m_comboBox_filament->Show();
|
||||
m_readonly_filament->Hide();
|
||||
if (!temp_min.IsEmpty() && temp_min != "0")
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(temp_min);
|
||||
if (!temp_max.IsEmpty() && temp_max != "0")
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(temp_max);
|
||||
}
|
||||
|
||||
if (obj->cali_version >= 0) {
|
||||
@@ -1182,6 +1221,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
}
|
||||
|
||||
m_comboBox_filament->Set(filament_items);
|
||||
m_comboBox_from_printer = 1;
|
||||
m_comboBox_filament->SetSelection(selection_idx);
|
||||
post_select_event(selection_idx);
|
||||
|
||||
@@ -1189,9 +1229,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
m_comboBox_filament->SetValue(wxEmptyString);
|
||||
}
|
||||
|
||||
// Set the flag whether to open the filament setting dialog from the device page
|
||||
m_comboBox_filament->SetClientData(new int(1));
|
||||
|
||||
update();
|
||||
Layout();
|
||||
Fit();
|
||||
@@ -1221,16 +1258,14 @@ void AMSMaterialsSetting::on_select_cali_result(wxCommandEvent &evt)
|
||||
void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
{
|
||||
// Get the flag whether to open the filament setting dialog from the device page
|
||||
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
|
||||
const bool initial_printer_selection = m_comboBox_from_printer == 1;
|
||||
|
||||
m_filament_type = "";
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
std::ostringstream stream;
|
||||
if (obj) {
|
||||
// Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires
|
||||
// only during the brief BBL startup window before firmware reports nozzle info. Without this,
|
||||
// the "0.0" lookup string returns an empty set and filament lookup yields no results.
|
||||
// Use the selected profile's nozzle diameter until the connected device reports one.
|
||||
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
|
||||
if (machine_diameter == 0.0f) {
|
||||
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
|
||||
@@ -1239,30 +1274,32 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
stream << std::fixed << std::setprecision(1) << machine_diameter;
|
||||
}
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
const std::string filament_printer_model = sGetFilamentPrinterModel(obj, preset_bundle);
|
||||
// Resolve the selection against the same list Popup() built the dropdown from, so the two
|
||||
// halves of the dialog cannot disagree about which filaments this machine can use.
|
||||
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
|
||||
if (!selected.empty()) {
|
||||
if (!selected.empty() && !filament_printer_model.empty()) {
|
||||
const std::string filament_id = map_filament_items[selected].filament_id;
|
||||
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
filament_printer_model, nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (it->filament_id != filament_id)
|
||||
continue;
|
||||
// ) if nozzle_temperature_range is found
|
||||
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
|
||||
if (opt_min) {
|
||||
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
|
||||
if (opt_min_ints) {
|
||||
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0));
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
|
||||
if (!initial_printer_selection) {
|
||||
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
|
||||
if (opt_min) {
|
||||
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
|
||||
if (opt_min_ints) {
|
||||
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0));
|
||||
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high");
|
||||
if (opt_max) {
|
||||
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
|
||||
if (opt_max_ints) {
|
||||
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0));
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
|
||||
ConfigOption* opt_max = it->config.option("nozzle_temperature_range_high");
|
||||
if (opt_max) {
|
||||
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
|
||||
if (opt_max_ints) {
|
||||
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0));
|
||||
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigOption* opt_type = it->config.option("filament_type");
|
||||
@@ -1301,13 +1338,16 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
m_button_confirm->Disable(); // ORCA No need to change style
|
||||
m_comboBox_cali_result->Clear();
|
||||
m_comboBox_cali_result->SetValue(wxEmptyString);
|
||||
m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
m_comboBox_filament->SetClientData(new int(0));
|
||||
if (!initial_printer_selection) {
|
||||
m_input_k_val->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(wxEmptyString);
|
||||
}
|
||||
m_comboBox_from_printer = 0;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
m_button_confirm->Enable(true); // ORCA No need to change style
|
||||
if (!m_view_only)
|
||||
m_button_confirm->Enable(true); // ORCA No need to change style
|
||||
}
|
||||
|
||||
//filament id
|
||||
@@ -1331,6 +1371,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
}
|
||||
}
|
||||
|
||||
if (!ams_filament_id.empty())
|
||||
m_clr_picker->is_empty(false);
|
||||
|
||||
wxArrayString items;
|
||||
m_pa_profile_items.clear();
|
||||
m_comboBox_cali_result->SetValue(wxEmptyString);
|
||||
@@ -1386,7 +1429,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
|
||||
m_comboBox_cali_result->Set(items);
|
||||
if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID) {
|
||||
if (from_printer && (*from_printer == 1)) {
|
||||
if (m_comboBox_from_printer == 1) {
|
||||
for (auto slot : obj->vt_slot) {
|
||||
if (slot.id == std::to_string(ams_id))
|
||||
cali_select_idx = CalibUtils::get_selected_calib_idx(m_pa_profile_items, slot.cali_idx);
|
||||
@@ -1403,7 +1446,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (from_printer && (*from_printer == 1)) {
|
||||
if (m_comboBox_from_printer == 1) {
|
||||
DevAmsTray* selected_tray = this->obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
|
||||
if (!selected_tray)
|
||||
{
|
||||
@@ -1437,7 +1480,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
else {
|
||||
if (!ams_filament_id.empty()) {
|
||||
//m_input_k_val->GetTextCtrl()->SetValue("0.00");
|
||||
m_input_k_val->Enable(true);
|
||||
m_input_k_val->Enable(!m_view_only);
|
||||
}
|
||||
else {
|
||||
//m_input_k_val->GetTextCtrl()->SetValue("0.00");
|
||||
@@ -1445,7 +1488,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
}
|
||||
}
|
||||
|
||||
m_comboBox_filament->SetClientData(new int(0));
|
||||
m_comboBox_from_printer = 0;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
|
||||
@@ -1707,6 +1750,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
cp->set_selected(false);
|
||||
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
|
||||
m_color_pickers.push_back(cp);
|
||||
m_def_color_pickers.push_back(cp);
|
||||
fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
|
||||
set_def_colour(cp->m_colour);
|
||||
@@ -1719,27 +1763,27 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
}
|
||||
|
||||
wxBoxSizer* m_sizer_other = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto m_title_other = new wxStaticText(m_def_color_box, wxID_ANY, _L("Other Color"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_title_other = new wxStaticText(m_def_color_box, wxID_ANY, _L("Other Color"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_title_other->SetFont(::Label::Body_14);
|
||||
m_title_other->SetBackgroundColour(wxColour(238, 238, 238));
|
||||
m_sizer_other->Add(m_title_other, 0, wxALL, 5);
|
||||
auto other_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
|
||||
other_line->SetMinSize(wxSize(-1, 1));
|
||||
other_line->SetMaxSize(wxSize(-1, 1));
|
||||
other_line->SetBackgroundColour(wxColour("#CECECE"));
|
||||
m_sizer_other->Add(other_line, 1, wxALIGN_CENTER, 0);
|
||||
m_other_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
|
||||
m_other_line->SetMinSize(wxSize(-1, 1));
|
||||
m_other_line->SetMaxSize(wxSize(-1, 1));
|
||||
m_other_line->SetBackgroundColour(wxColour("#CECECE"));
|
||||
m_sizer_other->Add(m_other_line, 1, wxALIGN_CENTER, 0);
|
||||
|
||||
//custom color
|
||||
wxBoxSizer* m_sizer_custom = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto m_title_custom = new wxStaticText(m_def_color_box, wxID_ANY, _L("Custom Color"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_title_custom = new wxStaticText(m_def_color_box, wxID_ANY, _L("Custom Color"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_title_custom->SetFont(::Label::Body_14);
|
||||
m_title_custom->SetBackgroundColour(wxColour(238, 238, 238));
|
||||
auto custom_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
|
||||
custom_line->SetBackgroundColour(wxColour("#CECECE"));
|
||||
custom_line->SetMinSize(wxSize(-1, 1));
|
||||
custom_line->SetMaxSize(wxSize(-1, 1));
|
||||
m_custom_line = new wxPanel(m_def_color_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
|
||||
m_custom_line->SetBackgroundColour(wxColour("#CECECE"));
|
||||
m_custom_line->SetMinSize(wxSize(-1, 1));
|
||||
m_custom_line->SetMaxSize(wxSize(-1, 1));
|
||||
m_sizer_custom->Add(m_title_custom, 0, wxALL, 5);
|
||||
m_sizer_custom->Add(custom_line, 1, wxALIGN_CENTER, 0);
|
||||
m_sizer_custom->Add(m_custom_line, 1, wxALIGN_CENTER, 0);
|
||||
|
||||
m_custom_cp = new StaticBox(m_def_color_box);
|
||||
m_custom_cp->SetSize(FromDIP(60), FromDIP(25));
|
||||
@@ -1879,6 +1923,22 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
Fit();
|
||||
}
|
||||
|
||||
// A printer with a fixed vocabulary only shows its own palette: the default
|
||||
// swatches and the custom picker would offer colors its screen cannot hold.
|
||||
void ColorPickerPopup::set_constrained(bool constrained)
|
||||
{
|
||||
const bool show = !constrained;
|
||||
m_title_other->Show(show);
|
||||
m_other_line->Show(show);
|
||||
for (ColorPicker* cp : m_def_color_pickers)
|
||||
cp->Show(show);
|
||||
m_title_custom->Show(show);
|
||||
m_custom_line->Show(show);
|
||||
m_custom_cp->Show(show);
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_def_colour(wxColour col)
|
||||
{
|
||||
m_def_col = col;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "DeviceManager.hpp"
|
||||
#include "wx/clrpicker.h"
|
||||
#include "wx/colourdata.h"
|
||||
#include "wx/panel.h"
|
||||
#include "wx/stattext.h"
|
||||
#include "Widgets/RadioBox.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "Widgets/RoundedRectangle.hpp"
|
||||
@@ -77,12 +79,20 @@ public:
|
||||
std::vector<wxColour> m_ams_colors;
|
||||
std::vector<ColorPicker*> m_color_pickers;
|
||||
std::vector<ColorPicker*> m_ams_color_pickers;
|
||||
// Sections a fixed-vocabulary printer hides in set_constrained: its own
|
||||
// palette is the only color its screen can show.
|
||||
std::vector<ColorPicker*> m_def_color_pickers;
|
||||
wxStaticText* m_title_other{nullptr};
|
||||
wxPanel* m_other_line{nullptr};
|
||||
wxStaticText* m_title_custom{nullptr};
|
||||
wxPanel* m_custom_line{nullptr};
|
||||
|
||||
public:
|
||||
ColorPickerPopup(wxWindow* parent);
|
||||
~ColorPickerPopup() {};
|
||||
void on_custom_clr_picker(wxMouseEvent& event);
|
||||
void set_ams_colours(std::vector<wxColour> ams);
|
||||
void set_constrained(bool constrained);
|
||||
void set_def_colour(wxColour col);
|
||||
void paintEvent(wxPaintEvent& evt);
|
||||
virtual void OnDismiss() wxOVERRIDE;
|
||||
@@ -123,6 +133,10 @@ public:
|
||||
std::string ams_setting_id;
|
||||
|
||||
bool m_is_third;
|
||||
|
||||
// Marks whether the filament combo was seeded from the printer's own tray.
|
||||
// Stored as a member so the combo's client data never owns an allocation.
|
||||
int m_comboBox_from_printer{0};
|
||||
// Orca: view-only mode (laser/cut). When set, the dialog is inspectable but every
|
||||
// editing control is disabled and no command is sent.
|
||||
bool m_view_only = false;
|
||||
|
||||
@@ -162,8 +162,10 @@ void DevCalib::ExtrusionCalibSetParse(const json & jj){
|
||||
int slot_id = tray_ams_slot_map.find(tray_id) != tray_ams_slot_map.end() ? tray_ams_slot_map[tray_id].second : -1;
|
||||
|
||||
if(tray_id == VIRTUAL_TRAY_MAIN_ID) {
|
||||
GetOwner()->vt_slot[MAIN_EXTRUDER_ID].k = jj.value("k_value", GetOwner()->vt_slot[MAIN_EXTRUDER_ID].k);
|
||||
GetOwner()->vt_slot[MAIN_EXTRUDER_ID].n = jj.value("n_value", GetOwner()->vt_slot[MAIN_EXTRUDER_ID].n);
|
||||
if (GetOwner()->vt_slot.size() > MAIN_EXTRUDER_ID) {
|
||||
GetOwner()->vt_slot[MAIN_EXTRUDER_ID].k = jj.value("k_value", GetOwner()->vt_slot[MAIN_EXTRUDER_ID].k);
|
||||
GetOwner()->vt_slot[MAIN_EXTRUDER_ID].n = jj.value("n_value", GetOwner()->vt_slot[MAIN_EXTRUDER_ID].n);
|
||||
}
|
||||
}else{
|
||||
auto tray_item = GetOwner()->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
|
||||
if (tray_item) {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "DevUtil.h"
|
||||
#include "DevUtilBackend.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace nlohmann;
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -18,23 +20,31 @@ static int _hex_digit_to_int(const char c) { return (c >= '0' && c <= '9') ? c -
|
||||
|
||||
wxColour DevAmsTray::decode_color(const std::string &color)
|
||||
{
|
||||
std::array<int, 4> ret = {0, 0, 0, 0};
|
||||
// 6 hex digits (RRGGBB) and 8 (RRGGBBAA) are both legal on the wire;
|
||||
// normalize_ams_color pads the short form, so accept it here too.
|
||||
const size_t hex_digits = (color.size() == 6 || color.size() == 8) ? color.size() : 0;
|
||||
if (hex_digits == 0) return wxColour(255, 255, 255, 255);
|
||||
|
||||
std::array<int, 4> ret = {0, 0, 0, 255};
|
||||
const char * c = color.data();
|
||||
if (color.size() == 8) {
|
||||
for (size_t j = 0; j < 4; ++j) {
|
||||
int digit1 = _hex_digit_to_int(*c++);
|
||||
int digit2 = _hex_digit_to_int(*c++);
|
||||
if (digit1 == -1 || digit2 == -1) break;
|
||||
ret[j] = static_cast<float>(digit1 * 16 + digit2);
|
||||
}
|
||||
} else { return wxColour(255, 255, 255, 255); }
|
||||
for (size_t j = 0; j < hex_digits / 2; ++j) {
|
||||
int digit1 = _hex_digit_to_int(*c++);
|
||||
int digit2 = _hex_digit_to_int(*c++);
|
||||
if (digit1 == -1 || digit2 == -1) break;
|
||||
ret[j] = digit1 * 16 + digit2;
|
||||
}
|
||||
|
||||
return wxColour(ret[0], ret[1], ret[2], ret[3]);
|
||||
}
|
||||
|
||||
void DevAmsTray::UpdateColorFromStr(const std::string& color)
|
||||
{
|
||||
if (color.empty()) return;
|
||||
// An empty color clears, exactly like a payload that omits the key.
|
||||
if (color.empty()) {
|
||||
wx_color = wxColour();
|
||||
this->color.clear();
|
||||
return;
|
||||
}
|
||||
if (this->color != color)
|
||||
{
|
||||
wx_color = "#" + wxString::FromUTF8(color);
|
||||
@@ -42,6 +52,15 @@ void DevAmsTray::UpdateColorFromStr(const std::string& color)
|
||||
}
|
||||
}
|
||||
|
||||
void DevAmsTray::UpdateEmptyState(bool material_fields_present)
|
||||
{
|
||||
const auto is_zero_or_empty = [](const std::string& value) {
|
||||
return value.empty() || std::all_of(value.begin(), value.end(), [](char c) { return c == '0'; });
|
||||
};
|
||||
is_empty = material_fields_present && setting_id.empty() && m_fila_type.empty() &&
|
||||
is_zero_or_empty(color) && is_zero_or_empty(tag_uid);
|
||||
}
|
||||
|
||||
void DevAmsTray::reset()
|
||||
{
|
||||
tag_uid = "";
|
||||
@@ -63,6 +82,7 @@ void DevAmsTray::reset()
|
||||
k = 0.0f;
|
||||
n = 0.0f;
|
||||
is_bbl = false;
|
||||
is_empty = false;
|
||||
hold_count = 0;
|
||||
remain = 0;
|
||||
}
|
||||
@@ -662,7 +682,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
//std::string type = (*tray_it)["tray_type"].get<std::string>();
|
||||
std::string type = MachineObject::setting_id_to_type(curr_tray->setting_id, (*tray_it)["tray_type"].get<std::string>());
|
||||
// curr_tray->setting_id is our OF id; GFS00/GFS01 are the printer's own support-filament ids.
|
||||
auto* agent = GUI::wxGetApp().getAgent();
|
||||
auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr;
|
||||
const std::string printer_filament_id = agent ? agent->from_orca_filament_id(curr_tray->setting_id) : curr_tray->setting_id;
|
||||
if (printer_filament_id == "GFS00")
|
||||
{
|
||||
@@ -717,7 +737,8 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
}
|
||||
else
|
||||
{
|
||||
curr_tray->color = "";
|
||||
// Absent and empty carry the same meaning.
|
||||
curr_tray->UpdateColorFromStr(std::string());
|
||||
}
|
||||
if (tray_it->contains("nozzle_temp_max"))
|
||||
{
|
||||
@@ -762,6 +783,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
{
|
||||
curr_tray->remain = -1;
|
||||
}
|
||||
curr_tray->UpdateEmptyState(tray_it->contains("tray_info_idx") && tray_it->contains("tray_type"));
|
||||
// The tray objects are reused across status updates. Reset this
|
||||
// state when a previously empty slot receives a filament again.
|
||||
curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder");
|
||||
@@ -849,154 +871,4 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
}
|
||||
}
|
||||
|
||||
static DevAms::AmsType ams_type_from_string(const std::string& s)
|
||||
{
|
||||
if (s == "ams_lite" || s == "ams-lite") return DevAms::AMS_LITE;
|
||||
if (s == "n3f") return DevAms::N3F;
|
||||
if (s == "n3s") return DevAms::N3S;
|
||||
return DevAms::AMS; // default
|
||||
}
|
||||
|
||||
void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system)
|
||||
{
|
||||
if (!system || !data.is_object())
|
||||
return;
|
||||
|
||||
// --- AMS units ---
|
||||
if (data.contains("units") && data["units"].is_array())
|
||||
{
|
||||
std::set<std::string> seen_units;
|
||||
|
||||
for (const auto& u : data["units"])
|
||||
{
|
||||
if (!u.is_object() || !u.contains("id"))
|
||||
continue;
|
||||
const std::string ams_id = u.value("id", std::string());
|
||||
if (ams_id.empty())
|
||||
continue;
|
||||
seen_units.insert(ams_id);
|
||||
|
||||
const int ext_id = u.value("extruder", MAIN_EXTRUDER_ID);
|
||||
const DevAms::AmsType type = ams_type_from_string(u.value("type", std::string("ams")));
|
||||
|
||||
DevAms* ams = nullptr;
|
||||
auto it = system->amsList.find(ams_id);
|
||||
if (it == system->amsList.end())
|
||||
{
|
||||
ams = new DevAms(ams_id, ext_id, type);
|
||||
system->amsList.insert(std::make_pair(ams_id, ams));
|
||||
}
|
||||
else
|
||||
{
|
||||
ams = it->second;
|
||||
ams->m_ext_id = ext_id;
|
||||
ams->SetAmsType(type);
|
||||
}
|
||||
|
||||
ams->m_exist = true;
|
||||
ams->m_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
|
||||
ams->m_humidity_percent = u.value("humidity_percent", -1);
|
||||
ams->m_left_dry_time = u.value("dry_time_min", 0);
|
||||
|
||||
// --- slots / trays ---
|
||||
std::set<std::string> seen_slots;
|
||||
if (u.contains("slots") && u["slots"].is_array())
|
||||
{
|
||||
for (const auto& s : u["slots"])
|
||||
{
|
||||
if (!s.is_object())
|
||||
continue;
|
||||
const std::string tray_id = std::to_string(s.value("index", -1));
|
||||
seen_slots.insert(tray_id);
|
||||
|
||||
DevAmsTray* tray = nullptr;
|
||||
auto tit = ams->m_trays.find(tray_id);
|
||||
if (tit == ams->m_trays.end())
|
||||
{
|
||||
tray = new DevAmsTray(tray_id);
|
||||
ams->m_trays.insert(std::make_pair(tray_id, tray));
|
||||
}
|
||||
else
|
||||
{
|
||||
tray = tit->second;
|
||||
}
|
||||
|
||||
tray->is_exists = s.value("loaded", false);
|
||||
tray->m_fila_type = s.value("material", std::string());
|
||||
tray->setting_id = s.value("preset_id", std::string());
|
||||
tray->UpdateColorFromStr(s.value("color", std::string()));
|
||||
tray->nozzle_temp_min = std::to_string(s.value("nozzle_temp_min", 0));
|
||||
tray->nozzle_temp_max = std::to_string(s.value("nozzle_temp_max", 0));
|
||||
tray->remain = s.value("remain_percent", -1);
|
||||
tray->k = s.value("k", 0.0f);
|
||||
if (s.contains("diameter_mm") && s["diameter_mm"].is_number())
|
||||
tray->diameter = std::to_string(s["diameter_mm"].get<double>());
|
||||
if (s.contains("weight_g") && s["weight_g"].is_number())
|
||||
tray->weight = std::to_string(s["weight_g"].get<int>());
|
||||
|
||||
tray->cols.clear();
|
||||
if (s.contains("colors") && s["colors"].is_array())
|
||||
{
|
||||
for (const auto& c : s["colors"])
|
||||
if (c.is_string())
|
||||
tray->cols.push_back(c.get<std::string>());
|
||||
}
|
||||
tray->ctype = tray->cols.size() > 1 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// prune trays no longer reported
|
||||
for (auto tit = ams->m_trays.begin(); tit != ams->m_trays.end();)
|
||||
{
|
||||
if (seen_slots.count(tit->first) == 0)
|
||||
{
|
||||
delete tit->second;
|
||||
tit = ams->m_trays.erase(tit);
|
||||
}
|
||||
else
|
||||
{
|
||||
++tit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prune units no longer reported
|
||||
for (auto it = system->amsList.begin(); it != system->amsList.end();)
|
||||
{
|
||||
if (seen_units.count(it->first) == 0)
|
||||
{
|
||||
delete it->second;
|
||||
it = system->amsList.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- external / direct spools -> obj->vt_slot ---
|
||||
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
|
||||
if (obj && data.contains("external") && data["external"].is_array())
|
||||
{
|
||||
obj->vt_slot.clear();
|
||||
for (const auto& e : data["external"])
|
||||
{
|
||||
if (!e.is_object())
|
||||
continue;
|
||||
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
|
||||
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
|
||||
DevAmsTray tray(std::to_string(vt_id));
|
||||
tray.is_exists = e.value("loaded", false);
|
||||
tray.m_fila_type = e.value("material", std::string());
|
||||
tray.setting_id = e.value("preset_id", std::string());
|
||||
tray.UpdateColorFromStr(e.value("color", std::string()));
|
||||
tray.nozzle_temp_min = std::to_string(e.value("nozzle_temp_min", 0));
|
||||
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
|
||||
tray.remain = e.value("remain_percent", -1);
|
||||
obj->vt_slot.push_back(tray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ struct DevFilamentDryingPreset;
|
||||
* - color: Hex color string without '#' prefix (e.g., "FF0000")
|
||||
* - cols: Multi-color component list for gradient/multi-color filaments
|
||||
* - ctype: Color type indicator
|
||||
* - is_exists: Whether filament is currently loaded in the tray
|
||||
* - is_exists: Whether the slot is present/configured (the user's layout wins);
|
||||
* not whether filament is loaded, so a configured empty slot stays present
|
||||
*/
|
||||
class DevAmsTray
|
||||
{
|
||||
@@ -82,6 +83,7 @@ public:
|
||||
wxColour wx_color;
|
||||
bool is_bbl;
|
||||
bool is_exists = false;
|
||||
bool is_empty = false; // Explicitly reported as having no filament.
|
||||
bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents
|
||||
int hold_count = 0;
|
||||
int remain = 0; // filament remain: 0 ~ 100
|
||||
@@ -90,13 +92,15 @@ public:
|
||||
// operators
|
||||
bool operator==(DevAmsTray const& o) const
|
||||
{
|
||||
return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id && color == o.color;
|
||||
return id == o.id && m_fila_type == o.m_fila_type && filament_setting_id == o.filament_setting_id &&
|
||||
color == o.color && is_empty == o.is_empty;
|
||||
}
|
||||
bool operator!=(DevAmsTray const& o) const { return !operator==(o); }
|
||||
|
||||
// setters
|
||||
void reset();
|
||||
void UpdateColorFromStr(const std::string& color);
|
||||
void UpdateEmptyState(bool material_fields_present);
|
||||
void set_hold_count() { hold_count = HOLD_COUNT_MAX; }
|
||||
|
||||
// getter
|
||||
@@ -415,8 +419,6 @@ class DevFilaSystemParser
|
||||
{
|
||||
public:
|
||||
static void ParseV1_0(const json& print_json, MachineObject* obj, DevFilaSystem* system, bool key_field_only);
|
||||
|
||||
static void ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system);
|
||||
};
|
||||
|
||||
struct DevFilamentDryingPreset
|
||||
@@ -433,4 +435,4 @@ struct DevFilamentDryingPreset
|
||||
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
|
||||
};
|
||||
|
||||
}// namespace Slic3r
|
||||
}// namespace Slic3r
|
||||
|
||||
@@ -68,7 +68,9 @@ namespace Slic3r
|
||||
|
||||
AppConfig* DeviceManager::get_app_config() const
|
||||
{
|
||||
return m_app_config ? m_app_config : GUI::wxGetApp().app_config;
|
||||
if (m_app_config)
|
||||
return m_app_config;
|
||||
return wxTheApp ? GUI::wxGetApp().app_config : nullptr;
|
||||
}
|
||||
|
||||
void DeviceManager::load_local_machines_from_config()
|
||||
@@ -476,7 +478,7 @@ namespace Slic3r
|
||||
|
||||
void DeviceManager::update_local_machine(const MachineObject& m)
|
||||
{
|
||||
update_local_machine(m, GUI::wxGetApp().app_config);
|
||||
update_local_machine(m, wxTheApp ? GUI::wxGetApp().app_config : nullptr);
|
||||
}
|
||||
|
||||
int DeviceManager::query_bind_status(std::string& msg, const std::string& provider)
|
||||
|
||||
+264
-223
@@ -378,16 +378,9 @@ wxString MachineObject::get_printer_type_display_str() const
|
||||
{
|
||||
std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type);
|
||||
|
||||
// Bambu printers use m_resource_file_path + "/printers/" + type_str + ".json", which is a semantic that only works for their profiles.
|
||||
// For any other profile, we can simply consult preset bundle if the model_id exists.
|
||||
if (display_name.empty()) {
|
||||
for (const auto& [vendor_id, vendor] : GUI::wxGetApp().preset_bundle->vendors) {
|
||||
for (const auto& model : vendor.models) {
|
||||
if (printer_type == model.model_id)
|
||||
display_name = model.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bambu names come from Bambu configs; other vendors resolve through preset model IDs.
|
||||
if (display_name.empty() && GUI::wxGetApp().preset_bundle)
|
||||
display_name = GUI::wxGetApp().preset_bundle->get_printer_model_display_name(printer_type);
|
||||
|
||||
if (!display_name.empty())
|
||||
return display_name;
|
||||
@@ -479,7 +472,8 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
|
||||
{
|
||||
this->access_code = code;
|
||||
if (only_refresh) {
|
||||
AppConfig* config = m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config;
|
||||
AppConfig* config = m_manager ? m_manager->get_app_config()
|
||||
: (wxTheApp ? GUI::wxGetApp().app_config : nullptr);
|
||||
if (config) {
|
||||
if (is_lan_mode_printer()) {
|
||||
// why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and
|
||||
@@ -539,6 +533,69 @@ PrinterSeries MachineObject::get_printer_series() const
|
||||
return PrinterSeries::SERIES_P1P;
|
||||
}
|
||||
|
||||
bool MachineObject::is_bbl_agent() const
|
||||
{
|
||||
// Empty id means "unattributed", treated as BBL to preserve the RFID lock.
|
||||
return printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty();
|
||||
}
|
||||
|
||||
bool MachineObject::is_orca_agent() const
|
||||
{
|
||||
return printer_agent_id == ORCA_PRINTER_AGENT_ID;
|
||||
}
|
||||
|
||||
void MachineObject::reset_orca_virtual_trays_for_reconnect()
|
||||
{
|
||||
if (!is_orca_agent())
|
||||
return;
|
||||
m_orca_seen_virtual_trays.clear();
|
||||
vt_slot.clear();
|
||||
vt_slot.emplace_back(std::to_string(VIRTUAL_TRAY_MAIN_ID));
|
||||
ams_support_virtual_tray = true;
|
||||
}
|
||||
|
||||
bool MachineObject::ams_filament_ack_failed(const nlohmann::json& jj, std::string& reason)
|
||||
{
|
||||
reason.clear();
|
||||
if (!jj.contains("result") || !jj["result"].is_string() ||
|
||||
jj["result"].get<std::string>() == "success")
|
||||
return false;
|
||||
if (jj.contains("reason") && jj["reason"].is_string())
|
||||
reason = jj["reason"].get<std::string>();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MachineObject::ams_filament_native_sync_pending(const nlohmann::json& jj)
|
||||
{
|
||||
return jj.contains("native_sync") && jj["native_sync"].is_string() &&
|
||||
jj["native_sync"].get<std::string>() == "pending";
|
||||
}
|
||||
|
||||
bool MachineObject::ams_filament_connector_only(const nlohmann::json& jj)
|
||||
{
|
||||
return jj.contains("native_sync") && jj["native_sync"].is_string() &&
|
||||
jj["native_sync"].get<std::string>() == "connector_only";
|
||||
}
|
||||
|
||||
void MachineObject::fill_ams_filament_ack_tray(DevAmsTray& tray, const nlohmann::json& jj)
|
||||
{
|
||||
// Every field is optional: `jj` is a mutable copy, so a bare operator[]
|
||||
// would insert null and the get<> below would throw type_error.
|
||||
if (jj.contains("nozzle_temp_max") && jj["nozzle_temp_max"].is_number())
|
||||
tray.nozzle_temp_max = std::to_string(jj["nozzle_temp_max"].get<int>());
|
||||
if (jj.contains("nozzle_temp_min") && jj["nozzle_temp_min"].is_number())
|
||||
tray.nozzle_temp_min = std::to_string(jj["nozzle_temp_min"].get<int>());
|
||||
if (jj.contains("tray_color") && jj["tray_color"].is_string())
|
||||
tray.color = jj["tray_color"].get<std::string>();
|
||||
if (jj.contains("tray_info_idx") && jj["tray_info_idx"].is_string())
|
||||
tray.setting_id = jj["tray_info_idx"].get<std::string>();
|
||||
if (jj.contains("tray_type") || jj.contains("tray_info_idx")) {
|
||||
const std::string type = (jj.contains("tray_type") && jj["tray_type"].is_string()) ? jj["tray_type"].get<std::string>() : tray.m_fila_type;
|
||||
tray.m_fila_type = setting_id_to_type(tray.setting_id, type);
|
||||
}
|
||||
tray.set_hold_count();
|
||||
}
|
||||
|
||||
PrinterArch MachineObject::get_printer_arch() const
|
||||
{
|
||||
return DevPrinterConfigUtil::get_printer_arch(printer_type);
|
||||
@@ -622,8 +679,7 @@ MachineObject::MachineObject(DeviceManager* manager, NetworkAgent* agent, std::s
|
||||
has_ipcam = true; // default true
|
||||
|
||||
|
||||
auto vslot = DevAmsTray(std::to_string(VIRTUAL_TRAY_MAIN_ID));
|
||||
vt_slot.push_back(vslot);
|
||||
// vt_slot is seeded by reset() above; do not push a second copy here.
|
||||
|
||||
{
|
||||
m_lamp = new DevLamp(this);
|
||||
@@ -750,6 +806,7 @@ DevAmsTray *MachineObject::get_curr_tray()
|
||||
{
|
||||
const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId();
|
||||
if (cur_ams_id.compare(std::to_string(VIRTUAL_TRAY_MAIN_ID)) == 0) {
|
||||
if (vt_slot.empty()) return nullptr;
|
||||
return &vt_slot[0];
|
||||
}
|
||||
|
||||
@@ -1676,18 +1733,14 @@ int MachineObject::check_resume_condition()
|
||||
}
|
||||
int MachineObject::command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp, int new_temp, std::optional<int> extruder_id)
|
||||
{
|
||||
if (!orca_ams_command_supported("print.ams_change_filament")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_change_filament: connector does not advertise the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
json j;
|
||||
try {
|
||||
auto tray_id = 0;
|
||||
if (atoi(ams_id.c_str()) < 16) {
|
||||
tray_id = atoi(ams_id.c_str()) * 4 + atoi(slot_id.c_str());
|
||||
}
|
||||
// TODO: Orca hack
|
||||
if (ams_id == "254")
|
||||
// Preserve the legacy alias outside Orca; OrcaSonar uses both ids for tool identity.
|
||||
if (ams_id == "254" && !is_orca_agent())
|
||||
ams_id = "255";
|
||||
|
||||
j["print"]["command"] = "ams_change_filament";
|
||||
@@ -1723,10 +1776,6 @@ int MachineObject::command_ams_change_filament(bool load, std::string ams_id, st
|
||||
|
||||
int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read_opt, bool remain_flag)
|
||||
{
|
||||
if (!orca_ams_command_supported("print.ams_user_setting")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_user_settings: connector does not advertise the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
json j;
|
||||
j["print"]["command"] = "ams_user_setting";
|
||||
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
|
||||
@@ -1749,25 +1798,8 @@ int MachineObject::command_ams_calibrate(int ams_id)
|
||||
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
|
||||
}
|
||||
|
||||
bool MachineObject::orca_ams_command_supported(const char* command) const
|
||||
{
|
||||
if (printer_agent_id != ORCA_PRINTER_AGENT_ID)
|
||||
return true;
|
||||
// fms is the AMS axis; the advertised command set is the per-command gate.
|
||||
if (!is_support_fms)
|
||||
return false;
|
||||
return command != nullptr && supported_commands.count(command) != 0;
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
|
||||
{
|
||||
// OrcaSonar: writing slot metadata is gated on the filament_slots capability.
|
||||
// Absent/false means unsupported; Bambu keeps the legacy behaviour.
|
||||
if (printer_agent_id == ORCA_PRINTER_AGENT_ID && !is_support_filament_slots) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_filament_settings: printer does not advertise filament_slots";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
int tag_tray_id = 0;
|
||||
int tag_ams_id = ams_id;
|
||||
int tag_slot_id = slot_id;
|
||||
@@ -1790,8 +1822,10 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
|
||||
j["print"]["tray_id"] = tag_tray_id;
|
||||
j["print"]["tray_info_idx"] = filament_id;
|
||||
j["print"]["setting_id"] = setting_id;
|
||||
// format "FFFFFFFF" RGBA
|
||||
j["print"]["tray_color"] = tray_color;
|
||||
// Always present: Bambu firmware echoes it into the tray handler, and a
|
||||
// slim request would make that echo throw. An empty value becomes the
|
||||
// protocol's own empty-tray color.
|
||||
j["print"]["tray_color"] = tray_color.empty() ? std::string("00000000") : tray_color;
|
||||
j["print"]["nozzle_temp_min"] = nozzle_temp_min;
|
||||
j["print"]["nozzle_temp_max"] = nozzle_temp_max;
|
||||
j["print"]["tray_type"] = tray_type;
|
||||
@@ -1802,13 +1836,6 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
|
||||
int MachineObject::command_ams_refresh_rfid(int ams_id, int slot_id)
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
// OrcaSonar: RFID read requires the connector to advertise the
|
||||
// `print.ams_get_rfid` command (which already implies fms plus the macro).
|
||||
// Unknown capability is not support; Bambu is untouched.
|
||||
if (!orca_ams_command_supported("print.ams_get_rfid")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_refresh_rfid: connector does not advertise the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
return command_with_dialog(m_agent->command_ams_refresh_rfid(get_dev_id(), ams_id, slot_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
|
||||
}
|
||||
|
||||
@@ -1822,21 +1849,11 @@ int MachineObject::command_start_camera()
|
||||
int MachineObject::command_ams_select_tray(std::string tray_id)
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
// OrcaSonar: this publishes the same macro-backed print.ams_change_filament
|
||||
// as command_ams_change_filament, so it takes the same capability gate.
|
||||
if (!orca_ams_command_supported("print.ams_change_filament")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_select_tray: connector does not advertise the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
return command_with_dialog(m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_control(std::string action)
|
||||
{
|
||||
if (!orca_ams_command_supported("print.ams_control")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_control: connector does not advertise the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
if (action == "resume" && check_resume_condition()) return 0;
|
||||
|
||||
//valid actions
|
||||
@@ -1852,10 +1869,6 @@ int MachineObject::command_ams_control(std::string action)
|
||||
|
||||
int MachineObject::command_ams_drying_stop()
|
||||
{
|
||||
if (!orca_ams_command_supported("print.auto_stop_ams_dry")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "command_ams_drying_stop: connector does not advertise the command";
|
||||
return command_with_dialog(ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
json j;
|
||||
j["print"]["command"] = "auto_stop_ams_dry";
|
||||
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
|
||||
@@ -2616,13 +2629,11 @@ void MachineObject::reset()
|
||||
json empty_j;
|
||||
print_json.diff2all_base_reset(empty_j);
|
||||
|
||||
for (auto i = 0; i < vt_slot.size(); i++) {
|
||||
vt_slot[i].reset();
|
||||
|
||||
if (i == 1) {
|
||||
vt_slot.erase(vt_slot.begin() + 1);
|
||||
}
|
||||
}
|
||||
// Restore the constructor seed and clear Orca's per-connection retained IDs.
|
||||
vt_slot.clear();
|
||||
vt_slot.push_back(DevAmsTray(std::to_string(VIRTUAL_TRAY_MAIN_ID)));
|
||||
m_orca_seen_virtual_trays.clear();
|
||||
ams_support_virtual_tray = true;
|
||||
// why: reset reuses MachineObject, so release its lazy subtask
|
||||
// before dropping the pointer to prevent reconnect leaks.
|
||||
if (subtask_) {
|
||||
@@ -2850,6 +2861,7 @@ int MachineObject::local_publish_json(std::string json_str, int qos, int flag)
|
||||
std::string MachineObject::setting_id_to_type(std::string setting_id, std::string tray_type)
|
||||
{
|
||||
std::string type;
|
||||
if (wxTheApp == nullptr) return tray_type;
|
||||
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
@@ -2927,6 +2939,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
|
||||
try {
|
||||
bool restored_json = false;
|
||||
// msg=0 (or a LAN frame without msg) is parsed as a full snapshot;
|
||||
// removal behavior remains field- and agent-specific.
|
||||
bool full_snapshot = true;
|
||||
json j;
|
||||
if (!parse_ok)
|
||||
j_pre = json::parse(payload);
|
||||
@@ -2943,11 +2958,12 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
BOOST_LOG_TRIVIAL(trace) << "static: get push_all msg, dev_id=" << dev_id;
|
||||
m_push_count++;
|
||||
m_full_msg_count++;
|
||||
|
||||
full_snapshot = true;
|
||||
if (!printer_type.empty())
|
||||
print_json.load_compatible_settings(printer_type, "");
|
||||
print_json.diff2all_base_reset(j_pre);
|
||||
} else if (j_pre["print"]["msg"].get<int>() == 1) { //diff message
|
||||
full_snapshot = false;
|
||||
if (print_json.diff2all(j_pre, j) == 0) {
|
||||
restored_json = true;
|
||||
} else {
|
||||
@@ -3175,7 +3191,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
}
|
||||
|
||||
if (!key_field_only) {
|
||||
if (!m_manager->IsMultiMachineEnabled() && !is_support_agora) {
|
||||
if ((!m_manager || !m_manager->IsMultiMachineEnabled()) && !is_support_agora) {
|
||||
if (jj.contains("support_tunnel_mqtt")) {
|
||||
if (jj["support_tunnel_mqtt"].is_boolean()) {
|
||||
is_support_tunnel_mqtt = jj["support_tunnel_mqtt"].get<bool>();
|
||||
@@ -3337,23 +3353,35 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
m_nozzle_mapping_ptr->ParseAutoNozzleMapping(jj);
|
||||
|
||||
if (jj["command"].get<std::string>() == "ams_change_filament") {
|
||||
if (jj.contains("errno")) {
|
||||
if (jj["errno"].is_number()) {
|
||||
if (jj.contains("soft_temp")) {
|
||||
int soft_temp = jj["soft_temp"].get<int>();
|
||||
if (jj["errno"].get<int>() == -2) {
|
||||
wxString text = wxString::Format(_L("The chamber temperature is too high, which may cause the filament to soften. Please wait until the chamber temperature drops below %d\u2103. You may open the front door or enable fans to cool down."), soft_temp);
|
||||
GUI::wxGetApp().push_notification(this, text);
|
||||
} else if (jj["errno"].get<int>() == -4) {
|
||||
wxString text = wxString::Format(_L("AMS temperature is too high, which may cause the filament to soften. Please wait until the AMS temperature drops below %d\u2103."), soft_temp);
|
||||
GUI::wxGetApp().push_notification(this, text);
|
||||
}
|
||||
} else {
|
||||
if (jj["errno"].get<int>() == -2) {
|
||||
wxString text = _L("The current chamber temperature or the target chamber temperature exceeds 45\u2103. In order to avoid extruder clogging, low temperature filament(PLA/PETG/TPU) is not allowed to be loaded.");
|
||||
GUI::wxGetApp().push_notification(this, text);
|
||||
}
|
||||
}
|
||||
const int ack_errno = jj.contains("errno") && jj["errno"].is_number()
|
||||
? jj["errno"].get<int>() : 0;
|
||||
bool temperature_failure_notified = false;
|
||||
if (jj.contains("soft_temp") && jj["soft_temp"].is_number()) {
|
||||
const int soft_temp = jj["soft_temp"].get<int>();
|
||||
if (ack_errno == -2) {
|
||||
wxString text = wxString::Format(_L("The chamber temperature is too high, which may cause the filament to soften. Please wait until the chamber temperature drops below %d\u2103. You may open the front door or enable fans to cool down."), soft_temp);
|
||||
GUI::wxGetApp().push_notification(this, text);
|
||||
temperature_failure_notified = true;
|
||||
} else if (ack_errno == -4) {
|
||||
wxString text = wxString::Format(_L("AMS temperature is too high, which may cause the filament to soften. Please wait until the AMS temperature drops below %d\u2103."), soft_temp);
|
||||
GUI::wxGetApp().push_notification(this, text);
|
||||
temperature_failure_notified = true;
|
||||
}
|
||||
} else if (ack_errno == -2) {
|
||||
wxString text = _L("The current chamber temperature or the target chamber temperature exceeds 45\u2103. In order to avoid extruder clogging, low temperature filament(PLA/PETG/TPU) is not allowed to be loaded.");
|
||||
GUI::wxGetApp().push_notification(this, text);
|
||||
temperature_failure_notified = true;
|
||||
}
|
||||
|
||||
if (is_orca_agent() && !temperature_failure_notified) {
|
||||
std::string ack_reason;
|
||||
bool ack_failed = ams_filament_ack_failed(jj, ack_reason);
|
||||
ack_failed = ack_failed || ack_errno != 0;
|
||||
if (ack_failed) {
|
||||
wxString text = _L("Failed to change filament");
|
||||
if (!ack_reason.empty())
|
||||
text += wxString::FromUTF8(": ") + wxString::FromUTF8(ack_reason);
|
||||
GUI::wxGetApp().push_notification(this, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4083,42 +4111,120 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
catch (...) {
|
||||
;
|
||||
}
|
||||
update_printer_preset_name();
|
||||
update_filament_list();
|
||||
// Both read the GUI preset bundle; skip them in a headless
|
||||
// process (unit tests), where wxTheApp is null.
|
||||
if (wxTheApp != nullptr) {
|
||||
update_printer_preset_name();
|
||||
update_filament_list();
|
||||
}
|
||||
if (jj.contains("ams")) {
|
||||
DevFilaSystemParser::ParseV1_0(jj, this, m_fila_system.get(), key_field_only);
|
||||
}
|
||||
|
||||
/* vitrual tray*/
|
||||
/* virtual tray */
|
||||
if (!key_field_only) {
|
||||
try {
|
||||
if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
|
||||
|
||||
for (auto it = jj["vir_slot"].begin(); it != jj["vir_slot"].end(); it++) {
|
||||
auto vslot = parse_vt_tray(it.value().get<json>());
|
||||
|
||||
if (vslot.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 0);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[0] = vslot;
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
}
|
||||
if (is_orca_agent()) {
|
||||
auto clear_unseen_seed = [&]() {
|
||||
if (m_orca_seen_virtual_trays.empty()) {
|
||||
vt_slot.clear();
|
||||
ams_support_virtual_tray = false;
|
||||
} else {
|
||||
ams_support_virtual_tray = true;
|
||||
}
|
||||
else if (vslot.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 1);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[1] = vslot;
|
||||
};
|
||||
auto merge_orca_tray = [&](DevAmsTray vslot) {
|
||||
if (vslot.id != std::to_string(VIRTUAL_TRAY_MAIN_ID)
|
||||
&& vslot.id != std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
|
||||
return;
|
||||
|
||||
m_orca_seen_virtual_trays.insert(vslot.id);
|
||||
auto held = std::find_if(vt_slot.begin(), vt_slot.end(), [&](const DevAmsTray& tray) {
|
||||
return tray.id == vslot.id;
|
||||
});
|
||||
if (held != vt_slot.end()) {
|
||||
*held = std::move(vslot);
|
||||
} else if (vslot.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) {
|
||||
vt_slot.insert(vt_slot.begin(), std::move(vslot));
|
||||
} else {
|
||||
auto main = std::find_if(vt_slot.begin(), vt_slot.end(), [](const DevAmsTray& tray) {
|
||||
return tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID);
|
||||
});
|
||||
if (main == vt_slot.end()) {
|
||||
vt_slot.insert(vt_slot.begin(), DevAmsTray(std::to_string(VIRTUAL_TRAY_MAIN_ID)));
|
||||
main = vt_slot.begin();
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
auto deputy = std::find_if(vt_slot.begin(), vt_slot.end(), [](const DevAmsTray& tray) {
|
||||
return tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID);
|
||||
});
|
||||
if (deputy == vt_slot.end())
|
||||
vt_slot.insert(std::next(main), std::move(vslot));
|
||||
}
|
||||
ams_support_virtual_tray = true;
|
||||
};
|
||||
|
||||
if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
|
||||
if (jj["vir_slot"].empty()) {
|
||||
if (full_snapshot)
|
||||
clear_unseen_seed();
|
||||
} else {
|
||||
for (const auto& entry : jj["vir_slot"]) {
|
||||
if (!entry.is_object() || !entry.contains("id") || !entry["id"].is_string())
|
||||
continue;
|
||||
const std::string id = entry["id"].get<std::string>();
|
||||
if (id == std::to_string(VIRTUAL_TRAY_MAIN_ID)
|
||||
|| id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
|
||||
merge_orca_tray(parse_vt_tray(entry.get<json>()));
|
||||
}
|
||||
if (full_snapshot)
|
||||
clear_unseen_seed();
|
||||
}
|
||||
} else if (jj.contains("vt_tray")) {
|
||||
auto main_slot = parse_vt_tray(jj["vt_tray"].get<json>());
|
||||
main_slot.id = std::to_string(VIRTUAL_TRAY_MAIN_ID);
|
||||
merge_orca_tray(std::move(main_slot));
|
||||
} else if (full_snapshot) {
|
||||
clear_unseen_seed();
|
||||
}
|
||||
} else if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
|
||||
if (jj["vir_slot"].empty()) {
|
||||
if (full_snapshot) {
|
||||
vt_slot.clear();
|
||||
ams_support_virtual_tray = false;
|
||||
}
|
||||
} else {
|
||||
ams_support_virtual_tray = true;
|
||||
if (full_snapshot) {
|
||||
std::vector<DevAmsTray> fresh;
|
||||
for (const auto& entry : jj["vir_slot"]) {
|
||||
auto vslot = parse_vt_tray(entry.get<json>());
|
||||
if (vslot.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) {
|
||||
if (fresh.empty())
|
||||
fresh.push_back(vslot);
|
||||
else
|
||||
fresh[0] = vslot;
|
||||
} else if (vslot.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID) && !fresh.empty()) {
|
||||
if (fresh.size() > 1)
|
||||
fresh[1] = vslot;
|
||||
else
|
||||
fresh.push_back(vslot);
|
||||
}
|
||||
}
|
||||
vt_slot = std::move(fresh);
|
||||
} else {
|
||||
for (const auto& entry : jj["vir_slot"]) {
|
||||
auto vslot = parse_vt_tray(entry.get<json>());
|
||||
for (auto& held : vt_slot) {
|
||||
if (held.id == vslot.id) {
|
||||
held = vslot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if (jj.contains("vt_tray")) {
|
||||
} else if (jj.contains("vt_tray")) {
|
||||
ams_support_virtual_tray = true;
|
||||
auto main_slot = parse_vt_tray(jj["vt_tray"].get<json>());
|
||||
main_slot.id = std::to_string(VIRTUAL_TRAY_MAIN_ID);
|
||||
|
||||
@@ -4131,9 +4237,8 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
vt_slot.push_back(main_slot);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ams_support_virtual_tray = false;
|
||||
}
|
||||
// For Bambu and legacy agents, missing keys do not change
|
||||
// the current tray state.
|
||||
}
|
||||
catch (...) {
|
||||
;
|
||||
@@ -4172,47 +4277,42 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
// BBS trigger ams UI update
|
||||
ams_version = -1;
|
||||
|
||||
if (jj["ams_id"].is_number()) {
|
||||
// OPCP acks a rejected/failed write with result "fail" and an
|
||||
// errno (-19 validation, -4 persist). Without this the failure
|
||||
// looked like success and left the old spool on screen.
|
||||
std::string ack_reason;
|
||||
const bool ack_failed = ams_filament_ack_failed(jj, ack_reason);
|
||||
if (ack_failed) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "ams_filament_setting rejected: errno="
|
||||
<< (jj.contains("errno") ? jj["errno"].dump() : "?")
|
||||
<< ", reason=" << ack_reason;
|
||||
wxString text = _L("Failed to set AMS filament");
|
||||
if (!ack_reason.empty())
|
||||
text += wxString::FromUTF8(": ") + wxString::FromUTF8(ack_reason);
|
||||
GUI::wxGetApp().push_notification(this, text);
|
||||
} else if (ams_filament_native_sync_pending(jj)) {
|
||||
GUI::wxGetApp().push_notification(this, _L("Filament settings are saved and will be applied when the printer reconnects."));
|
||||
} else if (ams_filament_connector_only(jj)) {
|
||||
GUI::wxGetApp().push_notification(this, _L("Filament settings were saved by OrcaSonar but could not be applied to the printer."));
|
||||
}
|
||||
|
||||
if (!ack_failed && jj["ams_id"].is_number()) {
|
||||
int ams_id = jj["ams_id"].get<int>();
|
||||
int tray_id = 0;
|
||||
if (jj.contains("tray_id")) {
|
||||
tray_id = jj["tray_id"].get<int>();
|
||||
}
|
||||
if (ams_id == 255 && tray_id == VIRTUAL_TRAY_MAIN_ID) {
|
||||
if (ams_id == 255 && tray_id == VIRTUAL_TRAY_MAIN_ID && !vt_slot.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "ams_filament_setting, parse tray info";
|
||||
vt_slot[0].nozzle_temp_max = std::to_string(jj["nozzle_temp_max"].get<int>());
|
||||
vt_slot[0].nozzle_temp_min = std::to_string(jj["nozzle_temp_min"].get<int>());
|
||||
vt_slot[0].color = jj["tray_color"].get<std::string>();
|
||||
vt_slot[0].setting_id = jj["tray_info_idx"].get<std::string>();
|
||||
//vt_tray.type = jj["tray_type"].get<std::string>();
|
||||
vt_slot[0].m_fila_type = setting_id_to_type(vt_slot[0].setting_id, jj["tray_type"].get<std::string>());
|
||||
// delay update
|
||||
vt_slot[0].set_hold_count();
|
||||
fill_ams_filament_ack_tray(vt_slot[0], jj);
|
||||
vt_slot[0].is_exists = true;
|
||||
} else {
|
||||
auto ams = m_fila_system->GetAmsById(std::to_string(ams_id));
|
||||
if (ams) {
|
||||
tray_id = jj["tray_id"].get<int>();
|
||||
auto tray_it = ams->GetTrays().find(std::to_string(tray_id));
|
||||
if (tray_it != ams->GetTrays().end()) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "ams_filament_setting, parse tray info";
|
||||
tray_it->second->nozzle_temp_max = std::to_string(jj["nozzle_temp_max"].get<int>());
|
||||
tray_it->second->nozzle_temp_min = std::to_string(jj["nozzle_temp_min"].get<int>());
|
||||
//tray_it->second->type = jj["tray_type"].get<std::string>();
|
||||
tray_it->second->color = jj["tray_color"].get<std::string>();
|
||||
|
||||
/*tray_it->second->cols.clear();
|
||||
if (jj.contains("cols")) {
|
||||
if (jj["cols"].is_array()) {
|
||||
for (auto it = jj["cols"].begin(); it != jj["cols"].end(); it++) {
|
||||
tray_it->second->cols.push_back(it.value().get<std::string>());
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
tray_it->second->setting_id = jj["tray_info_idx"].get<std::string>();
|
||||
tray_it->second->m_fila_type = setting_id_to_type(tray_it->second->setting_id, jj["tray_type"].get<std::string>());
|
||||
// delay update
|
||||
tray_it->second->set_hold_count();
|
||||
fill_ams_filament_ack_tray(*tray_it->second, jj);
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << "ams_filament_setting, can not find in trayList, tray_id=" << tray_id;
|
||||
}
|
||||
@@ -4331,7 +4431,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
;
|
||||
}
|
||||
}
|
||||
if (tray_id == VIRTUAL_TRAY_MAIN_ID) {
|
||||
if (tray_id == VIRTUAL_TRAY_MAIN_ID && !vt_slot.empty()) {
|
||||
if (jj.contains("k_value"))
|
||||
vt_slot[0].k = jj["k_value"].get<float>();
|
||||
if (jj.contains("n_coef"))
|
||||
@@ -4719,7 +4819,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
if (diff.count() > 10.0f) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "parse_json timeout = " << diff.count();
|
||||
}
|
||||
DeviceManager::update_local_machine(*this, m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config);
|
||||
AppConfig* app_config = m_manager ? m_manager->get_app_config()
|
||||
: (wxTheApp ? GUI::wxGetApp().app_config : nullptr);
|
||||
DeviceManager::update_local_machine(*this, app_config);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -5121,6 +5223,9 @@ bool MachineObject::is_firmware_info_valid()
|
||||
DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
{
|
||||
auto vt_tray = DevAmsTray(std::to_string(VIRTUAL_TRAY_MAIN_ID));
|
||||
// OrcaSonar emits a virtual slot only when the layout has it: the user's
|
||||
// configuration wins, so it is present even with no material loaded.
|
||||
vt_tray.is_exists = true;
|
||||
|
||||
if (vtray.contains("id"))
|
||||
vt_tray.id = vtray["id"].get<std::string>();
|
||||
@@ -5148,7 +5253,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
//std::string type = vtray["tray_type"].get<std::string>();
|
||||
std::string type = setting_id_to_type(vt_tray.setting_id, vtray["tray_type"].get<std::string>());
|
||||
// vt_tray.setting_id is our OF id (translated on the way in); the two support ids below are the printer's own.
|
||||
auto* agent = GUI::wxGetApp().getAgent();
|
||||
auto* agent = wxTheApp != nullptr ? GUI::wxGetApp().getAgent() : nullptr;
|
||||
const std::string printer_filament_id = agent ? agent->from_orca_filament_id(vt_tray.setting_id) : vt_tray.setting_id;
|
||||
if (printer_filament_id == "GFS00") {
|
||||
vt_tray.m_fila_type = "PLA-S";
|
||||
@@ -5243,6 +5348,7 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
else {
|
||||
vt_tray.remain = -1;
|
||||
}
|
||||
vt_tray.UpdateEmptyState(vtray.contains("tray_info_idx") && vtray.contains("tray_type"));
|
||||
}
|
||||
|
||||
return vt_tray;
|
||||
@@ -5536,14 +5642,10 @@ void MachineObject::parse_new_info2(const json& info)
|
||||
if (capabilities_it == info.end() || !capabilities_it->is_object())
|
||||
return;
|
||||
const auto flags_it = capabilities_it->find("flags");
|
||||
const bool has_flags = flags_it != capabilities_it->end() && flags_it->is_object();
|
||||
// Bambu keeps the legacy behavior: a reply without flags is ignored. Orca
|
||||
// connector-scope features/commands must still be parsed, so fall through
|
||||
// with an empty flags object (every parse_bool below then no-ops).
|
||||
if (!has_flags && printer_agent_id != ORCA_PRINTER_AGENT_ID)
|
||||
if (flags_it == capabilities_it->end() || !flags_it->is_object())
|
||||
return;
|
||||
static const json empty_flags = json::object();
|
||||
const json& flags = has_flags ? *flags_it : empty_flags;
|
||||
|
||||
const json& flags = *flags_it;
|
||||
BOOST_LOG_TRIVIAL(info) << "parse_new_info2: OrcaSonar capability flags=" << flags.dump();
|
||||
|
||||
auto parse_bool = [&flags](const char* name, bool& target) {
|
||||
@@ -5571,7 +5673,7 @@ void MachineObject::parse_new_info2(const json& info)
|
||||
parse_bool("support_build_plate_marker_detect", is_support_build_plate_marker_detect);
|
||||
parse_bool("support_nozzle_blob_detect", is_support_nozzle_blob_detection);
|
||||
|
||||
if (!m_manager->IsMultiMachineEnabled() && !is_support_agora)
|
||||
if ((!m_manager || !m_manager->IsMultiMachineEnabled()) && !is_support_agora)
|
||||
parse_bool("support_tunnel_mqtt", is_support_tunnel_mqtt);
|
||||
|
||||
const auto bed_leveling_it = flags.find("support_bed_leveling");
|
||||
@@ -5609,64 +5711,6 @@ void MachineObject::parse_new_info2(const json& info)
|
||||
|
||||
m_config->ParseConfig(device_config);
|
||||
m_fan->ParseV2_0(fan_config);
|
||||
|
||||
// OrcaSonar connector-scope capabilities. Absent or non-boolean means
|
||||
// unsupported. A reply without `flags` still parses them (the fall-through
|
||||
// above); Bambu (bbl) devices never enter this branch and keep the
|
||||
// flags-only path.
|
||||
if (printer_agent_id == ORCA_PRINTER_AGENT_ID) {
|
||||
auto parse_features = [this](const json& features) {
|
||||
if (!features.is_object())
|
||||
return;
|
||||
auto set_flag = [&features](const char* name, bool& target) {
|
||||
const auto it = features.find(name);
|
||||
if (it != features.end() && it->is_boolean())
|
||||
target = it->get<bool>();
|
||||
};
|
||||
set_flag("fms", is_support_fms);
|
||||
set_flag("filament_slots", is_support_filament_slots);
|
||||
set_flag("filament_mapping", is_support_filament_mapping);
|
||||
};
|
||||
|
||||
auto parse_commands = [this](const json& commands) {
|
||||
if (!commands.is_array())
|
||||
return;
|
||||
for (const auto& command : commands) {
|
||||
if (command.is_string())
|
||||
supported_commands.insert(command.get<std::string>());
|
||||
}
|
||||
};
|
||||
|
||||
// Clear before filling so a reply without commands does not retain stale values.
|
||||
supported_commands.clear();
|
||||
is_support_fms = false;
|
||||
is_support_filament_slots = false;
|
||||
is_support_filament_mapping = false;
|
||||
|
||||
const auto supported_features_it = info.find("supported_features");
|
||||
if (supported_features_it != info.end())
|
||||
parse_features(*supported_features_it);
|
||||
|
||||
const auto supported_commands_it = info.find("supported_commands");
|
||||
if (supported_commands_it != info.end())
|
||||
parse_commands(*supported_commands_it);
|
||||
|
||||
const auto protocol_it = capabilities_it->find("protocol");
|
||||
if (protocol_it != capabilities_it->end() && protocol_it->is_object()) {
|
||||
const auto protocol_features_it = protocol_it->find("features");
|
||||
if (protocol_features_it != protocol_it->end())
|
||||
parse_features(*protocol_features_it);
|
||||
|
||||
const auto protocol_commands_it = protocol_it->find("supported_commands");
|
||||
if (protocol_commands_it != protocol_it->end())
|
||||
parse_commands(*protocol_commands_it);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "parse_new_info2: fms=" << is_support_fms
|
||||
<< " filament_slots=" << is_support_filament_slots
|
||||
<< " filament_mapping=" << is_support_filament_mapping
|
||||
<< " supported_commands=" << supported_commands.size();
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_hex_digit(char c) {
|
||||
@@ -5794,7 +5838,9 @@ int MachineObject::get_flag_bits(int num, int start, int count, int base) const
|
||||
|
||||
void MachineObject::update_filament_list()
|
||||
{
|
||||
if (wxTheApp == nullptr) return;
|
||||
PresetBundle *preset_bundle = Slic3r::GUI::wxGetApp().preset_bundle;
|
||||
if (preset_bundle == nullptr) return;
|
||||
|
||||
// custom filament
|
||||
typedef std::map<std::string, std::pair<int, int>> map_pair;
|
||||
@@ -5864,6 +5910,7 @@ void MachineObject::update_filament_list()
|
||||
void MachineObject::update_printer_preset_name()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "start update preset_name";
|
||||
if (wxTheApp == nullptr) return;
|
||||
PresetBundle * preset_bundle = Slic3r::GUI::wxGetApp().preset_bundle;
|
||||
if (!preset_bundle) return;
|
||||
auto printer_model = DevPrinterConfigUtil::get_printer_display_name(this->printer_type);
|
||||
@@ -5938,14 +5985,11 @@ void MachineObject::check_ams_filament_valid()
|
||||
if (curr_tray->setting_id.size() == 8 && curr_tray->setting_id[0] == 'P' && filament_list.find(curr_tray->setting_id) == filament_list.end()) {
|
||||
if (checked_filament.find(curr_tray->setting_id) != checked_filament.end()) {
|
||||
need_checked_filament_id[nozzle_diameter_str].insert(curr_tray->setting_id);
|
||||
wxColour color = *wxWHITE;
|
||||
char col_buf[10];
|
||||
sprintf(col_buf, "%02X%02X%02XFF", (int) color.Red(), (int) color.Green(), (int) color.Blue());
|
||||
try {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " ams settings_id is not exist in filament_list and reset, ams_id: " << ams_id << " tray_id"
|
||||
<< slot_id << "filament_id: " << curr_tray->setting_id;
|
||||
|
||||
command_ams_filament_settings(std::stoi(ams_id), std::stoi(slot_id), "", "", std::string(col_buf), "", 0, 0);
|
||||
command_ams_filament_settings(std::stoi(ams_id), std::stoi(slot_id), "", "", "00000000", "", 0, 0);
|
||||
continue;
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " stoi error and ams_id: " << ams_id << " tray_id" << slot_id;
|
||||
@@ -6000,12 +6044,9 @@ void MachineObject::check_ams_filament_valid()
|
||||
if (vt_tray.setting_id.size() == 8 && vt_tray.setting_id[0] == 'P' && filament_list.find(vt_tray.setting_id) == filament_list.end()) {
|
||||
if (checked_filament.find(vt_tray.setting_id) != checked_filament.end()) {
|
||||
need_checked_filament_id[nozzle_diameter_str].insert(vt_tray.setting_id);
|
||||
wxColour color = *wxWHITE;
|
||||
char col_buf[10];
|
||||
sprintf(col_buf, "%02X%02X%02XFF", (int) color.Red(), (int) color.Green(), (int) color.Blue());
|
||||
try {
|
||||
BOOST_LOG_TRIVIAL(info) << "vt_tray.setting_id is not exist in filament_list and reset vt_tray and the filament_id is: " << vt_tray.setting_id;
|
||||
command_ams_filament_settings(vt_id, 0, "", "", std::string(col_buf), "", 0, 0);
|
||||
command_ams_filament_settings(vt_id, 0, "", "", "00000000", "", 0, 0);
|
||||
continue;
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " stoi error and tray_id" << vt_tray.id;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <optional>
|
||||
#include <boost/thread.hpp>
|
||||
@@ -114,6 +113,7 @@ private:
|
||||
std::string dev_name;
|
||||
std::string dev_ip;
|
||||
std::string access_code;
|
||||
std::unordered_set<std::string> m_orca_seen_virtual_trays;
|
||||
|
||||
// type, time stamp, delay
|
||||
std::vector<std::tuple<std::string, uint64_t, uint64_t>> message_delay;
|
||||
@@ -162,6 +162,20 @@ public:
|
||||
void set_agent(NetworkAgent* agent) { m_agent = agent; }
|
||||
NetworkAgent* get_agent() const { return m_agent; } // Orca: needed by DeviceCore modules (DevAxisCtrl)
|
||||
|
||||
// Orca: true only for devices managed by Bambu's own agent, where a
|
||||
// non-zero tag_uid is an RFID lock. Other agents report it as metadata.
|
||||
bool is_bbl_agent() const;
|
||||
bool is_orca_agent() const;
|
||||
void reset_orca_virtual_trays_for_reconnect();
|
||||
|
||||
// Orca: an OPCP failure ack (result != "success"). Fills reason when present.
|
||||
static bool ams_filament_ack_failed(const nlohmann::json& jj, std::string& reason);
|
||||
static bool ams_filament_native_sync_pending(const nlohmann::json& jj);
|
||||
static bool ams_filament_connector_only(const nlohmann::json& jj);
|
||||
// Orca: apply one ams_filament_setting echo to a tray. A slim ack omits
|
||||
// fields; an unguarded get here would throw and drop the whole push frame.
|
||||
static void fill_ams_filament_ack_tray(DevAmsTray& tray, const nlohmann::json& jj);
|
||||
|
||||
// Orca: these DeviceCore module accessors are unwired on the read side — axis/chamber/status
|
||||
// are fed every MQTT push but no GUI consumer reads them yet, and for calib/upgrade the inline
|
||||
// parse in DeviceManager.cpp remains authoritative. Do not wire DevUpgrade naively: its
|
||||
@@ -647,13 +661,6 @@ public:
|
||||
bool is_support_partskip{false};
|
||||
bool is_support_refresh_nozzle{false};
|
||||
|
||||
// OrcaSonar connector-scope capabilities (printer_agent_id == "orca"). Parsed from the
|
||||
// get_capabilities reply; never consulted on Bambu paths.
|
||||
bool is_support_fms{false};
|
||||
bool is_support_filament_slots{false};
|
||||
bool is_support_filament_mapping{false};
|
||||
std::set<std::string> supported_commands;
|
||||
|
||||
// refine printer function options
|
||||
bool is_support_spaghetti_detection{false};
|
||||
bool is_support_purgechutepileup_detection{false};
|
||||
@@ -795,10 +802,6 @@ public:
|
||||
int command_refresh_nozzle();
|
||||
int command_set_chamber(int temp);
|
||||
int check_resume_condition();
|
||||
// OrcaSonar: true when a macro-backed AMS command may be sent. Bambu paths
|
||||
// always allow; an Orca device must have advertised the command (unknown
|
||||
// capability is not support).
|
||||
bool orca_ams_command_supported(const char* command) const;
|
||||
// ams controls
|
||||
//int command_ams_switch(int tray_index, int old_temp = 210, int new_temp = 210);
|
||||
int command_ams_change_filament(bool load, std::string ams_id, std::string slot_id, int old_temp = 210, int new_temp = 210, std::optional<int> extruder_id = std::nullopt);
|
||||
|
||||
@@ -943,7 +943,11 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
|
||||
const auto& can = ams.cans[j];
|
||||
auto id = getTrayID(obj, ams.ams_id, can.can_id);
|
||||
auto material = can.material_name;
|
||||
if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND ||
|
||||
if (can.is_empty || can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY)
|
||||
{
|
||||
material = L("Empty");
|
||||
}
|
||||
else if (can.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND ||
|
||||
can.material_state == AMSCanType::AMS_CAN_TYPE_BRAND ||
|
||||
can.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)
|
||||
{
|
||||
@@ -952,11 +956,6 @@ int ReselectMachineDialog::CaculateSwitcherDistribution(MachineObject* obj, cons
|
||||
material = L("?");
|
||||
}
|
||||
}
|
||||
if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY)
|
||||
{
|
||||
material = "Empty";
|
||||
}
|
||||
|
||||
auto itOK = std::find_if(posOK.begin(), posOK.end(), [&](const trayHelper& tray){
|
||||
auto amsID = std::get<0>(tray);
|
||||
auto slotID = std::get<1>(tray);
|
||||
@@ -1043,4 +1042,4 @@ ReselectMachineDialog::~ReselectMachineDialog()
|
||||
|
||||
} // namespace GUI
|
||||
|
||||
} // namespace Slic3r
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
// True when the normalized ams_mapping2 carries any entry the agent's serializer
|
||||
// would put on the wire: every integer pair except the {255,255} unmatched
|
||||
// sentinel, external slots ({255,0}/{254,0}) included. This mirrors
|
||||
// OrcaPrinterAgent::build_filament_mapping exactly, so the GUI gate and the
|
||||
// agent gate agree; a mismatch lets an entry reach the agent and be refused late
|
||||
// with a generic publish error instead of the designed message.
|
||||
inline bool has_engaged_filament_mapping(const std::string& ams_mapping2)
|
||||
{
|
||||
const nlohmann::json mapping = nlohmann::json::parse(ams_mapping2, nullptr, false);
|
||||
if (mapping.is_discarded() || !mapping.is_array())
|
||||
return false;
|
||||
for (const auto& entry : mapping) {
|
||||
if (!entry.is_object())
|
||||
continue;
|
||||
const auto ams_id_it = entry.find("ams_id");
|
||||
const auto slot_id_it = entry.find("slot_id");
|
||||
if (ams_id_it == entry.end() || slot_id_it == entry.end())
|
||||
continue;
|
||||
if (!ams_id_it->is_number_integer() || !slot_id_it->is_number_integer())
|
||||
continue;
|
||||
if (ams_id_it->get<int>() == 255 && slot_id_it->get<int>() == 255)
|
||||
continue; // unmatched/unused sentinel: the serializer drops it
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A used filament with no target would be silently dropped from the wire
|
||||
// mapping, so the print must be refused rather than run the wrong material.
|
||||
// m_ams_mapping_result carries exactly the filaments the slice uses.
|
||||
inline bool has_used_filament_without_target(const std::vector<FilamentInfo>& result)
|
||||
{
|
||||
for (const auto& f : result) {
|
||||
if (f.get_ams_id() < 0 || f.get_slot_id() < 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// True when at least one used filament already has a target. The used-unmapped
|
||||
// refusal applies to a partially mapped print; a print with no mapping at all
|
||||
// is handled by the existing all-invalid send-path flow.
|
||||
inline bool has_any_mapped_target(const std::vector<FilamentInfo>& result)
|
||||
{
|
||||
for (const auto& f : result) {
|
||||
if (f.get_ams_id() >= 0 && f.get_slot_id() >= 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -2173,6 +2173,7 @@ void GUI_App::init_networking_callbacks()
|
||||
/* request_pushing */
|
||||
MachineObject* obj = m_device_manager->get_my_machine(tunnel ? dev_id.substr(7) : dev_id);
|
||||
if (obj) {
|
||||
obj->reset_orca_virtual_trays_for_reconnect();
|
||||
obj->is_tunnel_mqtt = tunnel;
|
||||
obj->command_request_push_all(true);
|
||||
obj->command_get_version();
|
||||
@@ -2215,6 +2216,7 @@ void GUI_App::init_networking_callbacks()
|
||||
|
||||
if (obj->is_lan_mode_printer()) {
|
||||
if (state == ConnectStatus::ConnectStatusOk) {
|
||||
obj->reset_orca_virtual_trays_for_reconnect();
|
||||
obj->command_request_push_all(true);
|
||||
obj->command_get_version();
|
||||
event.SetInt(0);
|
||||
@@ -2336,8 +2338,13 @@ void GUI_App::init_networking_callbacks()
|
||||
|
||||
if (MachineObject* obj = m_device_manager->get_my_machine(dev_id)) {
|
||||
obj->parse_json("lan", msg);
|
||||
// Orca: skip it if it doesn't support subscription based filament sync
|
||||
if (this->m_device_manager->get_selected_machine() == obj &&
|
||||
// Orca: skip it if it doesn't support subscription based filament
|
||||
// sync, and skip frames that carry no filament state at all
|
||||
// (progress, temps, hms) so the AMS list is not rebuilt every push.
|
||||
const bool has_filament_state = msg.find("\"vir_slot\"") != std::string::npos ||
|
||||
msg.find("\"ams\":") != std::string::npos;
|
||||
if (has_filament_state &&
|
||||
this->m_device_manager->get_selected_machine() == obj &&
|
||||
m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription) {
|
||||
GUI::wxGetApp().sidebar().load_ams_list(obj);
|
||||
}
|
||||
|
||||
+39
-10
@@ -737,6 +737,7 @@ struct Sidebar::priv
|
||||
ScalableButton * m_bpButton_add_filament;
|
||||
ScalableButton * m_bpButton_del_filament;
|
||||
ScalableButton * m_bpButton_ams_filament;
|
||||
bool m_ams_sync_button_show{true}; // last applied AMS sync button visibility
|
||||
ScalableButton * m_bpButton_set_filament;
|
||||
int m_menu_filament_id = -1;
|
||||
|
||||
@@ -3452,6 +3453,22 @@ void Sidebar::remove_unused_filament_combos(const size_t current_extruder_count)
|
||||
}
|
||||
}
|
||||
|
||||
void Sidebar::update_ams_sync_button()
|
||||
{
|
||||
if (!p->m_bpButton_ams_filament || !wxGetApp().preset_bundle)
|
||||
return;
|
||||
// BBL printers always advertise AMS sync; other agents follow the active
|
||||
// agent's filament sync mode, which flips to subscription only after the
|
||||
// printer's get_capabilities reply arrives.
|
||||
const bool show = wxGetApp().preset_bundle->use_bbl_network() ||
|
||||
(wxGetApp().getAgent() && wxGetApp().getAgent()->get_filament_sync_mode() != FilamentSyncMode::none);
|
||||
if (p->m_ams_sync_button_show == show)
|
||||
return;
|
||||
p->m_ams_sync_button_show = show;
|
||||
p->m_bpButton_ams_filament->Show(show);
|
||||
p->m_bpButton_ams_filament->GetParent()->Layout();
|
||||
}
|
||||
|
||||
void Sidebar::update_all_preset_comboboxes()
|
||||
{
|
||||
PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
|
||||
@@ -3469,7 +3486,7 @@ void Sidebar::update_all_preset_comboboxes()
|
||||
//p->btn_connect_printer->Hide();
|
||||
p->m_printer_connect->Hide();
|
||||
//only show sync-ams button for BBL printer
|
||||
p->m_bpButton_ams_filament->Show();
|
||||
update_ams_sync_button();
|
||||
//update print button default value for bbl or third-party printer
|
||||
p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate);
|
||||
} else {
|
||||
@@ -3478,11 +3495,7 @@ void Sidebar::update_all_preset_comboboxes()
|
||||
p->m_printer_connect->Show(!use_printer_agents);
|
||||
|
||||
// ORCA: show/hide sync-ams button based on filament sync mode
|
||||
auto agent = wxGetApp().getAgent();
|
||||
if (agent && agent->get_filament_sync_mode() != FilamentSyncMode::none)
|
||||
p->m_bpButton_ams_filament->Show();
|
||||
else
|
||||
p->m_bpButton_ams_filament->Hide();
|
||||
update_ams_sync_button();
|
||||
|
||||
// Orca: with "Support 3MF as gcode" (use_3mf) the local export is a .gcode.3mf bundle, so when no
|
||||
// printer host/IP is configured the default action is "Export plate sliced file" (mirrors the
|
||||
@@ -5693,10 +5706,20 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
}
|
||||
|
||||
auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) {
|
||||
// Type-only agents (OrcaSonar/Qidi Box) report the material but not the
|
||||
// slicer's preset id. Resolve it from the type so AMS sync can match a
|
||||
// preset instead of skipping every tray as "unknown".
|
||||
std::string filament_id = tray.setting_id;
|
||||
if (filament_id.empty() && !tray.m_fila_type.empty() && tray.is_exists) {
|
||||
// Strict lookup: an unmatched material leaves filament_id empty so
|
||||
// the sync flow reports it as unknown instead of binding Generic PLA.
|
||||
if (auto *bundle = wxGetApp().preset_bundle)
|
||||
bundle->filaments.filament_id_by_type(tray.m_fila_type, filament_id);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%")
|
||||
% name % tray.setting_id % tray.m_fila_type % tray.color;
|
||||
% name % filament_id % tray.m_fila_type % tray.color;
|
||||
DynamicPrintConfig tray_config;
|
||||
tray_config.set_key_value("filament_id", new ConfigOptionStrings{tray.setting_id});
|
||||
tray_config.set_key_value("filament_id", new ConfigOptionStrings{filament_id});
|
||||
tray_config.set_key_value("tag_uid", new ConfigOptionStrings{tray.tag_uid});
|
||||
tray_config.set_key_value("ams_id", new ConfigOptionStrings{ams_id});
|
||||
tray_config.set_key_value("slot_id", new ConfigOptionStrings{slot_id});
|
||||
@@ -5709,7 +5732,7 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{tray.is_slot_placeholder});
|
||||
std::optional<FilamentBaseInfo> info;
|
||||
if (wxGetApp().preset_bundle) {
|
||||
info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id);
|
||||
info = wxGetApp().preset_bundle->get_filament_by_filament_id(filament_id);
|
||||
}
|
||||
tray_config.set_key_value("filament_is_support", new ConfigOptionBools{ info.has_value() ? info->is_support : false});
|
||||
for (int i = 0; i < tray.cols.size(); ++i) {
|
||||
@@ -5903,7 +5926,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
|
||||
auto & list = wxGetApp().preset_bundle->filament_ams_list;
|
||||
if (list.empty()) {
|
||||
auto printer_name = p->plater->get_selected_printer_name_in_combox();
|
||||
p->plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, _L("Sync printer information"));
|
||||
p->plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::AMS_UNAVAILABLE, _L("Sync printer information"));
|
||||
return;
|
||||
}
|
||||
bool exist_at_list_one_filament =false;
|
||||
@@ -21143,6 +21166,8 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar
|
||||
content = _L("There are no filaments on the printer. Please load the filaments on the printer first.");
|
||||
} else if (type == PrinterWarningType::EMPTY_FILAMENT) {
|
||||
content = _L("The filaments on the printer are all unknown types. Please go to the printer screen or software device page to set the filament type.");
|
||||
} else if (type == PrinterWarningType::AMS_UNAVAILABLE) {
|
||||
content = _L("No AMS filament data is available for this printer. Make sure the printer's material system is detected and connected, then try again.");
|
||||
}
|
||||
MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page"));
|
||||
auto result = dlg.ShowModal();
|
||||
@@ -22455,10 +22480,14 @@ void Plater::update_machine_sync_status()
|
||||
DeviceManager *dev_maneger = wxGetApp().getDeviceManager();
|
||||
if (!dev_maneger) {
|
||||
GUI::wxGetApp().sidebar().update_sync_status(nullptr);
|
||||
GUI::wxGetApp().sidebar().update_ams_sync_button();
|
||||
return;
|
||||
}
|
||||
MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine();
|
||||
GUI::wxGetApp().sidebar().update_sync_status(obj);
|
||||
// The agent's sync mode flips once the printer's get_capabilities reply
|
||||
// arrives; re-evaluate the filament-sync button on every device update.
|
||||
GUI::wxGetApp().sidebar().update_ams_sync_button();
|
||||
}
|
||||
|
||||
bool Plater::get_machine_sync_status()
|
||||
|
||||
@@ -169,6 +169,8 @@ public:
|
||||
void init_filament_combo(PlaterPresetComboBox **combo, const int filament_idx);
|
||||
void remove_unused_filament_combos(const size_t current_extruder_count);
|
||||
void update_all_preset_comboboxes();
|
||||
// Show/hide the AMS filament-sync button from the active agent's sync mode.
|
||||
void update_ams_sync_button();
|
||||
//void update_partplate(PartPlateList& list);
|
||||
void update_presets(Slic3r::Preset::Type preset_type);
|
||||
//BBS
|
||||
@@ -646,7 +648,8 @@ public:
|
||||
NOT_CONNECTED,
|
||||
INCONSISTENT,
|
||||
UNINSTALL_FILAMENT,
|
||||
EMPTY_FILAMENT
|
||||
EMPTY_FILAMENT,
|
||||
AMS_UNAVAILABLE
|
||||
};
|
||||
void pop_warning_and_go_to_device_page(wxString printer_name, PrinterWarningType type, const wxString &title);
|
||||
bool check_printer_initialized(MachineObject *obj, bool only_warning = false,bool popup_warning = true);
|
||||
|
||||
@@ -1777,7 +1777,7 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj)
|
||||
auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
|
||||
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
|
||||
// ORCA enabling / disabling buttons with conditions enough to change its style
|
||||
m_button_ok->Enable(isValidEndpoint(str_ip.ToStdString()) &&
|
||||
m_button_ok->Enable(isIp(str_ip.ToStdString()) &&
|
||||
(str_access_code.IsEmpty() || str_access_code.Length() >= 8));
|
||||
|
||||
Layout();
|
||||
@@ -1815,29 +1815,19 @@ void InputIpAddressDialog::update_test_msg(wxString msg,bool connected)
|
||||
Fit();
|
||||
}
|
||||
|
||||
bool InputIpAddressDialog::isValidEndpoint(std::string endpoint)
|
||||
bool InputIpAddressDialog::isIp(std::string ipstr)
|
||||
{
|
||||
if (endpoint.empty() || std::any_of(endpoint.begin(), endpoint.end(), [](unsigned char c) {
|
||||
return std::isspace(c) != 0;
|
||||
}))
|
||||
return false;
|
||||
|
||||
const bool has_http_scheme = endpoint.rfind("http://", 0) == 0;
|
||||
const bool has_https_scheme = endpoint.rfind("https://", 0) == 0;
|
||||
const auto scheme_pos = endpoint.find("://");
|
||||
if (scheme_pos != std::string::npos && !has_http_scheme && !has_https_scheme)
|
||||
return false;
|
||||
|
||||
std::string port;
|
||||
const std::string host = Http::get_host_from_url(endpoint, &port);
|
||||
if (host.empty())
|
||||
return false;
|
||||
|
||||
// get_host_from_url returns its input when libcurl cannot parse it. For a
|
||||
// URL with a scheme, that means a failed parse still needs to be rejected.
|
||||
if (scheme_pos != std::string::npos && host == endpoint)
|
||||
return false;
|
||||
|
||||
istringstream ipstream(ipstr);
|
||||
int num[4];
|
||||
char point[3];
|
||||
string end;
|
||||
ipstream >> num[0] >> point[0] >> num[1] >> point[1] >> num[2] >> point[2] >> num[3] >> end;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (num[i] < 0 || num[i]>255) return false;
|
||||
if (point[i] != '.') return false;
|
||||
}
|
||||
if (num[3] < 0 || num[3]>255) return false;
|
||||
if (!end.empty()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2140,7 +2130,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
|
||||
|
||||
// ORCA enabling / disabling buttons with conditions enough to change its style
|
||||
bool valid_access_code_length = str_access_code.IsEmpty() || str_access_code.Length() >= 8;
|
||||
bool enable_btns = isValidEndpoint(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
|
||||
bool enable_btns = isIp(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
|
||||
m_button_manual_setup->Enable(enable_btns);
|
||||
m_button_ok->Enable(enable_btns);
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ public:
|
||||
void update_title(wxString title);
|
||||
void set_machine_obj(MachineObject* obj);
|
||||
void update_test_msg(wxString msg, bool connected);
|
||||
bool isValidEndpoint(std::string endpoint);
|
||||
bool isIp(std::string ipstr);
|
||||
void check_ip_address_failed(int result);
|
||||
void on_check_ip_address_failed(wxCommandEvent& evt);
|
||||
void on_ok(wxMouseEvent& evt);
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
#include "libslic3r/MultiNozzleUtils.hpp" // filament-change-gap model for the best-position popup
|
||||
#include "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result()
|
||||
#include "DeviceCore/DevStorage.h"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "FilamentMappingUtils.hpp"
|
||||
|
||||
#include <wx/progdlg.h>
|
||||
#include <wx/clipbrd.h>
|
||||
@@ -3480,9 +3478,6 @@ void SelectMachineDialog::navigate_to_timelapse_page()
|
||||
this->EndModal(wxID_CANCEL);
|
||||
}
|
||||
|
||||
// Mapping helpers live in FilamentMappingUtils.hpp (shared with
|
||||
// SendMultiMachinePage); they mirror the agent serializer exactly.
|
||||
|
||||
void SelectMachineDialog::on_send_print()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "print_job: on_ok to send";
|
||||
@@ -3537,26 +3532,6 @@ void SelectMachineDialog::on_send_print()
|
||||
|
||||
get_ams_mapping_result(ams_mapping_array,ams_mapping_array2, ams_mapping_info);
|
||||
|
||||
// OrcaSonar: a mapped print requires the connector to advertise
|
||||
// filament_mapping and the index correlation to be verified. Refuse rather
|
||||
// than start with the map silently dropped; Bambu keeps its behavior.
|
||||
if (obj_->printer_agent_id == ORCA_PRINTER_AGENT_ID) {
|
||||
const bool mapping_available = obj_->is_support_filament_mapping && ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED;
|
||||
if (!mapping_available && has_engaged_filament_mapping(ams_mapping_array2)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "print_job: filament mapping unavailable (capability=" << obj_->is_support_filament_mapping
|
||||
<< ", correlation_verified=" << ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED << "); refusing mapped print";
|
||||
m_status_bar->set_status_text(_L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."));
|
||||
Enable_Send_Button(true);
|
||||
return;
|
||||
}
|
||||
if (has_any_mapped_target(m_ams_mapping_result) && has_used_filament_without_target(m_ams_mapping_result)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "print_job: a used filament has no AMS target; refusing print";
|
||||
m_status_bar->set_status_text(_L("A filament used by this print has no AMS mapping. Assign it before printing."));
|
||||
Enable_Send_Button(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_print_type == PrintFromType::FROM_NORMAL) {
|
||||
result = m_plater->send_gcode(m_print_plate_idx, [this](int export_stage, int current, int total, bool& cancel) {
|
||||
if (this->m_is_canceled) return;
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
|
||||
#include "DeviceCore/DevManager.h"
|
||||
#include "DeviceCore/DevStorage.h"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "FilamentMappingUtils.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
@@ -694,8 +692,6 @@ bool SendMultiMachinePage::get_ams_mapping_result(std::string &mapping_array_str
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mapping helpers live in FilamentMappingUtils.hpp, shared with SelectMachine.
|
||||
|
||||
void SendMultiMachinePage::on_send(wxCommandEvent& event)
|
||||
{
|
||||
event.Skip();
|
||||
@@ -744,29 +740,6 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event)
|
||||
|
||||
if (!wxGetApp().is_blocking_printing(obj)) {
|
||||
PrintParams params = request_params(obj);
|
||||
// OrcaSonar: a mapped print requires the connector to advertise
|
||||
// filament_mapping, and a partially mapped print must not silently drop a
|
||||
// used filament. Any entry the serializer would put on the wire engages the
|
||||
// gate, external slots ({255,0}/{254,0}) included; the extra-spool branch
|
||||
// rewrites to exactly those. Bambu is unchanged.
|
||||
if (obj->printer_agent_id == ORCA_PRINTER_AGENT_ID) {
|
||||
const bool mapping_available = obj->is_support_filament_mapping && ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED;
|
||||
if (!mapping_available && has_engaged_filament_mapping(params.ams_mapping2)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: filament mapping unavailable (capability=" << obj->is_support_filament_mapping
|
||||
<< ", correlation_verified=" << ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED << "); refusing mapped print for "
|
||||
<< obj->get_dev_id();
|
||||
MessageDialog msg_wingow(nullptr, _L("AMS filament mapping is not available for this printer. Clear the AMS mapping before printing."), "", wxICON_WARNING | wxOK);
|
||||
msg_wingow.ShowModal();
|
||||
return;
|
||||
}
|
||||
if (params.task_use_ams && has_any_mapped_target(m_ams_mapping_result) &&
|
||||
has_used_filament_without_target(m_ams_mapping_result)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: a used filament has no target; refusing print for " << obj->get_dev_id();
|
||||
MessageDialog msg_wingow(nullptr, _L("A filament used by this print has no AMS mapping. Assign it before printing."), "", wxICON_WARNING | wxOK);
|
||||
msg_wingow.ShowModal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
print_params.push_back(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4350,12 +4350,12 @@ void StatusPanel::on_ams_load_curr()
|
||||
vt_slot_idx = 1;
|
||||
}
|
||||
|
||||
if (vt_slot_idx < 0 || vt_slot_idx >= (int)obj->vt_slot.size()) return;
|
||||
|
||||
int old_temp = -1;
|
||||
int new_temp = -1;
|
||||
DevAmsTray* curr_tray = &obj->vt_slot[vt_slot_idx];
|
||||
|
||||
if (!curr_tray) return;
|
||||
|
||||
try {
|
||||
if (!curr_tray->nozzle_temp_max.empty() && !curr_tray->nozzle_temp_min.empty())
|
||||
old_temp = (atoi(curr_tray->nozzle_temp_min.c_str()) + atoi(curr_tray->nozzle_temp_max.c_str())) / 2;
|
||||
@@ -4373,7 +4373,8 @@ void StatusPanel::on_ams_load_curr()
|
||||
}
|
||||
} catch (...) {}
|
||||
} else {
|
||||
obj->command_ams_change_filament(true, "254", "0", old_temp, new_temp, extruder_id);
|
||||
const std::string external_ams_id = obj->is_orca_agent() ? curr_ams_id : "254";
|
||||
obj->command_ams_change_filament(true, external_ams_id, "0", old_temp, new_temp, extruder_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4592,7 +4593,7 @@ void StatusPanel::on_filament_edit(wxCommandEvent &event)
|
||||
m_filament_setting_dlg->set_ctype(tray->ctype);
|
||||
m_filament_setting_dlg->ams_filament_id = tray->setting_id;
|
||||
|
||||
if (m_filament_setting_dlg->ams_filament_id.empty())
|
||||
if (tray->is_empty)
|
||||
{
|
||||
m_filament_setting_dlg->set_empty_color(color);
|
||||
}
|
||||
@@ -4602,15 +4603,25 @@ void StatusPanel::on_filament_edit(wxCommandEvent &event)
|
||||
m_filament_setting_dlg->set_colors(cols);
|
||||
}
|
||||
|
||||
m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(tray->tag_uid);
|
||||
// A non-zero tag_uid is an RFID lock only on Bambu's own agent.
|
||||
// Agent-managed printers (Orca/Qidi/Snapmaker) send it as
|
||||
// metadata, and the spec says it must never be interpreted — so
|
||||
// the tag alone must not make their trays read-only.
|
||||
m_filament_setting_dlg->m_is_third = !obj->is_bbl_agent() || !DevFilaSystem::IsBBL_Filament(tray->tag_uid);
|
||||
temp_max = tray->nozzle_temp_max;
|
||||
temp_min = tray->nozzle_temp_min;
|
||||
if (!m_filament_setting_dlg->m_is_third)
|
||||
{
|
||||
sn_number = tray->uuid;
|
||||
filament = tray->sub_brands;
|
||||
temp_max = tray->nozzle_temp_max;
|
||||
temp_min = tray->nozzle_temp_min;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No tray state for this slot: never show the previous slot's.
|
||||
m_filament_setting_dlg->set_empty_color(AMS_MATERIALS_SETTING_DEF_COLOUR);
|
||||
m_filament_setting_dlg->ams_filament_id = "";
|
||||
}
|
||||
|
||||
m_filament_setting_dlg->Move(wxPoint(current_position_x, current_position_y));
|
||||
m_filament_setting_dlg->Popup(filament, sn_number, temp_min, temp_max, k_val, n_val);
|
||||
@@ -4642,6 +4653,8 @@ void StatusPanel::on_ext_spool_edit(wxCommandEvent &event)
|
||||
m_filament_setting_dlg->slot_id = slot_id;
|
||||
int nozzle_index = ams_id == VIRTUAL_TRAY_MAIN_ID ? 0 : 1;
|
||||
|
||||
if (nozzle_index < 0 || nozzle_index >= (int)obj->vt_slot.size()) return;
|
||||
|
||||
try {
|
||||
std::string sn_number;
|
||||
std::string filament;
|
||||
@@ -4660,7 +4673,7 @@ void StatusPanel::on_ext_spool_edit(wxCommandEvent &event)
|
||||
}
|
||||
m_filament_setting_dlg->set_ctype(obj->vt_slot[nozzle_index].ctype);
|
||||
|
||||
if (m_filament_setting_dlg->ams_filament_id.empty()) {
|
||||
if (obj->vt_slot[nozzle_index].is_empty) {
|
||||
m_filament_setting_dlg->set_empty_color(color);
|
||||
}
|
||||
else {
|
||||
@@ -4668,12 +4681,12 @@ void StatusPanel::on_ext_spool_edit(wxCommandEvent &event)
|
||||
m_filament_setting_dlg->set_colors(cols);
|
||||
}
|
||||
|
||||
m_filament_setting_dlg->m_is_third = !DevFilaSystem::IsBBL_Filament(obj->vt_slot[nozzle_index].tag_uid);
|
||||
m_filament_setting_dlg->m_is_third = !obj->is_bbl_agent() || !DevFilaSystem::IsBBL_Filament(obj->vt_slot[nozzle_index].tag_uid);
|
||||
temp_max = obj->vt_slot[nozzle_index].nozzle_temp_max;
|
||||
temp_min = obj->vt_slot[nozzle_index].nozzle_temp_min;
|
||||
if (!m_filament_setting_dlg->m_is_third) {
|
||||
sn_number = obj->vt_slot[nozzle_index].uuid;
|
||||
filament = obj->vt_slot[nozzle_index].sub_brands;
|
||||
temp_max = obj->vt_slot[nozzle_index].nozzle_temp_max;
|
||||
temp_min = obj->vt_slot[nozzle_index].nozzle_temp_min;
|
||||
}
|
||||
|
||||
m_filament_setting_dlg->Move(wxPoint(current_position_x,current_position_y));
|
||||
|
||||
@@ -1373,12 +1373,6 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s
|
||||
BOOST_LOG_TRIVIAL(error) << "get_ams_mapping_result, plater is nullptr";
|
||||
}
|
||||
|
||||
// mapping_v1_json is built one entry per filament preset, in preset order, so the
|
||||
// array position is the logical filament index the generated G-code toolchange
|
||||
// references (the identifier handed to the Klipper toolchange macro; see
|
||||
// OrcaPrinterAgent::build_filament_mapping and the index-correlation test in
|
||||
// tests/slic3rutils/test_orca_printer_agent.cpp). Never re-densify after dropping
|
||||
// sentinel entries, or a used filament would be aimed at the wrong lane.
|
||||
for (int i = 0; i < wxGetApp().preset_bundle->filament_presets.size(); i++) {
|
||||
int tray_id = -1;
|
||||
json mapping_item_v1;
|
||||
|
||||
@@ -72,6 +72,7 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag,
|
||||
Caninfo info;
|
||||
// tray is exists
|
||||
if (it != ams->GetTrays().end() && it->second->is_exists) {
|
||||
info.is_empty = it->second->is_empty;
|
||||
if (it->second->is_tray_info_ready()) {
|
||||
info.can_id = it->second->id;
|
||||
info.ctype = it->second->ctype;
|
||||
@@ -139,6 +140,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
|
||||
this->ams_type = AMSModel::EXT_AMS;
|
||||
Caninfo info;
|
||||
info.can_id = std::to_string(0);
|
||||
info.is_empty = tray.is_empty;
|
||||
this->cans.clear();
|
||||
|
||||
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID))
|
||||
@@ -1101,9 +1103,9 @@ void AMSLib::render_lite_text(wxDC& dc)
|
||||
dc.SetTextForeground(temp_text_colour);
|
||||
|
||||
auto libsize = GetSize();
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|
||||
if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) {
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) {
|
||||
|
||||
if (m_info.material_name.empty()) {
|
||||
auto tsize = dc.GetMultiLineTextExtent("?");
|
||||
@@ -1151,7 +1153,7 @@ void AMSLib::render_lite_text(wxDC& dc)
|
||||
}
|
||||
}
|
||||
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) {
|
||||
auto tsize = dc.GetMultiLineTextExtent(_L("/"));
|
||||
auto pot = wxPoint((libsize.x - tsize.x) / 2 + FromDIP(2), (libsize.y - tsize.y) / 2 + FromDIP(3));
|
||||
dc.DrawText(_L("/"), pot);
|
||||
@@ -1202,9 +1204,9 @@ void AMSLib::render_generic_text(wxDC &dc)
|
||||
}
|
||||
|
||||
auto libsize = GetSize();
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|
||||
if (!m_info.is_empty && (m_info.material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_BRAND
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL) {
|
||||
|| m_info.material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL)) {
|
||||
|
||||
if (m_info.material_name.empty() /*&& m_info.material_state != AMSCanType::AMS_CAN_TYPE_VIRTUAL*/) {
|
||||
auto tsize = dc.GetMultiLineTextExtent("?");
|
||||
@@ -1284,7 +1286,7 @@ void AMSLib::render_generic_text(wxDC &dc)
|
||||
}
|
||||
}
|
||||
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
if (m_info.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || m_info.is_empty) {
|
||||
auto tsize = dc.GetMultiLineTextExtent(_L("Empty"));
|
||||
auto pot = wxPoint((libsize.x - tsize.x) / 2, (libsize.y - tsize.y) / 2 + FromDIP(3));
|
||||
dc.DrawText(_L("Empty"), pot);
|
||||
@@ -2781,7 +2783,7 @@ void AMSPreview::doRender(wxDC &dc)
|
||||
}
|
||||
else {
|
||||
wxRect rect(left, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, AMS_ITEM_CUBE_SIZE.x, AMS_ITEM_CUBE_SIZE.y);
|
||||
if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
if (iter->material_state == AMSCanType::AMS_CAN_TYPE_EMPTY || iter->is_empty) {
|
||||
dc.SetPen(wxPen(wxColor(0, 0, 0)));
|
||||
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1), rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1));
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ struct Caninfo
|
||||
wxString material_name;
|
||||
wxColour material_colour = {*wxWHITE};
|
||||
AMSCanType material_state;
|
||||
bool is_empty = false;
|
||||
int ctype=0;
|
||||
int material_remain = 100;
|
||||
int cali_idx = -1;
|
||||
@@ -198,6 +199,7 @@ public:
|
||||
material_name == other.material_name &&
|
||||
material_colour == other.material_colour &&
|
||||
material_state == other.material_state &&
|
||||
is_empty == other.is_empty &&
|
||||
ctype == other.ctype &&
|
||||
material_remain == other.material_remain &&
|
||||
cali_idx == other.cali_idx &&
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
#include "AmsPayload.hpp"
|
||||
|
||||
#include "Http.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
|
||||
#include "slic3r/GUI/DeviceCore/DevManager.h"
|
||||
#include "slic3r/GUI/DeviceCore/DevStorage.h"
|
||||
#include "slic3r/GUI/DeviceCore/DevFirmware.h"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/thread.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
std::string map_filament_type_to_generic_id(const std::string& filament_type)
|
||||
{
|
||||
std::string upper = filament_type;
|
||||
boost::trim(upper);
|
||||
std::transform(upper.begin(), upper.end(), upper.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::toupper(c)); });
|
||||
|
||||
// Normalize reported material names to an OrcaFilamentLibrary generic family. The
|
||||
// family's filament_id is resolved from the loaded system presets below rather than
|
||||
// hardcoded, so profile id re-mints never require touching this table.
|
||||
// scripts/test_moonraker_lane_data.py parses this initializer; keep the {"A", "B"} format.
|
||||
static const std::map<std::string, std::string> type_to_ofl_family = {
|
||||
// PLA variants
|
||||
{"PLA", "PLA"},
|
||||
{"PLA-CF", "PLA-CF"},
|
||||
{"PLA SILK", "PLA Silk"},
|
||||
{"PLA-SILK", "PLA Silk"},
|
||||
{"PLA HIGH SPEED", "PLA High Speed"},
|
||||
{"PLA-HS", "PLA High Speed"},
|
||||
{"PLA HS", "PLA High Speed"},
|
||||
|
||||
// ABS/ASA variants
|
||||
{"ABS", "ABS"},
|
||||
{"ASA", "ASA"},
|
||||
|
||||
// PETG/PET variants
|
||||
{"PETG", "PETG"},
|
||||
{"PET", "PETG"},
|
||||
{"PCTG", "PCTG"},
|
||||
|
||||
// PA/Nylon variants
|
||||
{"PA", "PA"},
|
||||
{"NYLON", "PA"},
|
||||
{"PA-CF", "PA-CF"},
|
||||
{"PPA", "PPA-CF"},
|
||||
{"PPA-CF", "PPA-CF"},
|
||||
{"PPA-GF", "PPA-GF"},
|
||||
|
||||
// PC variants
|
||||
{"PC", "PC"},
|
||||
|
||||
// PP/PE variants
|
||||
{"PE", "PE"},
|
||||
{"PP", "PP"},
|
||||
|
||||
// Support materials
|
||||
{"PVA", "PVA"},
|
||||
{"HIPS", "HIPS"},
|
||||
{"BVOH", "BVOH"},
|
||||
|
||||
// TPU variants
|
||||
{"TPU", "TPU"},
|
||||
|
||||
// Other materials
|
||||
{"EVA", "EVA"},
|
||||
{"PHA", "PHA"},
|
||||
{"COPE", "CoPE"},
|
||||
{"SBS", "SBS"},
|
||||
};
|
||||
|
||||
auto it = type_to_ofl_family.find(upper);
|
||||
if (it == type_to_ofl_family.end())
|
||||
return UNKNOWN_FILAMENT_ID;
|
||||
|
||||
auto* bundle = wxTheApp != nullptr ? GUI::wxGetApp().preset_bundle : nullptr;
|
||||
if (bundle) {
|
||||
const Preset* preset = bundle->filaments.find_preset("Generic " + it->second + " @System");
|
||||
if (preset != nullptr && preset->is_system && !preset->filament_id.empty())
|
||||
return preset->filament_id;
|
||||
}
|
||||
|
||||
// Unknown material, or no loaded preset data to resolve against
|
||||
return UNKNOWN_FILAMENT_ID;
|
||||
}
|
||||
|
||||
std::string normalize_ams_color(const std::string& color)
|
||||
{
|
||||
std::string value = color;
|
||||
boost::trim(value);
|
||||
|
||||
// Remove 0x or 0X prefix if present
|
||||
if (value.size() >= 2 && (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0)) {
|
||||
value = value.substr(2);
|
||||
}
|
||||
// Remove # prefix if present
|
||||
if (!value.empty() && value[0] == '#') {
|
||||
value = value.substr(1);
|
||||
}
|
||||
|
||||
// Extract only hex digits
|
||||
std::string normalized;
|
||||
for (char c : value) {
|
||||
if (std::isxdigit(static_cast<unsigned char>(c))) {
|
||||
normalized.push_back(static_cast<char>(std::toupper(static_cast<unsigned char>(c))));
|
||||
}
|
||||
}
|
||||
|
||||
// If 6 hex digits, add FF alpha
|
||||
if (normalized.size() == 6) {
|
||||
normalized += "FF";
|
||||
}
|
||||
|
||||
// Validate length - return default if invalid
|
||||
if (normalized.size() != 8) {
|
||||
return "00000000";
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
bool parse_moonraker_lane_data(const nlohmann::json& body,
|
||||
std::vector<AmsTrayData>& trays,
|
||||
int& max_lane_index)
|
||||
{
|
||||
// Expected structure: { "result": { "namespace": "lane_data", "value": { "lane1": {...}, ... } } }
|
||||
if (!body.is_object() || !body.contains("result") || !body["result"].is_object() ||
|
||||
!body["result"].contains("value") || !body["result"]["value"].is_object()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "AmsPayload: unexpected lane_data response structure";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& value = body["result"]["value"];
|
||||
trays.clear();
|
||||
max_lane_index = 0;
|
||||
|
||||
for (const auto& lane_item : value.items()) {
|
||||
const auto& lane_obj = lane_item.value();
|
||||
if (!lane_obj.is_object()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract lane index from the "lane" field (tool number, 0-based)
|
||||
int lane_index = -1;
|
||||
const auto lane_it = lane_obj.find("lane");
|
||||
if (lane_it != lane_obj.end() && lane_it->is_string()) {
|
||||
try {
|
||||
lane_index = std::stoi(lane_it->get<std::string>());
|
||||
} catch (...) {
|
||||
lane_index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (lane_index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = lane_index;
|
||||
if (const auto it = lane_obj.find("color"); it != lane_obj.end() && it->is_string())
|
||||
tray.tray_color = it->get<std::string>();
|
||||
if (const auto it = lane_obj.find("material"); it != lane_obj.end() && it->is_string())
|
||||
tray.tray_type = it->get<std::string>();
|
||||
if (const auto it = lane_obj.find("bed_temp"); it != lane_obj.end() && it->is_number())
|
||||
tray.bed_temp = it->get<int>();
|
||||
if (const auto it = lane_obj.find("nozzle_temp"); it != lane_obj.end() && it->is_number())
|
||||
tray.nozzle_temp = it->get<int>();
|
||||
|
||||
// Presence is independent of declared material: a physically loaded lane
|
||||
// with no user-declared material must not read as empty. Prefer an
|
||||
// explicit signal, fall back to the legacy "has a material type" rule.
|
||||
const auto has_it = lane_obj.find("has_filament");
|
||||
const auto loaded_it = lane_obj.find("loaded");
|
||||
if (has_it != lane_obj.end() && has_it->is_boolean())
|
||||
tray.has_filament = has_it->get<bool>();
|
||||
else if (loaded_it != lane_obj.end() && loaded_it->is_boolean())
|
||||
tray.has_filament = loaded_it->get<bool>();
|
||||
else
|
||||
tray.has_filament = !tray.tray_type.empty();
|
||||
|
||||
max_lane_index = std::max(max_lane_index, lane_index);
|
||||
trays.push_back(tray);
|
||||
}
|
||||
|
||||
if (trays.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "AmsPayload: no lanes found";
|
||||
return false;
|
||||
}
|
||||
|
||||
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 = wxTheApp != nullptr ? GUI::wxGetApp().preset_bundle : nullptr;
|
||||
for (auto& tray : trays) {
|
||||
// Absent lanes render as placeholders; resolving an empty type is busy
|
||||
// work at best and could bind a bogus id.
|
||||
if (!tray.has_filament)
|
||||
continue;
|
||||
// A vendor-aware resolver may already have matched this lane; only fill
|
||||
// what is still empty so the generic fallback cannot overwrite it.
|
||||
if (!tray.tray_info_idx.empty())
|
||||
continue;
|
||||
if (bundle) {
|
||||
std::string filament_id;
|
||||
if (bundle->filaments.filament_id_by_type(tray.tray_type, filament_id))
|
||||
tray.tray_info_idx = std::move(filament_id);
|
||||
} else {
|
||||
const std::string filament_id = map_filament_type_to_generic_id(tray.tray_type);
|
||||
if (filament_id != UNKNOWN_FILAMENT_ID)
|
||||
tray.tray_info_idx = filament_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json build_bbl_ams_json(const std::vector<AmsTrayData>& trays,
|
||||
int ams_count,
|
||||
int max_lane_index)
|
||||
{
|
||||
nlohmann::json ams_array = nlohmann::json::array();
|
||||
|
||||
// ams_exist_bits marks the units; tray_exist_bits marks the occupied
|
||||
// slots. uint64_t: unsigned long is 32-bit on MSVC, so 1UL << 32+ is UB.
|
||||
// BBL's fields are 64-bit; a lane past 63 cannot be represented at all.
|
||||
uint64_t ams_exist_bits = 0;
|
||||
uint64_t tray_exist_bits = 0;
|
||||
|
||||
auto set_exist_bit = [](uint64_t& bits, int index) {
|
||||
if (index < 0)
|
||||
return;
|
||||
if (index >= 64) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "AmsPayload: lane index " << index << " exceeds the 64-bit exist-bits field; bit dropped";
|
||||
return;
|
||||
}
|
||||
bits |= (uint64_t{1} << index);
|
||||
};
|
||||
|
||||
for (int ams_id = 0; ams_id < ams_count; ++ams_id) {
|
||||
set_exist_bit(ams_exist_bits, ams_id);
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
ams_unit["info"] = "0002"; // treat as AMS_LITE
|
||||
|
||||
nlohmann::json tray_array = nlohmann::json::array();
|
||||
int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4);
|
||||
for (int slot_id = 0; slot_id <= max_slot_in_this_ams; ++slot_id) {
|
||||
int slot_index = ams_id * 4 + slot_id;
|
||||
|
||||
// Find tray with matching slot_index
|
||||
const AmsTrayData* tray = nullptr;
|
||||
for (const auto& t : trays) {
|
||||
if (t.slot_index == slot_index) {
|
||||
tray = &t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json tray_json = nlohmann::json::object();
|
||||
tray_json["id"] = std::to_string(slot_id);
|
||||
tray_json["tag_uid"] = "0000000000000000";
|
||||
|
||||
if (tray && tray->has_filament) {
|
||||
set_exist_bit(tray_exist_bits, slot_index);
|
||||
|
||||
// Present lanes never carry tray_slot_placeholder: a loaded but
|
||||
// undeclared lane stays occupied with an empty type, which is
|
||||
// what distinguishes it from an absent slot.
|
||||
tray_json["tray_info_idx"] = tray->tray_info_idx;
|
||||
tray_json["tray_type"] = tray->tray_type;
|
||||
tray_json["tray_color"] = normalize_ams_color(tray->tray_color);
|
||||
|
||||
// Add temperature data if provided
|
||||
if (tray->bed_temp > 0) {
|
||||
tray_json["bed_temp"] = std::to_string(tray->bed_temp);
|
||||
}
|
||||
if (tray->nozzle_temp > 0) {
|
||||
tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp);
|
||||
}
|
||||
} else {
|
||||
tray_json["tray_info_idx"] = "";
|
||||
tray_json["tray_type"] = "";
|
||||
tray_json["tray_color"] = "00000000";
|
||||
tray_json["tray_slot_placeholder"] = "1";
|
||||
}
|
||||
|
||||
tray_array.push_back(tray_json);
|
||||
}
|
||||
ams_unit["tray"] = tray_array;
|
||||
ams_array.push_back(ams_unit);
|
||||
}
|
||||
|
||||
// Format as hex strings (matching BBL protocol)
|
||||
std::ostringstream ams_exist_ss;
|
||||
ams_exist_ss << std::hex << std::uppercase << ams_exist_bits;
|
||||
std::ostringstream tray_exist_ss;
|
||||
tray_exist_ss << std::hex << std::uppercase << tray_exist_bits;
|
||||
|
||||
nlohmann::json ams_json = nlohmann::json::object();
|
||||
ams_json["ams"] = ams_array;
|
||||
ams_json["ams_exist_bits"] = ams_exist_ss.str();
|
||||
ams_json["tray_exist_bits"] = tray_exist_ss.str();
|
||||
return ams_json;
|
||||
}
|
||||
|
||||
// --- Removal/absence state and op capability ---------------------------------
|
||||
|
||||
static std::mutex g_ams_state_mutex;
|
||||
|
||||
// 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;
|
||||
bool filament_slots = false;
|
||||
FilamentMetadataValues metadata;
|
||||
};
|
||||
static std::map<std::string, AmsDeviceCaps> g_ams_caps;
|
||||
|
||||
void register_ams_ops(const std::string& dev_id, const std::vector<std::string>& ops)
|
||||
{
|
||||
if (dev_id.empty())
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
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_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)
|
||||
{
|
||||
if (dev_id.empty())
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
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_caps.find(dev_id);
|
||||
return it != g_ams_caps.end() && it->second.has_ams;
|
||||
}
|
||||
|
||||
bool ams_caps_known(const std::string& dev_id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
auto it = g_ams_caps.find(dev_id);
|
||||
return it != g_ams_caps.end() && it->second.ops_known;
|
||||
}
|
||||
|
||||
void register_filament_slots(const std::string& dev_id, bool has_slots)
|
||||
{
|
||||
if (dev_id.empty())
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
g_ams_caps[dev_id].filament_slots = has_slots;
|
||||
}
|
||||
|
||||
bool has_filament_slots(const std::string& dev_id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
auto it = g_ams_caps.find(dev_id);
|
||||
return it != g_ams_caps.end() && it->second.filament_slots;
|
||||
}
|
||||
|
||||
void register_filament_metadata(const std::string& dev_id, const FilamentMetadataValues& values)
|
||||
{
|
||||
if (dev_id.empty())
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
g_ams_caps[dev_id].metadata = values;
|
||||
}
|
||||
|
||||
FilamentMetadataValues filament_metadata_values(const std::string& dev_id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
auto it = g_ams_caps.find(dev_id);
|
||||
if (it == g_ams_caps.end())
|
||||
return {};
|
||||
return it->second.metadata;
|
||||
}
|
||||
|
||||
static bool material_prefix(const std::string& value, const char* family)
|
||||
{
|
||||
const std::string prefix(family);
|
||||
if (value == prefix)
|
||||
return true;
|
||||
if (value.size() <= prefix.size() || value.compare(0, prefix.size(), prefix) != 0)
|
||||
return false;
|
||||
const char next = value[prefix.size()];
|
||||
return next == ' ' || next == '+' || next == '-' || next == '/';
|
||||
}
|
||||
|
||||
static bool qidi_pa_material(const std::string& value)
|
||||
{
|
||||
return material_prefix(value, "PA") || material_prefix(value, "PA12") ||
|
||||
material_prefix(value, "PAHT") || material_prefix(value, "ULTRAPA");
|
||||
}
|
||||
|
||||
// Keep distinct material families separate; only known variants share a stem.
|
||||
std::string normalize_qidi_material_family(const std::string& value)
|
||||
{
|
||||
const std::string upper = boost::algorithm::to_upper_copy(boost::algorithm::trim_copy(value));
|
||||
if (material_prefix(upper, "PLA-CF")) return "PLA-CF";
|
||||
if (material_prefix(upper, "PPA-CF")) return "PPA-CF";
|
||||
if (material_prefix(upper, "PC-ABS")) return "PC";
|
||||
if (material_prefix(upper, "PCTG")) return "PCTG";
|
||||
if (material_prefix(upper, "PPA")) return "PPA";
|
||||
if (material_prefix(upper, "PLA")) return "PLA";
|
||||
if (material_prefix(upper, "ABS")) return "ABS";
|
||||
if (material_prefix(upper, "PETG")) return "PETG";
|
||||
if (material_prefix(upper, "TPU")) return "TPU";
|
||||
if (material_prefix(upper, "ASA")) return "ASA";
|
||||
if (qidi_pa_material(upper) || material_prefix(upper, "NYLON")) return "PA";
|
||||
if (material_prefix(upper, "PC")) return "PC";
|
||||
if (material_prefix(upper, "PVA")) return "PVA";
|
||||
return upper;
|
||||
}
|
||||
|
||||
bool filament_material_compatible(const std::string& dev_id, const std::string& preset_type)
|
||||
{
|
||||
const FilamentMetadataValues values = filament_metadata_values(dev_id);
|
||||
if (!values.constrained)
|
||||
return true;
|
||||
if (boost::algorithm::trim_copy(preset_type).empty())
|
||||
return true; // a reply that never carried a type must not hide presets
|
||||
std::vector<std::string> parts;
|
||||
boost::algorithm::split(parts, preset_type, boost::algorithm::is_any_of(";"), boost::token_compress_on);
|
||||
for (const std::string& part : parts) {
|
||||
const std::string type = boost::algorithm::trim_copy(part);
|
||||
if (type.empty())
|
||||
continue;
|
||||
const std::string category = normalize_qidi_material_family(type);
|
||||
for (const std::string& declared : values.materials) {
|
||||
if (boost::algorithm::iequals(boost::algorithm::trim_copy(declared), type))
|
||||
return true;
|
||||
if (normalize_qidi_material_family(declared) == category)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear_ams_caps(const std::string& dev_id)
|
||||
{
|
||||
if (dev_id.empty())
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
|
||||
g_ams_caps.erase(dev_id);
|
||||
}
|
||||
|
||||
void build_ams_payload_for_device(const std::string& dev_id,
|
||||
const std::optional<std::string>& printer_type,
|
||||
int ams_count,
|
||||
int max_lane_index,
|
||||
const std::vector<AmsTrayData>& trays,
|
||||
const QueueOnMainFn& queue_fn,
|
||||
const TrayInfoResolver& vendor_resolver)
|
||||
{
|
||||
// A caller on the GUI thread must mutate DeviceManager inline: invoking
|
||||
// queue_fn (CallAfter) would defer the work until after the caller has
|
||||
// already read DevFilaSystem. Background callers route through queue_fn.
|
||||
const bool on_main = wxIsMainThread();
|
||||
auto apply = [dev_id, printer_type, ams_count, max_lane_index, trays, vendor_resolver]() {
|
||||
// Look up MachineObject via DeviceManager
|
||||
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
|
||||
if (!dev_manager) {
|
||||
return;
|
||||
}
|
||||
MachineObject* obj = dev_manager->get_my_machine(dev_id);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve preset ids here: reads the GUI preset bundle, which is only safe
|
||||
// on the main thread. trays is a copy, so mutating it is fine. The optional
|
||||
// vendor resolver runs first; the generic resolver fills the rest.
|
||||
std::vector<AmsTrayData> resolved = trays;
|
||||
if (vendor_resolver)
|
||||
vendor_resolver(resolved);
|
||||
resolve_tray_info_idx(resolved);
|
||||
|
||||
// Wrap in the expected structure for ParseV1_0
|
||||
nlohmann::json print_json = nlohmann::json::object();
|
||||
print_json["ams"] = build_bbl_ams_json(resolved, ams_count, max_lane_index);
|
||||
|
||||
// Call the parser to populate DevFilaSystem
|
||||
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
|
||||
BOOST_LOG_TRIVIAL(info) << "AmsPayload: parsed " << resolved.size() << " trays";
|
||||
|
||||
// Set printer_type so update_sync_status() can match it against the preset's
|
||||
// printer type. nullopt = leave it (the caller's push_status already carries
|
||||
// it); a value = assign, including empty, preserving the old behavior.
|
||||
if (printer_type.has_value()) {
|
||||
obj->printer_type = *printer_type;
|
||||
}
|
||||
|
||||
// Set push counters so is_info_ready() returns true for pull-mode agents.
|
||||
if (obj->m_push_count == 0) {
|
||||
obj->m_push_count = 1;
|
||||
}
|
||||
if (obj->m_full_msg_count == 0) {
|
||||
obj->m_full_msg_count = 1;
|
||||
}
|
||||
obj->last_push_time = std::chrono::system_clock::now();
|
||||
|
||||
// Set storage state - Moonraker printers use virtual_sdcard, storage is always available.
|
||||
// This is required for SelectMachineDialog to allow printing (otherwise it blocks with "No SD card").
|
||||
obj->GetStorage()->set_sdcard_state(DevStorage::HAS_SDCARD_NORMAL);
|
||||
|
||||
// Populate module_vers so is_info_ready() passes the version check.
|
||||
if (obj->module_vers.empty()) {
|
||||
DevFirmwareVersionInfo ota_info;
|
||||
ota_info.name = "ota";
|
||||
ota_info.sw_ver = "1.0.0"; // Placeholder version
|
||||
obj->module_vers.emplace("ota", ota_info);
|
||||
}
|
||||
};
|
||||
|
||||
if (queue_fn && !on_main) {
|
||||
queue_fn(apply);
|
||||
} else {
|
||||
apply();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,142 @@
|
||||
#ifndef __AMS_PAYLOAD_HPP__
|
||||
#define __AMS_PAYLOAD_HPP__
|
||||
|
||||
#include "bambu_networking.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// One lane of a multi-material system in the neutral shape every printer agent
|
||||
// shares before it is rendered into the BBL AMS payload.
|
||||
struct AmsTrayData {
|
||||
int slot_index = 0; // 0-based global slot index
|
||||
bool has_filament = false;
|
||||
std::string tray_type; // Material type (e.g. "PLA", "ASA")
|
||||
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
|
||||
std::string tray_info_idx; // OrcaFilamentLibrary preset id (optional)
|
||||
int bed_temp = 0; // Optional
|
||||
int nozzle_temp = 0; // Optional
|
||||
};
|
||||
|
||||
// Normalize a driver color string to the BBL RRGGBBAA form; "00000000" when invalid.
|
||||
std::string normalize_ams_color(const std::string& color);
|
||||
|
||||
// Map a reported material type to an OrcaFilamentLibrary generic family id.
|
||||
std::string map_filament_type_to_generic_id(const std::string& filament_type);
|
||||
|
||||
// Parse a Moonraker `/server/database/item?namespace=lane_data` response body:
|
||||
// result.value is keyed by lane, each entry carrying "lane", optional
|
||||
// "material"/"color"/"bed_temp"/"nozzle_temp" and an optional presence signal
|
||||
// ("has_filament" or "loaded"). Presence is preferred over material: a loaded
|
||||
// lane with no declared material must not read as empty. Returns false when
|
||||
// malformed or empty. Pure: tray_info_idx is left for resolve_tray_info_idx().
|
||||
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.
|
||||
void resolve_tray_info_idx(std::vector<AmsTrayData>& trays);
|
||||
|
||||
// Optional vendor-aware resolver, run on the main thread before the generic
|
||||
// resolver. Agents with brand/color matching (Snapmaker, Creality) supply one
|
||||
// so their results survive the shared path; the generic resolver only fills
|
||||
// ids this leaves empty.
|
||||
using TrayInfoResolver = std::function<void(std::vector<AmsTrayData>&)>;
|
||||
|
||||
// Assemble the BBL-format AMS JSON (ams[] units + ams_exist_bits/tray_exist_bits)
|
||||
// that DevFilaSystemParser::ParseV1_0 consumes. Pure and GUI-free: the
|
||||
// MachineObject mutation stays in build_ams_payload_for_device.
|
||||
nlohmann::json build_bbl_ams_json(const std::vector<AmsTrayData>& trays,
|
||||
int ams_count,
|
||||
int max_lane_index);
|
||||
|
||||
// Render trays into the BBL AMS payload and populate the device's DevFilaSystem.
|
||||
// The resolver and the MachineObject mutation both run on the main thread via
|
||||
// queue_fn when set. printer_type: nullopt leaves obj->printer_type untouched
|
||||
// (OrcaSonar, whose push_status already carries it); a value assigns it
|
||||
// verbatim (Moonraker).
|
||||
void build_ams_payload_for_device(const std::string& dev_id,
|
||||
const std::optional<std::string>& printer_type,
|
||||
int ams_count,
|
||||
int max_lane_index,
|
||||
const std::vector<AmsTrayData>& trays,
|
||||
const QueueOnMainFn& queue_fn,
|
||||
const TrayInfoResolver& vendor_resolver = {});
|
||||
|
||||
// Process-wide AMS capabilities parsed from info.get_capabilities. Before a
|
||||
// reply, writes are permissive; afterward material operations require fms and
|
||||
// their ams_ops token, while filament_setting uses filament_slots alone.
|
||||
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);
|
||||
|
||||
// Whether the device has a material system, from the get_capabilities reply's
|
||||
// protocol.features.fms (falling back to a non-empty ams_ops). No record reads
|
||||
// false: an unconfirmed printer must not advertise filament sync.
|
||||
void register_ams_capability(const std::string& dev_id, bool has_ams);
|
||||
bool has_ams_capability(const std::string& dev_id);
|
||||
|
||||
// Whether a valid protocol capability reply has been registered. Also separates
|
||||
// the permissive pre-reply write behavior from an explicit no-FMS declaration.
|
||||
bool ams_caps_known(const std::string& dev_id);
|
||||
|
||||
// Whether the device exposes the filament-slot model, from the
|
||||
// get_capabilities reply's protocol.features.filament_slots. The slot model is
|
||||
// connector state, independent of fms: a printer with no material hardware
|
||||
// still has slots, so this alone enables filament sync (REQ-FMS-001).
|
||||
void register_filament_slots(const std::string& dev_id, bool has_slots);
|
||||
bool has_filament_slots(const std::string& dev_id);
|
||||
|
||||
// The values a fixed-vocabulary printer can hold for filament metadata, from
|
||||
// protocol.filament_metadata.<driver>.values. Materials are the strings the
|
||||
// status projection reports and colors are RRGGBBAA. `constrained` is true only
|
||||
// when every writing driver declared values: one free-form driver keeps the
|
||||
// full picker available.
|
||||
struct FilamentMetadataValues
|
||||
{
|
||||
bool constrained = false;
|
||||
std::vector<std::string> materials;
|
||||
std::vector<std::string> colors;
|
||||
};
|
||||
void register_filament_metadata(const std::string& dev_id, const FilamentMetadataValues& values);
|
||||
FilamentMetadataValues filament_metadata_values(const std::string& dev_id);
|
||||
std::string normalize_qidi_material_family(const std::string& material);
|
||||
// Whether a preset's filament type can be represented by the device's declared
|
||||
// materials: an exact type, or the material category the two names share. An
|
||||
// unconstrained or unanswered device accepts everything, and a type the reply
|
||||
// never carried is kept rather than hidden.
|
||||
bool filament_material_compatible(const std::string& dev_id, const std::string& preset_type);
|
||||
|
||||
// Forget a device's declared capabilities, so a reconnect starts from "no
|
||||
// reply yet" instead of a stale declaration.
|
||||
void clear_ams_caps(const std::string& dev_id);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -150,11 +150,26 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
|
||||
// Communication
|
||||
// ============================================================================
|
||||
|
||||
std::string BBLPrinterAgent::ams_refresh_rfid_gcode(const std::string& slot_id)
|
||||
{
|
||||
return (boost::format("M620 R%1% \n") % slot_id).str();
|
||||
}
|
||||
|
||||
std::string BBLPrinterAgent::ams_calibrate_gcode(int ams_id)
|
||||
{
|
||||
return (boost::format("M620 C%1% \n") % ams_id).str();
|
||||
}
|
||||
|
||||
std::string BBLPrinterAgent::ams_select_tray_gcode(const std::string& tray_id)
|
||||
{
|
||||
return (boost::format("M620 P%1% \n") % tray_id).str();
|
||||
}
|
||||
|
||||
int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
nlohmann::json j;
|
||||
if (ams_id == -1) {
|
||||
const std::string gcode = (boost::format("M620 R%1% \n") % slot_id).str();
|
||||
const std::string gcode = ams_refresh_rfid_gcode(std::to_string(slot_id));
|
||||
j["print"]["command"] = "gcode_line";
|
||||
j["print"]["param"] = gcode;
|
||||
j["print"]["sequence_id"] = std::to_string(sequence_id);
|
||||
@@ -170,7 +185,7 @@ int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, in
|
||||
|
||||
int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
const std::string gcode = (boost::format("M620 C%1% \n") % ams_id).str();
|
||||
const std::string gcode = ams_calibrate_gcode(ams_id);
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "gcode_line";
|
||||
j["print"]["param"] = gcode;
|
||||
@@ -180,7 +195,7 @@ int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int s
|
||||
|
||||
int BBLPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
const std::string gcode = (boost::format("M620 P%1% \n") % tray_id).str();
|
||||
const std::string gcode = ams_select_tray_gcode(tray_id);
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "gcode_line";
|
||||
j["print"]["param"] = gcode;
|
||||
|
||||
@@ -32,6 +32,9 @@ public:
|
||||
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_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;
|
||||
static std::string ams_refresh_rfid_gcode(const std::string& slot_id);
|
||||
static std::string ams_calibrate_gcode(int ams_id);
|
||||
static std::string ams_select_tray_gcode(const std::string& tray_id);
|
||||
int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
|
||||
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode) override;
|
||||
std::string default_lan_username() const override { return "bblp"; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "Http.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
@@ -134,7 +133,7 @@ struct Http::priv
|
||||
std::string error_buffer; // Used for CURLOPT_ERRORBUFFER
|
||||
std::string headers;
|
||||
size_t limit;
|
||||
std::atomic_bool cancel;
|
||||
bool cancel;
|
||||
std::unique_ptr<form_file> putFile;
|
||||
|
||||
std::thread io_thread;
|
||||
@@ -272,9 +271,9 @@ int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_o
|
||||
self->progressfn(progress, cb_cancel);
|
||||
}
|
||||
|
||||
if (cb_cancel) { self->cancel.store(true); }
|
||||
if (cb_cancel) { self->cancel = true; }
|
||||
|
||||
return self->cancel.load();
|
||||
return self->cancel;
|
||||
}
|
||||
|
||||
int Http::priv::xfercb_legacy(void *userp, double dltotal, double dlnow, double ultotal, double ulnow)
|
||||
@@ -485,7 +484,7 @@ void Http::priv::http_perform()
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
if (res == CURLE_ABORTED_BY_CALLBACK) {
|
||||
if (cancel.load()) {
|
||||
if (cancel) {
|
||||
// The abort comes from the request being cancelled programatically
|
||||
Progress dummyprogress(0, 0, 0, 0, std::string());
|
||||
bool cancel = true;
|
||||
@@ -796,7 +795,7 @@ void Http::perform_sync()
|
||||
|
||||
void Http::cancel()
|
||||
{
|
||||
if (p) { p->cancel.store(true); }
|
||||
if (p) { p->cancel = true; }
|
||||
}
|
||||
|
||||
void Http::print() const
|
||||
|
||||
@@ -38,25 +38,12 @@ wxString Moonraker::get_test_failed_msg(wxString &msg) const
|
||||
|
||||
std::string Moonraker::make_url(const std::string &path) const
|
||||
{
|
||||
const bool has_scheme = m_host.find("http://") == 0 || m_host.find("https://") == 0;
|
||||
const bool use_ssl = m_host.find("https://") == 0;
|
||||
std::string base = has_scheme ? m_host : ("http://" + m_host);
|
||||
|
||||
const size_t authority_start = base.find("://") + 3;
|
||||
const size_t authority_end = base.find('/', authority_start);
|
||||
const std::string authority = base.substr(authority_start, authority_end - authority_start);
|
||||
const size_t closing_bracket = authority.rfind(']');
|
||||
const bool has_port = closing_bracket != std::string::npos
|
||||
? closing_bracket + 1 < authority.size() && authority[closing_bracket + 1] == ':'
|
||||
: authority.find(':') != std::string::npos;
|
||||
if (!has_port) {
|
||||
const std::string default_port = use_ssl ? ":7130" : ":7125";
|
||||
base.insert(authority_end == std::string::npos ? base.size() : authority_end, default_port);
|
||||
if (m_host.find("http://") == 0 || m_host.find("https://") == 0) {
|
||||
if (m_host.back() == '/')
|
||||
return (boost::format("%1%%2%") % m_host % path).str();
|
||||
return (boost::format("%1%/%2%") % m_host % path).str();
|
||||
}
|
||||
|
||||
if (base.back() == '/')
|
||||
return (boost::format("%1%%2%") % base % path).str();
|
||||
return (boost::format("%1%/%2%") % base % path).str();
|
||||
return (boost::format("http://%1%/%2%") % m_host % path).str();
|
||||
}
|
||||
|
||||
void Moonraker::set_auth(Http &http) const
|
||||
@@ -66,8 +53,6 @@ void Moonraker::set_auth(Http &http) const
|
||||
// filled the user/password fields — those are PrusaLink/OctoPrint conventions.
|
||||
if (!m_apikey.empty())
|
||||
http.header("X-Api-Key", m_apikey);
|
||||
const bool use_ssl = m_host.find("https://") == 0;
|
||||
http.tls_verify(use_ssl);
|
||||
if (!m_cafile.empty())
|
||||
http.ca_file(m_cafile);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
#include "ICloudServiceAgent.hpp"
|
||||
#include "AmsPayload.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -18,8 +19,6 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class Http;
|
||||
|
||||
bool moonraker_is_light_name(const std::string& name);
|
||||
|
||||
class MoonrakerWebsocket
|
||||
@@ -33,7 +32,7 @@ public:
|
||||
error,
|
||||
};
|
||||
|
||||
MoonrakerWebsocket(bool secure, std::string api_key, std::string ca_file);
|
||||
MoonrakerWebsocket(bool secure, std::string api_key);
|
||||
~MoonrakerWebsocket();
|
||||
|
||||
void connect(const std::string& host, const std::string& port, std::chrono::seconds timeout);
|
||||
@@ -101,15 +100,6 @@ public:
|
||||
std::string get_camera_url() const override;
|
||||
|
||||
protected:
|
||||
struct ConnectionSettings
|
||||
{
|
||||
std::string dev_id;
|
||||
std::string base_url;
|
||||
std::string api_key;
|
||||
bool use_ssl = false;
|
||||
std::string ca_file;
|
||||
};
|
||||
|
||||
struct MoonrakerDeviceInfo
|
||||
{
|
||||
std::string dev_id;
|
||||
@@ -123,28 +113,18 @@ protected:
|
||||
std::string klippy_state;
|
||||
float nozzle_diameter = 0.0f;
|
||||
bool use_ssl = false;
|
||||
std::string ca_file;
|
||||
} device_info;
|
||||
|
||||
// Tray data for AMS payload building
|
||||
struct AmsTrayData {
|
||||
int slot_index = 0; // 0-based slot index
|
||||
bool has_filament = false;
|
||||
std::string tray_type; // Material type (e.g., "PLA", "ASA")
|
||||
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
|
||||
std::string tray_info_idx; // Setting ID (optional)
|
||||
int bed_temp = 0; // Optional
|
||||
int nozzle_temp = 0; // Optional
|
||||
};
|
||||
|
||||
// Build ams JSON and call parser
|
||||
// Build ams JSON and call parser (AmsTrayData is shared; see AmsPayload.hpp).
|
||||
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
|
||||
// Overload for agents with vendor-aware matching (Snapmaker): the resolver
|
||||
// runs on the main thread inside the shared builder, preserving the match
|
||||
// without touching preset state on the fetch worker.
|
||||
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays, const TrayInfoResolver& vendor_resolver);
|
||||
|
||||
// Methods that derived classes may need to override or access
|
||||
virtual bool init_device_info(const PrinterConnectionParams& params);
|
||||
virtual bool fetch_device_info(const ConnectionSettings& connection, MoonrakerDeviceInfo& info, std::string& error) const;
|
||||
ConnectionSettings get_connection_settings() const;
|
||||
void configure_http(Http& http, const ConnectionSettings& connection) const;
|
||||
virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port);
|
||||
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const;
|
||||
static float parse_nozzle_diameter(const nlohmann::json& response);
|
||||
|
||||
// State access for derived classes
|
||||
@@ -165,16 +145,13 @@ protected:
|
||||
// Trim whitespace and convert to uppercase
|
||||
static std::string trim_and_upper(const std::string& input);
|
||||
|
||||
// Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility
|
||||
static std::string map_filament_type_to_generic_id(const std::string& filament_type);
|
||||
|
||||
// Send a G-code script via Moonraker (/printer/gcode/script)
|
||||
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
|
||||
bool send_gcode(const std::string& dev_id, const std::string& gcode,
|
||||
const ConnectionSettings& connection) const;
|
||||
const std::string& base_url, const std::string& api_key) const;
|
||||
bool post_print_action(const std::string& action) const;
|
||||
bool post_print_action(const std::string& action,
|
||||
const ConnectionSettings& connection) const;
|
||||
const std::string& base_url, const std::string& api_key) const;
|
||||
|
||||
bool send_ws_rpc(const std::string& method, const nlohmann::json& params);
|
||||
|
||||
@@ -189,8 +166,8 @@ private:
|
||||
int send_version_info(const std::string& dev_id);
|
||||
int send_access_code(const std::string& dev_id);
|
||||
|
||||
bool fetch_object_list(const ConnectionSettings& connection, std::set<std::string>& objects, std::string& error) const;
|
||||
bool query_printer_status(const ConnectionSettings& connection, nlohmann::json& status, std::string& error) const;
|
||||
bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set<std::string>& objects, std::string& error) const;
|
||||
bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const;
|
||||
bool send_gcode_sync(const std::string& dev_id, const std::string& gcode) const;
|
||||
void send_gcode_async(const std::string& dev_id, const std::string& gcode,
|
||||
std::function<void(bool)> on_result = {}) const;
|
||||
@@ -199,11 +176,11 @@ private:
|
||||
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg);
|
||||
void dispatch_printer_connected(const std::string& dev_id);
|
||||
void dispatch_message(const std::string& dev_id, const std::string& payload);
|
||||
void start_status_stream(const std::string& dev_id, ConnectionSettings connection);
|
||||
void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key);
|
||||
void stop_status_stream();
|
||||
void run_status_stream(std::string dev_id, ConnectionSettings connection);
|
||||
void handle_ws_message(std::string dev_id, std::string payload, ConnectionSettings connection);
|
||||
void refresh_thumbnail_url(const ConnectionSettings& connection);
|
||||
void run_status_stream(std::string dev_id, std::string base_url, std::string api_key);
|
||||
void handle_ws_message(std::string dev_id, std::string payload, std::string base_url, std::string api_key);
|
||||
void refresh_thumbnail_url(std::string base_url, std::string api_key);
|
||||
void update_status_cache(const nlohmann::json& updates);
|
||||
nlohmann::json build_print_payload_locked() const;
|
||||
|
||||
@@ -214,35 +191,33 @@ private:
|
||||
|
||||
// File upload
|
||||
bool upload_gcode(const std::string& local_path, const std::string& filename,
|
||||
const ConnectionSettings& connection,
|
||||
const std::string& base_url, const std::string& api_key,
|
||||
OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
|
||||
|
||||
// Start a print of a previously uploaded G-code file (path relative to the
|
||||
// Moonraker gcodes root).
|
||||
bool start_print_file(const ConnectionSettings& connection,
|
||||
bool start_print_file(const std::string& base_url, const std::string& api_key,
|
||||
const std::string& filename, std::string& error_msg) const;
|
||||
|
||||
// Connection thread management
|
||||
void perform_connection_async(const std::string& dev_id,
|
||||
ConnectionSettings connection,
|
||||
const std::string& base_url,
|
||||
const std::string& api_key,
|
||||
uint64_t generation);
|
||||
|
||||
// why: a printer with no /server/webcams/list entry can still name its stream directly;
|
||||
// subclasses (e.g. printers with a fixed webcam path) can override this instead.
|
||||
virtual std::string webcam_stream_override(const std::string& base_url) const { return {}; }
|
||||
void refresh_webcam_info() const;
|
||||
bool fetch_webcam_info(const ConnectionSettings& connection, uint64_t generation) const;
|
||||
bool fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation) const;
|
||||
|
||||
// System-specific filament fetch methods
|
||||
bool fetch_hh_filament_info(const ConnectionSettings& connection, std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
bool fetch_moonraker_filament_data(const ConnectionSettings& connection, std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
|
||||
// JSON helper methods
|
||||
static std::string safe_json_string(const nlohmann::json& obj, const char* key);
|
||||
static int safe_json_int(const nlohmann::json& obj, const char* key);
|
||||
static std::string safe_array_string(const nlohmann::json& arr, int idx);
|
||||
static int safe_array_int(const nlohmann::json& arr, int idx);
|
||||
static std::string normalize_color_value(const std::string& color);
|
||||
|
||||
std::string ssdp_announced_host;
|
||||
std::string ssdp_announced_id;
|
||||
|
||||
@@ -17,14 +17,6 @@ namespace Slic3r {
|
||||
static constexpr char ORCA_PRINTER_AGENT_ID[] = "orca";
|
||||
static constexpr char BBL_PRINTER_AGENT_ID[] = "bbl";
|
||||
|
||||
// Index-correlation merge gate (plan PR 3). The per-print `filament_mapping`
|
||||
// serializer is shipped disabled until a slice-level test proves that the
|
||||
// `ams_mapping2` array position equals the toolchange identifier the generated
|
||||
// G-code emits (T<filament_id> / next_filament_id). Both the GUI send gates and
|
||||
// OrcaPrinterAgent::start_sdcard_print consult this, so mapping is refused
|
||||
// visibly rather than silently dropped. Flip to true only with that test.
|
||||
static constexpr bool ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED = false;
|
||||
|
||||
// Factory function type for creating printer agents
|
||||
using PrinterAgentFactory =
|
||||
std::function<std::shared_ptr<IPrinterAgent>(std::shared_ptr<ICloudServiceAgent> cloud_agent, const std::string& log_dir)>;
|
||||
|
||||
@@ -163,8 +163,6 @@ public:
|
||||
|
||||
// Configuration
|
||||
void configure_urls(AppConfig* app_config);
|
||||
// Hostname only; cloud REST, MQTT, and WebRTC signaling use fixed TLS
|
||||
// endpoints on port 443.
|
||||
void set_api_base_url(const std::string& url);
|
||||
void set_auth_base_url(const std::string& url);
|
||||
void set_cloud_base_url(const std::string& url);
|
||||
|
||||
@@ -42,16 +42,10 @@ void OrcaCloudSignalingChannel::close()
|
||||
{
|
||||
m_stop.store(true);
|
||||
std::shared_ptr<Connection> conn;
|
||||
Http::Ptr request;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
conn = m_conn;
|
||||
request = m_inflight_requests;
|
||||
}
|
||||
// m_conn is installed only after the live-token request completes, so
|
||||
// cancel the request independently before waiting for the worker.
|
||||
if (request)
|
||||
request->cancel();
|
||||
if (conn) {
|
||||
// Established session: close the socket on the io_context's own thread so
|
||||
// the pending async_read completes and io_context.run() unwinds.
|
||||
@@ -97,6 +91,17 @@ std::string OrcaCloudSignalingChannel::encode_path_component(const std::string&
|
||||
return encoded.str();
|
||||
}
|
||||
|
||||
std::string OrcaCloudSignalingChannel::host_without_scheme(std::string value)
|
||||
{
|
||||
const auto scheme = value.find("://");
|
||||
if (scheme != std::string::npos)
|
||||
value.erase(0, scheme + 3);
|
||||
const auto slash = value.find('/');
|
||||
if (slash != std::string::npos)
|
||||
value.erase(slash);
|
||||
return value;
|
||||
}
|
||||
|
||||
void OrcaCloudSignalingChannel::unavailable(CameraUnavailableReason reason, std::string detail)
|
||||
{
|
||||
if (on_unavailable)
|
||||
@@ -112,10 +117,7 @@ void OrcaCloudSignalingChannel::run()
|
||||
return;
|
||||
}
|
||||
const std::string token = m_cloud->get_access_token();
|
||||
// OrcaCloud exposes a bare API hostname. WebRTC signaling uses the
|
||||
// fixed HTTPS/WSS endpoints on port 443; custom schemes, ports, and
|
||||
// base paths are not supported by this agent.
|
||||
const std::string host = m_cloud->get_cloud_service_host();
|
||||
const std::string host = host_without_scheme(m_cloud->get_cloud_service_host());
|
||||
if (token.empty() || host.empty()) {
|
||||
unavailable(CameraUnavailableReason::Error, "OrcaCloud session is unavailable");
|
||||
m_open.store(false);
|
||||
@@ -129,9 +131,8 @@ void OrcaCloudSignalingChannel::run()
|
||||
nlohmann::json token_response;
|
||||
std::string token_body;
|
||||
unsigned int http_code = 0;
|
||||
|
||||
auto request = std::make_shared<Slic3r::Http>(Http::post(live_token_url));
|
||||
request->set_post_body(std::string("{}"))
|
||||
auto request = Http::post(live_token_url);
|
||||
request.set_post_body(std::string("{}"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.tls_verify(true)
|
||||
@@ -142,32 +143,8 @@ void OrcaCloudSignalingChannel::run()
|
||||
})
|
||||
.on_error([&http_code](std::string, std::string, unsigned status) {
|
||||
http_code = status;
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_inflight_requests = request;
|
||||
}
|
||||
|
||||
// If close() raced with request setup, make sure this request is
|
||||
// cancelled before entering the blocking call.
|
||||
if (m_stop.load())
|
||||
request->cancel();
|
||||
try {
|
||||
request->perform_sync();
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_inflight_requests == request)
|
||||
m_inflight_requests.reset();
|
||||
throw;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_inflight_requests == request)
|
||||
m_inflight_requests.reset();
|
||||
}
|
||||
|
||||
})
|
||||
.perform_sync();
|
||||
try {
|
||||
token_response = nlohmann::json::parse(token_body);
|
||||
} catch (const std::exception&) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "ICameraSignalingChannel.hpp"
|
||||
#include "ICloudServiceAgent.hpp"
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "Http.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
@@ -50,11 +50,9 @@ private:
|
||||
void send_json(const std::string& message);
|
||||
void unavailable(CameraUnavailableReason reason, std::string detail);
|
||||
static std::string encode_path_component(const std::string& value);
|
||||
static std::string host_without_scheme(std::string value);
|
||||
|
||||
OrcaCloudServiceAgent* m_cloud;
|
||||
|
||||
Http::Ptr m_inflight_requests{nullptr};
|
||||
|
||||
std::string m_dev_id;
|
||||
std::atomic<bool> m_stop{false};
|
||||
std::atomic<bool> m_open{false};
|
||||
|
||||
@@ -23,12 +23,6 @@ struct OrcaMqttConnection::Connection {
|
||||
boost::asio::io_context io_context;
|
||||
boost::asio::ssl::context ssl_context;
|
||||
boost::asio::ip::tcp::resolver resolver;
|
||||
boost::asio::steady_timer keepalive_timer;
|
||||
boost::beast::flat_buffer read_buffer;
|
||||
std::deque<std::shared_ptr<std::vector<uint8_t>>> outbound_packets;
|
||||
boost::system::error_code terminal_error;
|
||||
std::atomic_bool async_session_started{false};
|
||||
bool write_in_progress{false};
|
||||
// Exactly one of these is engaged once ws_handshake() has run: wss for
|
||||
// wss:// endpoints, ws for plaintext ws://.
|
||||
std::optional<TlsWebSocket> wss;
|
||||
@@ -37,7 +31,6 @@ struct OrcaMqttConnection::Connection {
|
||||
Connection()
|
||||
: ssl_context(boost::asio::ssl::context::tls_client)
|
||||
, resolver(io_context)
|
||||
, keepalive_timer(io_context)
|
||||
{}
|
||||
};
|
||||
|
||||
@@ -88,31 +81,19 @@ void OrcaMqttConnection::stop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
if (active_connection) {
|
||||
if (active_connection->async_session_started.load()) {
|
||||
// The worker owns the live WebSocket. Stop dispatching its
|
||||
// asynchronous operations; the worker closes the socket after
|
||||
// leaving the event loop.
|
||||
active_connection->io_context.stop();
|
||||
} else {
|
||||
// Setup still uses synchronous operations on the worker. Wake a
|
||||
// pending resolve/connect/CONNACK read without competing with a
|
||||
// live asynchronous session.
|
||||
auto shutdown_socket = [](auto& websocket) {
|
||||
auto& socket = boost::beast::get_lowest_layer(websocket).socket();
|
||||
boost::system::error_code socket_error;
|
||||
if (socket.cancel(socket_error))
|
||||
return;
|
||||
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error))
|
||||
return;
|
||||
if (socket.close(socket_error))
|
||||
return;
|
||||
};
|
||||
if (active_connection->wss)
|
||||
shutdown_socket(*active_connection->wss);
|
||||
else if (active_connection->ws)
|
||||
shutdown_socket(*active_connection->ws);
|
||||
active_connection->resolver.cancel();
|
||||
}
|
||||
// Generic so it accepts either the TLS or the plaintext websocket.
|
||||
auto shutdown_socket = [](auto& websocket) {
|
||||
auto& socket = boost::beast::get_lowest_layer(websocket).socket();
|
||||
boost::system::error_code socket_error;
|
||||
socket.cancel(socket_error);
|
||||
socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error);
|
||||
socket.close(socket_error);
|
||||
};
|
||||
if (active_connection->wss)
|
||||
shutdown_socket(*active_connection->wss);
|
||||
else if (active_connection->ws)
|
||||
shutdown_socket(*active_connection->ws);
|
||||
active_connection->resolver.cancel();
|
||||
}
|
||||
}
|
||||
if (worker.joinable())
|
||||
@@ -149,10 +130,13 @@ void OrcaMqttConnection::flush_subscription_change() {
|
||||
}
|
||||
if (!conn || !connacked)
|
||||
return; // no live MQTT session yet — the worker sends the set on CONNACK
|
||||
boost::asio::post(conn->io_context, [this, conn] {
|
||||
if (!stopping.load() && connected.load())
|
||||
send_pending_subscriptions(conn);
|
||||
});
|
||||
|
||||
// beast permits a concurrent writer while the worker is blocked in
|
||||
// websocket.read(); every write is serialised by write_mutex inside ws_write().
|
||||
try {
|
||||
send_pending_subscriptions(*conn);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
}
|
||||
|
||||
bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
|
||||
@@ -173,7 +157,7 @@ bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
|
||||
pending_subscriptions.insert(topic);
|
||||
}
|
||||
state_cv.notify_all();
|
||||
flush_subscription_change(); // ask the worker to emit SUBSCRIBE now (no reconnect)
|
||||
flush_subscription_change(); // emit SUBSCRIBE now on the live socket (no reconnect)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -199,7 +183,7 @@ bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) {
|
||||
}
|
||||
}
|
||||
state_cv.notify_all();
|
||||
flush_subscription_change(); // ask the worker to emit UNSUBSCRIBE now (no reconnect)
|
||||
flush_subscription_change(); // emit UNSUBSCRIBE now on the live socket (no reconnect)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -314,6 +298,10 @@ void OrcaMqttConnection::ws_write(Connection& conn, const std::vector<uint8_t>&
|
||||
if (packet.empty()) {
|
||||
return;
|
||||
}
|
||||
// Writes come from the worker thread AND, for dynamic (un)subscribes, the
|
||||
// caller thread. Serialise them; the worker's concurrent read is fine (beast
|
||||
// allows one reader + one writer).
|
||||
std::lock_guard<std::mutex> lock(write_mutex);
|
||||
if (conn.wss) {
|
||||
conn.wss->binary(true);
|
||||
conn.wss->write(boost::asio::buffer(packet));
|
||||
@@ -323,110 +311,6 @@ void OrcaMqttConnection::ws_write(Connection& conn, const std::vector<uint8_t>&
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::close_connection(Connection& conn) {
|
||||
boost::system::error_code error;
|
||||
if (conn.wss) {
|
||||
auto& socket = boost::beast::get_lowest_layer(*conn.wss).socket();
|
||||
if (socket.cancel(error))
|
||||
return;
|
||||
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
|
||||
return;
|
||||
if (socket.close(error))
|
||||
return;
|
||||
} else if (conn.ws) {
|
||||
auto& socket = boost::beast::get_lowest_layer(*conn.ws).socket();
|
||||
if (socket.cancel(error))
|
||||
return;
|
||||
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
|
||||
return;
|
||||
if (socket.close(error))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::enqueue_packet(const std::shared_ptr<Connection>& conn,
|
||||
std::vector<uint8_t> packet) {
|
||||
if (!conn || packet.empty())
|
||||
return;
|
||||
conn->outbound_packets.emplace_back(std::make_shared<std::vector<uint8_t>>(std::move(packet)));
|
||||
start_async_write(conn);
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::start_async_write(const std::shared_ptr<Connection>& conn) {
|
||||
if (!conn || conn->write_in_progress || conn->outbound_packets.empty() || stopping.load())
|
||||
return;
|
||||
|
||||
conn->write_in_progress = true;
|
||||
const auto packet = conn->outbound_packets.front();
|
||||
auto on_write = [this, conn](const boost::system::error_code& error, std::size_t) {
|
||||
conn->write_in_progress = false;
|
||||
if (error) {
|
||||
if (!stopping.load())
|
||||
conn->terminal_error = error;
|
||||
conn->io_context.stop();
|
||||
return;
|
||||
}
|
||||
conn->outbound_packets.pop_front();
|
||||
start_async_write(conn);
|
||||
};
|
||||
if (conn->wss) {
|
||||
conn->wss->binary(true);
|
||||
conn->wss->async_write(boost::asio::buffer(*packet), std::move(on_write));
|
||||
} else if (conn->ws) {
|
||||
conn->ws->binary(true);
|
||||
conn->ws->async_write(boost::asio::buffer(*packet), std::move(on_write));
|
||||
} else {
|
||||
conn->write_in_progress = false;
|
||||
conn->outbound_packets.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::start_async_read(const std::shared_ptr<Connection>& conn) {
|
||||
if (!conn || stopping.load())
|
||||
return;
|
||||
|
||||
auto on_read = [this, conn](const boost::system::error_code& error, std::size_t) {
|
||||
if (error) {
|
||||
if (!stopping.load())
|
||||
conn->terminal_error = error;
|
||||
conn->io_context.stop();
|
||||
return;
|
||||
}
|
||||
const std::string packet = boost::beast::buffers_to_string(conn->read_buffer.data());
|
||||
conn->read_buffer.consume(conn->read_buffer.size());
|
||||
handle_packet(packet);
|
||||
start_async_read(conn);
|
||||
};
|
||||
if (conn->wss)
|
||||
conn->wss->async_read(conn->read_buffer, std::move(on_read));
|
||||
else if (conn->ws)
|
||||
conn->ws->async_read(conn->read_buffer, std::move(on_read));
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::schedule_keepalive(const std::shared_ptr<Connection>& conn) {
|
||||
const int keepalive = current_config.keepalive_seconds;
|
||||
if (!conn || keepalive <= 0 || stopping.load())
|
||||
return;
|
||||
|
||||
conn->keepalive_timer.expires_after(std::chrono::seconds(std::max(1, keepalive / 2)));
|
||||
conn->keepalive_timer.async_wait([this, conn](const boost::system::error_code& error) {
|
||||
if (error || stopping.load())
|
||||
return;
|
||||
enqueue_packet(conn, make_ping_packet());
|
||||
schedule_keepalive(conn);
|
||||
});
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::post_packet(const std::shared_ptr<Connection>& conn,
|
||||
std::vector<uint8_t> packet) {
|
||||
if (!conn || packet.empty())
|
||||
return;
|
||||
boost::asio::post(conn->io_context, [this, conn, packet = std::move(packet)]() mutable {
|
||||
if (!stopping.load())
|
||||
enqueue_packet(conn, std::move(packet));
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t OrcaMqttConnection::ws_read(Connection& conn, boost::beast::flat_buffer& buffer,
|
||||
boost::system::error_code& ec) {
|
||||
if (conn.wss)
|
||||
@@ -437,6 +321,14 @@ std::size_t OrcaMqttConnection::ws_read(Connection& conn, boost::beast::flat_buf
|
||||
return 0;
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::ws_close(Connection& conn) {
|
||||
boost::system::error_code close_error;
|
||||
if (conn.wss)
|
||||
conn.wss->close(boost::beast::websocket::close_code::normal, close_error);
|
||||
else if (conn.ws)
|
||||
conn.ws->close(boost::beast::websocket::close_code::normal, close_error);
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint) {
|
||||
const auto results = conn.resolver.resolve(endpoint.host, endpoint.port);
|
||||
|
||||
@@ -469,10 +361,7 @@ void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, co
|
||||
auto& tls_stream = websocket.next_layer();
|
||||
if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), endpoint.host.c_str()))
|
||||
throw std::runtime_error("failed to set Orca Cloud TLS server name");
|
||||
if (!config.ca_file.empty())
|
||||
conn.ssl_context.load_verify_file(config.ca_file);
|
||||
else
|
||||
conn.ssl_context.set_default_verify_paths();
|
||||
conn.ssl_context.set_default_verify_paths();
|
||||
Http::add_platform_root_certificates(conn.ssl_context.native_handle());
|
||||
tls_stream.set_verify_mode(boost::asio::ssl::verify_peer);
|
||||
tls_stream.set_verify_callback(boost::asio::ssl::host_name_verification(endpoint.host));
|
||||
@@ -523,7 +412,12 @@ bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::stri
|
||||
return true;
|
||||
}
|
||||
}
|
||||
post_packet(conn, make_publish_packet(request_topic(dev_id), payload));
|
||||
try {
|
||||
// ws_write() serialises the write via write_mutex; do not lock it here.
|
||||
ws_write(*conn, make_publish_packet(request_topic(dev_id), payload));
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -532,93 +426,94 @@ void OrcaMqttConnection::connect_and_read() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
active_connection = connection;
|
||||
if (stopping.load()) {
|
||||
active_connection.reset();
|
||||
if (stopping.load())
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto clear_connection = [this, connection] {
|
||||
close_connection(*connection);
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
if (active_connection == connection)
|
||||
active_connection.reset();
|
||||
};
|
||||
|
||||
try {
|
||||
Endpoint endpoint;
|
||||
if (!parse_endpoint(current_config.url, endpoint)) {
|
||||
throw std::runtime_error("invalid Orca Cloud WebSocket endpoint");
|
||||
}
|
||||
Endpoint endpoint;
|
||||
if (!parse_endpoint(current_config.url, endpoint)) {
|
||||
throw std::runtime_error("invalid Orca Cloud WebSocket endpoint");
|
||||
}
|
||||
|
||||
|
||||
ws_handshake(*connection, current_config, endpoint);
|
||||
ws_handshake(*connection, current_config, endpoint);
|
||||
|
||||
expires_never(*connection);
|
||||
// Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the
|
||||
// CONNECT username/password fields are omitted entirely (the cloud form).
|
||||
const bool use_bearer = static_cast<bool>(current_config.bearer_provider);
|
||||
ws_write(*connection, make_connect_packet(current_config.client_id,
|
||||
use_bearer ? std::string() : current_config.username,
|
||||
use_bearer ? std::string() : current_config.password,
|
||||
current_config.keepalive_seconds));
|
||||
expires_never(*connection);
|
||||
// Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the
|
||||
// CONNECT username/password fields are omitted entirely (the cloud form).
|
||||
const bool use_bearer = static_cast<bool>(current_config.bearer_provider);
|
||||
ws_write(*connection, make_connect_packet(current_config.client_id,
|
||||
use_bearer ? std::string() : current_config.username,
|
||||
use_bearer ? std::string() : current_config.password,
|
||||
current_config.keepalive_seconds));
|
||||
|
||||
boost::beast::flat_buffer buffer;
|
||||
expires_after(*connection, std::chrono::seconds(10));
|
||||
boost::system::error_code connack_error;
|
||||
ws_read(*connection, buffer, connack_error);
|
||||
if (connack_error)
|
||||
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
|
||||
const std::string connack = boost::beast::buffers_to_string(buffer.data());
|
||||
// rc: 0 accepted, 1..5 refusal, -1 malformed/not a CONNACK.
|
||||
const int rc = (connack.size() == 4 && static_cast<uint8_t>(connack[0]) == 0x20)
|
||||
? static_cast<int>(static_cast<uint8_t>(connack[3]))
|
||||
: -1;
|
||||
m_last_connack_rc.store(rc);
|
||||
if (rc != 0) {
|
||||
if (rc == 4 || rc == 5) {
|
||||
// Bad credentials / not authorized — retrying cannot help. Make run()'s
|
||||
// loop exit and unblock any waiting start().
|
||||
stopping.store(true);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
initial_completed = true;
|
||||
initial_result = false;
|
||||
}
|
||||
initial_cv.notify_all();
|
||||
boost::beast::flat_buffer buffer;
|
||||
expires_after(*connection, std::chrono::seconds(10));
|
||||
boost::system::error_code connack_error;
|
||||
ws_read(*connection, buffer, connack_error);
|
||||
if (connack_error)
|
||||
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
|
||||
const std::string connack = boost::beast::buffers_to_string(buffer.data());
|
||||
// rc: 0 accepted, 1..5 refusal, -1 malformed/not a CONNACK.
|
||||
const int rc = (connack.size() == 4 && static_cast<uint8_t>(connack[0]) == 0x20)
|
||||
? static_cast<int>(static_cast<uint8_t>(connack[3]))
|
||||
: -1;
|
||||
m_last_connack_rc.store(rc);
|
||||
if (rc != 0) {
|
||||
if (rc == 4 || rc == 5) {
|
||||
// Bad credentials / not authorized — retrying cannot help. Make run()'s
|
||||
// loop exit and unblock any waiting start().
|
||||
stopping.store(true);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
initial_completed = true;
|
||||
initial_result = false;
|
||||
}
|
||||
throw std::runtime_error("Orca MQTT CONNECT refused rc=" + std::to_string(rc));
|
||||
initial_cv.notify_all();
|
||||
}
|
||||
|
||||
// The subscription acknowledgement belongs to this MQTT session. Clear
|
||||
// the previous session's state before notifying the owner, because the
|
||||
// reconnect callback immediately queues the printer's initial requests.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
acknowledged_subscriptions.clear();
|
||||
pending_subscribe_packets.clear();
|
||||
}
|
||||
connection->async_session_started.store(true);
|
||||
notify_state(true);
|
||||
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
|
||||
send_current_subscriptions(connection);
|
||||
start_async_read(connection);
|
||||
schedule_keepalive(connection);
|
||||
const std::size_t handlers_run = connection->io_context.run();
|
||||
if (handlers_run == 0 && !stopping.load())
|
||||
throw std::runtime_error("Orca MQTT event loop stopped unexpectedly");
|
||||
if (connection->terminal_error && !stopping.load())
|
||||
throw boost::system::system_error(connection->terminal_error, "read Orca MQTT message");
|
||||
clear_connection();
|
||||
if (!stopping.load())
|
||||
notify_state(false);
|
||||
} catch (...) {
|
||||
clear_connection();
|
||||
throw;
|
||||
throw std::runtime_error("Orca MQTT CONNECT refused rc=" + std::to_string(rc));
|
||||
}
|
||||
|
||||
// The subscription acknowledgement belongs to this MQTT session. Clear
|
||||
// the previous session's state before notifying the owner, because the
|
||||
// reconnect callback immediately queues the printer's initial requests.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
acknowledged_subscriptions.clear();
|
||||
pending_subscribe_packets.clear();
|
||||
}
|
||||
notify_state(true);
|
||||
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
|
||||
send_current_subscriptions(*connection);
|
||||
std::chrono::steady_clock::time_point next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
|
||||
while (!stopping.load()) {
|
||||
send_pending_subscriptions(*connection);
|
||||
// Keepalive is driven every iteration, not only from the read-timeout branch:
|
||||
// a printer pushing faster than the 1s read deadline would otherwise keep the
|
||||
// read hot and the broker would drop us at 1.5 x keepalive.
|
||||
if (std::chrono::steady_clock::now() >= next_ping) {
|
||||
ws_write(*connection, make_ping_packet());
|
||||
next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
}
|
||||
buffer.consume(buffer.size());
|
||||
expires_after(*connection, std::chrono::seconds(1));
|
||||
boost::system::error_code error;
|
||||
ws_read(*connection, buffer, error);
|
||||
if (error == boost::beast::error::timeout)
|
||||
continue;
|
||||
if (error) {
|
||||
throw boost::system::system_error(error, "read Orca MQTT message");
|
||||
}
|
||||
handle_packet(boost::beast::buffers_to_string(buffer.data()));
|
||||
}
|
||||
|
||||
ws_close(*connection);
|
||||
if (!stopping.load())
|
||||
notify_state(false);
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::send_current_subscriptions(const std::shared_ptr<Connection>& conn) {
|
||||
void OrcaMqttConnection::send_current_subscriptions(Connection& conn) {
|
||||
std::vector<std::string> topics;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
@@ -634,11 +529,11 @@ void OrcaMqttConnection::send_current_subscriptions(const std::shared_ptr<Connec
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
pending_subscribe_packets[packet_id] = topic;
|
||||
}
|
||||
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
|
||||
ws_write(conn, make_subscribe_packet(packet_id, topic, 1));
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::send_pending_subscriptions(const std::shared_ptr<Connection>& conn) {
|
||||
void OrcaMqttConnection::send_pending_subscriptions(Connection& conn) {
|
||||
std::vector<std::string> subscribe_topics;
|
||||
std::vector<std::string> unsubscribe_topics;
|
||||
{
|
||||
@@ -654,11 +549,11 @@ void OrcaMqttConnection::send_pending_subscriptions(const std::shared_ptr<Connec
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
pending_subscribe_packets[packet_id] = topic;
|
||||
}
|
||||
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
|
||||
ws_write(conn, make_subscribe_packet(packet_id, topic, 1));
|
||||
}
|
||||
for (const std::string& topic : unsubscribe_topics) {
|
||||
const uint16_t packet_id = next_packet_id++;
|
||||
enqueue_packet(conn, make_unsubscribe_packet(packet_id, topic));
|
||||
ws_write(conn, make_unsubscribe_packet(packet_id, topic));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ public:
|
||||
struct Config {
|
||||
std::string url;
|
||||
bool use_tls = false;
|
||||
std::string ca_file;
|
||||
TokenProvider bearer_provider; // set => bearer on WS upgrade, CONNECT creds omitted
|
||||
std::string username;
|
||||
std::string password;
|
||||
@@ -96,23 +95,19 @@ private:
|
||||
static std::vector<uint8_t> make_ping_packet();
|
||||
|
||||
// Transport dispatch: each forwards to conn.wss (TLS) or conn.ws (plaintext).
|
||||
// These synchronous operations are called only by the MQTT worker during
|
||||
// connection setup. Once the MQTT session is established, all socket I/O is
|
||||
// asynchronous and owned by that worker's io_context.
|
||||
void ws_write(Connection& conn, const std::vector<uint8_t>& packet);
|
||||
void ws_write(Connection& conn, const std::vector<uint8_t>& packet); // locks write_mutex
|
||||
std::size_t ws_read(Connection& conn, boost::beast::flat_buffer& buffer, boost::system::error_code& ec);
|
||||
void ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint);
|
||||
void close_connection(Connection& conn);
|
||||
void enqueue_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
|
||||
void start_async_write(const std::shared_ptr<Connection>& conn);
|
||||
void start_async_read(const std::shared_ptr<Connection>& conn);
|
||||
void schedule_keepalive(const std::shared_ptr<Connection>& conn);
|
||||
void post_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
|
||||
// Ask the MQTT worker to emit subscription changes on its own io_context.
|
||||
void ws_close(Connection& conn);
|
||||
// Emit a queued SUBSCRIBE/UNSUBSCRIBE on the live socket right now (from the
|
||||
// caller thread), so a selection change is applied without waiting for the
|
||||
// blocking read loop to next return. No-op if no CONNACKed socket exists yet
|
||||
// (the worker sends the set on connect). The WebSocket is never dropped for a
|
||||
// subscription change.
|
||||
void flush_subscription_change();
|
||||
void connect_and_read();
|
||||
void send_current_subscriptions(const std::shared_ptr<Connection>& conn);
|
||||
void send_pending_subscriptions(const std::shared_ptr<Connection>& conn);
|
||||
void send_current_subscriptions(Connection& conn);
|
||||
void send_pending_subscriptions(Connection& conn);
|
||||
void handle_packet(const std::string& packet);
|
||||
void notify_state(bool is_now_connected);
|
||||
void run();
|
||||
@@ -126,6 +121,7 @@ private:
|
||||
std::thread worker;
|
||||
std::mutex mutex;
|
||||
std::mutex connection_mutex;
|
||||
std::mutex write_mutex; // serialises every websocket write (worker + caller threads)
|
||||
std::shared_ptr<Connection> active_connection;
|
||||
std::condition_variable initial_cv;
|
||||
std::condition_variable state_cv;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#include "OrcaPrinterAgent.hpp"
|
||||
|
||||
#include "AmsPayload.hpp"
|
||||
#include "IPrinterAgent.hpp"
|
||||
#include "Http.hpp"
|
||||
#include "NetworkAgentFactory.hpp"
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "bambu_networking.hpp"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
@@ -42,23 +41,8 @@ const std::string OrcaPrinterAgent_VERSION = "0.0.1";
|
||||
namespace {
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
// Per-device filament_mapping capability, mirrored from the get_capabilities
|
||||
// reply in merge_capabilities and read by start_sdcard_print so the field is
|
||||
// refused defensively when the connector never advertised it or the
|
||||
// index-correlation merge gate (ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED) is
|
||||
// not satisfied.
|
||||
std::mutex g_filament_mapping_mutex;
|
||||
std::unordered_map<std::string, bool> g_filament_mapping_cache;
|
||||
|
||||
// True only when the connector's get_capabilities reply advertised
|
||||
// filament_mapping for this device. Unknown is not support.
|
||||
bool filament_mapping_advertised(const std::string& dev_id)
|
||||
{
|
||||
std::lock_guard<std::mutex> l(g_filament_mapping_mutex);
|
||||
const auto it = g_filament_mapping_cache.find(dev_id);
|
||||
return it != g_filament_mapping_cache.end() && it->second;
|
||||
}
|
||||
constexpr int kVirtualTrayDeputyId = 254;
|
||||
constexpr int kVirtualTrayMainId = 255;
|
||||
|
||||
// params.filename is normally the exported .3mf archive; the sliced G-code sits
|
||||
// beside it with the same stem (".12345.0.3mf" -> ".12345.0.gcode"). params.dst_file,
|
||||
@@ -131,6 +115,18 @@ std::string next_gcode_file_sequence_id()
|
||||
return std::to_string(counter.fetch_add(1, std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
// pushall is replay-cached per (namespace, command, sequence_id), so a refresh
|
||||
// must not reuse the connect-band ids (20001..20004). Seed from the wall clock
|
||||
// like next_gcode_file_sequence_id and bump once per call within a run.
|
||||
std::string next_filament_refresh_sequence_id()
|
||||
{
|
||||
static std::atomic<uint64_t> counter{[] {
|
||||
const auto now = std::chrono::system_clock::now().time_since_epoch();
|
||||
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(now).count()) + 100000;
|
||||
}()};
|
||||
return std::to_string(counter.fetch_add(1, std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
static constexpr const char* ORCASONAR_FALLBACK = "orcasonar";
|
||||
|
||||
bool fetch_orcasonar_body(const std::string& url, std::string& body)
|
||||
@@ -556,7 +552,7 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
|
||||
// removes its storage too. Process-wide and keyed by the globally-unique
|
||||
// dev_id, with its own mutex; C++11 makes the one-time init thread-safe.
|
||||
static std::unordered_map<std::string, double> nozzle_diameter_cache;
|
||||
static std::mutex nozzle_diameter_cache_mutex;
|
||||
static std::mutex nozzle_diameter_cache_mutex;
|
||||
|
||||
bool modified = false;
|
||||
|
||||
@@ -572,7 +568,7 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
|
||||
// ("N/A" -> NozzleType::ntUndefine, as MoonrakerPrinterAgent
|
||||
// does; Klipper has no Bambu nozzle type) onto later push_status
|
||||
// frames that carry no real nozzle data.
|
||||
const auto info_it = envelope.find("info");
|
||||
const auto info_it = envelope.find("info");
|
||||
const bool is_capabilities_reply = info_it != envelope.end() && info_it->is_object() &&
|
||||
info_it->value("command", "") == "get_capabilities";
|
||||
|
||||
@@ -605,40 +601,10 @@ 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;
|
||||
}
|
||||
|
||||
// Per-device filament_mapping capability. Both feature maps carry it;
|
||||
// either being true means the connector advertised it.
|
||||
bool mapping_advertised = false;
|
||||
{
|
||||
const auto top_features = info_it->find("supported_features");
|
||||
if (top_features != info_it->end() && top_features->is_object()) {
|
||||
const auto it = top_features->find("filament_mapping");
|
||||
if (it != top_features->end() && it->is_boolean())
|
||||
mapping_advertised = it->get<bool>();
|
||||
}
|
||||
if (!mapping_advertised && caps_it != info_it->end() && caps_it->is_object()) {
|
||||
const auto protocol_it = caps_it->find("protocol");
|
||||
if (protocol_it != caps_it->end() && protocol_it->is_object()) {
|
||||
const auto features_it = protocol_it->find("features");
|
||||
if (features_it != protocol_it->end() && features_it->is_object()) {
|
||||
const auto it = features_it->find("filament_mapping");
|
||||
if (it != features_it->end() && it->is_boolean())
|
||||
mapping_advertised = it->get<bool>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> l(g_filament_mapping_mutex);
|
||||
g_filament_mapping_cache[dev_id] = mapping_advertised;
|
||||
}
|
||||
// The capabilities reply itself is forwarded unchanged.
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const auto print_it = envelope.find("print");
|
||||
if (print_it != envelope.end() && print_it->is_object() &&
|
||||
print_it->value("command", "") == "push_status" && !print_it->contains("nozzle_diameter")) {
|
||||
|
||||
if (print_it != envelope.end() && print_it->is_object() && print_it->value("command", "") == "push_status" &&
|
||||
!print_it->contains("nozzle_diameter")) {
|
||||
double nozzle_dia = 0.0;
|
||||
{
|
||||
std::lock_guard<std::mutex> l(nozzle_diameter_cache_mutex);
|
||||
@@ -649,7 +615,7 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
|
||||
if (nozzle_dia > 0.0) {
|
||||
(*print_it)["nozzle_diameter"] = nozzle_dia;
|
||||
(*print_it)["nozzle_type"] = "N/A";
|
||||
modified = true;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -658,9 +624,112 @@ 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");
|
||||
// A reply that is not a complete capabilities answer is ignored: treating a
|
||||
// transient malformed reply as "no capabilities" would gate every AMS write
|
||||
// until a good one arrives. Stale state is cleared on disconnect instead.
|
||||
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);
|
||||
|
||||
// features.filament_slots is the connector's slot model (REQ-FMS-001),
|
||||
// independent of fms: a printer with no material hardware still has slots.
|
||||
bool has_slots = false;
|
||||
if (features_it != proto_it->end() && features_it->is_object()) {
|
||||
const auto slots_it = features_it->find("filament_slots");
|
||||
if (slots_it != features_it->end() && slots_it->is_boolean())
|
||||
has_slots = slots_it->get<bool>();
|
||||
}
|
||||
register_filament_slots(dev_id, has_slots);
|
||||
|
||||
// filament_metadata.<driver>.values publishes what a fixed-vocabulary
|
||||
// driver can hold. It constrains the picker only when every writing driver
|
||||
// declared values: one free-form driver (Happy Hare, AFC) keeps the full
|
||||
// color picker and preset list available. Registered on every reply so a
|
||||
// later one without the key clears a stale palette.
|
||||
FilamentMetadataValues metadata;
|
||||
const auto meta_it = proto_it->find("filament_metadata");
|
||||
if (meta_it != proto_it->end() && meta_it->is_object() && !meta_it->empty()) {
|
||||
const auto collect = [](std::vector<std::string>& out, const nlohmann::json& values, const char* key) {
|
||||
const auto arr = values.find(key);
|
||||
if (arr == values.end() || !arr->is_array())
|
||||
return;
|
||||
for (const auto& v : *arr) {
|
||||
if (!v.is_string())
|
||||
continue;
|
||||
const std::string entry = v.get<std::string>();
|
||||
if (std::find(out.begin(), out.end(), entry) == out.end())
|
||||
out.push_back(entry);
|
||||
}
|
||||
};
|
||||
bool all_constrained = true;
|
||||
for (auto driver = meta_it->begin(); driver != meta_it->end() && all_constrained; ++driver) {
|
||||
const auto values_it = driver.value().find("values");
|
||||
if (values_it == driver.value().end() || !values_it->is_object() || values_it->empty()) {
|
||||
all_constrained = false;
|
||||
break;
|
||||
}
|
||||
collect(metadata.materials, *values_it, "material");
|
||||
collect(metadata.colors, *values_it, "color");
|
||||
}
|
||||
if (all_constrained && (!metadata.materials.empty() || !metadata.colors.empty()))
|
||||
metadata.constrained = true;
|
||||
else
|
||||
metadata = FilamentMetadataValues{};
|
||||
}
|
||||
register_filament_metadata(dev_id, metadata);
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local)
|
||||
{
|
||||
parse_ipcam_info(dev_id, payload);
|
||||
register_ams_capabilities(dev_id, payload);
|
||||
maybe_refresh_filament_capabilities(dev_id, payload, local);
|
||||
std::string merged_payload = merge_capabilities(dev_id, payload);
|
||||
|
||||
OnMessageFn fn;
|
||||
@@ -671,10 +740,11 @@ void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::str
|
||||
q = queue_on_main_fn;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering " << (local ? "local" : "cloud") << " report payload dev_id=" << dev_id
|
||||
<< " payload=" << merged_payload
|
||||
<< " callback=" << (fn ? "set" : "null") << " queue_on_main=" << (q ? "set" : "null");
|
||||
<< " payload=" << merged_payload << " callback=" << (fn ? "set" : "null")
|
||||
<< " queue_on_main=" << (q ? "set" : "null");
|
||||
if (!fn) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping " << (local ? "local" : "cloud") << " message because on_message_fn is not set"
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping " << (local ? "local" : "cloud")
|
||||
<< " message because on_message_fn is not set"
|
||||
<< " dev_id=" << dev_id;
|
||||
return;
|
||||
}
|
||||
@@ -696,6 +766,7 @@ void OrcaPrinterAgent::dispatch_local_connect(int state, const std::string& dev_
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection callback state=" << state << " dev_id=" << dev_id << " message=" << message
|
||||
<< " callback=" << (callback ? "set" : "null") << " queue_on_main=" << (queue ? "set" : "null");
|
||||
|
||||
if (!callback)
|
||||
return;
|
||||
|
||||
@@ -747,26 +818,45 @@ int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int
|
||||
|
||||
int OrcaPrinterAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int tray_id, int sequence_id, bool lan_mode)
|
||||
{
|
||||
(void) ams_id;
|
||||
// int tray_number = 0;
|
||||
// if (!parse_nonnegative_command_id(tray_id, tray_number)) {
|
||||
// BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid RFID tray id=" << tray_id;
|
||||
// return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
// }
|
||||
return route_send(lan_mode, dev_id, build_ams_refresh_rfid_body(ams_id, tray_id, sequence_id));
|
||||
}
|
||||
|
||||
int OrcaPrinterAgent::command_ams_calibrate(std::string /*dev_id*/, int /*ams_id*/, int /*sequence_id*/, bool /*lan_mode*/)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: AMS calibration is not part of the OrcaSonar API";
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
std::string OrcaPrinterAgent::build_ams_change_filament_body(int tray_number, int sequence_id)
|
||||
{
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "ams_change_filament";
|
||||
j["print"]["sequence_id"] = std::to_string(sequence_id);
|
||||
// tray_id is the BBL tray id (ams_id*4 + tray). Send the coordinates, not a
|
||||
// fabricated flat lane: the server's one resolver maps wide and sparse
|
||||
// boxes correctly (REQ-STS-008 §7.8).
|
||||
j["print"]["selector"] = "lane";
|
||||
if (tray_number == kVirtualTrayMainId || tray_number == kVirtualTrayDeputyId) {
|
||||
j["print"]["ams_id"] = tray_number;
|
||||
j["print"]["slot_id"] = 0;
|
||||
} else {
|
||||
j["print"]["ams_id"] = tray_number / 4;
|
||||
j["print"]["slot_id"] = tray_number % 4;
|
||||
}
|
||||
return j.dump();
|
||||
}
|
||||
|
||||
std::string OrcaPrinterAgent::build_ams_refresh_rfid_body(int ams_id, int tray_or_slot_id, int sequence_id)
|
||||
{
|
||||
const int tray_id = ams_id == -1
|
||||
? tray_or_slot_id
|
||||
: (ams_id >= 240 && ams_id <= 255 ? ams_id : ams_id * 4 + tray_or_slot_id);
|
||||
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "ams_get_rfid";
|
||||
j["print"]["sequence_id"] = std::to_string(sequence_id);
|
||||
j["print"]["tray_id"] = tray_id;
|
||||
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;
|
||||
return j.dump();
|
||||
}
|
||||
|
||||
int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
|
||||
@@ -777,51 +867,7 @@ int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tr
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "ams_change_filament";
|
||||
j["print"]["sequence_id"] = std::to_string(sequence_id);
|
||||
j["print"]["target"] = tray_number;
|
||||
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());
|
||||
return route_send(lan_mode, dev_id, build_ams_change_filament_body(tray_number, sequence_id));
|
||||
}
|
||||
|
||||
int OrcaPrinterAgent::command_set_bed(std::string dev_id, int temp, bool /*supports_mqtt_bed_ctrl*/, int sequence_id, bool lan_mode)
|
||||
@@ -878,6 +924,58 @@ int OrcaPrinterAgent::command_axis_control(std::string dev_id,
|
||||
return route_send(lan_mode, dev_id, j.dump());
|
||||
}
|
||||
|
||||
FilamentSyncMode OrcaPrinterAgent::get_filament_sync_mode() const
|
||||
{
|
||||
std::string dev_id;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
dev_id = (m_current_connection == LAN) ? m_lan_dev_id : selected_machine;
|
||||
}
|
||||
if (!dev_id.empty() && (has_ams_capability(dev_id) || has_filament_slots(dev_id))) {
|
||||
return FilamentSyncMode::subscription;
|
||||
}
|
||||
return FilamentSyncMode::none;
|
||||
}
|
||||
|
||||
// Moonraker's client bootstrap: /access/api_key hands the façade key to a
|
||||
// trusted source (the default LAN ranges include the slicer). Cache it per
|
||||
// connection generation. If the request is refused — a hardened trusted_clients
|
||||
// list, or no façade — fall back to the access code, which is what earlier
|
||||
// builds sent, so behavior degrades to the status quo rather than breaking.
|
||||
std::string OrcaPrinterAgent::lan_api_key(const std::string& origin)
|
||||
{
|
||||
if (origin.empty())
|
||||
return {};
|
||||
|
||||
std::string fallback;
|
||||
uint64_t gen;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (!m_lan_api_key.empty() && m_lan_api_key_gen == m_lan_generation.load())
|
||||
return m_lan_api_key;
|
||||
fallback = m_lan_password;
|
||||
gen = m_lan_generation.load();
|
||||
}
|
||||
|
||||
std::string resolved;
|
||||
if (std::string body; fetch_orcasonar_body(origin + "/access/api_key", body)) {
|
||||
auto j = nlohmann::json::parse(body, nullptr, false, true);
|
||||
if (!j.is_discarded() && j.contains("result") && j["result"].is_string())
|
||||
resolved = j["result"].get<std::string>();
|
||||
}
|
||||
if (resolved.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: /access/api_key unavailable; using the access code as X-Api-Key";
|
||||
resolved = fallback;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (m_lan_generation.load() == gen && !resolved.empty()) {
|
||||
m_lan_api_key = resolved;
|
||||
m_lan_api_key_gen = gen;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
bool OrcaPrinterAgent::parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port)
|
||||
{
|
||||
std::string s = dev_ip;
|
||||
@@ -1030,6 +1128,38 @@ void OrcaPrinterAgent::emit_connect_sequence(const std::string& dev_id,
|
||||
request(build_get_capabilities(seq(4)));
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::request_filament_capabilities(const std::string& dev_id, bool local)
|
||||
{
|
||||
if (dev_id.empty())
|
||||
return;
|
||||
OrcaMqttConnection* conn = get_appropriate_mqtt_connection(local);
|
||||
if (!conn)
|
||||
return;
|
||||
conn->send_request(dev_id, build_get_capabilities(next_filament_refresh_sequence_id()));
|
||||
conn->send_request(dev_id, build_pushall(next_filament_refresh_sequence_id()));
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::maybe_refresh_filament_capabilities(const std::string& dev_id, const std::string& payload, bool local)
|
||||
{
|
||||
if (dev_id.empty() || ams_caps_known(dev_id))
|
||||
return;
|
||||
if (payload.find("push_status") == std::string::npos)
|
||||
return;
|
||||
// Only filament state is a reason to ask: ams_exist_bits/tray_exist_bits or
|
||||
// the vir_slot array. A status without either cannot resolve topology.
|
||||
if (payload.find("\"vir_slot\"") == std::string::npos && payload.find("\"ams\":") == std::string::npos)
|
||||
return;
|
||||
{
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto it = m_filament_caps_refresh_at.find(dev_id);
|
||||
if (it != m_filament_caps_refresh_at.end() && now - it->second < std::chrono::seconds(5))
|
||||
return;
|
||||
m_filament_caps_refresh_at[dev_id] = now;
|
||||
}
|
||||
request_filament_capabilities(dev_id, local);
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation)
|
||||
{
|
||||
// Called from both connect paths with whichever epoch that path captured; the two
|
||||
@@ -1046,6 +1176,7 @@ int OrcaPrinterAgent::connect_printer(const PrinterConnectionParams& params)
|
||||
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: connect_printer requested dev_id=" << params.dev_id << " dev_ip=" << params.host
|
||||
<< " username=" << (params.username.empty() ? "<default>" : params.username) << " password_present=" << (!params.password.empty())
|
||||
<< " use_ssl=" << params.use_ssl;
|
||||
(void) params.use_ssl; // OrcaSonar LAN is plaintext ws://
|
||||
if (params.dev_id.empty() || params.host.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected missing dev_id or dev_ip";
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
@@ -1055,13 +1186,14 @@ int OrcaPrinterAgent::connect_printer(const PrinterConnectionParams& params)
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected unparsable LAN endpoint dev_ip=" << params.host;
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
}
|
||||
if (!params.port.empty())
|
||||
port = params.port;
|
||||
disconnect_printer();
|
||||
const uint64_t gen = ++m_lan_generation;
|
||||
|
||||
OrcaMqttConnection::Config cfg;
|
||||
cfg.url = (params.use_ssl ? "wss://" : "ws://") + host + ":" + port + "/mqtt";
|
||||
cfg.use_tls = params.use_ssl;
|
||||
cfg.ca_file = params.ca_file;
|
||||
cfg.url = "ws://" + host + ":" + port + "/mqtt";
|
||||
cfg.use_tls = false;
|
||||
cfg.username = params.username.empty() ? std::string("orcasonar") : params.username;
|
||||
cfg.password = params.password;
|
||||
cfg.client_id = make_lan_client_id(params.dev_id);
|
||||
@@ -1075,11 +1207,12 @@ int OrcaPrinterAgent::connect_printer(const PrinterConnectionParams& params)
|
||||
CurrentConn previous_connection;
|
||||
{
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
previous_connection = m_current_connection;
|
||||
m_lan_dev_id = params.dev_id;
|
||||
m_lan_url = cfg.url;
|
||||
m_lan_use_ssl = params.use_ssl;
|
||||
m_lan_ca_file = params.ca_file;
|
||||
previous_connection = m_current_connection;
|
||||
m_lan_dev_id = params.dev_id;
|
||||
m_lan_url = cfg.url;
|
||||
m_lan_password = params.password; // access code; the façade key is bootstrapped lazily
|
||||
m_lan_api_key.clear();
|
||||
m_lan_api_key_gen = gen;
|
||||
m_camera_stream_mode = CameraStreamMode::none;
|
||||
m_camera_url.clear();
|
||||
m_current_connection = LAN;
|
||||
@@ -1153,8 +1286,7 @@ int OrcaPrinterAgent::disconnect_printer()
|
||||
doomed = std::move(lan_mqtt_connection);
|
||||
prev_dev = m_lan_dev_id;
|
||||
m_lan_dev_id.clear();
|
||||
m_lan_use_ssl = false;
|
||||
m_lan_ca_file.clear();
|
||||
m_lan_api_key.clear();
|
||||
if (m_current_connection == LAN) {
|
||||
m_current_connection = NONE;
|
||||
m_camera_stream_mode = CameraStreamMode::none;
|
||||
@@ -1167,6 +1299,10 @@ int OrcaPrinterAgent::disconnect_printer()
|
||||
<< " connected=" << (doomed && doomed->is_connected() ? "yes" : "no")
|
||||
<< " transport=" << connection_type_name(previous_connection) << "->"
|
||||
<< connection_type_name(current_connection);
|
||||
// Drop the device's declared capabilities so a reconnect starts from "no
|
||||
// reply yet" instead of a stale declaration from the previous session.
|
||||
if (!prev_dev.empty())
|
||||
clear_ams_caps(prev_dev);
|
||||
// Tell the printer to stop pushing and drop the report topic before the socket
|
||||
// goes away (§3.2/§5.4: deselect issues pushing.stop on both transports).
|
||||
if (doomed && !prev_dev.empty() && doomed->is_connected()) {
|
||||
@@ -1184,11 +1320,150 @@ int OrcaPrinterAgent::disconnect_printer()
|
||||
int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int /*qos*/, int /*flag*/)
|
||||
{ return route_send(/*is_lan=*/true, dev_id, json_str); }
|
||||
|
||||
// Rewrite recognized Bambu-coordinate requests to the canonical OrcaSonar
|
||||
// bodies (OPCP spec §7.8) and enforce the device's declared capabilities.
|
||||
// Explicit selectors and legacy target requests retain their wire semantics.
|
||||
std::string OrcaPrinterAgent::canonicalize_ams_payload(const std::string& dev_id, const std::string& json_str, bool* unsupported)
|
||||
{
|
||||
if (unsupported)
|
||||
*unsupported = false;
|
||||
try {
|
||||
auto envelope = nlohmann::json::parse(json_str, nullptr, false);
|
||||
if (envelope.is_discarded() || !envelope.is_object() || !envelope.contains("print") || !envelope["print"].is_object())
|
||||
return json_str;
|
||||
auto& print = envelope["print"];
|
||||
const std::string cmd = print.value("command", std::string());
|
||||
if (cmd.empty() || (cmd.rfind("ams_", 0) != 0 && cmd != "auto_stop_ams_dry"))
|
||||
return json_str;
|
||||
|
||||
// filament_setting is exempt from the ams_ops union: filament_slots
|
||||
// advertises the canonical slot write, while native metadata support is
|
||||
// reported separately by the driver (OrcaSonar OPCP §7.8).
|
||||
auto op_allowed = [&dev_id](const std::string& o) {
|
||||
if (o == "filament_setting")
|
||||
return !ams_caps_known(dev_id) || has_filament_slots(dev_id);
|
||||
return o.empty() || ams_op_supported(dev_id, o);
|
||||
};
|
||||
const bool fms_allowed = !ams_caps_known(dev_id) || has_ams_capability(dev_id);
|
||||
|
||||
if (cmd == "ams_change_filament" && print.contains("selector")) {
|
||||
// Already canonical (e.g. command_ams_select_tray): gate the op,
|
||||
// but never rewrite the body.
|
||||
const std::string sel = print.value("selector", std::string());
|
||||
bool lane_has_coordinates = false;
|
||||
bool coordinate_is_external = false;
|
||||
if (sel == "lane") {
|
||||
const auto ams_it = print.find("ams_id");
|
||||
const auto slot_it = print.find("slot_id");
|
||||
lane_has_coordinates = ams_it != print.end() && ams_it->is_number_integer()
|
||||
&& slot_it != print.end() && slot_it->is_number_integer();
|
||||
if (lane_has_coordinates) {
|
||||
const int ams = ams_it->get<int>();
|
||||
coordinate_is_external = ams >= 240 && ams <= 255;
|
||||
}
|
||||
}
|
||||
const bool selector_op_allowed = sel == "lane"
|
||||
? (lane_has_coordinates
|
||||
? op_allowed(coordinate_is_external ? "external" : "change_filament")
|
||||
: (op_allowed("change_filament") || op_allowed("external")))
|
||||
: op_allowed(sel);
|
||||
const bool allowed = fms_allowed && selector_op_allowed;
|
||||
if (!allowed && unsupported)
|
||||
*unsupported = true;
|
||||
return json_str;
|
||||
}
|
||||
|
||||
auto int_or = [&print](const char* key, int fallback) {
|
||||
const auto it = print.find(key);
|
||||
return (it != print.end() && it->is_number_integer()) ? it->get<int>() : fallback;
|
||||
};
|
||||
std::string op;
|
||||
if (cmd == "ams_change_filament") {
|
||||
const int target = int_or("target", -1);
|
||||
const int slot = int_or("slot_id", -1);
|
||||
const int ams = int_or("ams_id", -1);
|
||||
const bool has_target = target >= 0 && target <= 255 && print.contains("target");
|
||||
const bool legacy_unload = target == 255 && slot == 255;
|
||||
const bool has_coordinates = ams >= 0 && slot >= 0;
|
||||
|
||||
// The old request path retains macro semantics. Only translate the
|
||||
// Slicer-generated Bambu form when its coordinates identify a slot.
|
||||
const bool ambiguous_external_target = target == 255 && ams < 240;
|
||||
if (!has_target || legacy_unload || ambiguous_external_target || !has_coordinates || ams > 255) {
|
||||
const bool legacy_op_allowed = op_allowed("change_filament")
|
||||
|| op_allowed("external") || op_allowed("unload");
|
||||
if ((!fms_allowed || !legacy_op_allowed) && unsupported)
|
||||
*unsupported = true;
|
||||
return json_str;
|
||||
}
|
||||
|
||||
print.erase("target");
|
||||
print.erase("tray_id");
|
||||
print["selector"] = "lane";
|
||||
print["ams_id"] = ams;
|
||||
print["slot_id"] = slot;
|
||||
op = ams >= 240 ? "external" : "change_filament";
|
||||
} else if (cmd == "ams_filament_setting") {
|
||||
op = "filament_setting";
|
||||
const int ams = int_or("ams_id", -1);
|
||||
const int slot = int_or("slot_id", -1);
|
||||
if (ams >= 0 && slot >= 0) {
|
||||
// Coordinates are the server resolver's own form (REQ-FMS-001
|
||||
// §6.2.13). Forward them unchanged for box and external spools;
|
||||
// never fabricate a flat lane (tray id != layout lane).
|
||||
print["ams_id"] = ams;
|
||||
print["slot_id"] = slot;
|
||||
print.erase("tray_id");
|
||||
}
|
||||
// A lane-only or coordinate-less body is left as sent so the server
|
||||
// applies the dual-form rule and its own -19, never -5.
|
||||
} else if (cmd == "ams_control") {
|
||||
std::string action = print.value("action", print.value("param", std::string()));
|
||||
op = action; // only "pause" is ever declared; the rest gate out
|
||||
} else if (cmd == "ams_user_setting") {
|
||||
op = "user_setting";
|
||||
} else if (cmd == "ams_get_rfid") {
|
||||
op = "get_rfid";
|
||||
if (!print.contains("tray_id")) {
|
||||
const int ams = int_or("ams_id", -1);
|
||||
const int slot = int_or("slot_id", -1);
|
||||
if (ams == -1 && slot >= 0) {
|
||||
print["tray_id"] = slot;
|
||||
print.erase("ams_id");
|
||||
print.erase("slot_id");
|
||||
} else if (ams >= 240 && ams <= 255 && slot >= 0) {
|
||||
print["tray_id"] = ams;
|
||||
print.erase("ams_id");
|
||||
print.erase("slot_id");
|
||||
} else if (ams >= 0 && ams < 240 && slot >= 0) {
|
||||
print["tray_id"] = ams * 4 + slot; // legacy ams+slot call shape
|
||||
print.erase("ams_id");
|
||||
print.erase("slot_id");
|
||||
}
|
||||
}
|
||||
} else if (cmd == "auto_stop_ams_dry") {
|
||||
op = "stop_dry";
|
||||
}
|
||||
const bool needs_fms = cmd != "ams_filament_setting" && !op.empty();
|
||||
if (((needs_fms && !fms_allowed) || !op_allowed(op)) && unsupported)
|
||||
*unsupported = true;
|
||||
return envelope.dump();
|
||||
} catch (const std::exception&) {
|
||||
return json_str;
|
||||
}
|
||||
}
|
||||
|
||||
int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const std::string& json_str)
|
||||
{
|
||||
bool unsupported = false;
|
||||
const std::string canonical = canonicalize_ams_payload(dev_id, json_str, &unsupported);
|
||||
if (unsupported) {
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send: ams op not declared by device capabilities, dev_id=" << dev_id;
|
||||
return ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE;
|
||||
}
|
||||
std::string command = "<unparsed>";
|
||||
try {
|
||||
const nlohmann::json envelope = nlohmann::json::parse(json_str);
|
||||
const nlohmann::json envelope = nlohmann::json::parse(canonical);
|
||||
for (const char* namespace_name : {"pushing", "info", "print", "system", "camera", "xcam", "upgrade", "event", "files"}) {
|
||||
const auto namespace_it = envelope.find(namespace_name);
|
||||
if (namespace_it != envelope.end() && namespace_it->is_object()) {
|
||||
@@ -1210,7 +1485,7 @@ int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const s
|
||||
OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan);
|
||||
if (!conn)
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
const bool queued = conn->send_request(dev_id, json_str);
|
||||
const bool queued = conn->send_request(dev_id, canonical);
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued << " is_lan=" << is_lan
|
||||
<< " dev_id=" << dev_id;
|
||||
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
|
||||
@@ -1310,11 +1585,13 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
|
||||
{
|
||||
auto* cloud = get_orca_cloud_agent();
|
||||
std::string previous;
|
||||
std::string lan_dev_id;
|
||||
CurrentConn previous_connection;
|
||||
CurrentConn current_connection;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
previous_connection = m_current_connection;
|
||||
lan_dev_id = m_lan_dev_id;
|
||||
// An empty cloud selection must not clear an independently active LAN
|
||||
// selection. Conversely, selecting a cloud machine with the same id
|
||||
// while LAN is active is still a transport switch and must proceed.
|
||||
@@ -1343,6 +1620,10 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: previous=" << previous << " new=" << dev_id
|
||||
<< " cloud=" << (cloud ? "set" : "<null>") << " transport=" << connection_type_name(previous_connection) << "->"
|
||||
<< connection_type_name(current_connection);
|
||||
// Forget the deselected cloud device's declaration so a later session starts
|
||||
// from "no reply yet". A LAN feed for the same id keeps its capabilities.
|
||||
if (!previous.empty() && previous != dev_id && previous != lan_dev_id)
|
||||
clear_ams_caps(previous);
|
||||
if (!cloud) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca cloud agent";
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
@@ -1384,9 +1665,9 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
|
||||
auto state_handler = [this](bool connected, bool initial) {
|
||||
if (!connected || initial)
|
||||
return;
|
||||
auto* current_cloud = get_orca_cloud_agent();
|
||||
auto* current_cloud = get_orca_cloud_agent();
|
||||
OrcaMqttConnection* current_conn = current_cloud ? current_cloud->get_mqtt_connection() : nullptr;
|
||||
const std::string selected = get_user_selected_machine();
|
||||
const std::string selected = get_user_selected_machine();
|
||||
if (current_conn && !selected.empty())
|
||||
on_connected(selected, current_conn, m_cloud_generation.load());
|
||||
};
|
||||
@@ -1409,7 +1690,8 @@ AgentInfo OrcaPrinterAgent::get_agent_info_static()
|
||||
// Print Job Operations - All Stubs
|
||||
// ============================================================================
|
||||
|
||||
// Simply uploads the file to the printer via HTTP (cloud) then sends a HTTP request to start print. In the future, this might be a MQTT command to start print instead of HTTP.
|
||||
// Simply uploads the file to the printer via HTTP (cloud) then sends a HTTP request to start print. In the future, this might be a MQTT
|
||||
// command to start print instead of HTTP.
|
||||
int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
|
||||
{
|
||||
(void) wait_fn;
|
||||
@@ -1459,17 +1741,11 @@ int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
int OrcaPrinterAgent::start_local_print_with_record(PrintParams /*params*/,
|
||||
OnUpdateStatusFn /*update_fn*/,
|
||||
WasCancelledFn /*cancel_fn*/,
|
||||
OnWaitFn /*wait_fn*/)
|
||||
{
|
||||
// OrcaSonar has no FTP "send with record" path. Report a non-success result so
|
||||
// PrintJob falls back to start_print() (cloud upload + start_sdcard_print) instead
|
||||
// of treating a print that was never sent as successful.
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_local_print_with_record is unimplemented; deferring to start_print";
|
||||
return BAMBU_NETWORK_ERR_FTP_UPLOAD_FAILED;
|
||||
}
|
||||
int OrcaPrinterAgent::start_local_print_with_record(PrintParams params,
|
||||
OnUpdateStatusFn update_fn,
|
||||
WasCancelledFn cancel_fn,
|
||||
OnWaitFn wait_fn)
|
||||
{ return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
// Upload one G-code file to the printer's `gcodes` root over OrcaSonar's
|
||||
// Moonraker-compatible HTTP facade. No print is started here (print=false); the
|
||||
@@ -1500,17 +1776,9 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params,
|
||||
return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED;
|
||||
}
|
||||
|
||||
bool use_ssl = false;
|
||||
std::string ca_file;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
use_ssl = m_lan_use_ssl;
|
||||
ca_file = m_lan_ca_file;
|
||||
}
|
||||
|
||||
std::string host, port, origin;
|
||||
if (parse_lan_endpoint(params.dev_ip, host, port))
|
||||
origin = (use_ssl ? "https://" : "http://") + host;
|
||||
origin = "http://" + host;
|
||||
else
|
||||
origin = http_origin_from_lan_ws(lan_connection_target());
|
||||
if (origin.empty()) {
|
||||
@@ -1530,41 +1798,49 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params,
|
||||
std::string http_error;
|
||||
std::string response_body;
|
||||
|
||||
// One façade key for the whole upload exchange (directory probe + POST).
|
||||
// If the bootstrapped key is unavailable (and there was no stored access
|
||||
// code), fall back to the job's own password rather than sending none.
|
||||
std::string api_key = lan_api_key(origin);
|
||||
if (api_key.empty())
|
||||
api_key = params.password;
|
||||
|
||||
// check if printer has enough storage
|
||||
auto directory_http = Http::get(origin + "/server/files/directory?path=gcodes");
|
||||
directory_http.tls_verify(use_ssl);
|
||||
if (!ca_file.empty())
|
||||
directory_http.ca_file(ca_file);
|
||||
directory_http
|
||||
.on_complete([&](std::string body, unsigned status) {
|
||||
if (body.empty()) {
|
||||
http_status = 400;
|
||||
http_error = "Failed to get gcodes directory.";
|
||||
}
|
||||
|
||||
int free = 0;
|
||||
|
||||
nlohmann::json json = nlohmann::json::parse(body);
|
||||
if (json.contains("result")) {
|
||||
json = json["result"];
|
||||
if (json.contains("disk_usage")) {
|
||||
json = json["disk_usage"];
|
||||
if (json.contains("free"))
|
||||
free = json["free"].get<int>();
|
||||
{
|
||||
auto dir_req = Http::get(origin + "/server/files/directory?path=gcodes");
|
||||
if (!api_key.empty())
|
||||
dir_req.header("X-Api-Key", api_key);
|
||||
dir_req
|
||||
.on_complete([&](std::string body, unsigned status) {
|
||||
if (body.empty()) {
|
||||
http_status = 400;
|
||||
http_error = "Failed to get gcodes directory.";
|
||||
}
|
||||
}
|
||||
|
||||
if (free < file_size) {
|
||||
http_status = 507;
|
||||
http_error = "Not enough storage on the printer.";
|
||||
}
|
||||
})
|
||||
.on_error([&](std::string body, std::string err, unsigned status) {
|
||||
http_status = status;
|
||||
http_error = std::move(err);
|
||||
response_body = std::move(body);
|
||||
})
|
||||
.perform_sync();
|
||||
int free = 0;
|
||||
|
||||
nlohmann::json json = nlohmann::json::parse(body);
|
||||
if (json.contains("result")) {
|
||||
json = json["result"];
|
||||
if (json.contains("disk_usage")) {
|
||||
json = json["disk_usage"];
|
||||
if (json.contains("free"))
|
||||
free = json["free"].get<int>();
|
||||
}
|
||||
}
|
||||
|
||||
if (free < file_size) {
|
||||
http_status = 507;
|
||||
http_error = "Not enough storage on the printer.";
|
||||
}
|
||||
})
|
||||
.on_error([&](std::string body, std::string err, unsigned status) {
|
||||
http_status = status;
|
||||
http_error = std::move(err);
|
||||
response_body = std::move(body);
|
||||
})
|
||||
.perform_sync();
|
||||
}
|
||||
|
||||
if (http_status >= 400) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " failed with error code: " << http_status << ", " << http_error;
|
||||
@@ -1572,11 +1848,8 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params,
|
||||
}
|
||||
|
||||
auto http = Http::post(origin + "/server/files/upload");
|
||||
http.tls_verify(use_ssl);
|
||||
if (!ca_file.empty())
|
||||
http.ca_file(ca_file);
|
||||
if (!params.password.empty())
|
||||
http.header("X-Api-Key", params.password); // trusted LAN facades may not require it; harmless when they do not
|
||||
if (!api_key.empty())
|
||||
http.header("X-Api-Key", api_key); // bootstrapped façade key, or the access code fallback
|
||||
http.form_add("root", "gcodes")
|
||||
.form_add("print", "false")
|
||||
.form_add_file("file", source, upload_name)
|
||||
@@ -1637,59 +1910,6 @@ int OrcaPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn upd
|
||||
return start_sdcard_print(params, update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
// Serialize PrintParams::ams_mapping2 (the dialog's mapping_v1_json, one entry per logical
|
||||
// filament in preset order) into the print.gcode_file `filament_mapping` array. The array
|
||||
// position becomes `filament_index`; {255,255} (unmatched/unused) is dropped. Returns an
|
||||
// empty array when nothing usable remains so the caller can omit the field entirely.
|
||||
nlohmann::json OrcaPrinterAgent::build_filament_mapping(const std::string& ams_mapping2)
|
||||
{
|
||||
nlohmann::json mapping = nlohmann::json::array();
|
||||
if (ams_mapping2.empty())
|
||||
return mapping;
|
||||
|
||||
const nlohmann::json parsed = nlohmann::json::parse(ams_mapping2, nullptr, false);
|
||||
if (parsed.is_discarded() || !parsed.is_array()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: ams_mapping2 is not a JSON array; not sending filament_mapping";
|
||||
return mapping;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < parsed.size(); ++i) {
|
||||
const nlohmann::json& entry = parsed[i];
|
||||
if (!entry.is_object())
|
||||
continue;
|
||||
const auto ams_id_it = entry.find("ams_id");
|
||||
const auto slot_id_it = entry.find("slot_id");
|
||||
if (ams_id_it == entry.end() || slot_id_it == entry.end())
|
||||
continue;
|
||||
if (!ams_id_it->is_number_integer() || !slot_id_it->is_number_integer())
|
||||
continue;
|
||||
|
||||
const int ams_id = ams_id_it->get<int>();
|
||||
const int slot_id = slot_id_it->get<int>();
|
||||
if (ams_id == 255 && slot_id == 255)
|
||||
continue;
|
||||
|
||||
mapping.push_back({{"filament_index", static_cast<int>(i)}, {"ams_id", ams_id}, {"slot_id", slot_id}});
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
// Pure builder for the print.gcode_file payload: the base command plus, only
|
||||
// when non-empty, the filament_mapping array. An empty mapping leaves the
|
||||
// payload byte-identical to today's unmapped command.
|
||||
nlohmann::json OrcaPrinterAgent::build_gcode_file_payload(const std::string& sequence_id,
|
||||
const std::string& target,
|
||||
const nlohmann::json& filament_mapping)
|
||||
{
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "gcode_file";
|
||||
j["print"]["sequence_id"] = sequence_id;
|
||||
j["print"]["param"] = target;
|
||||
if (filament_mapping.is_array() && !filament_mapping.empty())
|
||||
j["print"]["filament_mapping"] = filament_mapping;
|
||||
return j;
|
||||
}
|
||||
|
||||
// Start a file that already lives on the printer by publishing the canonical
|
||||
// OPCP print.gcode_file command to device/<dev_id>/request. The acknowledgement
|
||||
// and lifecycle progress arrive asynchronously as print.push_status on the
|
||||
@@ -1707,23 +1927,10 @@ int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up
|
||||
// otherwise start what start_send_gcode_to_sdcard just uploaded to `gcodes`.
|
||||
const std::string target = params.dst_file.empty() ? remote_gcode_name(params) : fs::path(params.dst_file).filename().string();
|
||||
|
||||
// Per-print mapping. A mapped print is refused when the connector did not
|
||||
// advertise filament_mapping or the index correlation is unverified: the GUI
|
||||
// send gates make this visible first, and this is the defensive gate for
|
||||
// callers that bypass them (calibration, plugin). Never start a mapped print
|
||||
// with the map silently dropped.
|
||||
const nlohmann::json filament_mapping = build_filament_mapping(params.ams_mapping2);
|
||||
if (!filament_mapping.empty()) {
|
||||
const bool mapping_capable = filament_mapping_advertised(params.dev_id);
|
||||
if (!mapping_capable || !ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: refusing mapped print (capable=" << mapping_capable
|
||||
<< ", correlation_verified=" << ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED << ")";
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: start_sdcard_print emitting filament_mapping entries=" << filament_mapping.size();
|
||||
}
|
||||
|
||||
nlohmann::json j = build_gcode_file_payload(next_gcode_file_sequence_id(), target, filament_mapping);
|
||||
nlohmann::json j;
|
||||
j["print"]["command"] = "gcode_file";
|
||||
j["print"]["sequence_id"] = next_gcode_file_sequence_id();
|
||||
j["print"]["param"] = target;
|
||||
|
||||
if (update_fn)
|
||||
update_fn(PrintingStageSending, 0, "Starting print...");
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "OrcaMqttConnection.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -64,8 +66,6 @@ public:
|
||||
|
||||
// Print Job Operations
|
||||
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
// Unimplemented on OrcaSonar: reports a non-success result so callers fall back to
|
||||
// start_print() instead of treating the missing send as success.
|
||||
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
|
||||
@@ -84,10 +84,6 @@ public:
|
||||
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int 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,
|
||||
@@ -100,6 +96,13 @@ public:
|
||||
int sequence_id,
|
||||
bool lan_mode) override;
|
||||
|
||||
// Filament sync (subscription): `subscription` when the selected printer
|
||||
// reports a material system (get_capabilities protocol.features.fms) or the
|
||||
// filament-slot model (protocol.features.filament_slots), `none` otherwise.
|
||||
// The AMS view comes from the pushed `ams`/`vir_slot` in push_status, so
|
||||
// there is no fetch to keep fresh and the mode is transport-agnostic.
|
||||
FilamentSyncMode get_filament_sync_mode() const override;
|
||||
|
||||
// Test-only: drive emit_connect_sequence directly (no socket).
|
||||
void run_connect_sequence_for_test(const std::string& dev_id)
|
||||
{
|
||||
@@ -108,10 +111,19 @@ 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; }
|
||||
|
||||
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::subscription; }
|
||||
// Test-only: Bambu ams_* wire JSON -> canonical OrcaSonar bodies (§7.8).
|
||||
// Returns the (possibly rewritten) payload; *unsupported is set when the
|
||||
// device's declared ams_ops exclude the operation (CAP_NOT_AVAILABLE in
|
||||
// the live route_send path).
|
||||
static std::string canonicalize_ams_payload(const std::string& dev_id, const std::string& json_str, bool* unsupported);
|
||||
|
||||
// Test-only: the canonical ams_change_filament body for a BBL tray id
|
||||
// (ams_id*4 + tray). command_ams_select_tray routes this exact body.
|
||||
static std::string build_ams_change_filament_body(int tray_number, int sequence_id);
|
||||
|
||||
// Build an RFID request from coordinates or a legacy flat tray id.
|
||||
static std::string build_ams_refresh_rfid_body(int ams_id, int tray_or_slot_id, int sequence_id);
|
||||
|
||||
protected:
|
||||
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
|
||||
@@ -130,6 +142,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.
|
||||
@@ -142,18 +160,6 @@ protected:
|
||||
// Pure LAN-address parsing + client-id. protected static so the test Probe reaches them.
|
||||
static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port);
|
||||
static std::string make_lan_client_id(const std::string& dev_id);
|
||||
|
||||
// Pure serializer for PrintParams::ams_mapping2 -> print.gcode_file.filament_mapping.
|
||||
// Entries are re-keyed by their array position; {255,255} is dropped. Empty when the
|
||||
// input is empty, malformed, or has no usable entries. protected static for the test Probe.
|
||||
static nlohmann::json build_filament_mapping(const std::string& ams_mapping2);
|
||||
|
||||
// Pure builder for the print.gcode_file command payload. A non-empty mapping is
|
||||
// included as filament_mapping; an empty one is omitted so the payload is
|
||||
// byte-identical to an unmapped print. protected static for the test Probe.
|
||||
static nlohmann::json build_gcode_file_payload(const std::string& sequence_id,
|
||||
const std::string& target,
|
||||
const nlohmann::json& filament_mapping);
|
||||
// Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none).
|
||||
std::string lan_connection_target() const;
|
||||
// Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall,
|
||||
@@ -165,6 +171,17 @@ protected:
|
||||
virtual void emit_connect_sequence(const std::string& dev_id,
|
||||
std::function<void(const std::string&)> subscribe,
|
||||
std::function<void(const std::string&)> request);
|
||||
|
||||
// Re-ask a device for its capabilities and a full status. Sent when a
|
||||
// filament frame arrives before the capabilities reply (the topology
|
||||
// bootstrapped after connect, or Klipper restarted), so a session that
|
||||
// latched FilamentSyncMode::none can still reach subscription mode.
|
||||
virtual void request_filament_capabilities(const std::string& dev_id, bool local);
|
||||
|
||||
// deliver_to_sink hook: re-request capabilities on a filament frame while
|
||||
// the device's topology is still unconfirmed, throttled per device.
|
||||
void maybe_refresh_filament_capabilities(const std::string& dev_id, const std::string& payload, bool local);
|
||||
|
||||
static std::string seq(int n); // decimal string in the OrcaSlicer 20000..29999 band
|
||||
static std::string build_pushing_start(const std::string& sequence_id);
|
||||
static std::string build_pushing_stop(const std::string& sequence_id);
|
||||
@@ -203,12 +220,23 @@ private:
|
||||
|
||||
std::unique_ptr<OrcaSonarDiscovery> m_discovery;
|
||||
|
||||
std::string m_lan_dev_id; // guarded by state_mutex
|
||||
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
|
||||
bool m_lan_use_ssl = false; // guarded by state_mutex
|
||||
std::string m_lan_ca_file; // guarded by state_mutex
|
||||
std::string m_lan_dev_id; // guarded by state_mutex
|
||||
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
|
||||
std::string m_lan_password; // guarded by state_mutex — MQTT access code; X-Api-Key fallback
|
||||
std::string m_lan_api_key; // guarded by state_mutex — cached Moonraker-façade key, "" = unresolved
|
||||
uint64_t m_lan_api_key_gen = 0; // m_lan_generation the cached key belongs to
|
||||
CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex
|
||||
std::string m_camera_url; // guarded by state_mutex
|
||||
std::string m_camera_url; // guarded by state_mutex
|
||||
|
||||
// Last capability re-request per device; bounds the refresh to one per
|
||||
// device while a filament frame keeps arriving without a reply.
|
||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point> m_filament_caps_refresh_at; // guarded by state_mutex
|
||||
|
||||
// The Moonraker-façade X-Api-Key, bootstrapped from /access/api_key (trusted
|
||||
// clients only) and cached per connection generation; falls back to the
|
||||
// access code when the endpoint is unavailable (untrusted/hardened config).
|
||||
// Used by the LAN upload path.
|
||||
std::string lan_api_key(const std::string& origin);
|
||||
|
||||
OrcaCloudServiceAgent* get_orca_cloud_agent();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "QidiPrinterAgent.hpp"
|
||||
#include "Http.hpp"
|
||||
#include "IPrinterAgent.hpp"
|
||||
#include "slic3r/Utils/AmsPayload.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
@@ -66,13 +67,14 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode
|
||||
// Snapshot only what the fetch needs, rather than reading device_info live from the
|
||||
// background thread below — device_info can be concurrently rewritten by a reconnect
|
||||
// on another thread while this fetch is still in flight.
|
||||
ConnectionSettings connection = get_connection_settings();
|
||||
std::string base_url = device_info.base_url;
|
||||
std::string api_key = device_info.api_key;
|
||||
std::string model_id = device_info.model_id;
|
||||
std::string model_name = device_info.model_name;
|
||||
|
||||
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
std::thread([this, connection = std::move(connection), model_id, model_name]() mutable {
|
||||
std::thread([this, base_url, api_key, model_id, model_name]() {
|
||||
InFlightGuard guard{filament_fetch_in_flight};
|
||||
|
||||
std::string error;
|
||||
@@ -81,7 +83,7 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode
|
||||
std::string series_id;
|
||||
{
|
||||
MoonrakerDeviceInfo info;
|
||||
if (fetch_device_info(connection, info, error)) {
|
||||
if (fetch_device_info(base_url, api_key, info, error)) {
|
||||
series_id = infer_series_id(info.model_id, info.dev_name);
|
||||
}
|
||||
}
|
||||
@@ -92,14 +94,14 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode
|
||||
|
||||
// 2. Fetch filament dictionary
|
||||
QidiFilamentDict dict;
|
||||
if (!fetch_filament_dict(connection, dict, error)) {
|
||||
if (!fetch_filament_dict(base_url, api_key, dict, error)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error;
|
||||
}
|
||||
|
||||
// 3. Fetch slot info and build AmsTrayData directly
|
||||
std::vector<AmsTrayData> trays;
|
||||
int box_count = 0;
|
||||
if (!fetch_slot_info(connection, dict, series_id, trays, box_count, error)) {
|
||||
if (!fetch_slot_info(base_url, api_key, dict, series_id, trays, box_count, error)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error;
|
||||
return;
|
||||
}
|
||||
@@ -185,14 +187,15 @@ int QidiPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up
|
||||
return MoonrakerPrinterAgent::start_sdcard_print(std::move(params), update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::fetch_slot_info(const ConnectionSettings& connection,
|
||||
bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
const std::string& api_key,
|
||||
const QidiFilamentDict& dict,
|
||||
const std::string& series_id,
|
||||
std::vector<AmsTrayData>& trays,
|
||||
int& box_count,
|
||||
std::string& error)
|
||||
{
|
||||
std::string url = join_url(connection.base_url, "/printer/objects/query?save_variables=variables");
|
||||
std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables");
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button";
|
||||
}
|
||||
@@ -202,9 +205,8 @@ bool QidiPrinterAgent::fetch_slot_info(const ConnectionSettings& connection,
|
||||
std::string http_error;
|
||||
|
||||
auto http = Http::get(url);
|
||||
configure_http(http, connection);
|
||||
if (!connection.api_key.empty()) {
|
||||
http.header("X-Api-Key", connection.api_key);
|
||||
if (!api_key.empty()) {
|
||||
http.header("X-Api-Key", api_key);
|
||||
}
|
||||
http.timeout_connect(5)
|
||||
.timeout_max(10)
|
||||
@@ -332,20 +334,20 @@ bool QidiPrinterAgent::parse_slot_response(const std::string& response_body,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::fetch_filament_dict(const ConnectionSettings& connection,
|
||||
bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url,
|
||||
const std::string& api_key,
|
||||
QidiFilamentDict& dict,
|
||||
std::string& error) const
|
||||
{
|
||||
std::string url = join_url(connection.base_url, "/server/files/config/officiall_filas_list.cfg");
|
||||
std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg");
|
||||
|
||||
std::string response_body;
|
||||
bool success = false;
|
||||
std::string http_error;
|
||||
|
||||
auto http = Http::get(url);
|
||||
configure_http(http, connection);
|
||||
if (!connection.api_key.empty()) {
|
||||
http.header("X-Api-Key", connection.api_key);
|
||||
if (!api_key.empty()) {
|
||||
http.header("X-Api-Key", api_key);
|
||||
}
|
||||
http.timeout_connect(5)
|
||||
.timeout_max(10)
|
||||
@@ -506,26 +508,7 @@ std::string QidiPrinterAgent::infer_series_id(const std::string& model_id, const
|
||||
|
||||
std::string QidiPrinterAgent::normalize_filament_type(const std::string& filament_type)
|
||||
{
|
||||
const std::string upper = trim_and_upper(filament_type);
|
||||
|
||||
if (upper.find("PLA") != std::string::npos)
|
||||
return "PLA";
|
||||
if (upper.find("ABS") != std::string::npos)
|
||||
return "ABS";
|
||||
if (upper.find("PETG") != std::string::npos)
|
||||
return "PETG";
|
||||
if (upper.find("TPU") != std::string::npos)
|
||||
return "TPU";
|
||||
if (upper.find("ASA") != std::string::npos)
|
||||
return "ASA";
|
||||
if (upper.find("PA") != std::string::npos || upper.find("NYLON") != std::string::npos)
|
||||
return "PA";
|
||||
if (upper.find("PC") != std::string::npos)
|
||||
return "PC";
|
||||
if (upper.find("PVA") != std::string::npos)
|
||||
return "PVA";
|
||||
|
||||
return upper;
|
||||
return normalize_qidi_material_family(filament_type);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -47,13 +47,14 @@ private:
|
||||
};
|
||||
|
||||
// Qidi-specific methods
|
||||
bool fetch_slot_info(const ConnectionSettings& connection,
|
||||
bool fetch_slot_info(const std::string& base_url,
|
||||
const std::string& api_key,
|
||||
const QidiFilamentDict& dict,
|
||||
const std::string& series_id,
|
||||
std::vector<AmsTrayData>& trays,
|
||||
int& box_count,
|
||||
std::string& error);
|
||||
bool fetch_filament_dict(const ConnectionSettings& connection, QidiFilamentDict& dict, std::string& error) const;
|
||||
bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const;
|
||||
std::string normalize_filament_type(const std::string& filament_type);
|
||||
std::string infer_series_id(const std::string& model_id, const std::string& dev_name);
|
||||
std::string normalize_model_key(std::string value);
|
||||
|
||||
@@ -239,25 +239,6 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
|
||||
tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), safe_at(filament_sub_type, i, empty_str));
|
||||
tray.tray_color = safe_at(filament_color, i, default_color);
|
||||
|
||||
auto* bundle = GUI::wxGetApp().preset_bundle;
|
||||
// Try to find a matching preset for this filament based on vendor, type and color.
|
||||
// If not found, default to traditional search by type only or generic type mapping.
|
||||
if (bundle) {
|
||||
std::string vendor = safe_at(filament_vendor, i, empty_str);
|
||||
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type,
|
||||
tray.tray_color);
|
||||
|
||||
if (!filament_id.empty()) {
|
||||
tray.tray_info_idx = filament_id;
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "Filament sync: Found manufacturer-specific profile for slot " << i << ": " << filament_id;
|
||||
} else {
|
||||
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
|
||||
}
|
||||
} else {
|
||||
tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type);
|
||||
}
|
||||
|
||||
// Extract NFC temperature data if available
|
||||
if (nfc_info.is_array() && i < static_cast<int>(nfc_info.size()) && nfc_info[i].is_object()) {
|
||||
auto& nfc_slot = nfc_info[i];
|
||||
@@ -272,12 +253,46 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
|
||||
trays.emplace_back(std::move(tray));
|
||||
}
|
||||
|
||||
build_ams_payload(1, slot_count - 1, trays);
|
||||
// Preset matching (vendor + closest color) reads GUI preset state, so it
|
||||
// runs on the main thread via this resolver, not on the fetch worker.
|
||||
std::vector<std::string> vendors;
|
||||
vendors.reserve(slot_count);
|
||||
for (int i = 0; i < slot_count; ++i)
|
||||
vendors.push_back(safe_at(filament_vendor, i, empty_str));
|
||||
|
||||
build_ams_payload(1, slot_count - 1, trays,
|
||||
[vendors](std::vector<AmsTrayData>& resolved) { resolve_snapmaker_tray_info(resolved, vendors); });
|
||||
}).detach();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SnapmakerPrinterAgent::resolve_snapmaker_tray_info(std::vector<AmsTrayData>& trays,
|
||||
const std::vector<std::string>& vendors)
|
||||
{
|
||||
auto* bundle = GUI::wxGetApp().preset_bundle;
|
||||
for (auto& tray : trays) {
|
||||
if (!tray.has_filament)
|
||||
continue;
|
||||
const std::string vendor = (tray.slot_index >= 0 && tray.slot_index < static_cast<int>(vendors.size()))
|
||||
? vendors[tray.slot_index]
|
||||
: std::string();
|
||||
if (bundle) {
|
||||
// Try a matching preset by vendor, type and color; fall back to
|
||||
// type only, then to the generic family map.
|
||||
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type, tray.tray_color);
|
||||
if (!filament_id.empty()) {
|
||||
tray.tray_info_idx = filament_id;
|
||||
BOOST_LOG_TRIVIAL(warning) << "Filament sync: Found manufacturer-specific profile for slot " << tray.slot_index << ": " << filament_id;
|
||||
} else {
|
||||
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
|
||||
}
|
||||
} else {
|
||||
tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FilamentSyncMode SnapmakerPrinterAgent::get_filament_sync_mode() const
|
||||
{
|
||||
if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -28,6 +29,12 @@ private:
|
||||
// Combine filament_type + filament_sub_type into a unified type string
|
||||
static std::string combine_filament_type(const std::string& type, const std::string& sub_type);
|
||||
|
||||
// Resolve tray_info_idx for the collected trays. Reads the GUI preset
|
||||
// bundle, so it is invoked on the main thread from within the shared
|
||||
// build_ams_payload_for_device() (never on the fetch worker).
|
||||
static void resolve_snapmaker_tray_info(std::vector<AmsTrayData>& trays,
|
||||
const std::vector<std::string>& vendors);
|
||||
|
||||
void start_camera_monitor();
|
||||
void on_status_loop_tick(const std::string& dev_id) override;
|
||||
|
||||
|
||||
@@ -1407,6 +1407,21 @@ TEST_CASE("Filaments offered for a machine follow the app's compatibility rule",
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Printer model ids resolve to vendor display names", "[Preset][Bundle]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
VendorProfile qidi("Qidi");
|
||||
qidi.name = "Qidi";
|
||||
VendorProfile::PrinterModel model;
|
||||
model.model_id = "Qidi-Q1Pro";
|
||||
model.name = "Qidi Q1 Pro";
|
||||
qidi.models.push_back(model);
|
||||
bundle.vendors.emplace(qidi.id, qidi);
|
||||
|
||||
CHECK(bundle.get_printer_model_display_name("Qidi-Q1Pro") == "Qidi Q1 Pro");
|
||||
CHECK(bundle.get_printer_model_display_name("unknown-model").empty());
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -5482,3 +5497,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<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<ConfigOptionStrings>("filament_type")->values = {"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,8 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_dev_mapping.cpp
|
||||
test_filament_bitmap_utils.cpp
|
||||
test_device_progress.cpp
|
||||
test_device_manager.cpp
|
||||
test_ams_item.cpp
|
||||
test_device_manager_integration.cpp
|
||||
test_web_media_controller.cpp
|
||||
test_network_versions.cpp
|
||||
|
||||
@@ -127,9 +127,6 @@ public:
|
||||
// MQTT CONNECTs seen; increments again after a reconnect.
|
||||
int connect_count() const { return m_connect_count.load(); }
|
||||
|
||||
// MQTT PINGREQs seen while the client has no other traffic.
|
||||
int ping_count() const { return m_ping_count.load(); }
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
@@ -234,7 +231,6 @@ private:
|
||||
return true;
|
||||
}
|
||||
case 0xc0: // PINGREQ
|
||||
++m_ping_count;
|
||||
write_packet(stream, {0xd0, 0x00});
|
||||
return true;
|
||||
case 0xe0: // DISCONNECT
|
||||
@@ -281,8 +277,8 @@ private:
|
||||
}
|
||||
|
||||
// Every write - the worker's own replies and push_report() from the test
|
||||
// thread - is serialised by m_mutex. The production client keeps all of its
|
||||
// WebSocket operations on its MQTT worker instead.
|
||||
// thread - is serialised by m_mutex. beast permits a writer while the worker
|
||||
// is blocked in read(), which is the same arrangement OrcaMqttConnection uses.
|
||||
void write_packet(ws::stream<beast::tcp_stream>& stream, const std::vector<std::uint8_t>& packet)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -298,14 +294,11 @@ private:
|
||||
m_stream_ready = false;
|
||||
boost::system::error_code ec;
|
||||
auto& socket = beast::get_lowest_layer(*m_stream).socket();
|
||||
if (socket.cancel(ec))
|
||||
return;
|
||||
socket.cancel(ec);
|
||||
// shutdown() before close() is what actually wakes a blocking read on the
|
||||
// worker thread; close() alone does not on POSIX.
|
||||
if (socket.shutdown(tcp::socket::shutdown_both, ec))
|
||||
return;
|
||||
if (socket.close(ec))
|
||||
return;
|
||||
socket.shutdown(tcp::socket::shutdown_both, ec);
|
||||
socket.close(ec);
|
||||
}
|
||||
|
||||
const bool m_refuse_auth;
|
||||
@@ -315,7 +308,6 @@ private:
|
||||
std::thread m_thread;
|
||||
std::atomic_bool m_stopping{false};
|
||||
std::atomic<int> m_connect_count{0};
|
||||
std::atomic<int> m_ping_count{0};
|
||||
mutable std::mutex m_mutex;
|
||||
std::optional<ws::stream<beast::tcp_stream>> m_stream; // guarded by m_mutex
|
||||
bool m_stream_ready = false; // guarded by m_mutex
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// why: match the GUI include order to avoid rpcndr.h byte/std::byte
|
||||
// ambiguity in the Windows COM headers.
|
||||
#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 <wx/timer.h>
|
||||
|
||||
#include "slic3r/GUI/Widgets/AMSItem.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::GUI;
|
||||
|
||||
TEST_CASE("Configured empty and partial AMS trays remain distinct from unknown trays", "[AMSItem]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
machine.printer_agent_id = "orca";
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0,"ams":{
|
||||
"ams_exist_bits":"1","tray_exist_bits":"7","ams":[
|
||||
{"id":"0","info":"0001","tray":[
|
||||
{"id":"0","tag_uid":"0000000000000000","tray_info_idx":"","tray_type":"","tray_color":"00000000"},
|
||||
{"id":"1"},
|
||||
{"id":"2","tag_uid":"0000000000000000","tray_info_idx":"","tray_type":"PLA","tray_color":"FF0000FF"}
|
||||
]}
|
||||
]
|
||||
}}})", false);
|
||||
|
||||
const auto& ams_list = machine.GetFilaSystem()->GetAmsList();
|
||||
const auto ams_it = ams_list.find("0");
|
||||
REQUIRE(ams_it != ams_list.end());
|
||||
auto* ams = ams_it->second;
|
||||
REQUIRE(ams != nullptr);
|
||||
REQUIRE(ams->GetTray("0") != nullptr);
|
||||
CHECK(ams->GetTray("0")->is_exists);
|
||||
AMSinfo info;
|
||||
REQUIRE(info.parse_ams_info(&machine, ams));
|
||||
REQUIRE(info.cans.size() == 3);
|
||||
|
||||
CHECK(info.cans[0].is_empty);
|
||||
CHECK(info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND);
|
||||
CHECK_FALSE(info.cans[1].is_empty);
|
||||
CHECK(info.cans[1].material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND);
|
||||
CHECK_FALSE(info.cans[2].is_empty);
|
||||
CHECK(info.cans[2].material_state == AMSCanType::AMS_CAN_TYPE_THIRDBRAND);
|
||||
}
|
||||
|
||||
TEST_CASE("Empty external slots remain distinct from unknown slots", "[AMSItem]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
machine.printer_agent_id = "orca";
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0,"vir_slot":[
|
||||
{"id":"255","tag_uid":"0000000000000000","tray_info_idx":"","tray_type":"","tray_color":"00000000"},
|
||||
{"id":"254"}
|
||||
]}})", false);
|
||||
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
CHECK(machine.vt_slot[0].is_exists);
|
||||
AMSinfo empty_info;
|
||||
empty_info.parse_ext_info(&machine, machine.vt_slot[0]);
|
||||
CHECK(empty_info.cans[0].is_empty);
|
||||
CHECK(empty_info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL);
|
||||
|
||||
AMSinfo unknown_info;
|
||||
unknown_info.parse_ext_info(&machine, machine.vt_slot[1]);
|
||||
CHECK_FALSE(unknown_info.cans[0].is_empty);
|
||||
CHECK(unknown_info.cans[0].material_state == AMSCanType::AMS_CAN_TYPE_VIRTUAL);
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
// 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;
|
||||
|
||||
// Bambu virtual trays follow full-snapshot removals; Orca virtual trays are
|
||||
// retained after first observation until reconnect.
|
||||
|
||||
// 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");
|
||||
machine.printer_agent_id = "bbl";
|
||||
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");
|
||||
machine.printer_agent_id = "bbl";
|
||||
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.printer_agent_id = "bbl";
|
||||
|
||||
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.printer_agent_id = "bbl";
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
// 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.printer_agent_id = "orca";
|
||||
|
||||
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.printer_agent_id = "bbl";
|
||||
|
||||
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.printer_agent_id = "bbl";
|
||||
|
||||
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.printer_agent_id = "bbl";
|
||||
|
||||
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("Orca retains seen virtual trays across full snapshots", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
machine.printer_agent_id = "orca";
|
||||
|
||||
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":0,"vir_slot":[{"id":"255","tag_uid":"0123456789ABCDEF"}]}})", false);
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
CHECK(machine.vt_slot[0].tag_uid == "0123456789ABCDEF");
|
||||
CHECK(machine.vt_slot[1].id == "254");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0,"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);
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0}})", false);
|
||||
CHECK(machine.vt_slot.size() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("Orca forgets retained virtual trays at reconnect", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
machine.printer_agent_id = "orca";
|
||||
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.reset_orca_virtual_trays_for_reconnect();
|
||||
REQUIRE(machine.vt_slot.size() == 1);
|
||||
CHECK(machine.vt_slot[0].id == "255");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0,"vir_slot":[]}})", false);
|
||||
CHECK(machine.vt_slot.empty());
|
||||
CHECK_FALSE(machine.ams_support_virtual_tray);
|
||||
}
|
||||
|
||||
TEST_CASE("Orca stores a deputy virtual tray at its stable index", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
machine.printer_agent_id = "orca";
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0,"vir_slot":[{"id":"254"}]}})", false);
|
||||
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
CHECK(machine.vt_slot[0].id == "255");
|
||||
CHECK_FALSE(machine.vt_slot[0].is_exists);
|
||||
CHECK(machine.vt_slot[1].id == "254");
|
||||
CHECK(machine.vt_slot[1].is_exists);
|
||||
}
|
||||
|
||||
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]")
|
||||
{
|
||||
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());
|
||||
CHECK_FALSE(machine.is_orca_agent());
|
||||
|
||||
machine.printer_agent_id = "orca";
|
||||
CHECK_FALSE(machine.is_bbl_agent());
|
||||
CHECK(machine.is_orca_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));
|
||||
}
|
||||
|
||||
TEST_CASE("AMS filament acknowledgements report native sync state", "[DeviceManager]")
|
||||
{
|
||||
CHECK(MachineObject::ams_filament_native_sync_pending(json::parse(R"({"native_sync":"pending"})")));
|
||||
CHECK_FALSE(MachineObject::ams_filament_native_sync_pending(json::parse(R"({"native_sync":"applied"})")));
|
||||
CHECK(MachineObject::ams_filament_connector_only(json::parse(R"({"native_sync":"connector_only"})")));
|
||||
CHECK_FALSE(MachineObject::ams_filament_connector_only(json::parse(R"({"native_sync":"not_applicable"})")));
|
||||
}
|
||||
|
||||
// Every tray field in the echo is optional: an unguarded get would throw
|
||||
// type_error into parse_json's frame-wide catch and drop the rest of the push.
|
||||
TEST_CASE("A slim AMS filament acknowledgement leaves the tray untouched", "[DeviceManager]")
|
||||
{
|
||||
DevAmsTray tray("0");
|
||||
tray.color = "FF0000FF";
|
||||
tray.nozzle_temp_min = "190";
|
||||
tray.m_fila_type = "PLA";
|
||||
|
||||
CHECK_NOTHROW(MachineObject::fill_ams_filament_ack_tray(
|
||||
tray, json::parse(R"({"command":"ams_filament_setting","result":"success","errno":0})")));
|
||||
CHECK(tray.color == "FF0000FF");
|
||||
CHECK(tray.nozzle_temp_min == "190");
|
||||
CHECK(tray.m_fila_type == "PLA");
|
||||
|
||||
CHECK_NOTHROW(MachineObject::fill_ams_filament_ack_tray(
|
||||
tray, json::parse(R"({"tray_color":"0000FFFF","tray_type":"ABS","tray_info_idx":"GFA01","nozzle_temp_min":210,"nozzle_temp_max":240})")));
|
||||
CHECK(tray.color == "0000FFFF");
|
||||
CHECK(tray.setting_id == "GFA01");
|
||||
CHECK(tray.nozzle_temp_min == "210");
|
||||
CHECK(tray.nozzle_temp_max == "240");
|
||||
CHECK(tray.m_fila_type == "ABS");
|
||||
}
|
||||
|
||||
// An echo that carries no tray fields at all must leave the frame alive: the
|
||||
// frame-wide catch would silently swallow every later message in the push.
|
||||
TEST_CASE("An AMS filament acknowledgement without tray fields does not abort the frame", "[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","result":"success","errno":0,"ams_id":0,"tray_id":0}})", false));
|
||||
|
||||
const DevAmsTray* tray = machine.GetFilaSystem()->GetAmsTray("0", "0");
|
||||
REQUIRE(tray != nullptr);
|
||||
CHECK(tray->color.empty());
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
@@ -157,119 +157,3 @@ TEST_CASE("Device manager filters and rehomes devices by printer-agent ownership
|
||||
CHECK(manager.get_my_machine_list("integration-agent-a").empty());
|
||||
CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Orca capability reply populates connector-scope features and commands", "[DeviceManager][integration]")
|
||||
{
|
||||
ScopedAppConfig app_config;
|
||||
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("orca"));
|
||||
DeviceManager manager(&network, false, &app_config.config);
|
||||
|
||||
BBLocalMachine orca_machine;
|
||||
orca_machine.dev_id = "orca-device";
|
||||
orca_machine.dev_name = "Orca device";
|
||||
orca_machine.dev_ip = "192.0.2.21";
|
||||
orca_machine.printer_type = "C11";
|
||||
MachineObject* orca_obj = manager.insert_local_device(orca_machine, "lan", "free", "", "access-code");
|
||||
REQUIRE(orca_obj != nullptr);
|
||||
orca_obj->printer_agent_id = "orca";
|
||||
|
||||
orca_obj->parse_new_info2(json::parse(R"({
|
||||
"command": "get_capabilities",
|
||||
"supported_features": {"fms": true, "filament_slots": true, "filament_mapping": true},
|
||||
"supported_commands": ["print.push_status", "print.ams_get_rfid"],
|
||||
"capabilities": {
|
||||
"flags": {},
|
||||
"protocol": {
|
||||
"features": {"filament_mapping": true},
|
||||
"supported_commands": ["print.ams_change_filament"]
|
||||
}
|
||||
}
|
||||
})"));
|
||||
CHECK(orca_obj->is_support_fms);
|
||||
CHECK(orca_obj->is_support_filament_slots);
|
||||
CHECK(orca_obj->is_support_filament_mapping);
|
||||
CHECK(orca_obj->supported_commands.count("print.push_status") == 1);
|
||||
CHECK(orca_obj->supported_commands.count("print.ams_get_rfid") == 1);
|
||||
CHECK(orca_obj->supported_commands.count("print.ams_change_filament") == 1);
|
||||
|
||||
// Absent or false reads as unsupported, and a later reply without commands clears the set.
|
||||
orca_obj->parse_new_info2(json::parse(R"({
|
||||
"command": "get_capabilities",
|
||||
"supported_features": {"fms": false, "filament_slots": false, "filament_mapping": false},
|
||||
"capabilities": {"flags": {}}
|
||||
})"));
|
||||
CHECK_FALSE(orca_obj->is_support_fms);
|
||||
CHECK_FALSE(orca_obj->is_support_filament_slots);
|
||||
CHECK_FALSE(orca_obj->is_support_filament_mapping);
|
||||
CHECK(orca_obj->supported_commands.empty());
|
||||
|
||||
// A reply without capabilities.flags must still parse the Orca features and
|
||||
// commands (fail-closed: don't retain stale "supported" values).
|
||||
orca_obj->parse_new_info2(json::parse(R"({
|
||||
"command": "get_capabilities",
|
||||
"supported_features": {"filament_mapping": true},
|
||||
"capabilities": {
|
||||
"protocol": {"supported_commands": ["print.ams_get_rfid"]}
|
||||
}
|
||||
})"));
|
||||
CHECK(orca_obj->is_support_filament_mapping);
|
||||
CHECK_FALSE(orca_obj->is_support_filament_slots);
|
||||
CHECK(orca_obj->supported_commands.count("print.ams_get_rfid") == 1);
|
||||
|
||||
// A non-Orca agent id leaves the connector-scope fields untouched.
|
||||
BBLocalMachine bbl_machine;
|
||||
bbl_machine.dev_id = "bbl-device";
|
||||
bbl_machine.dev_name = "Bambu device";
|
||||
bbl_machine.dev_ip = "192.0.2.22";
|
||||
bbl_machine.printer_type = "C11";
|
||||
MachineObject* bbl_obj = manager.insert_local_device(bbl_machine, "lan", "free", "", "access-code");
|
||||
REQUIRE(bbl_obj != nullptr);
|
||||
bbl_obj->printer_agent_id = "bbl";
|
||||
|
||||
bbl_obj->parse_new_info2(json::parse(R"({
|
||||
"command": "get_capabilities",
|
||||
"supported_features": {"fms": true, "filament_slots": true, "filament_mapping": true},
|
||||
"supported_commands": ["print.push_status"],
|
||||
"capabilities": {
|
||||
"flags": {},
|
||||
"protocol": {
|
||||
"features": {"fms": true, "filament_slots": true, "filament_mapping": true},
|
||||
"supported_commands": ["print.ams_get_rfid"]
|
||||
}
|
||||
}
|
||||
})"));
|
||||
CHECK_FALSE(bbl_obj->is_support_fms);
|
||||
CHECK_FALSE(bbl_obj->is_support_filament_slots);
|
||||
CHECK_FALSE(bbl_obj->is_support_filament_mapping);
|
||||
CHECK(bbl_obj->supported_commands.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Orca per-command AMS gate requires fms and the advertised command", "[DeviceManager][integration]")
|
||||
{
|
||||
ScopedAppConfig app_config;
|
||||
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("orca"));
|
||||
DeviceManager manager(&network, false, &app_config.config);
|
||||
|
||||
BBLocalMachine machine;
|
||||
machine.dev_id = "orca-gate";
|
||||
machine.dev_name = "Orca gate";
|
||||
machine.dev_ip = "192.0.2.30";
|
||||
machine.printer_type = "C11";
|
||||
MachineObject* obj = manager.insert_local_device(machine, "lan", "free", "", "access-code");
|
||||
REQUIRE(obj != nullptr);
|
||||
obj->printer_agent_id = "orca";
|
||||
|
||||
// fms off: no macro-backed AMS command is allowed even if listed.
|
||||
obj->is_support_fms = false;
|
||||
obj->supported_commands.insert("print.ams_control");
|
||||
CHECK_FALSE(obj->orca_ams_command_supported("print.ams_control"));
|
||||
|
||||
// fms on: only the commands actually advertised are allowed.
|
||||
obj->is_support_fms = true;
|
||||
CHECK(obj->orca_ams_command_supported("print.ams_control"));
|
||||
CHECK_FALSE(obj->orca_ams_command_supported("print.ams_get_rfid"));
|
||||
|
||||
// Bambu keeps the legacy permissive path.
|
||||
obj->printer_agent_id = "bbl";
|
||||
CHECK(obj->orca_ams_command_supported("print.anything"));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -182,20 +182,6 @@ static void run_round_trip(bool use_tls_flag_only) {
|
||||
TEST_CASE("OrcaMqtt round-trip — LAN-style config", "[OrcaMqtt][.integration]") { run_round_trip(false); }
|
||||
TEST_CASE("OrcaMqtt round-trip — cloud-style config", "[OrcaMqtt][.integration]") { run_round_trip(true); }
|
||||
|
||||
TEST_CASE("OrcaMqtt keepalive runs while the connection is idle", "[OrcaMqtt][.integration]") {
|
||||
orca_mqtt_test::MockBroker broker;
|
||||
OrcaMqttConnection conn;
|
||||
OrcaMqttConnection::Config cfg;
|
||||
cfg.url = broker.ws_url();
|
||||
cfg.keepalive_seconds = 2;
|
||||
|
||||
REQUIRE(conn.start(cfg, [](const std::string&, const std::string&) {}, [](bool, bool) {}));
|
||||
for (int i = 0; i < 200 && broker.ping_count() == 0; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
CHECK(broker.ping_count() > 0);
|
||||
conn.stop();
|
||||
}
|
||||
|
||||
TEST_CASE("OrcaMqtt reconnects and re-subscribes after a socket drop", "[OrcaMqtt][.integration]") {
|
||||
orca_mqtt_test::MockBroker broker;
|
||||
OrcaMqttConnection conn;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <slic3r/GUI/FilamentMappingUtils.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <slic3r/Utils/AmsPayload.hpp>
|
||||
#include <slic3r/Utils/IPrinterAgent.hpp>
|
||||
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
|
||||
#include <slic3r/Utils/OrcaPrinterAgent.hpp>
|
||||
@@ -23,8 +24,6 @@ struct Probe : OrcaPrinterAgent {
|
||||
using OrcaPrinterAgent::parse_lan_endpoint;
|
||||
using OrcaPrinterAgent::make_lan_client_id;
|
||||
using OrcaPrinterAgent::lan_connection_target;
|
||||
using OrcaPrinterAgent::build_filament_mapping;
|
||||
using OrcaPrinterAgent::build_gcode_file_payload;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,7 +49,7 @@ TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto pus
|
||||
// is cached for the device.
|
||||
agent.deliver_to_sink(
|
||||
"dev-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"topology":{"tools":[{"id":"T0","nozzle":{"diameter_mm":0.4}}]}}}})",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"topology":{"tools":[{"id":"T0","nozzle":{"diameter_mm":0.4}}]},"protocol":{"ams_ops":["change_filament"]}}}})",
|
||||
/*local=*/false);
|
||||
CHECK(last_payload.find("\"command\":\"get_capabilities\"") != std::string::npos);
|
||||
CHECK(last_payload.find("\"print\"") == std::string::npos);
|
||||
@@ -69,6 +68,14 @@ TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto pus
|
||||
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","nozzle_diameter":0.6}})", /*local=*/false);
|
||||
CHECK(last_payload.find("\"nozzle_diameter\":0.6") != std::string::npos);
|
||||
CHECK(last_payload.find("N/A") == std::string::npos);
|
||||
|
||||
// The reply's declared ams_ops register on the device (OPCP §7.8): the one
|
||||
// declared op passes the client gate, an undeclared one is rejected.
|
||||
bool unsupported = false;
|
||||
OrcaPrinterAgent::canonicalize_ams_payload("dev-1", R"({"print":{"command":"ams_change_filament","target":1}})", &unsupported);
|
||||
CHECK_FALSE(unsupported);
|
||||
OrcaPrinterAgent::canonicalize_ams_payload("dev-1", R"({"print":{"command":"ams_control","param":"pause"}})", &unsupported);
|
||||
CHECK(unsupported);
|
||||
}
|
||||
|
||||
TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") {
|
||||
@@ -87,105 +94,10 @@ TEST_CASE("OrcaPrinterAgent::make_lan_client_id is stable and prefixed", "[OrcaP
|
||||
CHECK(a.rfind("orcaslicer-lan-dev-1-", 0) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("filament mapping is keyed by the ams_mapping2 array position", "[OrcaPrinterAgent]") {
|
||||
const nlohmann::json mapping = Probe::build_filament_mapping(
|
||||
R"([{"ams_id":1,"slot_id":5},{"ams_id":255,"slot_id":255},{"ams_id":255,"slot_id":0}])");
|
||||
REQUIRE(mapping.is_array());
|
||||
REQUIRE(mapping.size() == 2);
|
||||
CHECK(mapping[0]["filament_index"] == 0);
|
||||
CHECK(mapping[0]["ams_id"] == 1);
|
||||
CHECK(mapping[0]["slot_id"] == 5);
|
||||
// The unmatched middle entry is dropped; the third entry keeps index 2.
|
||||
CHECK(mapping[1]["filament_index"] == 2);
|
||||
CHECK(mapping[1]["ams_id"] == 255);
|
||||
CHECK(mapping[1]["slot_id"] == 0);
|
||||
}
|
||||
|
||||
// Index-correlation merge gate (plan PR 3). `ams_mapping2` is built one entry
|
||||
// per logical filament, so its array position is the identifier the generated
|
||||
// G-code toolchange passes to the Klipper macro (`next_filament_id`). The
|
||||
// serializer must key `filament_index` by that position and must never
|
||||
// re-densify after dropping unused/sentinel entries, or a used filament would
|
||||
// be aimed at the wrong lane. This test covers the plan's matrix: preset order
|
||||
// differing from used order, a middle filament unused, and external slots.
|
||||
TEST_CASE("filament mapping index correlates with the ams_mapping2 position", "[OrcaPrinterAgent]") {
|
||||
// Positions 0..4. Used filaments are 0, 2 and 4; 1 and 3 are unused.
|
||||
const nlohmann::json mapping = Probe::build_filament_mapping(
|
||||
R"([{"ams_id":0,"slot_id":1},{"ams_id":255,"slot_id":255},{"ams_id":2,"slot_id":3},{"ams_id":255,"slot_id":255},{"ams_id":255,"slot_id":0}])");
|
||||
REQUIRE(mapping.size() == 3);
|
||||
CHECK(mapping[0]["filament_index"] == 0);
|
||||
CHECK(mapping[1]["filament_index"] == 2);
|
||||
CHECK(mapping[2]["filament_index"] == 4); // external slot keeps its position
|
||||
CHECK(mapping[2]["ams_id"] == 255);
|
||||
CHECK(mapping[2]["slot_id"] == 0);
|
||||
// No re-densification: a used filament after a dropped sentinel keeps its
|
||||
// original logical index.
|
||||
for (const auto& entry : mapping)
|
||||
CHECK(entry.contains("filament_index"));
|
||||
}
|
||||
|
||||
TEST_CASE("filament mapping is omitted when nothing remains", "[OrcaPrinterAgent]") {
|
||||
CHECK(Probe::build_filament_mapping("").empty());
|
||||
CHECK(Probe::build_filament_mapping(R"([{"ams_id":255,"slot_id":255}])").empty());
|
||||
CHECK(Probe::build_filament_mapping("not json").empty());
|
||||
CHECK(Probe::build_filament_mapping(R"({"ams_id":1,"slot_id":0})").empty()); // not an array
|
||||
}
|
||||
|
||||
// The GUI capability gate and the agent serializer must classify the same
|
||||
// entries as engaged. External slots ({255,0}/{254,0}) are normalized as-is, so
|
||||
// they engage; only the {255,255} unmatched sentinel is dropped. A mismatch lets
|
||||
// an entry past the GUI and refused late with a generic publish error.
|
||||
TEST_CASE("the GUI mapping gate engages exactly the entries the serializer sends", "[OrcaPrinterAgent]") {
|
||||
using Slic3r::GUI::has_engaged_filament_mapping;
|
||||
CHECK_FALSE(has_engaged_filament_mapping(""));
|
||||
CHECK_FALSE(has_engaged_filament_mapping("[]"));
|
||||
CHECK_FALSE(has_engaged_filament_mapping("not json"));
|
||||
CHECK_FALSE(has_engaged_filament_mapping(R"([{"ams_id":255,"slot_id":255}])"));
|
||||
CHECK(has_engaged_filament_mapping(R"([{"ams_id":255,"slot_id":0}])")); // external main
|
||||
CHECK(has_engaged_filament_mapping(R"([{"ams_id":254,"slot_id":0}])")); // external deputy
|
||||
CHECK(has_engaged_filament_mapping(R"([{"ams_id":0,"slot_id":0}])")); // box slot
|
||||
CHECK(has_engaged_filament_mapping(R"([{"ams_id":255,"slot_id":255},{"ams_id":1,"slot_id":2}])"));
|
||||
|
||||
for (const char* s : {"", "[]", "not json", R"([{"ams_id":255,"slot_id":255}])",
|
||||
R"([{"ams_id":255,"slot_id":0}])", R"([{"ams_id":254,"slot_id":0}])",
|
||||
R"([{"ams_id":0,"slot_id":0}])",
|
||||
R"([{"ams_id":255,"slot_id":255},{"ams_id":1,"slot_id":2}])"}) {
|
||||
CHECK(has_engaged_filament_mapping(s) == !Probe::build_filament_mapping(s).empty());
|
||||
}
|
||||
}
|
||||
|
||||
// An empty mapping must leave the gcode_file payload byte-identical to today:
|
||||
// exactly command, sequence_id and param, with no filament_mapping key.
|
||||
TEST_CASE("gcode_file payload omits filament_mapping when the map is empty", "[OrcaPrinterAgent]") {
|
||||
const nlohmann::json empty = Probe::build_gcode_file_payload("7", "job.gcode", nlohmann::json::array());
|
||||
REQUIRE(empty.contains("print"));
|
||||
CHECK(empty["print"].size() == 3);
|
||||
CHECK(empty["print"]["command"] == "gcode_file");
|
||||
CHECK(empty["print"]["sequence_id"] == "7");
|
||||
CHECK(empty["print"]["param"] == "job.gcode");
|
||||
CHECK_FALSE(empty["print"].contains("filament_mapping"));
|
||||
|
||||
const nlohmann::json mapping = Probe::build_filament_mapping(R"([{"ams_id":1,"slot_id":0},{"ams_id":255,"slot_id":255}])");
|
||||
const nlohmann::json with = Probe::build_gcode_file_payload("8", "job.gcode", mapping);
|
||||
REQUIRE(with["print"].contains("filament_mapping"));
|
||||
REQUIRE(with["print"]["filament_mapping"].size() == 1);
|
||||
CHECK(with["print"]["filament_mapping"][0]["filament_index"] == 0);
|
||||
}
|
||||
|
||||
// The FTP "send with record" transport does not exist on OrcaSonar. It must
|
||||
// report a non-success result so PrintJob falls back to start_print() rather
|
||||
// than treating a print that was never sent as successful.
|
||||
TEST_CASE("start_local_print_with_record never reports silent success", "[OrcaPrinterAgent]") {
|
||||
Probe agent("/tmp");
|
||||
Slic3r::PrintParams params;
|
||||
const int rc = agent.start_local_print_with_record(params, {}, {}, {});
|
||||
CHECK(rc < 0);
|
||||
}
|
||||
|
||||
TEST_CASE("connect_printer wires up a LAN Config", "[OrcaPrinterAgent][.integration]") {
|
||||
Probe agent("/tmp");
|
||||
Slic3r::PrinterConnectionParams params{
|
||||
"dev-1", "10.255.255.1", "orcasonar", "code", "", false, ""
|
||||
"dev-1", "10.255.255.1", "", "orcasonar", "code", false, ""
|
||||
};
|
||||
const int rc = agent.connect_printer(params);
|
||||
CHECK(rc == BAMBU_NETWORK_SUCCESS);
|
||||
@@ -194,6 +106,94 @@ TEST_CASE("connect_printer wires up a LAN Config", "[OrcaPrinterAgent][.integrat
|
||||
agent.disconnect_printer();
|
||||
}
|
||||
|
||||
// why hidden: connect_printer starts a detached connect worker, like the LAN
|
||||
// test above. The sync mode follows the printer's own AMS capability, not the
|
||||
// transport: a connected printer stays `none` until its get_capabilities reply
|
||||
// declares a material system. The registry is process-wide, so this test uses
|
||||
// ids no other test feeds capabilities for.
|
||||
TEST_CASE("filament sync follows the printer's AMS capability", "[OrcaPrinterAgent][.integration]") {
|
||||
Probe agent("/tmp");
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
|
||||
|
||||
REQUIRE(agent.connect_printer(Slic3r::PrinterConnectionParams{
|
||||
"dev-ams-1", "10.255.255.1", "", "orcasonar", "code", false, ""
|
||||
}) == BAMBU_NETWORK_SUCCESS);
|
||||
// Connected, but no capability reply yet: still none.
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
|
||||
|
||||
// features.fms=true is the authoritative material-system flag.
|
||||
agent.deliver_to_sink("dev-ams-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":true},"ams_ops":["change_filament"]}}}})",
|
||||
/*local=*/true);
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
|
||||
|
||||
// An explicit false clears it again.
|
||||
agent.deliver_to_sink("dev-ams-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":false}}}}})",
|
||||
/*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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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");
|
||||
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"));
|
||||
|
||||
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]") {
|
||||
struct SeqProbe : OrcaPrinterAgent {
|
||||
using OrcaPrinterAgent::OrcaPrinterAgent;
|
||||
@@ -276,7 +276,7 @@ TEST_CASE("send_message_to_printer publishes on the LAN connection", "[OrcaPrint
|
||||
orca_mqtt_test::MockBroker broker;
|
||||
OrcaPrinterAgent agent("/tmp");
|
||||
const auto ep = broker.host_port();
|
||||
agent.connect_printer(Slic3r::PrinterConnectionParams{"dev-1", ep.first + ":" + ep.second, "orcasonar", "code", "", false, ""});
|
||||
agent.connect_printer(Slic3r::PrinterConnectionParams{"dev-1", ep.first + ":" + ep.second, "", "orcasonar", "code", false, ""});
|
||||
|
||||
for (int i = 0; i < 150 && broker.connect_count() == 0; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
@@ -299,8 +299,357 @@ TEST_CASE("send_message_to_printer publishes on the LAN connection", "[OrcaPrint
|
||||
TEST_CASE("destroying an agent mid-connect does not hang or crash", "[OrcaPrinterAgent]") {
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
auto agent = std::make_unique<OrcaPrinterAgent>("/tmp");
|
||||
agent->connect_printer(Slic3r::PrinterConnectionParams{"dev-1", "127.0.0.1:1", "orcasonar", "code", "", false, ""}); // nothing listening: instant ECONNREFUSED
|
||||
agent->connect_printer(Slic3r::PrinterConnectionParams{"dev-1", "127.0.0.1:1", "", "orcasonar", "code", false, ""}); // nothing listening: instant ECONNREFUSED
|
||||
agent.reset(); // ~OrcaPrinterAgent must stop the conn, join the thread, and not hang/crash
|
||||
}
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
// OPCP spec §7.8: Bambu wire conventions must be decoded at the agent funnel,
|
||||
// never smuggled through as numeric lanes (an external-spool select once read
|
||||
// as slot_id=0 and physically loaded gate 0).
|
||||
TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical OrcaSonar bodies", "[OrcaPrinterAgent]") {
|
||||
auto canon = [](const std::string& dev, const std::string& in, bool* unsupported = nullptr) {
|
||||
bool local = false;
|
||||
return OrcaPrinterAgent::canonicalize_ams_payload(dev, in, unsupported ? unsupported : &local);
|
||||
};
|
||||
|
||||
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");
|
||||
// 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"]["tar_temp"] == 220);
|
||||
|
||||
// External slots retain their tool identity through the coordinate address.
|
||||
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"] == "lane");
|
||||
CHECK(out["print"]["ams_id"] == 255);
|
||||
CHECK(out["print"]["slot_id"] == 0);
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("target"));
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":254,"target":254,"slot_id":0}})"));
|
||||
CHECK(out["print"]["selector"] == "lane");
|
||||
CHECK(out["print"]["ams_id"] == 254);
|
||||
|
||||
const std::string legacy_unload = R"({"print":{"command":"ams_change_filament","ams_id":0,"target":255,"slot_id":255}})";
|
||||
CHECK(canon("dev-c1", legacy_unload) == legacy_unload);
|
||||
|
||||
// Target-only requests keep the legacy macro path.
|
||||
const std::string target_only = R"({"print":{"command":"ams_change_filament","target":6}})";
|
||||
CHECK(canon("dev-c1", target_only) == target_only);
|
||||
|
||||
// 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"]["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");
|
||||
|
||||
// tray_color is the one field this sync exists to carry: it must survive
|
||||
// the funnel, both as a real color and as the empty-tray marker a reset
|
||||
// sends.
|
||||
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","tray_color":"00FF00FF","tray_info_idx":"GFL01"}})"));
|
||||
CHECK(out["print"]["tray_color"] == "00FF00FF");
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":2,"tray_id":2,"tray_type":"","tray_info_idx":"","tray_color":"00000000"}})"));
|
||||
CHECK(out["print"]["tray_color"] == "00000000");
|
||||
CHECK(out["print"]["tray_type"] == "");
|
||||
|
||||
// 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);
|
||||
CHECK(!out["print"].contains("ams_id"));
|
||||
|
||||
const std::string canonical = R"({"print":{"command":"ams_change_filament","selector":"lane","lane":3}})";
|
||||
CHECK(canon("dev-c1", canonical) == canonical);
|
||||
const std::string combined = R"({"print":{"command":"ams_change_filament","target":3,"selector":"lane","lane":3}})";
|
||||
CHECK(canon("dev-c1", combined) == combined);
|
||||
CHECK(canon("dev-c1", R"({"print":{"command":"pause"}})") == R"({"print":{"command":"pause"}})");
|
||||
CHECK(canon("dev-c1", "not json at all") == "not json at all");
|
||||
|
||||
// A legacy unload still uses the legacy macro path, which is covered by
|
||||
// change_filament; selector unload requires its own token.
|
||||
Slic3r::register_ams_ops("dev-c2", {"change_filament"});
|
||||
Slic3r::register_ams_capability("dev-c2", true);
|
||||
bool unsupported = false;
|
||||
canon("dev-c2", R"({"print":{"command":"ams_change_filament","target":255,"slot_id":255}})", &unsupported);
|
||||
CHECK_FALSE(unsupported);
|
||||
unsupported = false;
|
||||
canon("dev-c2", R"({"print":{"command":"ams_control","param":"pause"}})", &unsupported);
|
||||
CHECK(unsupported);
|
||||
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);
|
||||
unsupported = false;
|
||||
canon("dev-c2", R"({"print":{"command":"ams_change_filament","target":254,"ams_id":254,"slot_id":0}})", &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"});
|
||||
Slic3r::register_ams_capability("dev-c4", true);
|
||||
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":"lane","ams_id":0,"slot_id":0}})", &unsupported);
|
||||
CHECK(unsupported);
|
||||
unsupported = false;
|
||||
canon("dev-c4", R"({"print":{"command":"ams_change_filament","selector":"lane","ams_id":254,"slot_id":0}})", &unsupported);
|
||||
CHECK_FALSE(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);
|
||||
CHECK(!unsupported);
|
||||
}
|
||||
|
||||
// filament_setting is advertised by filament_slots alone, independently of fms
|
||||
// and ams_ops (OPCP §7.8).
|
||||
TEST_CASE("filament slot writes remain independent of FMS", "[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);
|
||||
|
||||
agent.deliver_to_sink("dev-slots",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":false,"filament_slots":true},"ams_ops":["change_filament"]}}}}})",
|
||||
/*local=*/true);
|
||||
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);
|
||||
|
||||
agent.deliver_to_sink("dev-no-slots",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":true,"filament_slots":false},"ams_ops":["filament_setting"]}}}})",
|
||||
/*local=*/true);
|
||||
unsupported = false;
|
||||
OrcaPrinterAgent::canonicalize_ams_payload(
|
||||
"dev-no-slots",
|
||||
R"({"print":{"command":"ams_filament_setting","ams_id":0,"slot_id":0,"tray_type":"PLA"}})",
|
||||
&unsupported);
|
||||
CHECK(unsupported);
|
||||
}
|
||||
|
||||
// A fixed-vocabulary printer publishes the values it can hold, so the dialog
|
||||
// offers only those: an exact type and its variants pass the filter, a material
|
||||
// the printer lacks does not. A free-form driver and an unanswered device stay
|
||||
// unconstrained.
|
||||
TEST_CASE("A printer publishes its filament metadata values", "[OrcaPrinterAgent]") {
|
||||
Probe agent("/tmp");
|
||||
agent.deliver_to_sink("dev-qidi",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{
|
||||
"filament_metadata":{"qidi_box":{"read":["material","color"],"write":["material","color"],
|
||||
"values":{"material":["PLA","PETG"],"color":["FF0000FF","00FF00FF"]}}}}}}})",
|
||||
/*local=*/true);
|
||||
|
||||
const Slic3r::FilamentMetadataValues values = Slic3r::filament_metadata_values("dev-qidi");
|
||||
CHECK(values.constrained);
|
||||
CHECK(values.materials.size() == 2);
|
||||
CHECK(values.colors.size() == 2);
|
||||
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi", "PLA"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi", "PETG"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi", "PLA+"));
|
||||
CHECK_FALSE(Slic3r::filament_material_compatible("dev-qidi", "HIPS"));
|
||||
|
||||
CHECK(Slic3r::normalize_qidi_material_family("PCTG") == "PCTG");
|
||||
CHECK(Slic3r::normalize_qidi_material_family("PPA") == "PPA");
|
||||
CHECK(Slic3r::normalize_qidi_material_family("PLA-CF") == "PLA-CF");
|
||||
CHECK(Slic3r::normalize_qidi_material_family("PA12-CF") == "PA");
|
||||
CHECK(Slic3r::normalize_qidi_material_family("PC-ABS-FR") == "PC");
|
||||
|
||||
agent.deliver_to_sink("dev-qidi-polymer",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{
|
||||
"filament_metadata":{"qidi_box":{"read":["material","color"],"write":["material","color"],
|
||||
"values":{"material":["PC","PA"],"color":["FF0000FF"]}}}}}}})",
|
||||
/*local=*/true);
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PC"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PA Nylon"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PA-CF"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PA12-CF"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PAHT-CF"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PAHT-GF"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PAHT-S"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "UltraPA"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "UltraPA-CF25"));
|
||||
CHECK(Slic3r::filament_material_compatible("dev-qidi-polymer", "PC-ABS-FR"));
|
||||
CHECK_FALSE(Slic3r::filament_material_compatible("dev-qidi-polymer", "PCTG"));
|
||||
CHECK_FALSE(Slic3r::filament_material_compatible("dev-qidi-polymer", "PPA"));
|
||||
|
||||
agent.deliver_to_sink("dev-happyhare",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{
|
||||
"filament_metadata":{"happy_hare":{"read":["material","color"],"write":["material","color"]}}}}}})",
|
||||
/*local=*/true);
|
||||
CHECK_FALSE(Slic3r::filament_metadata_values("dev-happyhare").constrained);
|
||||
CHECK(Slic3r::filament_material_compatible("dev-happyhare", "HIPS"));
|
||||
|
||||
CHECK(Slic3r::filament_material_compatible("dev-unanswered", "HIPS"));
|
||||
}
|
||||
|
||||
TEST_CASE("An unknown lane material does not bind to an arbitrary filament id", "[OrcaPrinterAgent]") {
|
||||
AmsTrayData tray;
|
||||
tray.has_filament = true;
|
||||
tray.tray_type = "Unlisted polymer family";
|
||||
std::vector<AmsTrayData> trays{tray};
|
||||
|
||||
Slic3r::resolve_tray_info_idx(trays);
|
||||
|
||||
CHECK(trays[0].tray_info_idx.empty());
|
||||
}
|
||||
|
||||
// A box wider than 4 slots is shown as several 4-tray units, so the BBL tray id
|
||||
// a panel reports must split into that unit's own (ams_id, slot_id). A flat lane
|
||||
// would be the tray id, which is not the layout lane for a wide box
|
||||
// (REQ-STS-008 §7.8).
|
||||
TEST_CASE("an AMS tray selection sends the tray's ams_id and slot_id", "[OrcaPrinterAgent]") {
|
||||
// Unit 2 tray 1 is BBL tray 9: a 6-slot box's slot 5 addressed as (2, 1).
|
||||
auto body = nlohmann::json::parse(OrcaPrinterAgent::build_ams_change_filament_body(9, 123));
|
||||
CHECK(body["print"]["selector"] == "lane");
|
||||
CHECK(body["print"]["ams_id"] == 2);
|
||||
CHECK(body["print"]["slot_id"] == 1);
|
||||
CHECK_FALSE(body["print"].contains("lane"));
|
||||
|
||||
// Unit 0 tray 3 is tray 3.
|
||||
body = nlohmann::json::parse(OrcaPrinterAgent::build_ams_change_filament_body(3, 124));
|
||||
CHECK(body["print"]["ams_id"] == 0);
|
||||
CHECK(body["print"]["slot_id"] == 3);
|
||||
|
||||
body = nlohmann::json::parse(OrcaPrinterAgent::build_ams_change_filament_body(254, 125));
|
||||
CHECK(body["print"]["ams_id"] == 254);
|
||||
CHECK(body["print"]["slot_id"] == 0);
|
||||
body = nlohmann::json::parse(OrcaPrinterAgent::build_ams_change_filament_body(255, 126));
|
||||
CHECK(body["print"]["ams_id"] == 255);
|
||||
CHECK(body["print"]["slot_id"] == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("RFID refresh maps coordinates and preserves flat tray ids", "[OrcaPrinterAgent]") {
|
||||
auto body = nlohmann::json::parse(OrcaPrinterAgent::build_ams_refresh_rfid_body(1, 2, 127));
|
||||
CHECK(body["print"]["tray_id"] == 6);
|
||||
|
||||
body = nlohmann::json::parse(OrcaPrinterAgent::build_ams_refresh_rfid_body(-1, 9, 128));
|
||||
CHECK(body["print"]["tray_id"] == 9);
|
||||
|
||||
body = nlohmann::json::parse(OrcaPrinterAgent::build_ams_refresh_rfid_body(254, 0, 129));
|
||||
CHECK(body["print"]["tray_id"] == 254);
|
||||
}
|
||||
|
||||
// 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(Slic3r::PrinterConnectionParams{
|
||||
"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();
|
||||
}
|
||||
|
||||
@@ -68,12 +68,171 @@ TEST_CASE("Moonraker parses nozzle diameter from raw config and tolerates missin
|
||||
CHECK(MoonrakerParserProbe::parse_nozzle_diameter(missing_response) == 0.0f);
|
||||
}
|
||||
|
||||
// why: the lane_data projection is shared by Moonraker and OrcaSonar; the
|
||||
// parser is pure so it can be checked without a live printer or a GUI.
|
||||
TEST_CASE("unit: lane_data projection maps lanes into AMS trays", "[unit][moonraker]")
|
||||
{
|
||||
const auto body = nlohmann::json::parse(R"({
|
||||
"result": {
|
||||
"namespace": "lane_data",
|
||||
"value": {
|
||||
"lane0": {"lane": "0", "material": "PLA", "color": "FF0000FF"},
|
||||
"lane1": {"lane": "1"},
|
||||
"lane2": {"lane": "not-a-number"},
|
||||
"lane3": {"lane": "3", "material": "PETG", "color": "#00FF00", "nozzle_temp": 240}
|
||||
}
|
||||
}
|
||||
})");
|
||||
|
||||
std::vector<AmsTrayData> trays;
|
||||
int max_lane_index = -1;
|
||||
REQUIRE(parse_moonraker_lane_data(body, trays, max_lane_index));
|
||||
REQUIRE(trays.size() == 3);
|
||||
CHECK(trays[0].slot_index == 0);
|
||||
CHECK(trays[0].has_filament);
|
||||
CHECK(trays[0].tray_type == "PLA");
|
||||
CHECK(trays[0].tray_color == "FF0000FF");
|
||||
CHECK(trays[1].slot_index == 1);
|
||||
CHECK_FALSE(trays[1].has_filament);
|
||||
CHECK(trays[1].tray_type.empty());
|
||||
CHECK(trays[2].slot_index == 3);
|
||||
CHECK(trays[2].nozzle_temp == 240);
|
||||
CHECK(max_lane_index == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("unit: lane_data projection rejects malformed and empty responses", "[unit][moonraker]")
|
||||
{
|
||||
std::vector<AmsTrayData> trays;
|
||||
int max_lane_index = -1;
|
||||
CHECK_FALSE(parse_moonraker_lane_data(nlohmann::json::object(), trays, max_lane_index));
|
||||
CHECK_FALSE(parse_moonraker_lane_data(nlohmann::json::parse(R"({"result":{"value":{}}})"), trays, max_lane_index));
|
||||
CHECK_FALSE(parse_moonraker_lane_data(
|
||||
nlohmann::json::parse(R"({"result":{"value":{"lane0":{"lane":"x"},"lane1":42}}})"), trays, max_lane_index));
|
||||
}
|
||||
|
||||
// why: OrcaSonar projects sensed presence separately from declared material, so a
|
||||
// loaded lane with no type must not collapse to "empty".
|
||||
TEST_CASE("unit: lane_data presence is independent of declared material", "[unit][moonraker]")
|
||||
{
|
||||
const auto body = nlohmann::json::parse(R"({
|
||||
"result": {"value": {
|
||||
"lane0": {"lane": "0", "has_filament": true},
|
||||
"lane1": {"lane": "1", "has_filament": false, "material": "PLA"},
|
||||
"lane2": {"lane": "2", "loaded": true},
|
||||
"lane3": {"lane": "3", "material": "ASA"}
|
||||
}}
|
||||
})");
|
||||
|
||||
std::vector<AmsTrayData> trays;
|
||||
int max_lane_index = -1;
|
||||
REQUIRE(parse_moonraker_lane_data(body, trays, max_lane_index));
|
||||
REQUIRE(trays.size() == 4);
|
||||
|
||||
CHECK(trays[0].has_filament);
|
||||
CHECK(trays[0].tray_type.empty());
|
||||
|
||||
CHECK_FALSE(trays[1].has_filament);
|
||||
CHECK(trays[1].tray_type == "PLA");
|
||||
|
||||
CHECK(trays[2].has_filament);
|
||||
|
||||
CHECK(trays[3].has_filament);
|
||||
CHECK(trays[3].tray_type == "ASA");
|
||||
}
|
||||
|
||||
// why: the exist bits and per-slot placeholder flag drive the sidebar's occupied
|
||||
// vs empty rendering; the builder is pure so this needs no GUI.
|
||||
TEST_CASE("unit: AMS payload sets exist bits and omits placeholder for loaded lanes", "[unit][moonraker]")
|
||||
{
|
||||
std::vector<AmsTrayData> trays = {
|
||||
{0, true, "PLA", "FF0000FF", "", 0, 0},
|
||||
{2, true, "", "", "", 0, 0}, // loaded but undeclared
|
||||
{5, true, "PETG", "00FF00", "", 0, 0},
|
||||
};
|
||||
const auto ams = build_bbl_ams_json(trays, 2, 5);
|
||||
|
||||
// Units 0 and 1 exist; slots 0, 2 and 5 hold filament (0b100101).
|
||||
CHECK(ams["ams_exist_bits"].get<std::string>() == "3");
|
||||
CHECK(ams["tray_exist_bits"].get<std::string>() == "25");
|
||||
|
||||
const auto& u0 = ams["ams"][0]["tray"];
|
||||
CHECK_FALSE(u0[0].contains("tray_slot_placeholder"));
|
||||
CHECK(u0[1].contains("tray_slot_placeholder"));
|
||||
CHECK_FALSE(u0[2].contains("tray_slot_placeholder"));
|
||||
CHECK(u0[2]["tray_type"].get<std::string>() == "");
|
||||
CHECK(u0[2]["tray_color"].get<std::string>() == "00000000");
|
||||
|
||||
const auto& u1 = ams["ams"][1]["tray"];
|
||||
CHECK(u1[0].contains("tray_slot_placeholder")); // slot 4 absent
|
||||
CHECK_FALSE(u1[1].contains("tray_slot_placeholder")); // slot 5 present
|
||||
CHECK(u1[1]["tray_color"].get<std::string>() == "00FF00FF"); // 6-hex padded
|
||||
}
|
||||
|
||||
// why: the exist-bits fields are 64-bit; multi-box rigs past lane 31 would be
|
||||
// corrupted by a 32-bit unsigned long shift on MSVC. Guards the 64-bit path.
|
||||
TEST_CASE("unit: AMS payload sets exist bits beyond 31 lanes", "[unit][moonraker]")
|
||||
{
|
||||
std::vector<AmsTrayData> trays = {
|
||||
{33, true, "PLA", "FF0000FF", "", 0, 0},
|
||||
};
|
||||
const auto ams = build_bbl_ams_json(trays, 9, 33);
|
||||
|
||||
CHECK(ams["ams_exist_bits"].get<std::string>() == "1FF"); // units 0..8
|
||||
CHECK(ams["tray_exist_bits"].get<std::string>() == "200000000"); // 1 << 33
|
||||
|
||||
const auto& u8 = ams["ams"][8]["tray"];
|
||||
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]")
|
||||
{
|
||||
CHECK_FALSE(has_ams_capability("cap-none"));
|
||||
register_ams_capability("cap-true", true);
|
||||
CHECK(has_ams_capability("cap-true"));
|
||||
register_ams_capability("cap-false", false);
|
||||
CHECK_FALSE(has_ams_capability("cap-false"));
|
||||
// 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.
|
||||
TEST_CASE("unit: BBL AMS gcode builders preserve command bytes", "[unit][bbl]")
|
||||
{
|
||||
CHECK(BBLPrinterAgent::ams_refresh_rfid_gcode("123") == "M620 R123 \n");
|
||||
CHECK(BBLPrinterAgent::ams_calibrate_gcode(123) == "M620 C123 \n");
|
||||
CHECK(BBLPrinterAgent::ams_select_tray_gcode("123") == "M620 P123 \n");
|
||||
}
|
||||
|
||||
// why: an agent without a Bambu-dialect translation must refuse these commands before any network or wx path.
|
||||
TEST_CASE("unit: default AMS commands report not supported", "[unit][moonraker]")
|
||||
{
|
||||
MoonrakerPrinterAgent agent("");
|
||||
|
||||
CHECK(agent.command_ams_refresh_rfid("dev", 123, 1, 0, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.command_ams_refresh_rfid("dev", -1, 123, 1, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.command_ams_calibrate("dev", 1, 2, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.command_ams_select_tray("dev", "123", 3, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user