validator: detect ambiguous (duplicate) filament subtypes per printer (#14459)

* validator: detect duplicate filament subtype per printer (opt-in)

A filament is matched from the AMS by (filament_id + printer compatibility);
if two compatible filament presets for one printer share a filament_id, the
match is ambiguous and the runtime silently picks whichever loads first.

Add PresetBundle::check_duplicate_filament_subtypes(), gated behind a new
has_errors(check_duplicate_filament_subtypes) parameter and the validator's
-f/--check_filament_subtypes flag (off by default). For each system printer it
groups its vendor's compatible filament presets by filament_id and errors on
any group of 2+, reporting each preset as a clickable file:// URI with a single
"how to fix" hint. CI runs it for BBL only (-v BBL -f) until the other vendors'
profiles are cleaned up.

* profiles: fix ambiguous BBL filament matches

Resolve the duplicate-filament-subtype errors flagged by the validator:
- align compatible_printers with Bambu Studio where Orca over-claimed a nozzle
  that already has a dedicated preset (Bambu PLA Basic/Matte/ABS @BBL H2DP;
  Bambu ASA/PETG HF @BBL H2DP 0.6 nozzle; Fiberon PETG-ESD @BBL X1)
- fix a copy-pasted printer name in Overture Matte PLA @BBL A1M 0.2 nozzle
- fix a wrong inherits in Panchroma PLA Silk @BBL X1C 0.2 nozzle (was inheriting
  Panchroma PLA @base, giving it filament_id GFPM001 instead of GFPM004)

Bump BBL profile version.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
SoftFever
2026-06-28 00:42:02 +08:00
parent 3808f95a26
commit d1369e45c8
13 changed files with 134 additions and 21 deletions

View File

@@ -49,6 +49,14 @@ jobs:
set +e set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log ./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]} exit ${PIPESTATUS[0]}
# For now run filament subtype check only for BBL profiles until we fix other vendors' profiles.
- name: validate filament subtype check for BBL profiles
id: validate_filament_subtypes
continue-on-error: true
run: |
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -v BBL -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
exit ${PIPESTATUS[0]}
- name: validate custom presets - name: validate custom presets
id: validate_custom id: validate_custom
@@ -68,7 +76,7 @@ jobs:
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
- name: Prepare comment artifact - name: Prepare comment artifact
if: ${{ always() && github.event_name == 'pull_request' && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} if: ${{ always() && github.event_name == 'pull_request' && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: | run: |
{ {
# Marker matched by check_profiles_comment.yml to delete prior comments. # Marker matched by check_profiles_comment.yml to delete prior comments.
@@ -94,6 +102,15 @@ jobs:
echo "" echo ""
fi fi
if [ "${{ steps.validate_filament_subtypes.outcome }}" = "failure" ]; then
echo "### BBL Filament Subtype Validation Failed"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/validate_filament_subtypes.log || echo "No output captured"
echo '```'
echo ""
fi
if [ "${{ steps.validate_custom.outcome }}" = "failure" ]; then if [ "${{ steps.validate_custom.outcome }}" = "failure" ]; then
echo "### Custom Preset Validation Failed" echo "### Custom Preset Validation Failed"
echo "" echo ""
@@ -116,7 +133,7 @@ jobs:
retention-days: 1 retention-days: 1
- name: Fail if any check failed - name: Fail if any check failed
if: ${{ always() && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} if: ${{ always() && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: | run: |
echo "One or more profile checks failed. See above for details." echo "One or more profile checks failed. See above for details."
exit 1 exit 1

View File

@@ -1,7 +1,7 @@
{ {
"name": "Bambulab", "name": "Bambulab",
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json", "url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
"version": "02.01.00.17", "version": "02.01.00.18",
"force_update": "0", "force_update": "0",
"description": "BBL configurations", "description": "BBL configurations",
"machine_model_list": [ "machine_model_list": [

View File

@@ -123,8 +123,7 @@
"0 0 0 0 0 0" "0 0 0 0 0 0"
], ],
"compatible_printers": [ "compatible_printers": [
"Bambu Lab H2D Pro 0.4 nozzle", "Bambu Lab H2D Pro 0.4 nozzle"
"Bambu Lab H2D Pro 0.6 nozzle"
], ],
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n" "; filament start gcode\n"

View File

@@ -117,8 +117,7 @@
"0 0 0 0 0 0" "0 0 0 0 0 0"
], ],
"compatible_printers": [ "compatible_printers": [
"Bambu Lab H2D Pro 0.6 nozzle", "Bambu Lab H2D Pro 0.6 nozzle"
"Bambu Lab H2D Pro 0.8 nozzle"
], ],
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n" "; filament start gcode\n"

View File

@@ -132,8 +132,7 @@
"0 0 0 0 0 0" "0 0 0 0 0 0"
], ],
"compatible_printers": [ "compatible_printers": [
"Bambu Lab H2D Pro 0.6 nozzle", "Bambu Lab H2D Pro 0.6 nozzle"
"Bambu Lab H2D Pro 0.8 nozzle"
], ],
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n" "; filament start gcode\n"

View File

@@ -144,8 +144,7 @@
"0 0 0 0 0 0" "0 0 0 0 0 0"
], ],
"compatible_printers": [ "compatible_printers": [
"Bambu Lab H2D Pro 0.4 nozzle", "Bambu Lab H2D Pro 0.4 nozzle"
"Bambu Lab H2D Pro 0.6 nozzle"
], ],
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n" "; filament start gcode\n"

View File

@@ -150,8 +150,7 @@
"0 0 0 0 0 0" "0 0 0 0 0 0"
], ],
"compatible_printers": [ "compatible_printers": [
"Bambu Lab H2D Pro 0.4 nozzle", "Bambu Lab H2D Pro 0.4 nozzle"
"Bambu Lab H2D Pro 0.6 nozzle"
], ],
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n" "; filament start gcode\n"

View File

@@ -9,6 +9,6 @@
"1.8" "1.8"
], ],
"compatible_printers": [ "compatible_printers": [
"Bambu Lab P1P 0.2 nozzle" "Bambu Lab A1 mini 0.2 nozzle"
] ]
} }

