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
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
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
id: validate_custom
@@ -68,7 +76,7 @@ jobs:
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
- 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: |
{
# Marker matched by check_profiles_comment.yml to delete prior comments.
@@ -94,6 +102,15 @@ jobs:
echo ""
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
echo "### Custom Preset Validation Failed"
echo ""
@@ -116,7 +133,7 @@ jobs:
retention-days: 1
- 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: |
echo "One or more profile checks failed. See above for details."
exit 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,8 +7,6 @@
"instantiation": "true",
"compatible_printers": [
"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"
],
"overhang_fan_speed": [

View File

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

View File

@@ -95,6 +95,7 @@ int main(int argc, char* argv[])
#endif
("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")
("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.");
// clang-format on
@@ -118,6 +119,7 @@ int main(int argc, char* argv[])
std::string vendor = vm["vendor"].as<std::string>();
int log_level = vm["log_level"].as<int>();
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
if (!fs::exists(path) || !fs::is_directory(path)) {
@@ -164,7 +166,7 @@ int main(int argc, char* argv[])
return 0;
}
if (preset_bundle->has_errors()) {
if (preset_bundle->has_errors(check_filament_subtypes)) {
std::cout << "Validation failed" << std::endl;
return 1;
}

View File

@@ -5471,7 +5471,7 @@ void PresetBundle::set_default_suppressed(bool 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)
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;
}
// 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
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 };
}
// Orca: for validation only
bool has_errors() const;
// Orca: for validation only. The duplicate filament subtype check is opt-in for now
bool has_errors(bool check_duplicate_filament_subtypes = false) const;
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);
//BBS: add json related logic
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule);