Generic-профиль слайсера теряет машинные настройки

Профили филамента в слайсерах семейства Bambu Studio / OrcaSlicer / QIDI Studio образуют цепочку наследования, и от того, к какой ветке прицепиться, зависит гораздо больше, чем кажется. Обычная ошибка — взять Generic PETG @System (или библиотечный профиль бренда) и подкрутить температуры. Всё, что знает про машину, при этом теряется молча: ни предупреждения, ни подсветки в интерфейсе.

Как это выглядит

У меня в профиле стоял Generic ASA @System. Вся геометрия и температуры выглядели разумно, печать шла. При разборе среза выяснилось, что в стартовый G-code уезжает

PRINT_START BED=105 HOTEND=260 CHAMBER=0 EXTRUDER=0

то есть камера не грелась вообще, хотя принтер её греть умеет и для ASA это критично.

Механика видна в машинном стартовом G-code:

PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature] EXTRUDER=[initial_no_support_extruder]
...
M141 S[chamber_temperature]

chamber_temperature — поле профиля филамента. У вендорского профиля под этот принтер там 55, у библиотечного Generic этого ключа нет вовсе, и подставляется ноль. После замены базы в том же месте среза появилось CHAMBER=60, M141 S60.

Вторая потеря из того же места, видна в конфиг-блоке того же файла:

; enable_pressure_advance = 0

Профиль печатал без компенсации давления, и это тоже нигде не видно, кроме среза. У вендорской цепочки там 1, и в срезе появляется строка SET_PRESSURE_ADVANCE ADVANCE=0.03.

Сколько именно теряется

Я сравнил вендорский профиль под свой принтер с библиотечным профилем того же бренда и материала. В вендорском есть 33 ключа, которых в библиотечном нет вообще:

chamber_temperature, box_temperature, box_temperature_range_low, box_temperature_range_high, bed_type, additional_cooling_fan_speed, additional_cooling_fan_speed_unseal, dont_slow_down_outer_wall, filament_adhesiveness_category, filament_dev_ams_drying_ams_limitations, filament_dev_ams_drying_heat_distortion_temperature, filament_dev_ams_drying_temperature, filament_dev_ams_drying_time, filament_dev_chamber_drying_bed_temperature, filament_dev_chamber_drying_time, filament_dev_drying_cooling_temperature, filament_dev_drying_softening_temperature, filament_flush_temp, filament_flush_volumetric_speed, filament_long_retractions_when_ec, filament_pre_cooling_temperature, filament_ramming_travel_time, filament_ramming_volumetric_speed, filament_retraction_distances_when_ec, filament_tower_interface_pre_extrusion_dist, filament_tower_interface_pre_extrusion_length, filament_tower_interface_print_temp, filament_tower_interface_purge_volume, filament_tower_ironing_area, impact_strength_z, textured_cool_plate_temp, textured_cool_plate_temp_initial_layer.

Смысл групп: температура камеры и сушки в автоподатчике, дополнительный обдув, категория адгезии для авто-подбора, вся обвязка смены материала (продувка, рамминг, башня очистки), поведение при обрезке прутка, температуры разных типов столов.

И это не считая ключей, которые в библиотечном профиле есть, но с другими значениями. Мой случай, ASA:

КлючБиблиотечный профиль брендаВендорский под мой принтер
nozzle_temperature270255
nozzle_temperature_initial_layer270250
hot_plate_temp9090
filament_max_volumetric_speed1216
fan_max_speed8050
overhang_fan_speed8080
pressure_advance0.040.03
filament_density1.11.07
chamber_temperatureключа нет55

Скрипт: схлопнуть цепочку и сравнить два профиля

Ключи разбросаны по цепочке, и глазами это не проверить. Разбор в лоб: собрать индекс всех системных профилей по имени, рекурсивно применить inherits, сравнить два результата.

import json, os
 
SYS = os.path.expanduser("~/Library/Application Support/OrcaSlicer/system")
 
index = {}
for root, _, files in os.walk(SYS):
    for fn in files:
        if not fn.endswith(".json"):
            continue
        try:
            d = json.load(open(os.path.join(root, fn), encoding="utf-8"))
        except Exception:
            continue
        if d.get("type") == "filament" and "name" in d:
            index[d["name"]] = d
 