View File

@@ -7,8 +7,6 @@
"instantiation": "true", "instantiation": "true",
"compatible_printers": [ "compatible_printers": [
"Bambu Lab X1 0.4 nozzle", "Bambu Lab X1 0.4 nozzle",
"Bambu Lab X1 Carbon 0.4 nozzle",
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab X1E 0.4 nozzle" "Bambu Lab X1E 0.4 nozzle"
], ],
"overhang_fan_speed": [ "overhang_fan_speed": [

View File

@@ -1,7 +1,7 @@
{ {
"type": "filament", "type": "filament",
"name": "Panchroma PLA Silk @BBL X1C 0.2 nozzle", "name": "Panchroma PLA Silk @BBL X1C 0.2 nozzle",
"inherits": "Panchroma PLA @base", "inherits": "Panchroma PLA Silk @base",
"from": "system", "from": "system",
"setting_id": "GFSPM004_09", "setting_id": "GFSPM004_09",
"instantiation": "true", "instantiation": "true",

View File

@@ -95,6 +95,7 @@ int main(int argc, char* argv[])
#endif #endif
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified") ("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified")
("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test") ("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test")
("check_filament_subtypes,f", po::bool_switch()->default_value(false), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.")
("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs."); ("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs.");
// clang-format on // clang-format on
@@ -118,6 +119,7 @@ int main(int argc, char* argv[])
std::string vendor = vm["vendor"].as<std::string>(); std::string vendor = vm["vendor"].as<std::string>();
int log_level = vm["log_level"].as<int>(); int log_level = vm["log_level"].as<int>();
bool generate_user_preset = vm["generate_presets"].as<bool>(); bool generate_user_preset = vm["generate_presets"].as<bool>();
bool check_filament_subtypes = vm["check_filament_subtypes"].as<bool>();
// check if path is valid, and return error if not // check if path is valid, and return error if not
if (!fs::exists(path) || !fs::is_directory(path)) { if (!fs::exists(path) || !fs::is_directory(path)) {
@@ -164,7 +166,7 @@ int main(int argc, char* argv[])
return 0; return 0;
} }
if (preset_bundle->has_errors()) { if (preset_bundle->has_errors(check_filament_subtypes)) {
std::cout << "Validation failed" << std::endl; std::cout << "Validation failed" << std::endl;
return 1; return 1;
} }

View File

@@ -5471,7 +5471,7 @@ void PresetBundle::set_default_suppressed(bool default_suppressed)
printers.set_default_suppressed(default_suppressed); printers.set_default_suppressed(default_suppressed);
} }
bool PresetBundle::has_errors() const bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const
{ {
if (m_errors != 0 || printers.m_errors != 0 || filaments.m_errors != 0 || prints.m_errors != 0) if (m_errors != 0 || printers.m_errors != 0 || filaments.m_errors != 0 || prints.m_errors != 0)
return true; return true;
@@ -5491,9 +5491,106 @@ bool PresetBundle::has_errors() const
} }
} }
if (check_duplicate_filament_subtypes && this->check_duplicate_filament_subtypes())
has_errors = true;
return has_errors; return has_errors;
} }
// Orca: turn a preset file path into an absolute file:// URI for log messages.
// Printed unquoted on its own line, this is the one format clickable in both the
// VS Code integrated terminal (Cmd/Ctrl+click) and macOS Terminal.app
// (Cmd+double-click); quotes or literal spaces break link detection in both, so
// the characters that would terminate the URI token are percent-encoded.
static std::string preset_file_uri(const std::string &file)
{
std::string path;
try {
path = boost::filesystem::canonical(file).generic_string();
} catch (...) {
path = file;
}
std::string uri = "file://";
if (path.empty() || path.front() != '/')
uri += '/'; // Windows drive paths (e.g. C:/...) need the extra leading slash
for (char c : path) {
switch (c) {
case ' ': uri += "%20"; break;
case '#': uri += "%23"; break;
case '%': uri += "%25"; break;
default: uri += c;
}
}
return uri;
}
// Orca: a filament is matched from the AMS by (filament_id + printer compatibility).
// For any one printer, at most one instantiated filament preset with a given
// filament_id may be compatible - otherwise the AMS match is ambiguous and the
// runtime silently picks whichever loads first. This validator-only check flags
// any printer that has two or more compatible filament presets sharing a filament_id.
bool PresetBundle::check_duplicate_filament_subtypes() const
{
// Pre-collect system filament presets (each carries its effective filament_id,
// inherited from its @base at load time), grouped by vendor so we only test a
// printer against its own vendor's filaments. A vendor's compatible_printers
// only names that vendor's printers, so same-vendor scoping is correctness
// preserving and avoids an O(all printers x all filaments) sweep.
std::map<std::string, std::vector<const Preset *>> filaments_by_vendor;
for (const auto &preset : filaments) {
if (!preset.is_system || preset.filament_id.empty() || preset.vendor == nullptr)
continue;
filaments_by_vendor[preset.vendor->name].push_back(&preset);
}
bool found_duplicates = false;
for (const auto &printer : printers) {
if (!printer.is_system || printer.vendor == nullptr)
continue;
auto vendor_it = filaments_by_vendor.find(printer.vendor->name);
if (vendor_it == filaments_by_vendor.end())
continue;
const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(printer);
// std::map keeps the reported errors in a deterministic (sorted) order.
std::map<std::string, std::vector<const Preset *>> by_filament_id;
for (const Preset *fil : vendor_it->second)
if (is_compatible_with_printer(filaments.get_preset_with_vendor_profile(*fil), active_printer))
by_filament_id[fil->filament_id].push_back(fil);
for (const auto &entry : by_filament_id) {
if (entry.second.size() < 2)
continue;
found_duplicates = true;
// List each conflicting preset with a clickable file:// URI on its own
// line, so the profile author can jump straight to the files to fix.
std::string presets;
for (const Preset *p : entry.second)
presets += "\n - " + p->name + "\n " + preset_file_uri(p->file);
BOOST_LOG_TRIVIAL(error)
<< "Ambiguous AMS filament match: " << entry.second.size()
<< " filament presets share filament_id \"" << entry.first
<< "\" and are all compatible with printer \"" << printer.name
<< "\". When matching an AMS spool the slicer cannot tell them apart and"
" silently picks whichever loads first." << presets;
}
}
// Print the troubleshooting guidance once, not per error, to keep the log readable.
if (found_duplicates)
BOOST_LOG_TRIVIAL(error) << "\n========================================\n"
<< "How to fix \"Ambiguous AMS filament match\" errors: make sure only ONE filament"
" preset with a given filament_id is compatible with each printer. Either"
"\n (a) remove the overlapping printer from a preset's \"compatible_printers\""
" list (e.g. a '@printer' preset over-claiming a nozzle that already has its own"
" '@printer 0.x nozzle' preset), or"
"\n (b) if these are genuinely different materials, give each its own"
" \"filament_id\" - a common cause is a wrong \"inherits\" pointing at another"
" material's @base preset.";
return found_duplicates;
}
// Orca: BundleMetadata method implementations // Orca: BundleMetadata method implementations
bool BundleMetadata::load_from_json(const std::string& path) bool BundleMetadata::load_from_json(const std::string& path)
{ {

View File

@@ -480,10 +480,14 @@ public:
return { Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL }; return { Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL };
} }
// Orca: for validation only // Orca: for validation only. The duplicate filament subtype check is opt-in for now
bool has_errors() const; bool has_errors(bool check_duplicate_filament_subtypes = false) const;
private: private:
// Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match).
bool check_duplicate_filament_subtypes() const;
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule); //std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
//BBS: add json related logic //BBS: add json related logic
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule); std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule);