mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-29 11:51:24 +00:00
feat: enable filament mapping and resolve printer-set AMS trays by material type
Drop the index-correlation gate so mapped prints are gated only on the filament_mapping capability Resolve AMS trays set on the printer UI to the matching Generic preset using their material type and color Skip the forced external-spool selection on printers without an AMS and add coverage for the mapping gates
This commit is contained in:
@@ -3708,42 +3708,58 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
|||||||
ams_array_maps.push_back(temp);
|
ams_array_maps.push_back(temp);
|
||||||
index++;
|
index++;
|
||||||
if (filament_id.empty()) {
|
if (filament_id.empty()) {
|
||||||
if (use_map) {
|
// A tray written by the printer UI carries a material type but no
|
||||||
for (int j = maps.size() - 1; j >= 0; j--) {
|
// OrcaSlicer preset id. Resolve it to the matching Generic preset with
|
||||||
if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) {
|
// the tray's own color instead of dropping it (direct sync) or forcing
|
||||||
maps.erase(j);
|
// "Generic PLA" (mapping sync). Placeholders and typeless trays keep
|
||||||
|
// the previous behavior.
|
||||||
|
const auto tray_type = ams.opt_string("filament_type", 0u);
|
||||||
|
if (is_placeholder || tray_type.empty()) {
|
||||||
|
if (use_map) {
|
||||||
|
for (int j = maps.size() - 1; j >= 0; j--) {
|
||||||
|
if (maps[j].slot_id == slot_id && maps[j].ams_id == ams_id) {
|
||||||
|
maps.erase(j);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||||
|
auto default_unknown_color = "#CECECE";
|
||||||
|
ams_filament_colors.push_back(default_unknown_color);
|
||||||
|
ams_filament_color_types.push_back("1");
|
||||||
|
if (filament_multi_color.size() == 0) {
|
||||||
|
filament_multi_color.push_back(default_unknown_color);
|
||||||
|
}
|
||||||
|
ams_multi_color_filment.push_back(filament_multi_color);
|
||||||
|
} else if (is_placeholder) {
|
||||||
|
// Orca: push placeholders to keep index alignment with ams_infos
|
||||||
|
ams_filament_presets.push_back("");
|
||||||
|
ams_filament_colors.push_back("");
|
||||||
|
ams_filament_color_types.push_back("");
|
||||||
|
ams_multi_color_filment.push_back({});
|
||||||
}
|
}
|
||||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
continue;
|
||||||
auto default_unknown_color = "#CECECE";
|
|
||||||
ams_filament_colors.push_back(default_unknown_color);
|
|
||||||
ams_filament_color_types.push_back("1");
|
|
||||||
if (filament_multi_color.size() == 0) {
|
|
||||||
filament_multi_color.push_back(default_unknown_color);
|
|
||||||
}
|
|
||||||
ams_multi_color_filment.push_back(filament_multi_color);
|
|
||||||
} else if (is_placeholder) {
|
|
||||||
// Orca: push placeholders to keep index alignment with ams_infos
|
|
||||||
ams_filament_presets.push_back("");
|
|
||||||
ams_filament_colors.push_back("");
|
|
||||||
ams_filament_color_types.push_back("");
|
|
||||||
ams_multi_color_filment.push_back({});
|
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
if (!filament_changed && this->filament_presets.size() > ams_filament_presets.size()) {
|
if (!filament_changed && this->filament_presets.size() > ams_filament_presets.size()) {
|
||||||
ams_filament_presets.push_back(this->filament_presets[ams_filament_presets.size()]);
|
ams_filament_presets.push_back(this->filament_presets[ams_filament_presets.size()]);
|
||||||
ams_filament_colors.push_back(filament_color);
|
ams_filament_colors.push_back(filament_color);
|
||||||
ams_filament_color_types.push_back(filament_color_type);
|
ams_filament_color_types.push_back(filament_color_type);
|
||||||
ams_multi_color_filment.push_back(filament_multi_color);
|
ams_multi_color_filment.push_back(filament_multi_color);
|
||||||
|
ams_infos.back().valid = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
bool has_type = false;
|
bool has_type = false;
|
||||||
auto filament_type = ams.opt_string("filament_type", 0u);
|
auto filament_type = ams.opt_string("filament_type", 0u);
|
||||||
auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
|
auto iter = filaments.end();
|
||||||
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
if (!filament_id.empty()) {
|
||||||
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
|
||||||
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
||||||
|
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||||
|
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
||||||
|
} else {
|
||||||
|
// The material type is the only identity a printer-set tray carries.
|
||||||
|
has_type = std::any_of(filaments.begin(), filaments.end(), [&filament_type](auto &f) {
|
||||||
|
return f.is_compatible && f.config.opt_string("filament_type", 0u) == filament_type; });
|
||||||
|
}
|
||||||
if (iter == filaments.end()) {
|
if (iter == filaments.end()) {
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
|
||||||
if (!filament_type.empty()) {
|
if (!filament_type.empty()) {
|
||||||
@@ -3790,6 +3806,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
|||||||
ams_filament_colors.push_back(filament_color);
|
ams_filament_colors.push_back(filament_color);
|
||||||
ams_filament_color_types.push_back(filament_color_type);
|
ams_filament_color_types.push_back(filament_color_type);
|
||||||
ams_multi_color_filment.push_back(filament_multi_color);
|
ams_multi_color_filment.push_back(filament_multi_color);
|
||||||
|
ams_infos.back().valid = true;
|
||||||
unknowns.emplace_back(&ams, has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") :
|
unknowns.emplace_back(&ams, has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") :
|
||||||
L("The filament model is unknown. Still using the previous filament preset."));
|
L("The filament model is unknown. Still using the previous filament preset."));
|
||||||
continue;
|
continue;
|
||||||
@@ -3816,6 +3833,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
|||||||
ams_filament_colors.push_back(filament_color);
|
ams_filament_colors.push_back(filament_color);
|
||||||
ams_filament_color_types.push_back(filament_color_type);
|
ams_filament_color_types.push_back(filament_color_type);
|
||||||
ams_multi_color_filment.push_back(filament_multi_color);
|
ams_multi_color_filment.push_back(filament_multi_color);
|
||||||
|
ams_infos.back().valid = true;
|
||||||
}
|
}
|
||||||
if (ams_filament_presets.empty())
|
if (ams_filament_presets.empty())
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ inline bool has_engaged_filament_mapping(const std::string& ams_mapping2)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A device with no AMS units has a single source: the external spool. OrcaSlicer's
|
||||||
|
// auto-mapping force-selects it for every filament (DevMapping.cpp), but that is not
|
||||||
|
// a lane choice: keep it out of the send gate and off print.gcode_file.
|
||||||
|
inline void drop_forced_external_selection(bool device_has_ams, std::string& ams_mapping2)
|
||||||
|
{
|
||||||
|
if (!device_has_ams)
|
||||||
|
ams_mapping2.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// A used filament with no target would be silently dropped from the wire
|
// 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.
|
// mapping, so the print must be refused rather than run the wrong material.
|
||||||
// m_ams_mapping_result carries exactly the filaments the slice uses.
|
// m_ams_mapping_result carries exactly the filaments the slice uses.
|
||||||
|
|||||||
@@ -3537,14 +3537,17 @@ void SelectMachineDialog::on_send_print()
|
|||||||
|
|
||||||
get_ams_mapping_result(ams_mapping_array,ams_mapping_array2, ams_mapping_info);
|
get_ams_mapping_result(ams_mapping_array,ams_mapping_array2, ams_mapping_info);
|
||||||
|
|
||||||
|
// A device with no AMS unit has one source, the external spool: OrcaSlicer's
|
||||||
|
// auto-mapping force-selects it, which is not a lane choice. Bambu unchanged.
|
||||||
|
if (obj_->printer_agent_id == ORCA_PRINTER_AGENT_ID)
|
||||||
|
drop_forced_external_selection(obj_->HasAms(), ams_mapping_array2);
|
||||||
|
|
||||||
// OrcaSonar: a mapped print requires the connector to advertise
|
// OrcaSonar: a mapped print requires the connector to advertise
|
||||||
// filament_mapping and the index correlation to be verified. Refuse rather
|
// filament_mapping. Refuse rather than start with the map silently dropped;
|
||||||
// than start with the map silently dropped; Bambu keeps its behavior.
|
// Bambu keeps its behavior.
|
||||||
if (obj_->printer_agent_id == ORCA_PRINTER_AGENT_ID) {
|
if (obj_->printer_agent_id == ORCA_PRINTER_AGENT_ID) {
|
||||||
const bool mapping_available = obj_->is_support_filament_mapping && ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED;
|
if (!obj_->is_support_filament_mapping && has_engaged_filament_mapping(ams_mapping_array2)) {
|
||||||
if (!mapping_available && has_engaged_filament_mapping(ams_mapping_array2)) {
|
BOOST_LOG_TRIVIAL(warning) << "print_job: connector does not advertise filament_mapping; refusing mapped print";
|
||||||
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."));
|
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);
|
Enable_Send_Button(true);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -744,16 +744,18 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event)
|
|||||||
|
|
||||||
if (!wxGetApp().is_blocking_printing(obj)) {
|
if (!wxGetApp().is_blocking_printing(obj)) {
|
||||||
PrintParams params = request_params(obj);
|
PrintParams params = request_params(obj);
|
||||||
|
// A device with no AMS unit has one source, the external spool: OrcaSlicer's
|
||||||
|
// auto-mapping force-selects it, which is not a lane choice.
|
||||||
|
if (obj->printer_agent_id == ORCA_PRINTER_AGENT_ID)
|
||||||
|
drop_forced_external_selection(obj->HasAms(), params.ams_mapping2);
|
||||||
// OrcaSonar: a mapped print requires the connector to advertise
|
// OrcaSonar: a mapped print requires the connector to advertise
|
||||||
// filament_mapping, and a partially mapped print must not silently drop a
|
// 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
|
// 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
|
// gate, external slots ({255,0}/{254,0}) included; the extra-spool branch
|
||||||
// rewrites to exactly those. Bambu is unchanged.
|
// rewrites to exactly those. Bambu is unchanged.
|
||||||
if (obj->printer_agent_id == ORCA_PRINTER_AGENT_ID) {
|
if (obj->printer_agent_id == ORCA_PRINTER_AGENT_ID) {
|
||||||
const bool mapping_available = obj->is_support_filament_mapping && ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED;
|
if (!obj->is_support_filament_mapping && has_engaged_filament_mapping(params.ams_mapping2)) {
|
||||||
if (!mapping_available && has_engaged_filament_mapping(params.ams_mapping2)) {
|
BOOST_LOG_TRIVIAL(warning) << "SendMultiMachinePage: connector does not advertise filament_mapping; refusing mapped print for "
|
||||||
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();
|
<< 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);
|
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();
|
msg_wingow.ShowModal();
|
||||||
|
|||||||
@@ -15,15 +15,7 @@
|
|||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
static constexpr char ORCA_PRINTER_AGENT_ID[] = "orca";
|
static constexpr char ORCA_PRINTER_AGENT_ID[] = "orca";
|
||||||
static constexpr char BBL_PRINTER_AGENT_ID[] = "bbl";
|
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
|
// Factory function type for creating printer agents
|
||||||
using PrinterAgentFactory =
|
using PrinterAgentFactory =
|
||||||
@@ -32,17 +24,20 @@ using PrinterAgentFactory =
|
|||||||
// Information about a registered printer agent
|
// Information about a registered printer agent
|
||||||
struct PrinterAgentInfo
|
struct PrinterAgentInfo
|
||||||
{
|
{
|
||||||
std::string id; // Registry/config key, e.g. "orca" or a plugin AgentInfo::id
|
std::string id; // Registry/config key, e.g. "orca" or a plugin AgentInfo::id
|
||||||
std::string display_name; // e.g., "Orca Native", "Bambu Lab"
|
std::string display_name; // e.g., "Orca Native", "Bambu Lab"
|
||||||
std::string plugin_identifier; // Empty for built-ins, otherwise <plugin_key>;<uuid>;<capability_name>
|
std::string plugin_identifier; // Empty for built-ins, otherwise <plugin_key>;<uuid>;<capability_name>
|
||||||
PrinterAgentFactory factory; // Function to create the agent
|
PrinterAgentFactory factory; // Function to create the agent
|
||||||
|
|
||||||
bool is_plugin() const { return !plugin_identifier.empty(); }
|
bool is_plugin() const { return !plugin_identifier.empty(); }
|
||||||
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, PrinterAgentFactory factory_)
|
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, PrinterAgentFactory factory_)
|
||||||
: id(id_), display_name(display_name_), factory(std::move(factory_))
|
: id(id_), display_name(display_name_), factory(std::move(factory_))
|
||||||
{}
|
{}
|
||||||
|
|
||||||
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, const std::string& plugin_identifier, PrinterAgentFactory factory_)
|
PrinterAgentInfo(const std::string& id_,
|
||||||
|
const std::string& display_name_,
|
||||||
|
const std::string& plugin_identifier,
|
||||||
|
PrinterAgentFactory factory_)
|
||||||
: id(id_), display_name(display_name_), plugin_identifier(plugin_identifier), factory(std::move(factory_))
|
: id(id_), display_name(display_name_), plugin_identifier(plugin_identifier), factory(std::move(factory_))
|
||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
@@ -124,9 +119,9 @@ public:
|
|||||||
* @param log_dir Directory for log files
|
* @param log_dir Directory for log files
|
||||||
* @return Shared pointer to IPrinterAgent, or nullptr if ID not found
|
* @return Shared pointer to IPrinterAgent, or nullptr if ID not found
|
||||||
*/
|
*/
|
||||||
static std::shared_ptr<IPrinterAgent> create_printer_agent_by_id(const std::string& id,
|
static std::shared_ptr<IPrinterAgent> create_printer_agent_by_id(const std::string& id,
|
||||||
std::shared_ptr<ICloudServiceAgent> cloud_agent,
|
std::shared_ptr<ICloudServiceAgent> cloud_agent,
|
||||||
const std::string& log_dir);
|
const std::string& log_dir);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the printer agent cache.
|
* Clear the printer agent cache.
|
||||||
|
|||||||
@@ -554,7 +554,6 @@ void OrcaMqttConnection::connect_and_read() {
|
|||||||
|
|
||||||
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
|
// Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the
|
||||||
// CONNECT username/password fields are omitted entirely (the cloud form).
|
// CONNECT username/password fields are omitted entirely (the cloud form).
|
||||||
const bool use_bearer = static_cast<bool>(current_config.bearer_provider);
|
const bool use_bearer = static_cast<bool>(current_config.bearer_provider);
|
||||||
@@ -569,6 +568,8 @@ void OrcaMqttConnection::connect_and_read() {
|
|||||||
ws_read(*connection, buffer, connack_error);
|
ws_read(*connection, buffer, connack_error);
|
||||||
if (connack_error)
|
if (connack_error)
|
||||||
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
|
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
|
||||||
|
|
||||||
|
expires_never(*connection);
|
||||||
const std::string connack = boost::beast::buffers_to_string(buffer.data());
|
const std::string connack = boost::beast::buffers_to_string(buffer.data());
|
||||||
// rc: 0 accepted, 1..5 refusal, -1 malformed/not a CONNACK.
|
// rc: 0 accepted, 1..5 refusal, -1 malformed/not a CONNACK.
|
||||||
const int rc = (connack.size() == 4 && static_cast<uint8_t>(connack[0]) == 0x20)
|
const int rc = (connack.size() == 4 && static_cast<uint8_t>(connack[0]) == 0x20)
|
||||||
|
|||||||
@@ -45,9 +45,7 @@ namespace fs = boost::filesystem;
|
|||||||
|
|
||||||
// Per-device filament_mapping capability, mirrored from the get_capabilities
|
// Per-device filament_mapping capability, mirrored from the get_capabilities
|
||||||
// reply in merge_capabilities and read by start_sdcard_print so the field is
|
// reply in merge_capabilities and read by start_sdcard_print so the field is
|
||||||
// refused defensively when the connector never advertised it or the
|
// refused defensively when the connector never advertised it.
|
||||||
// index-correlation merge gate (ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED) is
|
|
||||||
// not satisfied.
|
|
||||||
std::mutex g_filament_mapping_mutex;
|
std::mutex g_filament_mapping_mutex;
|
||||||
std::unordered_map<std::string, bool> g_filament_mapping_cache;
|
std::unordered_map<std::string, bool> g_filament_mapping_cache;
|
||||||
|
|
||||||
@@ -1708,16 +1706,13 @@ int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up
|
|||||||
const std::string target = params.dst_file.empty() ? remote_gcode_name(params) : fs::path(params.dst_file).filename().string();
|
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
|
// Per-print mapping. A mapped print is refused when the connector did not
|
||||||
// advertise filament_mapping or the index correlation is unverified: the GUI
|
// advertise filament_mapping: the GUI send gates make this visible first, and
|
||||||
// send gates make this visible first, and this is the defensive gate for
|
// this is the defensive gate for callers that bypass them (calibration,
|
||||||
// callers that bypass them (calibration, plugin). Never start a mapped print
|
// plugin). Never start a mapped print with the map silently dropped.
|
||||||
// with the map silently dropped.
|
|
||||||
const nlohmann::json filament_mapping = build_filament_mapping(params.ams_mapping2);
|
const nlohmann::json filament_mapping = build_filament_mapping(params.ams_mapping2);
|
||||||
if (!filament_mapping.empty()) {
|
if (!filament_mapping.empty()) {
|
||||||
const bool mapping_capable = filament_mapping_advertised(params.dev_id);
|
if (!filament_mapping_advertised(params.dev_id)) {
|
||||||
if (!mapping_capable || !ORCA_FILAMENT_MAPPING_CORRELATION_VERIFIED) {
|
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: refusing mapped print, connector does not advertise filament_mapping";
|
||||||
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;
|
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||||
}
|
}
|
||||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: start_sdcard_print emitting filament_mapping entries=" << filament_mapping.size();
|
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: start_sdcard_print emitting filament_mapping entries=" << filament_mapping.size();
|
||||||
|
|||||||
@@ -40,6 +40,20 @@ static std::set<int> tools_for_role(const std::string& gcode, const std::string&
|
|||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every id named by a Tn command anywhere in `gcode`. A non-BBL single-extruder multi-filament
|
||||||
|
// setup emits T<filament_id>, and that id is what the Klipper toolchange macro consumes.
|
||||||
|
static std::set<int> tool_ids(const std::string& gcode)
|
||||||
|
{
|
||||||
|
std::set<int> tools;
|
||||||
|
GCodeReader reader;
|
||||||
|
reader.parse_buffer(gcode, [&tools](GCodeReader&, const GCodeReader::GCodeLine& line) {
|
||||||
|
const std::string cmd(line.cmd());
|
||||||
|
if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char)cmd[1]))
|
||||||
|
tools.insert(std::stoi(cmd.substr(1)));
|
||||||
|
});
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
// X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks:
|
// X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks:
|
||||||
// the nearest preceding G1 carrying an X (the park travel emitted just before the wait).
|
// the nearest preceding G1 carrying an X (the park travel emitted just before the wait).
|
||||||
static std::vector<double> wait_park_xs(const std::string& gcode)
|
static std::vector<double> wait_park_xs(const std::string& gcode)
|
||||||
@@ -287,6 +301,29 @@ TEST_CASE("Each feature prints with its assigned filament (three filaments)", "[
|
|||||||
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2
|
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The per-print wire mapping keys each entry by the filament's index in the config filament
|
||||||
|
// arrays (the ams_mapping2 position). Pin that the id the toolchange emits is that index even
|
||||||
|
// when the used filaments skip a middle one: filament 2 (index 1) is configured but unused, so
|
||||||
|
// a renumbering to the used set would emit {0, 1} instead of {0, 2} and aim the map at the
|
||||||
|
// wrong lane.
|
||||||
|
TEST_CASE("Toolchange ids keep the config index of the used filament when a middle filament is unused", "[MultiFilament]")
|
||||||
|
{
|
||||||
|
const std::string gcode = slice({ cube(20) },
|
||||||
|
multifilament_config(3, {
|
||||||
|
{ "sparse_infill_filament_id", 1 },
|
||||||
|
{ "internal_solid_filament_id", 1 },
|
||||||
|
{ "top_surface_filament_id", 1 },
|
||||||
|
{ "bottom_surface_filament_id", 1 },
|
||||||
|
{ "outer_wall_filament_id", 3 },
|
||||||
|
{ "inner_wall_filament_id", 3 },
|
||||||
|
{ "skirt_loops", 0 },
|
||||||
|
{ "brim_type", "no_brim" },
|
||||||
|
}));
|
||||||
|
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 2 }); // filament 3
|
||||||
|
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 0 }); // filament 1
|
||||||
|
CHECK(tool_ids(gcode) == std::set<int>{ 0, 2 });
|
||||||
|
}
|
||||||
|
|
||||||
// The override must survive tool ordering: object 1's walls print on their filament's
|
// The override must survive tool ordering: object 1's walls print on their filament's
|
||||||
// tool, object 0 stays on the first. If dropped, every wall prints on tool 0.
|
// tool, object 0 stays on the first. If dropped, every wall prints on tool 0.
|
||||||
TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
|
TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_config_variant_expansion.cpp
|
test_config_variant_expansion.cpp
|
||||||
test_toolordering_nozzle_group.cpp
|
test_toolordering_nozzle_group.cpp
|
||||||
test_preset_bundle_loading.cpp
|
test_preset_bundle_loading.cpp
|
||||||
|
test_preset_bundle_ams_sync.cpp
|
||||||
test_preset_setting_id.cpp
|
test_preset_setting_id.cpp
|
||||||
test_preset_diff.cpp
|
test_preset_diff.cpp
|
||||||
test_vendor_cache.cpp
|
test_vendor_cache.cpp
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "libslic3r/Preset.hpp"
|
||||||
|
#include "libslic3r/PresetBundle.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// In-memory system filament preset. Mirrors the helper in test_preset_bundle_loading.cpp.
|
||||||
|
Preset &add_system_filament(PresetBundle &bundle, const std::string &name, const std::string &filament_id, const std::string &filament_type)
|
||||||
|
{
|
||||||
|
DynamicPrintConfig config(bundle.filaments.default_preset().config);
|
||||||
|
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "";
|
||||||
|
config.option<ConfigOptionStrings>("filament_type", true)->values = {filament_type};
|
||||||
|
Preset &preset = bundle.filaments.load_preset(std::string(), name, config, /*select=*/false);
|
||||||
|
preset.is_system = true;
|
||||||
|
preset.filament_id = filament_id;
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One AMS tray entry as build_filament_ams_list produces it.
|
||||||
|
DynamicPrintConfig make_tray(const std::string &filament_id, const std::string &filament_type, const std::string &color, const std::string &ams_id,
|
||||||
|
const std::string &slot_id)
|
||||||
|
{
|
||||||
|
DynamicPrintConfig tray;
|
||||||
|
tray.set_key_value("filament_id", new ConfigOptionStrings{filament_id});
|
||||||
|
tray.set_key_value("filament_type", new ConfigOptionStrings{filament_type});
|
||||||
|
tray.set_key_value("filament_colour", new ConfigOptionStrings{color});
|
||||||
|
tray.set_key_value("filament_colour_type", new ConfigOptionStrings{"1"});
|
||||||
|
tray.set_key_value("filament_multi_colour", new ConfigOptionStrings{});
|
||||||
|
tray.set_key_value("ams_id", new ConfigOptionStrings{ams_id});
|
||||||
|
tray.set_key_value("slot_id", new ConfigOptionStrings{slot_id});
|
||||||
|
return tray;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// A tray the printer UI wrote carries a material type but no OrcaSlicer preset id. It must still
|
||||||
|
// sync, resolved to Generic <type> with the tray's own color.
|
||||||
|
TEST_CASE("AMS sync resolves a printer-set tray to Generic by material type", "[Preset][AMS]")
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||||
|
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
|
||||||
|
|
||||||
|
bundle.filament_ams_list[0] = make_tray("OFYPdQJh", "PETG", "#FE717A", "0", "0"); // preset-backed
|
||||||
|
bundle.filament_ams_list[1] = make_tray("", "PETG", "#898F9B", "0", "1"); // printer-set, no id
|
||||||
|
bundle.filament_ams_list[2] = make_tray("", "PLA", "#FAFAFA", "0", "2"); // printer-set, no id
|
||||||
|
bundle.filament_ams_list[3] = make_tray("", "", "#000000", "0", "3"); // empty slot
|
||||||
|
|
||||||
|
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
|
||||||
|
std::map<int, AMSMapInfo> maps;
|
||||||
|
MergeFilamentInfo merge;
|
||||||
|
|
||||||
|
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge);
|
||||||
|
|
||||||
|
CHECK(count == 3);
|
||||||
|
REQUIRE(bundle.filament_presets.size() == 3);
|
||||||
|
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
|
||||||
|
CHECK(bundle.filament_presets[1] == "Generic PETG @Q2");
|
||||||
|
CHECK(bundle.filament_presets[2] == "Generic PLA @Q2");
|
||||||
|
|
||||||
|
const ConfigOptionStrings *colors = bundle.project_config.option<ConfigOptionStrings>("filament_colour");
|
||||||
|
REQUIRE(colors != nullptr);
|
||||||
|
REQUIRE(colors->values.size() == 3);
|
||||||
|
CHECK(colors->values[0] == "#FE717A");
|
||||||
|
CHECK(colors->values[1] == "#898F9B");
|
||||||
|
CHECK(colors->values[2] == "#FAFAFA");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without a preset id and without a material type there is nothing to resolve; the tray stays
|
||||||
|
// skipped in a direct sync.
|
||||||
|
TEST_CASE("AMS sync skips a tray without a preset id or a material type", "[Preset][AMS]")
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||||
|
bundle.filament_ams_list[0] = make_tray("", "", "#000000", "0", "0");
|
||||||
|
|
||||||
|
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
|
||||||
|
std::map<int, AMSMapInfo> maps;
|
||||||
|
MergeFilamentInfo merge;
|
||||||
|
|
||||||
|
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 0);
|
||||||
|
CHECK(bundle.filament_presets.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard the pre-existing behavior: a non-empty id that matches no preset still resolves by type.
|
||||||
|
TEST_CASE("AMS sync still resolves an unmatched preset id by material type", "[Preset][AMS]")
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||||
|
bundle.filament_ams_list[0] = make_tray("REMOVED_ID", "PETG", "#123456", "0", "0");
|
||||||
|
|
||||||
|
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
|
||||||
|
std::map<int, AMSMapInfo> maps;
|
||||||
|
MergeFilamentInfo merge;
|
||||||
|
|
||||||
|
CHECK(bundle.sync_ams_list(unknowns, /*use_map=*/false, maps, /*enable_append=*/false, merge) == 1);
|
||||||
|
REQUIRE(bundle.filament_presets.size() == 1);
|
||||||
|
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping-mode sync used to force "Generic PLA" for an id-less tray; it must use the type instead.
|
||||||
|
TEST_CASE("AMS sync in mapping mode resolves a printer-set tray to Generic, not Generic PLA", "[Preset][AMS]")
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||||
|
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
|
||||||
|
|
||||||
|
bundle.filament_presets = {"Generic PLA @Q2"};
|
||||||
|
bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values = {"#000000"};
|
||||||
|
bundle.project_config.option<ConfigOptionStrings>("filament_colour_type")->values = {"1"};
|
||||||
|
|
||||||
|
bundle.filament_ams_list[0] = make_tray("", "PETG", "#898F9B", "0", "1");
|
||||||
|
|
||||||
|
std::map<int, AMSMapInfo> maps;
|
||||||
|
maps[0] = AMSMapInfo{"0", "1"};
|
||||||
|
|
||||||
|
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
|
||||||
|
MergeFilamentInfo merge;
|
||||||
|
|
||||||
|
const unsigned int count = bundle.sync_ams_list(unknowns, /*use_map=*/true, maps, /*enable_append=*/false, merge);
|
||||||
|
|
||||||
|
CHECK(count == 1);
|
||||||
|
REQUIRE(bundle.filament_presets.size() == 1);
|
||||||
|
CHECK(bundle.filament_presets[0] == "Generic PETG @Q2");
|
||||||
|
CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values[0] == "#898F9B");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dialog builds ams_mapping2 one entry per filament_presets index, while the generated
|
||||||
|
// G-code's toolchange names the filament's index in the slice's filament config arrays. Those
|
||||||
|
// are the same index only if full_config() collects the filament arrays in filament_presets
|
||||||
|
// order; if a refactor reorders one side, every mapped print aims at the wrong lane.
|
||||||
|
TEST_CASE("Full config filament arrays follow the selected filament preset order", "[Preset][AMS]")
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
add_system_filament(bundle, "Generic PETG @Q2", "OFYPdQJh", "PETG");
|
||||||
|
add_system_filament(bundle, "Generic PLA @Q2", "GFL99", "PLA");
|
||||||
|
|
||||||
|
// Deliberately not the load order: the arrays must follow this selection.
|
||||||
|
bundle.filament_presets = {"Generic PLA @Q2", "Generic PETG @Q2"};
|
||||||
|
|
||||||
|
const DynamicPrintConfig full = bundle.full_config(/*apply_extruder=*/false);
|
||||||
|
REQUIRE(full.option<ConfigOptionStrings>("filament_type") != nullptr);
|
||||||
|
CHECK(full.option<ConfigOptionStrings>("filament_settings_id")->values == std::vector<std::string>{"Generic PLA @Q2", "Generic PETG @Q2"});
|
||||||
|
CHECK(full.option<ConfigOptionStrings>("filament_type")->values == std::vector<std::string>{"PLA", "PETG"});
|
||||||
|
// The AMS sync matches trays by filament_id, so this array must follow the same order.
|
||||||
|
CHECK(full.option<ConfigOptionStrings>("filament_ids")->values == std::vector<std::string>{"GFL99", "OFYPdQJh"});
|
||||||
|
}
|
||||||
@@ -154,6 +154,45 @@ TEST_CASE("the GUI mapping gate engages exactly the entries the serializer sends
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The used-unmapped refusal reads m_ams_mapping_result: an entry with no target carries
|
||||||
|
// empty ams_id/slot_id, while an external-spool assignment (ams_id 255/254) is a real
|
||||||
|
// target. A wholly unmapped print reports no target and falls to the existing send flow.
|
||||||
|
TEST_CASE("used-filament targets split mapped from unmapped", "[OrcaPrinterAgent]") {
|
||||||
|
using Slic3r::GUI::has_any_mapped_target;
|
||||||
|
using Slic3r::GUI::has_used_filament_without_target;
|
||||||
|
|
||||||
|
Slic3r::FilamentInfo box; box.ams_id = "0"; box.slot_id = "1"; // box slot
|
||||||
|
Slic3r::FilamentInfo external; external.ams_id = "255"; external.slot_id = "0"; // external spool
|
||||||
|
Slic3r::FilamentInfo unmapped; // no target
|
||||||
|
|
||||||
|
CHECK_FALSE(has_used_filament_without_target({box, external}));
|
||||||
|
CHECK(has_used_filament_without_target({box, unmapped}));
|
||||||
|
CHECK(has_used_filament_without_target({external, unmapped}));
|
||||||
|
CHECK(has_used_filament_without_target({unmapped})); // wholly unmapped: all-invalid flow, not this refusal
|
||||||
|
CHECK(has_any_mapped_target({box, unmapped}));
|
||||||
|
CHECK(has_any_mapped_target({external, unmapped}));
|
||||||
|
CHECK_FALSE(has_any_mapped_target({unmapped}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A device with no AMS units has one source, the external spool. OrcaSlicer's
|
||||||
|
// auto-mapping force-selects it for every filament; that is not a lane choice, so it
|
||||||
|
// must not gate the print or reach print.gcode_file.
|
||||||
|
TEST_CASE("a no-AMS device drops the forced external-spool selection", "[OrcaPrinterAgent]") {
|
||||||
|
using Slic3r::GUI::drop_forced_external_selection;
|
||||||
|
using Slic3r::GUI::has_engaged_filament_mapping;
|
||||||
|
|
||||||
|
std::string external_only = R"([{"ams_id":255,"slot_id":0}])";
|
||||||
|
drop_forced_external_selection(/*device_has_ams=*/false, external_only);
|
||||||
|
CHECK(external_only.empty());
|
||||||
|
CHECK_FALSE(has_engaged_filament_mapping(external_only));
|
||||||
|
CHECK(Probe::build_filament_mapping(external_only).empty());
|
||||||
|
|
||||||
|
std::string box_mapping = R"([{"ams_id":0,"slot_id":2}])";
|
||||||
|
drop_forced_external_selection(/*device_has_ams=*/true, box_mapping);
|
||||||
|
CHECK(box_mapping == R"([{"ams_id":0,"slot_id":2}])");
|
||||||
|
CHECK(has_engaged_filament_mapping(box_mapping));
|
||||||
|
}
|
||||||
|
|
||||||
// An empty mapping must leave the gcode_file payload byte-identical to today:
|
// An empty mapping must leave the gcode_file payload byte-identical to today:
|
||||||
// exactly command, sequence_id and param, with no filament_mapping key.
|
// 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]") {
|
TEST_CASE("gcode_file payload omits filament_mapping when the map is empty", "[OrcaPrinterAgent]") {
|
||||||
@@ -172,6 +211,23 @@ TEST_CASE("gcode_file payload omits filament_mapping when the map is empty", "[O
|
|||||||
CHECK(with["print"]["filament_mapping"][0]["filament_index"] == 0);
|
CHECK(with["print"]["filament_mapping"][0]["filament_index"] == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The defensive gate: a mapped print is refused before anything is published when the
|
||||||
|
// connector never advertised filament_mapping. The GUI send gates make this visible first;
|
||||||
|
// this covers callers that bypass them (calibration, plugin). A sentinel-only mapping does
|
||||||
|
// not engage the gate and falls through to the normal publish path.
|
||||||
|
TEST_CASE("an engaged mapping is refused when the connector never advertised filament_mapping", "[OrcaPrinterAgent]") {
|
||||||
|
OrcaPrinterAgent agent("/tmp");
|
||||||
|
Slic3r::PrintParams params;
|
||||||
|
params.dev_id = "dev-no-mapping-cap";
|
||||||
|
params.dst_file = "/tmp/job.gcode";
|
||||||
|
params.ams_mapping2 = R"([{"ams_id":0,"slot_id":2}])";
|
||||||
|
|
||||||
|
CHECK(agent.start_sdcard_print(params, {}, {}) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||||
|
|
||||||
|
params.ams_mapping2 = R"([{"ams_id":255,"slot_id":255}])";
|
||||||
|
CHECK(agent.start_sdcard_print(params, {}, {}) == BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
// The FTP "send with record" transport does not exist on OrcaSonar. It must
|
// 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
|
// report a non-success result so PrintJob falls back to start_print() rather
|
||||||
// than treating a print that was never sent as successful.
|
// than treating a print that was never sent as successful.
|
||||||
|
|||||||
Reference in New Issue
Block a user