def resolve(d, depth=0):
    if depth > 12:
        raise RuntimeError("цикл наследования")
    parent = d.get("inherits")
    if not parent:
        out = dict(d); out.pop("inherits", None); return out
    base = resolve(index[parent], depth + 1)
    base.update({k: v for k, v in d.items() if k != "inherits"})
    return base
 
mine    = resolve(json.load(open("<путь к моему профилю>", encoding="utf-8")))
factory = resolve(index["<имя вендорского профиля>"])
 
# ключи, которых у кандидата нет вовсе
print(sorted(set(factory) - set(mine)))
# и расхождения значений
for k in sorted(set(mine) & set(factory)):
    if mine[k] != factory[k]:
        print(k, factory[k], "->", mine[k])

Каталог системных профилей в OrcaSlicer на macOS — ~/Library/Application Support/OrcaSlicer/system/<Вендор>/filament/, пользовательские — .../user/default/filament/. Профили конкретной серии принтеров могут лежать в подкаталоге (у меня — filament/Q2/), поэтому обход только верхнего уровня половину пропустит; нужен именно os.walk.

Правило

Наследоваться от вендорского профиля под конкретный принтер и сопло (<Бренд> <Материал> @<Принтер> 0.4 nozzle), а поверх переопределять только то, что относится к катушке: температуры, плотность, коэффициент подачи, pressure advance, лимит расхода, вендор.

Если нужного бренда у вендора нет — брать вендорский профиль ближайшего материала того же класса и переопределять под свой бренд. Это правильнее, чем брать библиотечный профиль своего бренда: класс материала одинаковый, а машинная обвязка сохраняется. Именно так я и сделал: свой ASA собран поверх вендорского ASA под мою машину, а не поверх библиотечного ASA моего бренда.

Итоговая цепочка выглядит так:

Мой профиль -> <Вендор> ASA @<Принтер> 0.4 nozzle -> <Вендор> ASA@<Серия> -> fdm_filament_<вендор>_common

Гоча: «Generic» и «библиотечный профиль бренда» — не одно и то же

Я сначала записал у себя, что enable_pressure_advance = 1 встречается только в вендорской цепочке, и это неверно. У библиотечного профиля бренда он тоже 1. Ноль стоит именно у Generic <материал> @System — то есть у самого общего профиля, который наследуется напрямую от шаблона материала (fdm_filament_asa и подобных).

Разница важна практически: библиотечный профиль бренда — это «правильные температуры без машины», а Generic — «ни машины, ни бренда». Оба не годятся как база, но по разным причинам, и путать их не стоит. Ошибку я поймал только когда сел считать расхождения скриптом, а не по памяти.

Гоча: имя нового профиля может столкнуться с алиасом

У системных профилей бывает поле renamed_from — старое имя, под которым слайсер их ещё узнаёт. У библиотечного <Бренд> ASA @System это <Бренд> ASA. Поэтому свой профиль нельзя называть ровно так же: имя формально свободно (файла с ним нет), но алиас на него уже указывает. Я назвал свой <Бренд> ASA <Принтер>.

Второй файл рядом с профилем — .info, где поле base_id указывает на setting_id родительского системного профиля. При ручном создании профиля его надо заполнять тем же setting_id, который стоит в родителе, иначе слайсер считает профиль осиротевшим.

Почему ошибка живучая

  • Печать не падает. Деталь выходит, просто хуже — а «хуже» на глаз списывается на материал или на калибровку.
  • Интерфейс слайсера показывает поле «температура камеры» и в Generic-профиле тоже. Оно там просто равно нулю, и ноль выглядит как «не задано», а не как «выключено».
  • Профиль легко переживает смену принтера: скопировали настройки со старой машины — и потеряли всю обвязку новой. У меня ровно так и вышло: старые профили остались от предыдущего принтера и продолжали работать «нормально».
  • Ни один линтер слайсера этого не ловит: с точки зрения формата профиль корректен.

Как проверить, что у вас

  1. Схлопнуть цепочку наследования скриптом выше и сравнить с вендорским кандидатом.
  2. Прочитать конфиг-блок среза — в конце файла G-code слайсер печатает все эффективные значения строками ; ключ = значение. Это единственный источник правды: там уже применены и наследование, и дефолты самого слайсера, которых в цепочке вообще нет. См. Проверка настроек слайсера по срезанному gcode.
  3. Проверить точечно три ключа, которые чаще всего оказываются нулём или дефолтом: chamber_temperature, enable_pressure_advance, filament_max_volumetric_speed.

Источники