From 5b64e49c53e3487694028b7de8f8aca5f0a93f30 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 11:12:00 +0800 Subject: [PATCH 01/30] docs: add AMS drying control design spec Port AMS filament drying control feature from BambuStudio. Covers data model changes, commands, UI dialog, and integration plan. --- .../2026-07-07-ams-drying-control-design.md | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-07-ams-drying-control-design.md diff --git a/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md b/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md new file mode 100644 index 0000000000..940534a501 --- /dev/null +++ b/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md @@ -0,0 +1,309 @@ +# AMS Drying Control — Design Spec + +**Date:** 2026-07-07 +**Source:** Port from BambuStudio `AMSDryControl` feature +**Branch:** `dev/ams-heat` + +## Overview + +Port the AMS filament drying control feature from BambuStudio into OrcaSlicer. This allows users to start, monitor, and stop AMS-based filament drying directly from the slicer UI. Currently OrcaSlicer can monitor drying state and stop drying, but cannot initiate it. This feature adds the missing start-drying command, the full drying control dialog, and the drying preset lookup infrastructure. + +**Target AMS models:** N3F (AMS 2 Pro, max 65°C) and N3S (AMS HT, max 85°C) — the only AMS units with heating hardware. + +## Architecture + +The feature spans three layers, matching the existing OrcaSlicer `DeviceCore/` pattern: + +``` +┌─────────────────────────────────────────────────────┐ +│ UI Layer │ +│ AMSDryControl.hpp/.cpp (new dialog) │ +│ AMSControl.cpp (hook: click → dialog) │ +│ DeviceErrorDialog.cpp (stop drying on error) │ +├─────────────────────────────────────────────────────┤ +│ Command/Control Layer │ +│ DevFilaSystemCtrl.cpp (start/stop drying) │ +│ DevUtilBackend.h/.cpp (preset lookup) │ +├─────────────────────────────────────────────────────┤ +│ Data Model Layer │ +│ DevFilaSystem.h/.cpp (enums, structs, parse) │ +│ DevDefs.h (DevAmsType enum) │ +└─────────────────────────────────────────────────────┘ +│ Communication Layer (already exists) │ +│ MachineObject::publish_json() → MQTT → printer │ +└─────────────────────────────────────────────────────┘ +``` + +### New files (4 source + headers) + +| File | Purpose | +|------|---------| +| `src/slic3r/GUI/AMSDryControl.hpp` | AMS drying control dialog class declarations | +| `src/slic3r/GUI/AMSDryControl.cpp` | Dialog implementation (~1800 lines) | +| `src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp` | `CtrlAmsStartDryingHour()` and `CtrlAmsStopDrying()` | +| `src/slic3r/GUI/DeviceCore/DevUtilBackend.h` | Static utility class declaring `GetFilamentDryingPreset()` | +| `src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp` | Reads filament drying presets from printer config | + +### Modified files + +| File | Changes | +|------|---------| +| `DevFilaSystem.h` | Add drying enums/structs to `DevAms`; add `DevFilamentDryingPreset` struct; add ctrl method declarations to `DevFilaSystem`; add `get_ams_drying_preset()` to `DevAmsTray` | +| `DevFilaSystem.cpp` | Parse dry status fields in `DevFilaSystemParser`; implement `IsSupportRemoteDry()`, `AmsIsDrying()`, `get_ams_drying_preset()` | +| `DevDefs.h` | Add `DevAmsType` as a global enum | +| `AMSControl.cpp` | Wire humidity indicator click to open `AMSDryCtrWin` for N3F/N3S AMS | +| `DeviceErrorDialog.cpp` | Update stop-drying to use `DevFilaSystem::CtrlAmsStopDrying()` | +| `DeviceCore/CMakeLists.txt` | Register new source files | + +### New image assets (~14) + +Humidity level icons (5), drying status per AMS type (8: heating/dehumidifying/error/cooling × N3F/N3S), heating icon, enable/disable indicators, guide image. + +## Data Model + +### `DevDefs.h` — Global `DevAmsType` enum + +Promoted from `DevAms::AmsType` (currently local to the class) to a global enum so `DevFilamentDryingPreset` and `DevUtilBackend` can reference it without depending on `DevAms`: + +```cpp +enum DevAmsType : int { + EXT_SPOOL = 0, + AMS = 1, + AMS_LITE = 2, + N3F = 3, // AMS 2 Pro + N3S = 4, // AMS HT +}; +``` + +### `DevAms` class additions (in `DevFilaSystem.h`) + +**New enums:** + +| Enum | Values | +|------|--------| +| `DryCtrlMode` | `Off=0, OnTime=1, OnHumidity=2` | +| `DryStatus` | `Off=0, Checking=1, Drying=2, Cooling=3, Stopping=4, Error=5, CannotStopHeatOutofControl=6, PrdTesting=7` | +| `DrySubStatus` | `Off=0, Heating=1, Dehumidify=2` | +| `DryFanStatus` | `Off=0, On=1` | +| `CannotDryReason` | `TaskOccupied=0, InsufficientPower=1, AmsBusy=2, ConsumableAtAmsOutlet=3, InitiatingAmsDrying=4, NotSupportedIn2dMode=5, DryingInProgress=6, Upgrading=7, InsufficientPowerNeedPluginPower=8, FilamentAtAmsOutletManualUnload=10` | + +**New struct:** + +```cpp +struct DrySettings { + std::string dry_filament; + int dry_temp = -1; // -1 means invalid + int dry_hour = -1; // hours +}; +``` + +**New getters on `DevAms`:** + +- `std::optional GetDryStatus() const` +- `std::optional GetDrySubStatus() const` +- `std::optional GetFan1Status() const` +- `std::optional GetFan2Status() const` +- `std::optional> GetCannotDryReason() const` +- `std::optional GetDrySettings() const` +- `bool IsSupportRemoteDry(const MachineObject* obj) const` +- `bool AmsIsDrying()` + +**New private members:** `m_dry_status`, `m_dry_sub_status`, `m_dry_fan1_status`, `m_dry_fan2_status`, `m_dry_cannot_reasons`, `m_dry_settings` — all `std::optional<>`. + +### `DevFilamentDryingPreset` struct (namespace scope in `DevFilaSystem.h`) + +```cpp +struct DevFilamentDryingPreset { + std::string filament_id; + std::unordered_set ams_limitations; + std::unordered_map filament_dev_ams_drying_time_on_idle; // hours + std::unordered_map filament_dev_ams_drying_temperature_on_idle; + std::unordered_map filament_dev_ams_drying_time_on_print; // hours + std::unordered_map filament_dev_ams_drying_temperature_on_print; + float filament_dev_drying_cooling_temperature; + float filament_dev_drying_softening_temperature; + float filament_dev_ams_drying_heat_distortion_temperature; +}; +``` + +Populated from filament preset config keys (`filament_dev_ams_drying_temperature`, `filament_dev_ams_drying_time`, `filament_dev_drying_softening_temperature`, etc.). + +### `DevAmsTray` addition + +```cpp +std::optional get_ams_drying_preset() const; +``` + +Delegates to `DevUtilBackend::GetFilamentDryingPreset(setting_id)`. + +### JSON parsing + +`DevFilaSystemParser::ParseV1_0()` extended to parse the following fields from the printer's AMS status JSON: + +| JSON field | DevAms member | +|------------|---------------| +| `dry_status` | `m_dry_status` (`DryStatus` enum) | +| `dry_sub_status` | `m_dry_sub_status` (`DrySubStatus` enum) | +| `dry_fan1_status` | `m_dry_fan1_status` | +| `dry_fan2_status` | `m_dry_fan2_status` | +| `dry_cannot_reasons` | `m_dry_cannot_reasons` (vector of `CannotDryReason`) | +| `dry_settings` | `m_dry_settings` (`DrySettings` struct) | + +## Commands + +### `DevFilaSystem::CtrlAmsStartDryingHour()` + +Publishes JSON command `"ams_filament_drying"` via `MachineObject::publish_json()`: + +```json +{ + "print": { + "command": "ams_filament_drying", + "sequence_id": "", + "ams_id": , + "mode": 1, + "filament": "", + "temp": , + "duration": , + "humidity": 0, + "rotate_tray": , + "cooling_temp": , + "close_power_conflict": false + } +} +``` + +### `DevFilaSystem::CtrlAmsStopDrying()` + +Same command with `mode: 0` (Off). + +### Existing `MachineObject::command_ams_drying_stop()` + +Kept for backward compatibility. The error dialog will be updated to call `CtrlAmsStopDrying()` instead, which is the preferred path. + +## Backend Utility + +### `DevUtilBackend` (new, `DeviceCore/DevUtilBackend.h/.cpp`) + +Static utility class. Only method needed for this feature: + +```cpp +static std::optional GetFilamentDryingPreset(const std::string& fila_id); +``` + +Iterates `wxGetApp().preset_bundle->filaments` to find the matching filament_id, then reads config keys into a `DevFilamentDryingPreset`. Uses a static map `{"0" → N3F, "1" → N3S}` for the `ams_limitations` string-to-enum conversion. + +## UI Dialog + +### `AMSDryCtrWin` (extends `DPIDialog`) + +Three pages in a `wxSimplebook`: + +#### Page 0: Main Page + +Split left/right layout: + +**Left panel — Status display:** +- Large humidity/drying state image: humidity level icon when idle; heating/dehumidifying/error animation when active +- Status label with animated heating icon ("Idle", "Drying-Heating", "Drying-Dehumidifying") +- Three stat labels: Humidity (%), Temperature (°C), Left Time (HH:MM). Left Time hidden when idle. + +**Right panel — Controls.** Three sub-panels shown/hidden based on `DryStatus` and `CannotDryReason`: + +| Sub-panel | When shown | Contents | +|-----------|------------|----------| +| Normal state | `DryStatus` is Off or Cooling | Filament type combobox → Temperature input (numeric, validated to AMS limits) → Time input (hours, 1–24) → Validation warning text → **Start** button | +| Cannot dry | `CannotDryReason` is non-empty (excluding only `DryingInProgress`) | Formatted reason text + **Unload** button (only for `ConsumableAtAmsOutlet` reason) | +| Drying error | `DryStatus` is Error | Error message + **Stop** button | +| Drying active | `DryStatus` is Checking/Drying/Stopping | Read-only temp/time display + **Stop** button | + +**Temperature validation rules:** +- Hardware limits: N3F 45–65°C, N3S 45–85°C +- Heat distortion check: temp must not exceed `filament_dev_ams_drying_heat_distortion_temperature` when filament is present +- Printing mode: temp must not exceed recommended drying temp when this AMS is actively feeding a print +- Time: 1–24 hours + +#### Page 1: Guide Page + +Shown after clicking Start on Page 0, before sending the command: +- Instructional text about removing heat-sensitive filament +- `AMSFilamentPanel` showing each tray's filament type with ✅/⚠ icons based on whether drying temp exceeds that filament's softening point +- "Rotate spool when drying" checkbox +- **Back** and **Start** buttons + +#### Page 2: Progress Page + +Shown while waiting for printer to confirm drying has started: +- Animated progress bar (cycles 0–99% until printer state changes) +- Cycling status messages +- Auto-transitions back to Page 0 when printer reports drying active + +### Helper classes (in `AMSDryControl.hpp`) + +- **`FilamentItemPanel`** — Renders a single filament item with icon and text, with custom rounded-rectangle border painting +- **`AMSFilamentPanel`** — Container for multiple `FilamentItemPanel` instances, labeled with AMS name +- **`DryingPreset` struct, `DryCtrState` enum, `DryCtrDev` enum** — Local types for tracking preset state + +### Dialog lifecycle + +- Created on first click of the AMS humidity indicator +- Updated on each `AMSControl::parse_object()` cycle via `update(fila_system, obj)` +- Auto-closes if: AMS is removed from the system, AMS no longer supports remote drying, or AMS ID becomes invalid +- Closed manually via close button or dialog destruction +- On close: stops progress timer, resets to main page, restores button states + +## Integration + +### Entry point: AMS humidity click → dialog + +In `AMSControl.cpp`, the `EVT_AMS_SHOW_HUMIDITY_TIPS` handler currently shows either `AmsHumidityTipPopup` or `uiAmsPercentHumidityDryPopup`. For N3F and N3S AMS types only, replace this with opening `AMSDryCtrWin`: + +1. Lazily create `AMSDryCtrWin*` member on `AMSControl` (like `m_percent_humidity_dry_popup` today) +2. Call `set_ams_id()` and `update(fila_system, obj)` before showing +3. Show the dialog modally or modelessly + +### Periodic updates + +`AMSControl::parse_object()` updates AMS state on a timer. Extend it to: +1. If `m_dry_ctr_win` is shown, call `update()` to refresh readings +2. Update the dialog's AMS ID if the user switches AMS view + +### Error dialog + +`DeviceErrorDialog.cpp` line 452 calls `m_obj->command_ams_drying_stop()`. Update to use `obj->GetFilaSystem()->CtrlAmsStopDrying(ams_id)` for the correct AMS ID. + +### CMakeLists + +- `src/slic3r/GUI/DeviceCore/CMakeLists.txt`: add `DevFilaSystemCtrl.cpp`, `DevUtilBackend.h`, `DevUtilBackend.cpp` +- GUI sources list: add `AMSDryControl.hpp`, `AMSDryControl.cpp` + +## Testing + +### Manual verification + +1. **Prerequisite:** Connect to a printer with N3F or N3S AMS hardware +2. Click AMS humidity indicator → verify dialog opens with correct humidity/temperature readings +3. Select a filament from the combobox → verify temp/time auto-fill from preset +4. Enter valid temp/time → verify Start button enables +5. Enter invalid temp (e.g., 100°C for N3F) → verify warning appears and button disables +6. Click Start → verify guide page shows filament tray status +7. Click Start on guide page → verify progress page appears, command is sent +8. Verify printer begins drying → dialog shows active drying state with countdown +9. Click Stop → verify drying stops +10. Test "cannot dry" states (e.g., during print, filament at outlet) + +### Code-level checks + +- Verify `CtrlAmsStartDryingHour` publishes correct JSON structure +- Verify `CtrlAmsStopDrying` publishes stop command +- Verify `GetFilamentDryingPreset` returns correct values for known filament IDs +- Verify dark mode rendering of all dialog pages + +## Constraints + +- **N3F/N3S only:** Standard AMS and AMS Lite do not have heating hardware; the dialog is not shown for them +- **Backward compatibility:** Existing `command_ams_drying_stop()` is preserved; new commands are additive +- **Cross-platform:** wxWidgets UI must work on Windows, macOS, Linux +- **Dark mode:** Dialog must support dark mode via `wxGetApp().dark_mode()` and `StateColor::darkModeColorFor()` +- **Profile compatibility:** Existing profiles with `filament_dev_ams_drying_*` keys (Qidi) continue to work; the feature reads the same keys +- **No printer-agent changes needed:** All commands go through existing `publish_json()` → `NetworkAgent` path, which already handles MQTT for connected printers From 885f14ed6533a472e394552d71b34ad55d6233c9 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 11:13:35 +0800 Subject: [PATCH 02/30] docs: fix spec self-review issues Clarify dialog modality, precise image asset list, DevAmsType naming consistency. --- .../2026-07-07-ams-drying-control-design.md | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md b/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md index 940534a501..114164a670 100644 --- a/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md +++ b/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md @@ -55,24 +55,38 @@ The feature spans three layers, matching the existing OrcaSlicer `DeviceCore/` p | `DeviceErrorDialog.cpp` | Update stop-drying to use `DevFilaSystem::CtrlAmsStopDrying()` | | `DeviceCore/CMakeLists.txt` | Register new source files | -### New image assets (~14) +### New image assets (14) -Humidity level icons (5), drying status per AMS type (8: heating/dehumidifying/error/cooling × N3F/N3S), heating icon, enable/disable indicators, guide image. +| Asset | Purpose | +|-------|---------| +| `hum_level1_no_num_light.svg` – `hum_level5_no_num_light.svg` | 5 humidity level icons | +| `dev_ams_dry_ctr_n3f_heating.png`, `_dehumidifying.png`, `_error.png`, `_cooling.png` | 4 N3F drying state images | +| `dev_ams_dry_ctr_n3s_heating.png`, `_dehumidifying.png`, `_error.png`, `_cooling.png` | 4 N3S drying state images | +| `dev_ams_dry_ctr_heating_icon.svg` | Animated heating indicator | +| `dev_ams_dry_ctr_filament_in_chamber.png` | Guide page illustration | +| `dev_ams_dry_ctr_enable.svg`, `dev_ams_dry_ctr_disable.svg` | Filament tray status icons | + +Additionally, OrcaSlicer already has `ams_drying.svg` and `ams_is_drying.svg`. ## Data Model ### `DevDefs.h` — Global `DevAmsType` enum -Promoted from `DevAms::AmsType` (currently local to the class) to a global enum so `DevFilamentDryingPreset` and `DevUtilBackend` can reference it without depending on `DevAms`: +Promoted from `DevAms::AmsType` (currently local to the class) to a global enum in `DevDefs.h` so `DevFilamentDryingPreset` and `DevUtilBackend` can reference it without depending on `DevAms`. `DevAms::AmsType` is updated to be a typedef for `DevAmsType`, and all existing references to `DevAms::AmsType` remain valid (it's the same type): ```cpp +// DevDefs.h — global enum using OrcaSlicer's existing names (same integer values as before) enum DevAmsType : int { - EXT_SPOOL = 0, + DUMMY = 0, AMS = 1, AMS_LITE = 2, N3F = 3, // AMS 2 Pro N3S = 4, // AMS HT }; + +// DevFilaSystem.h — DevAms class updated: +// Replace "enum AmsType { DUMMY=0, AMS=1, AMS_LITE=2, N3F=3, N3S=4 }" +// with "using AmsType = DevAmsType;" (keeps existing code compiling) ``` ### `DevAms` class additions (in `DevFilaSystem.h`) @@ -260,7 +274,7 @@ In `AMSControl.cpp`, the `EVT_AMS_SHOW_HUMIDITY_TIPS` handler currently shows ei 1. Lazily create `AMSDryCtrWin*` member on `AMSControl` (like `m_percent_humidity_dry_popup` today) 2. Call `set_ams_id()` and `update(fila_system, obj)` before showing -3. Show the dialog modally or modelessly +3. Show the dialog modally via `ShowModal()` (matching BambuStudio behavior) ### Periodic updates From e0d80ee953920eda6f015953a5a43663c196e0c0 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 11:21:00 +0800 Subject: [PATCH 03/30] docs: add BambuStudio reference path to spec --- .../specs/2026-07-07-ams-drying-control-design.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md b/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md index 114164a670..cecbf7852f 100644 --- a/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md +++ b/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md @@ -2,6 +2,7 @@ **Date:** 2026-07-07 **Source:** Port from BambuStudio `AMSDryControl` feature +**Reference:** `D:\projects\BambuStudio` — match its behavior as much as possible; when in doubt, check the BambuStudio source **Branch:** `dev/ams-heat` ## Overview @@ -75,9 +76,9 @@ Additionally, OrcaSlicer already has `ams_drying.svg` and `ams_is_drying.svg`. Promoted from `DevAms::AmsType` (currently local to the class) to a global enum in `DevDefs.h` so `DevFilamentDryingPreset` and `DevUtilBackend` can reference it without depending on `DevAms`. `DevAms::AmsType` is updated to be a typedef for `DevAmsType`, and all existing references to `DevAms::AmsType` remain valid (it's the same type): ```cpp -// DevDefs.h — global enum using OrcaSlicer's existing names (same integer values as before) +// DevDefs.h — global enum matching BambuStudio's DevAmsType naming enum DevAmsType : int { - DUMMY = 0, + EXT_SPOOL = 0, AMS = 1, AMS_LITE = 2, N3F = 3, // AMS 2 Pro @@ -86,7 +87,8 @@ enum DevAmsType : int { // DevFilaSystem.h — DevAms class updated: // Replace "enum AmsType { DUMMY=0, AMS=1, AMS_LITE=2, N3F=3, N3S=4 }" -// with "using AmsType = DevAmsType;" (keeps existing code compiling) +// with "using AmsType = DevAmsType;" +// Existing references to DevAms::DUMMY become DevAms::EXT_SPOOL (same value, different name). ``` ### `DevAms` class additions (in `DevFilaSystem.h`) From c983f60d00169a43bfb4f15b169b5e011c85ec80 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 11:26:38 +0800 Subject: [PATCH 04/30] docs: add AMS drying control implementation plan 12 tasks covering: data model changes, DevUtilBackend utility, drying commands, JSON parsing, UI dialog port, integration wiring, image assets, and verification. --- .../2026-07-07-ams-drying-control-plan.md | 1046 +++++++++++++++++ 1 file changed, 1046 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-07-ams-drying-control-plan.md diff --git a/docs/superpowers/plans/2026-07-07-ams-drying-control-plan.md b/docs/superpowers/plans/2026-07-07-ams-drying-control-plan.md new file mode 100644 index 0000000000..6cd673c5a6 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-ams-drying-control-plan.md @@ -0,0 +1,1046 @@ +# AMS Drying Control — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Port the AMS filament drying control feature from BambuStudio into OrcaSlicer, enabling users to start, monitor, and stop filament drying via the AMS unit from the slicer UI. + +**Architecture:** Add drying status enums/structs to the `DevAms` data model, extend the JSON parser to populate them, add `CtrlAmsStartDryingHour`/`CtrlAmsStopDrying` commands to `DevFilaSystem`, create a `DevUtilBackend` utility for filament drying preset lookup, and build the `AMSDryCtrWin` dialog. Wire the dialog to open when clicking the AMS humidity indicator for N3F/N3S AMS types. + +**Tech Stack:** C++17, wxWidgets, nlohmann/json, Boost + +## Global Constraints + +- **N3F/N3S only:** Standard AMS and AMS Lite do not have heating hardware; the dialog is not shown for them +- **Backward compatibility:** Existing `command_ams_drying_stop()` is preserved; new commands are additive +- **Cross-platform:** wxWidgets UI must work on Windows, macOS, Linux +- **Dark mode:** Dialog must support dark mode via `wxGetApp().dark_mode()` and `StateColor::darkModeColorFor()` +- **Profile compatibility:** Existing profiles with `filament_dev_ams_drying_*` keys (Qidi) continue to work; the feature reads the same keys +- **No printer-agent changes needed:** All commands go through existing `publish_json()` → `NetworkAgent` path +- **Reference:** `D:\projects\BambuStudio` — match its behavior as much as possible; when in doubt, check the BambuStudio source + +--- + +### Task 1: Global `DevAmsType` enum in `DevDefs.h` + +**Files:** +- Modify: `src/slic3r/GUI/DeviceCore/DevDefs.h` +- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystem.h` (replace local `AmsType` enum with typedef) +- Modify: `src/slic3r/GUI/DeviceCore/DevMapping.cpp` (rename `DevAms::DUMMY` → `DevAms::EXT_SPOOL`) + +**Interfaces:** +- Produces: Global `enum DevAmsType { EXT_SPOOL=0, AMS=1, AMS_LITE=2, N3F=3, N3S=4 }` in `DevDefs.h` +- Produces: `using AmsType = DevAmsType;` inside `DevAms` class (replaces old local enum) +- Note: Old `DevAms::DUMMY` becomes `DevAms::EXT_SPOOL`; all existing references update accordingly + +- [ ] **Step 1: Add global `DevAmsType` enum to `DevDefs.h`** + +In `src/slic3r/GUI/DeviceCore/DevDefs.h`, add before the closing `namespace Slic3r` or after existing enums: + +```cpp +enum DevAmsType : int +{ + EXT_SPOOL = 0, // EXT + AMS = 1, // AMS1 + AMS_LITE = 2, // AMS-Lite + N3F = 3, // N3F, AMS 2PRO + N3S = 4, // N3S, AMS HT +}; +``` + +- [ ] **Step 2: Replace `DevAms::AmsType` local enum with typedef** + +In `src/slic3r/GUI/DeviceCore/DevFilaSystem.h`, find the `DevAms` class and replace the local enum: + +```cpp +// Before (delete this): + enum AmsType : int + { + DUMMY = 0, + AMS = 1, // AMS + AMS_LITE = 2, // AMS-Lite + N3F = 3, // N3F + N3S = 4, // N3S + }; + +// After (replace with): + using AmsType = DevAmsType; +``` + +- [ ] **Step 3: Rename `DevAms::DUMMY` → `DevAms::EXT_SPOOL` in `DevMapping.cpp`** + +In `src/slic3r/GUI/DeviceCore/DevMapping.cpp` line 189: + +```cpp +// Before: +_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::DUMMY, tray, info); +// After: +_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info); +``` + +- [ ] **Step 4: Rename `DUMMY` → `EXT_SPOOL` in `DevFilaSystem.cpp`** + +In `src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp` line 114: + +```cpp +// Before: +assert(DUMMY < type && m_ams_type <= N3S); +// After: +assert(EXT_SPOOL < type && m_ams_type <= N3S); +``` + +- [ ] **Step 5: Verify the build compiles** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +Expected: build succeeds, no `DUMMY` or `AmsType` related compile errors. + +- [ ] **Step 6: Commit** + +```bash +git add src/slic3r/GUI/DeviceCore/DevDefs.h src/slic3r/GUI/DeviceCore/DevFilaSystem.h src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp src/slic3r/GUI/DeviceCore/DevMapping.cpp +git commit -m "refactor: promote DevAmsType to global enum, rename DUMMY to EXT_SPOOL + +Matches BambuStudio's DevAmsType naming convention." +``` + +--- + +### Task 2: Drying enums and structs in `DevAms` + +**Files:** +- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystem.h` + +**Interfaces:** +- Produces: `DevAms::DryCtrlMode`, `DevAms::DryStatus`, `DevAms::DrySubStatus`, `DevAms::DryFanStatus`, `DevAms::CannotDryReason` enums +- Produces: `DevAms::DrySettings` struct +- Produces: Getters: `GetDryStatus()`, `GetDrySubStatus()`, `GetFan1Status()`, `GetFan2Status()`, `GetCannotDryReason()`, `GetDrySettings()` +- Produces: `IsSupportRemoteDry(const MachineObject*)`, `AmsIsDrying()` +- Produces: `SupportDrying()` — already exists as `m_ams_type > AMS_LITE`; update to check N3F/N3S +- Produces: Members: `m_dry_status`, `m_dry_sub_status`, `m_dry_fan1_status`, `m_dry_fan2_status`, `m_dry_cannot_reasons`, `m_dry_settings` + +**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevFilaSystem.h` lines 119–262 + +- [ ] **Step 1: Add enums inside `DevAms` class** + +In `src/slic3r/GUI/DeviceCore/DevFilaSystem.h`, inside the `DevAms` class (after the `using AmsType = DevAmsType;` line, before the `public:` constructor section), add: + +```cpp +public: + + enum class DryCtrlMode : int + { + Off = 0, + OnTime = 1, + OnHumidity = 2, + }; + + enum class DryStatus : char + { + Off = 0, + Checking = 1, + Drying = 2, + Cooling = 3, + Stopping = 4, + Error = 5, + CannotStopHeatOutofControl = 6, + PrdTesting = 7, + }; + + enum class DrySubStatus + { + Off = 0, + Heating = 1, + Dehumidify = 2, + }; + + enum class DryFanStatus : char + { + Off = 0, + On = 1, + }; + + enum class CannotDryReason : int + { + TaskOccupied = 0, + InsufficientPower = 1, + AmsBusy = 2, + ConsumableAtAmsOutlet = 3, + InitiatingAmsDrying = 4, + NotSupportedIn2dMode = 5, + DryingInProgress = 6, + Upgrading = 7, + InsufficientPowerNeedPluginPower = 8, + FilamentAtAmsOutletManualUnload = 10, + }; + + struct DrySettings + { + std::string dry_filament; + int dry_temp = -1; // -1 means invalid + int dry_hour = -1; // -1 means invalid, hours + }; +``` + +- [ ] **Step 2: Add new getters to `DevAms`** + +After the existing `GetLeftDryTime()` getter, add: + +```cpp + // remote drying control + bool IsSupportRemoteDry(const MachineObject* obj) const; + std::optional GetDryStatus() const { return m_dry_status; }; + std::optional GetDrySubStatus() const { return m_dry_sub_status; } + std::optional GetFan1Status() const { return m_dry_fan1_status; } + std::optional GetFan2Status() const { return m_dry_fan2_status; } + std::optional> GetCannotDryReason() const { return m_dry_cannot_reasons; } + std::optional GetDrySettings() const { return m_dry_settings; }; + + bool AmsIsDrying(); +``` + +- [ ] **Step 3: Add new private members to `DevAms`** + +In the private section of `DevAms`, after `m_left_dry_time`, add: + +```cpp + // see is_support_remote_dry + std::optional m_dry_status; + std::optional m_dry_sub_status; + std::optional m_dry_fan1_status; + std::optional m_dry_fan2_status; + std::optional> m_dry_cannot_reasons; + std::optional m_dry_settings; +``` + +Note: In BambuStudio these members are `public:` (prefixed with `public:` before `m_dry_status`). Keep them private and use getters. The friends (`DevFilaSystemParser`) can access them. + +Also remove the duplicate `GetAmsType()` which currently returns `m_ams_type` — since `m_ams_type` is now `DevAmsType`, the getter still works. + +- [ ] **Step 4: Add forward declaration for `DevFilamentDryingPreset`** + +At the top of the file, after the existing forward declarations: + +```cpp +struct DevFilamentDryingPreset; +``` + +- [ ] **Step 5: Verify the header compiles** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +Expected: build succeeds. If there are compilation errors related to `GetAmsType()` return type, check that `AmsType` typedef resolves correctly. + +- [ ] **Step 6: Commit** + +```bash +git add src/slic3r/GUI/DeviceCore/DevFilaSystem.h +git commit -m "feat: add drying enums, structs, getters to DevAms + +Adds DryCtrlMode, DryStatus, DrySubStatus, DryFanStatus, +CannotDryReason enums and DrySettings struct for AMS drying control." +``` + +--- + +### Task 3: `DevFilamentDryingPreset` struct and `DevFilaSystem` ctrl declarations + +**Files:** +- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystem.h` + +**Interfaces:** +- Produces: `struct DevFilamentDryingPreset` at namespace scope +- Produces: `DevFilaSystem::CtrlAmsStartDryingHour(int ams_id, std::string filament_type, int tag_temp, int tag_duration_hour, bool rotate_tray, int cooling_temp, bool close_power_conflict = false) const` +- Produces: `DevFilaSystem::CtrlAmsStopDrying(int ams_id) const` +- Produces: `DevAmsTray::get_ams_drying_preset()` method + +**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevFilaSystem.h` lines 350–362 + +- [ ] **Step 1: Add `DevFilamentDryingPreset` struct at namespace scope** + +In `src/slic3r/GUI/DeviceCore/DevFilaSystem.h`, add after the `DevFilaSystemParser` class (at the very end of the file, before `}// namespace Slic3r`): + +```cpp +struct DevFilamentDryingPreset +{ + std::string filament_id; + + std::unordered_set ams_limitations; // only use ams types in the set + std::unordered_map filament_dev_ams_drying_time_on_idle; // hour + std::unordered_map filament_dev_ams_drying_temperature_on_idle; + std::unordered_map filament_dev_ams_drying_time_on_print; // hour + std::unordered_map filament_dev_ams_drying_temperature_on_print; + float filament_dev_drying_cooling_temperature = 0.0f; + float filament_dev_drying_softening_temperature = 0.0f; + float filament_dev_ams_drying_heat_distortion_temperature = 0.0f; +}; +``` + +- [ ] **Step 2: Add control method declarations to `DevFilaSystem`** + +In the `DevFilaSystem` class, after the existing `CtrlAmsReset()` declaration: + +```cpp + // crtl + int CtrlAmsStartDryingHour(int ams_id, std::string filament_type, int tag_temp, int tag_duration_hour, bool rotate_tray, int cooling_temp, bool close_power_conflict = false) const; + int CtrlAmsStopDrying(int ams_id) const; +``` + +- [ ] **Step 3: Add `get_ams_drying_preset()` to `DevAmsTray`** + +In the `DevAmsTray` class, add after the `get_filament_type()` method: + +```cpp + std::optional get_ams_drying_preset() const; +``` + +Also add `#include ` and `#include ` to the includes if not already present. +Add `#include ` if not already present. + +- [ ] **Step 4: Verify build** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +- [ ] **Step 5: Commit** + +```bash +git add src/slic3r/GUI/DeviceCore/DevFilaSystem.h +git commit -m "feat: add DevFilamentDryingPreset, ctrl method declarations, tray preset getter" +``` + +--- + +### Task 4: `DevUtilBackend` utility class + +**Files:** +- Create: `src/slic3r/GUI/DeviceCore/DevUtilBackend.h` +- Create: `src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp` +- Modify: `src/slic3r/GUI/DeviceCore/CMakeLists.txt` + +**Interfaces:** +- Produces: `DevUtilBackend::GetFilamentDryingPreset(const std::string& fila_id)` → `std::optional` + +**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevUtilBackend.h`, `DevUtilBackend.cpp` + +- [ ] **Step 1: Create `DevUtilBackend.h`** + +Create `src/slic3r/GUI/DeviceCore/DevUtilBackend.h`: + +```cpp +/** + * @file DevUtilBackend.h + * @brief Provides common static utility methods for backend (preset/slicing). + */ + +#pragma once +#include "DevDefs.h" +#include "DevFilaSystem.h" + +#include +#include + +namespace Slic3r +{ + +class DevUtilBackend +{ +public: + DevUtilBackend() = delete; + +public: + // for filament preset + static std::optional GetFilamentDryingPreset(const std::string& fila_id); +}; + +}; // namespace Slic3r +``` + +- [ ] **Step 2: Create `DevUtilBackend.cpp`** + +Create `src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp`: + +```cpp +#include "DevUtilBackend.h" + +#include "slic3r/GUI/GUI_App.hpp" + +#include "libslic3r/Preset.hpp" + +#include + +namespace Slic3r +{ + +static std::unordered_map s_ams_type_map = { + {"0", DevAmsType::N3F}, + {"1", DevAmsType::N3S}, +}; + +std::optional DevUtilBackend::GetFilamentDryingPreset(const std::string& fila_id) +{ + if (fila_id.empty() || !GUI::wxGetApp().preset_bundle) { + return std::nullopt; + } + + for (auto iter = GUI::wxGetApp().preset_bundle->filaments.begin(); iter != GUI::wxGetApp().preset_bundle->filaments.end(); ++iter) { + const Preset& filament_preset = *iter; + const auto& config = filament_preset.config; + if (filament_preset.filament_id == fila_id) { + DevFilamentDryingPreset info; + info.filament_id = fila_id; + try { + if (config.has("filament_dev_ams_drying_ams_limitations")) { + std::vector types = config.option("filament_dev_ams_drying_ams_limitations")->values; + for (auto type : types) { + if (s_ams_type_map.count(type) == 0) { + continue; + } + info.ams_limitations.insert(s_ams_type_map[type]); + } + } + + if (config.has("filament_dev_ams_drying_temperature")) { + info.filament_dev_ams_drying_temperature_on_idle[DevAmsType::N3F] = config.option("filament_dev_ams_drying_temperature")->get_at(0); + info.filament_dev_ams_drying_temperature_on_idle[DevAmsType::N3S] = config.option("filament_dev_ams_drying_temperature")->get_at(1); + info.filament_dev_ams_drying_temperature_on_print[DevAmsType::N3F] = config.option("filament_dev_ams_drying_temperature")->get_at(2); + info.filament_dev_ams_drying_temperature_on_print[DevAmsType::N3S] = config.option("filament_dev_ams_drying_temperature")->get_at(3); + } + + if (config.has("filament_dev_ams_drying_time")) { + info.filament_dev_ams_drying_time_on_idle[DevAmsType::N3F] = config.option("filament_dev_ams_drying_time")->get_at(0); + info.filament_dev_ams_drying_time_on_idle[DevAmsType::N3S] = config.option("filament_dev_ams_drying_time")->get_at(1); + info.filament_dev_ams_drying_time_on_print[DevAmsType::N3F] = config.option("filament_dev_ams_drying_time")->get_at(2); + info.filament_dev_ams_drying_time_on_print[DevAmsType::N3S] = config.option("filament_dev_ams_drying_time")->get_at(3); + } + + if (config.has("filament_dev_drying_softening_temperature")) { + info.filament_dev_drying_softening_temperature = config.option("filament_dev_drying_softening_temperature")->get_at(0); + } + + if (config.has("filament_dev_ams_drying_heat_distortion_temperature")) { + info.filament_dev_ams_drying_heat_distortion_temperature = config.option("filament_dev_ams_drying_heat_distortion_temperature")->get_at(0); + } + + if (config.has("filament_dev_drying_cooling_temperature")) { + info.filament_dev_drying_cooling_temperature = config.option("filament_dev_drying_cooling_temperature")->get_at(0); + } + + return info; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " exception: " << e.what(); + } + } + } + + return std::nullopt; +} + +}; // namespace Slic3r +``` + +- [ ] **Step 3: Register new files in CMakeLists.txt** + +In `src/slic3r/GUI/DeviceCore/CMakeLists.txt`, add the new files. After the existing `DevFilaSystemCtrl.cpp` line: + +```cmake + GUI/DeviceCore/DevUtilBackend.h + GUI/DeviceCore/DevUtilBackend.cpp +``` + +- [ ] **Step 4: Verify build** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +Expected: build succeeds. If `ConfigOptionStrings` or `ConfigOptionFloats` are not found, check the includes — they're in `libslic3r/Preset.hpp` and `libslic3r/PrintConfig.hpp`. + +- [ ] **Step 5: Commit** + +```bash +git add src/slic3r/GUI/DeviceCore/DevUtilBackend.h src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp src/slic3r/GUI/DeviceCore/CMakeLists.txt +git commit -m "feat: add DevUtilBackend with GetFilamentDryingPreset + +Reads filament_dev_ams_drying_* config keys from filament presets +and returns structured DevFilamentDryingPreset data." +``` + +--- + +### Task 5: JSON parsing for drying status fields + +**Files:** +- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp` + +**Interfaces:** +- Consumes: `DevAms::DryStatus`, `DevAms::DrySubStatus`, `DevAms::DryFanStatus`, `DevAms::CannotDryReason`, `DevAms::DrySettings` +- Parses from JSON: `dry_time`, info flag bits (dry_status/fan1/fan2/sub_status), `dry_setting`, `dry_sf_reason` + +**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevFilaSystem.cpp` lines 690–712 + +- [ ] **Step 1: Locate the AMS parsing section in `DevFilaSystemParser::ParseV1_0()`** + +First, read `DevFilaSystem.cpp` to find where `m_left_dry_time` is parsed (look for "dry_time") and where the AMS info section is. The parsing function is `DevFilaSystemParser::ParseV1_0`. + +- [ ] **Step 2: Add drying status field parsing** + +After the existing `m_left_dry_time` parsing line (which parses `"dry_time"` from JSON), add the drying status parsing. The exact location will be in the AMS loop where `curr_ams` is populated. + +After `DevJsonValParser::ParseVal(j_ams, "dry_time", curr_ams->m_left_dry_time);` (or the equivalent line), add: + +```cpp + // Drying status — only parse if printer supports remote drying + if (obj->is_support_remote_dry) { + if (j_ams.contains("info")) { + const std::string& info = j_ams["info"].get(); + curr_ams->m_dry_status = (DevAms::DryStatus)DevUtil::get_flag_bits(info, 4, 4); + curr_ams->m_dry_fan1_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 18, 2); + curr_ams->m_dry_fan2_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 20, 2); + curr_ams->m_dry_sub_status = (DevAms::DrySubStatus)DevUtil::get_flag_bits(info, 22, 2); + } + + if (j_ams.contains("dry_setting")) { + const auto& j_dry_settings = j_ams["dry_setting"]; + DevAms::DrySettings dry_settings; + DevJsonValParser::ParseVal(j_dry_settings, "dry_filament", dry_settings.dry_filament); + DevJsonValParser::ParseVal(j_dry_settings, "dry_temperature", dry_settings.dry_temp); + DevJsonValParser::ParseVal(j_dry_settings, "dry_duration", dry_settings.dry_hour); + curr_ams->m_dry_settings = dry_settings; + } + + if (j_ams.contains("dry_sf_reason")) { + curr_ams->m_dry_cannot_reasons = DevJsonValParser::GetVal>(j_ams, "dry_sf_reason"); + } + } +``` + +Note: Check if `DevUtil::get_flag_bits` and `DevJsonValParser` exist in OrcaSlicer. If not, these utilities must be added. Check BambuStudio's `DevUtil.h` for `get_flag_bits` and `DevJsonValParser` for the parsing helper. If these are missing, add minimal versions or inline the parsing. + +Check specifically whether `DevUtil.h` in OrcaSlicer has `get_flag_bits`. The `DevUtil.h` exists in the CMakeLists. Check its contents — it should have a static `get_flag_bits(std::string, int, int)` method. If not present, port it from BambuStudio. + +- [ ] **Step 3: Implement `IsSupportRemoteDry()` and `AmsIsDrying()`** + +In the same file, add the implementations after the existing `DevAms` methods: + +```cpp +bool DevAms::IsSupportRemoteDry(const MachineObject* obj) const +{ + if (obj && obj->is_support_remote_dry) { + return SupportDrying(); + } + return false; +} + +bool DevAms::AmsIsDrying() +{ + if (!GetDryStatus().has_value()) { + return false; + } + + return GetDryStatus().value() == DevAms::DryStatus::Checking + || GetDryStatus().value() == DevAms::DryStatus::Drying + || GetDryStatus().value() == DevAms::DryStatus::Error + || GetDryStatus().value() == DevAms::DryStatus::CannotStopHeatOutofControl; +} +``` + +- [ ] **Step 4: Implement `DevAmsTray::get_ams_drying_preset()`** + +```cpp +std::optional DevAmsTray::get_ams_drying_preset() const +{ + return DevUtilBackend::GetFilamentDryingPreset(setting_id); +} +``` + +- [ ] **Step 5: Ensure `#include "DevUtilBackend.h"` is present in `DevFilaSystem.cpp`** + +Add at the top if not present. + +- [ ] **Step 6: Verify build** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +- [ ] **Step 7: Commit** + +```bash +git add src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +git commit -m "feat: add drying status JSON parsing and DevAms helper methods + +Parse dry_status, dry_sub_status, dry_fan statuses, +dry_settings, and dry_cannot_reasons from printer JSON. +Implement IsSupportRemoteDry, AmsIsDrying, get_ams_drying_preset." +``` + +--- + +### Task 6: `is_support_remote_dry` flag on `MachineObject` + +**Files:** +- Modify: `src/slic3r/GUI/DeviceManager.hpp` (add field) +- Modify: `src/slic3r/GUI/DeviceManager.cpp` (parse the flag) + +**Interfaces:** +- Produces: `MachineObject::is_support_remote_dry` (bool) + +**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceManager.hpp` line 566, `DeviceManager.cpp` line 4403 + +- [ ] **Step 1: Add `is_support_remote_dry` field to `MachineObject`** + +In `src/slic3r/GUI/DeviceManager.hpp`, find the section with other `is_support_*` flags (near `is_support_ams_humidity`) and add: + +```cpp + bool is_support_remote_dry = false; +``` + +- [ ] **Step 2: Parse `is_support_remote_dry` from firmware flags** + +In `src/slic3r/GUI/DeviceManager.cpp`, find the `parse_version_func()` or `parse_new_info()` method where other `is_support_*` flags are set. Look for the `is_support_ams_humidity` parsing pattern. + +Add parsing for `is_support_remote_dry` from the `fun2` field. Check the BambuStudio reference: it uses `get_flag_bits_no_border(fun2, 5) == 1` for bit 5. The exact location depends on how OrcaSlicer parses firmware capability bits. + +If a `fun2` parsing block exists, add: +```cpp + is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1); +``` + +If `fun2` parsing or `get_flag_bits_no_border` don't exist yet in OrcaSlicer, check how similar flags like `is_support_ams_humidity` are parsed and follow the same pattern. If the needed helper methods are missing, port them from BambuStudio's `DeviceManager.cpp`. + +- [ ] **Step 3: Verify build** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/slic3r/GUI/DeviceManager.hpp src/slic3r/GUI/DeviceManager.cpp +git commit -m "feat: add is_support_remote_dry flag to MachineObject + +Parsed from firmware fun2 bit 5. Controls whether drying +status fields are parsed and drying commands are available." +``` + +--- + +### Task 7: Start/Stop drying commands in `DevFilaSystemCtrl.cpp` + +**Files:** +- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp` + +**Interfaces:** +- Consumes: `MachineObject::m_sequence_id`, `MachineObject::publish_json()` +- Produces: `DevFilaSystem::CtrlAmsStartDryingHour(...)`, `DevFilaSystem::CtrlAmsStopDrying(int)` + +**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevFilaSystemCtrl.cpp` + +- [ ] **Step 1: Add `CtrlAmsStartDryingHour` and `CtrlAmsStopDrying`** + +In `src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp`, add after the existing `CtrlAmsReset()` method: + +```cpp +int DevFilaSystem::CtrlAmsStartDryingHour(int ams_id, + std::string filament_type, + int tag_temp, + int tag_duration_hour, + bool rotate_tray, + int cooling_temp, + bool close_power_conflict) const +{ + json jj_command; + jj_command["print"]["command"] = "ams_filament_drying"; + jj_command["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); + jj_command["print"]["ams_id"] = ams_id; + jj_command["print"]["mode"] = DevAms::DryCtrlMode::OnTime; + jj_command["print"]["filament"] = filament_type; + jj_command["print"]["temp"] = tag_temp; + jj_command["print"]["duration"] = tag_duration_hour; + jj_command["print"]["humidity"] = 0; + jj_command["print"]["rotate_tray"] = rotate_tray; + jj_command["print"]["cooling_temp"] = cooling_temp; + jj_command["print"]["close_power_conflict"] = close_power_conflict; + return m_owner->publish_json(jj_command); +} + +int DevFilaSystem::CtrlAmsStopDrying(int ams_id) const +{ + json jj_command; + jj_command["print"]["command"] = "ams_filament_drying"; + jj_command["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); + jj_command["print"]["ams_id"] = ams_id; + jj_command["print"]["mode"] = DevAms::DryCtrlMode::Off; + jj_command["print"]["filament"] = ""; + jj_command["print"]["temp"] = 0; + jj_command["print"]["duration"] = 0; + jj_command["print"]["humidity"] = 0; + jj_command["print"]["rotate_tray"] = false; + jj_command["print"]["cooling_temp"] = 0; + jj_command["print"]["close_power_conflict"] = false; + return m_owner->publish_json(jj_command); +} +``` + +Note: The `DevAms::DryCtrlMode` enum class uses `DevAms::DryCtrlMode::OnTime` in C++. When serialized to JSON (`jj_command["print"]["mode"] = DevAms::DryCtrlMode::OnTime`), nlohmann/json will convert the enum class to its underlying int (1). This matches the BambuStudio behavior. If the implicit conversion doesn't compile, add a `static_cast()` wrapper. + +- [ ] **Step 2: Verify build** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp +git commit -m "feat: add CtrlAmsStartDryingHour and CtrlAmsStopDrying commands + +Publish 'ams_filament_drying' JSON commands via MQTT for +starting and stopping AMS filament drying." +``` + +--- + +### Task 8: Copy image assets from BambuStudio + +**Files:** +- Copy 14 image files from `D:\projects\BambuStudio\resources\images\` to `resources\images\` + +**Assets to copy:** + +```powershell +Copy-Item "D:\projects\BambuStudio\resources\images\hum_level1_no_num_light.svg" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\hum_level2_no_num_light.svg" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\hum_level3_no_num_light.svg" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\hum_level4_no_num_light.svg" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\hum_level5_no_num_light.svg" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3f_heating.png" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3f_dehumidifying.png" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3f_error.png" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3f_cooling.png" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3s_heating.png" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3s_dehumidifying.png" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3s_error.png" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3s_cooling.png" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_heating_icon.svg" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_filament_in_chamber.png" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_enable.svg" -Destination "resources\images\" +Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_disable.svg" -Destination "resources\images\" +``` + +- [ ] **Step 1: Copy each asset** + +Run each Copy-Item command. If any file is missing from the source, note it — the dialog will degrade gracefully with null bitmaps. + +- [ ] **Step 2: Verify files exist** + +```powershell +Get-ChildItem resources\images\dev_ams_dry_ctr_*, resources\images\hum_level* | Select Name +``` + +Expected: all 17 files listed. + +- [ ] **Step 3: Commit** + +```bash +git add resources/images/dev_ams_dry_ctr_* resources/images/hum_level* +git commit -m "assets: add AMS drying control images from BambuStudio + +14 images: humidity level icons, drying state images per AMS type, +heating icon, guide illustration, tray status icons." +``` + +--- + +### Task 9: `AMSDryControl` UI dialog + +**Files:** +- Create: `src/slic3r/GUI/AMSDryControl.hpp` +- Create: `src/slic3r/GUI/AMSDryControl.cpp` + +**Interfaces:** +- Produces: `AMSDryCtrWin` dialog class extending `DPIDialog` +- Produces: `FilamentItemPanel`, `AMSFilamentPanel` helper widget classes +- Consumes: `DevFilaSystem`, `MachineObject`, `DevAms`, `DevUtilBackend::GetFilamentDryingPreset()` + +**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\AMSDryControl.hpp` and `AMSDryControl.cpp` + +- [ ] **Step 1: Create `AMSDryControl.hpp`** + +Create `src/slic3r/GUI/AMSDryControl.hpp`. Port the full header from BambuStudio `AMSDryControl.hpp`, adapting includes for OrcaSlicer paths: + +Key adaptations: +- Change `#include "GUI_ObjectLayers.hpp"` to appropriate OrcaSlicer equivalent (check if it exists; if not, it may not be needed or can be replaced with `wx/wx.h`) +- Keep includes for `slic3r/GUI/Widgets/AMSItem.hpp`, `Widgets/Label.hpp`, `Widgets/PopupWindow.hpp`, `wxExtensions.hpp`, `DeviceCore/DevFilaSystem.h` +- Keep the full class declarations for `FilamentItemPanel`, `AMSFilamentPanel`, `AMSDryCtrWin` + +The complete header from `D:\projects\BambuStudio\src\slic3r\GUI\AMSDryControl.hpp` (lines 1–252) should be ported with path adjustments. Due to the file's length (~250 lines), copy the full content from the reference and adapt. + +- [ ] **Step 2: Create `AMSDryControl.cpp`** + +Create `src/slic3r/GUI/AMSDryControl.cpp`. Port from `D:\projects\BambuStudio\src\slic3r\GUI\AMSDryControl.cpp` (lines 1–1860). Key adaptations: + +Replace these BambuStudio includes: +```cpp +#include "AMSDryControl.hpp" +#include "DeviceCore/DevFilaSystem.h" +#include "GUI_App.hpp" +#include "I18N.hpp" +#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" +#include "slic3r/GUI/DeviceCore/DevUpgrade.h" +#include "slic3r/GUI/DeviceCore/DevManager.h" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/Widgets/AnimaController.hpp" +#include "slic3r/GUI/Widgets/Label.hpp" +#include "slic3r/GUI/Widgets/ComboBox.hpp" +#include "slic3r/GUI/Widgets/ProgressBar.hpp" +#include "DeviceCore/DevUtilBackend.h" +``` + +Adapt includes for OrcaSlicer paths. Check which of these already exist: +- `GUI_App.hpp` → existing +- `I18N.hpp` → existing +- `DeviceCore/DevFilaSystem.h` → existing +- `Widgets/Label.hpp`, `Widgets/ComboBox.hpp`, `Widgets/ProgressBar.hpp` → check existence in OrcaSlicer + +Since this file is ~1860 lines, port it in full from the BambuStudio reference, adapting only the include paths and namespace references as needed for OrcaSlicer. The implementation logic should be identical to BambuStudio. + +Key parts to verify during port: +- `AMS_ITEMS_PANEL_SIZE`, `AMS_CONTROL_DEF_BLOCK_BK_COLOUR`, `AMS_CONTROL_BRAND_COLOUR`, `AMS_CONTROL_DISABLE_COLOUR`, `AMS_CONTROL_WHITE_COLOUR` — these constants are defined in `AMSItem.hpp`. Verify they exist in OrcaSlicer's version. +- `_L()` and `_CTX_utf8()` — I18N macros; verify they work the same in OrcaSlicer +- `StateColor` — widget state color class; verify it exists in OrcaSlicer +- `ScalableBitmap` — verify it exists in OrcaSlicer +- `ComboBox` — OrcaSlicer custom combobox; verify path +- `ProgressBar` — OrcaSlicer custom progress bar; verify path +- `FromDIP()` — DPI scaling helper; should exist in OrcaSlicer +- `encode_path()` — path encoding utility; should exist +- `resources_dir()` — resources directory getter; verify it exists +- `bool is_support_user_preset` on `MachineObject` — verify this field exists + +- [ ] **Step 3: Register `AMSDryControl` in CMakeLists** + +In `src/slic3r/CMakeLists.txt`, add after the `AMSSetting` entries (around line 30): + +```cmake + GUI/AMSDryControl.cpp + GUI/AMSDryControl.hpp +``` + +- [ ] **Step 4: Verify build** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +Fix any compilation errors from missing includes or type mismatches between OrcaSlicer and BambuStudio APIs. + +- [ ] **Step 5: Commit** + +```bash +git add src/slic3r/GUI/AMSDryControl.hpp src/slic3r/GUI/AMSDryControl.cpp +git commit -m "feat: add AMSDryControl dialog for AMS filament drying + +Three-page wizard: main status/control page, guide page with +filament tray status, and progress page. Supports N3F and N3S AMS types." +``` + +--- + +### Task 10: Wire humidity click to open `AMSDryCtrWin` + +**Files:** +- Modify: `src/slic3r/GUI/Widgets/AMSControl.hpp` +- Modify: `src/slic3r/GUI/Widgets/AMSControl.cpp` + +**Interfaces:** +- Consumes: `AMSDryCtrWin` dialog class +- Modifies: `EVT_AMS_SHOW_HUMIDITY_TIPS` handler — for N3F/N3S AMS types, opens `AMSDryCtrWin` instead of the popup + +- [ ] **Step 1: Add member and include to `AMSControl.hpp`** + +In `src/slic3r/GUI/Widgets/AMSControl.hpp`: + +Add forward declaration: +```cpp +class AMSDryCtrWin; +``` + +Add member variable near the existing `m_percent_humidity_dry_popup`: +```cpp + AMSDryCtrWin* m_dry_ctr_win{nullptr}; +``` + +- [ ] **Step 2: Add include to `AMSControl.cpp`** + +```cpp +#include "slic3r/GUI/AMSDryControl.hpp" +``` + +- [ ] **Step 3: Modify the `EVT_AMS_SHOW_HUMIDITY_TIPS` handler** + +In the handler at around line 243, modify the `else` branch (for non-GENERIC_AMS types) to check if the AMS type is N3F/N3S and open the `AMSDryCtrWin` instead of the popup: + +```cpp + Bind(EVT_AMS_SHOW_HUMIDITY_TIPS, [this](wxCommandEvent& evt) { + uiAmsHumidityInfo *info = (uiAmsHumidityInfo *) evt.GetClientData(); + if (info) + { + if (info->ams_type == AMSModel::GENERIC_AMS) + { + wxPoint img_pos = ClientToScreen(wxPoint(0, 0)); + wxPoint popup_pos(img_pos.x - m_Humidity_tip_popup.GetSize().GetWidth() + FromDIP(150), img_pos.y - FromDIP(80)); + m_Humidity_tip_popup.Position(popup_pos, wxSize(0, 0)); + + int humidity_value = info->humidity_display_idx; + if (humidity_value > 0 && humidity_value <= 5) { m_Humidity_tip_popup.set_humidity_level(humidity_value); } + m_Humidity_tip_popup.Popup(); + } + else if (info->ams_type == AMSModel::N3F_AMS || info->ams_type == AMSModel::N3S_AMS) + { + // Open full drying control dialog for N3F/N3S AMS + if (!m_dry_ctr_win) { + m_dry_ctr_win = new AMSDryCtrWin(this); + } + m_dry_ctr_win->set_ams_id(info->ams_id); + // Get fila system and machine object via the parent chain + // The dialog will be updated in parse_object() + m_dry_ctr_win->ShowModal(); + } + else + { + m_percent_humidity_dry_popup->Update(info); + + wxPoint img_pos = ClientToScreen(wxPoint(0, 0)); + wxPoint popup_pos(img_pos.x - m_percent_humidity_dry_popup->GetSize().GetWidth() + FromDIP(150), img_pos.y - FromDIP(80)); + m_percent_humidity_dry_popup->Move(popup_pos); + m_percent_humidity_dry_popup->ShowModal(); + } + } + + delete info; + }); +``` + +Note: The `AMSDryCtrWin` needs `DevFilaSystem` and `MachineObject` references for its `update()` method. The dialog's `update()` should be called before `ShowModal()`. The `MachineObject*` can be obtained from the parent widget chain or stored as a member of `AMSControl`. Check how `AMSControl` currently accesses `MachineObject` (likely through `m_obj` or similar). Pass it to the dialog's `update()` call. + +- [ ] **Step 4: Add periodic update for the dialog in `parse_object()`** + +In the `parse_object()` method (around line 954), find the existing humidity popup update block and extend it to also update the dry control dialog: + +```cpp + /*update AMS dry control dialog*/ + if (m_dry_ctr_win && m_dry_ctr_win->IsShown()) + { + // Get the MachineObject and DevFilaSystem from the current device + // m_dry_ctr_win->update(obj->GetFilaSystem_ptr(), obj); + } +``` + +Note: The exact code depends on how `AMSControl` accesses `MachineObject*`. Check existing patterns — it likely has access through `m_obj` or a similar member. The update will need both the `DevFilaSystem` (as `shared_ptr`) and `MachineObject*`. + +- [ ] **Step 5: Verify build** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +- [ ] **Step 6: Commit** + +```bash +git add src/slic3r/GUI/Widgets/AMSControl.hpp src/slic3r/GUI/Widgets/AMSControl.cpp +git commit -m "feat: wire AMS humidity click to open AMSDryControl dialog + +For N3F and N3S AMS types, clicking the humidity indicator +now opens the full drying control dialog instead of the popup." +``` + +--- + +### Task 11: Update error dialog stop-drying call + +**Files:** +- Modify: `src/slic3r/GUI/DeviceErrorDialog.cpp` + +**Interfaces:** +- Consumes: `DevFilaSystem::CtrlAmsStopDrying(int)` + +- [ ] **Step 1: Update `STOP_DRYING` case in `DeviceErrorDialog.cpp`** + +In `src/slic3r/GUI/DeviceErrorDialog.cpp` around line 452, update the stop drying action: + +```cpp + case DeviceErrorDialog::STOP_DRYING: { + // Use the canonical CtrlAmsStopDrying path + if (m_obj && m_obj->GetFilaSystem()) { + // Get the AMS ID — check how the dialog knows which AMS is involved + // For now, stop all AMS units or use the first one + m_obj->GetFilaSystem()->CtrlAmsStopDrying(0); + } + // Fallback to the old command for backward compatibility + m_obj->command_ams_drying_stop(); + break; + } +``` + +Note: The exact AMS ID to pass depends on the error context. Check how BambuStudio handles this — it may use a specific AMS ID from the error data, or stop all drying. The fallback to `command_ams_drying_stop()` ensures backward compatibility if `CtrlAmsStopDrying` fails or the fila system isn't available. + +- [ ] **Step 2: Verify build** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/slic3r/GUI/DeviceErrorDialog.cpp +git commit -m "feat: update error dialog stop-drying to use CtrlAmsStopDrying + +Keeps command_ams_drying_stop() fallback for backward compatibility." +``` + +--- + +### Task 12: Build, verify, and final integration test + +**Files:** All modified files + +- [ ] **Step 1: Full clean build** + +```powershell +cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m +``` + +Expected: Zero errors, zero warnings related to the new code. + +- [ ] **Step 2: Verify no regressions in existing AMS features** + +- Confirm the app launches +- Confirm AMS panel renders correctly for printers without N3F/N3S +- Confirm humidity popup still works for standard AMS and AMS Lite +- Confirm existing `command_ams_drying_stop()` still functions + +- [ ] **Step 3: Manual UI smoke test (requires N3F/N3S printer)** + +If a printer with N3F or N3S AMS is available: +1. Connect to the printer +2. Click AMS humidity indicator → verify `AMSDryCtrWin` opens +3. Verify humidity/temperature readings match printer data +4. Verify filament combobox is populated +5. Verify temp/time auto-fill when selecting a filament +6. Verify validation warnings for invalid temperatures +7. Verify the dialog closes cleanly + +- [ ] **Step 4: Commit any final fixes** + +```bash +git add -A +git commit -m "fix: final integration fixes for AMS drying control" +``` From 272e88a3379f3e8f537d34dc1f2a1137cb566b61 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 11:49:55 +0800 Subject: [PATCH 05/30] refactor: promote DevAmsType to global enum, rename DUMMY to EXT_SPOOL Matches BambuStudio's DevAmsType naming convention. --- src/slic3r/GUI/DeviceCore/DevDefs.h | 9 +++++++++ src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 2 +- src/slic3r/GUI/DeviceCore/DevFilaSystem.h | 14 ++++++-------- src/slic3r/GUI/DeviceCore/DevMapping.cpp | 2 +- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/slic3r/GUI/DeviceCore/DevDefs.h b/src/slic3r/GUI/DeviceCore/DevDefs.h index 468ed0184a..7cbf4d26c4 100644 --- a/src/slic3r/GUI/DeviceCore/DevDefs.h +++ b/src/slic3r/GUI/DeviceCore/DevDefs.h @@ -40,6 +40,15 @@ enum AmsStatusMain AMS_STATUS_MAIN_UNKNOWN = 0xFF, }; +enum DevAmsType : int +{ + EXT_SPOOL = 0, // EXT + AMS = 1, // AMS1 + AMS_LITE = 2, // AMS-Lite + N3F = 3, // N3F, AMS 2PRO + N3S = 4, // N3S, AMS HT +}; + // Slots and Tray #define VIRTUAL_TRAY_MAIN_ID 255 #define VIRTUAL_TRAY_DEPUTY_ID 254 diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 8f35ccf454..100e4c30e9 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -111,7 +111,7 @@ DevAms::DevAms(const std::string& ams_id, int nozzle_id, int type) m_ams_id = ams_id; m_ext_id = nozzle_id; m_ams_type = (AmsType)type; - assert(DUMMY < type && m_ams_type <= N3S); + assert(EXT_SPOOL < type && m_ams_type <= N3S); } DevAms::~DevAms() diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h index 37a93729c8..9d5e2f748c 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h @@ -130,14 +130,12 @@ class DevAms { friend class DevFilaSystemParser; public: - enum AmsType : int - { - DUMMY = 0, - AMS = 1, // AMS - AMS_LITE = 2, // AMS-Lite - N3F = 3, // N3F - N3S = 4, // N3S - }; + using AmsType = DevAmsType; + static constexpr AmsType EXT_SPOOL = DevAmsType::EXT_SPOOL; + static constexpr AmsType AMS = DevAmsType::AMS; + static constexpr AmsType AMS_LITE = DevAmsType::AMS_LITE; + static constexpr AmsType N3F = DevAmsType::N3F; + static constexpr AmsType N3S = DevAmsType::N3S; public: DevAms(const std::string& ams_id, int extruder_id, AmsType type); diff --git a/src/slic3r/GUI/DeviceCore/DevMapping.cpp b/src/slic3r/GUI/DeviceCore/DevMapping.cpp index af3539f372..a98b6402e8 100644 --- a/src/slic3r/GUI/DeviceCore/DevMapping.cpp +++ b/src/slic3r/GUI/DeviceCore/DevMapping.cpp @@ -186,7 +186,7 @@ namespace Slic3r } } FilamentInfo info; - _parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::DUMMY, tray, info); + _parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info); tray_filaments.emplace(std::make_pair(info.tray_id, info)); } } From c88535ce9eb8388a1ce8b0287f5e6350168089bc Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 12:00:53 +0800 Subject: [PATCH 06/30] feat: add drying enums, structs, getters to DevAms Adds DryCtrlMode, DryStatus, DrySubStatus, DryFanStatus, CannotDryReason enums and DrySettings struct for AMS drying control. --- src/slic3r/GUI/DeviceCore/DevFilaSystem.h | 80 ++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h index 9d5e2f748c..c507b61900 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include @@ -17,6 +20,7 @@ namespace Slic3r { class MachineObject; +struct DevFilamentDryingPreset; /** * DevAmsTray - Represents a single filament tray/slot in an AMS unit or virtual tray. @@ -137,6 +141,61 @@ public: static constexpr AmsType N3F = DevAmsType::N3F; static constexpr AmsType N3S = DevAmsType::N3S; +public: + + enum class DryCtrlMode : int + { + Off = 0, + OnTime = 1, + OnHumidity = 2, + }; + + enum class DryStatus : char + { + Off = 0, + Checking = 1, + Drying = 2, + Cooling = 3, + Stopping = 4, + Error = 5, + CannotStopHeatOutofControl = 6, + PrdTesting = 7, + }; + + enum class DrySubStatus + { + Off = 0, + Heating = 1, + Dehumidify = 2, + }; + + enum class DryFanStatus : char + { + Off = 0, + On = 1, + }; + + enum class CannotDryReason : int + { + TaskOccupied = 0, + InsufficientPower = 1, + AmsBusy = 2, + ConsumableAtAmsOutlet = 3, + InitiatingAmsDrying = 4, + NotSupportedIn2dMode = 5, + DryingInProgress = 6, + Upgrading = 7, + InsufficientPowerNeedPluginPower = 8, + FilamentAtAmsOutletManualUnload = 10, + }; + + struct DrySettings + { + std::string dry_filament; + int dry_temp = -1; // -1 means invalid + int dry_hour = -1; // -1 means invalid, hours + }; + public: DevAms(const std::string& ams_id, int extruder_id, AmsType type); DevAms(const std::string& ams_id, int nozzle_id, int type); @@ -168,9 +227,20 @@ public: int GetHumidityLevel() const { return m_humidity_level; } int GetHumidityPercent() const { return m_humidity_percent; } - bool SupportDrying() const { return m_ams_type > AMS_LITE; } + bool SupportDrying() const { return m_ams_type == N3F || m_ams_type == N3S; } int GetLeftDryTime() const { return m_left_dry_time; } + // remote drying control + bool IsSupportRemoteDry(const MachineObject* obj) const; + std::optional GetDryStatus() const { return m_dry_status; }; + std::optional GetDrySubStatus() const { return m_dry_sub_status; } + std::optional GetFan1Status() const { return m_dry_fan1_status; } + std::optional GetFan2Status() const { return m_dry_fan2_status; } + std::optional> GetCannotDryReason() const { return m_dry_cannot_reasons; } + std::optional GetDrySettings() const { return m_dry_settings; }; + + bool AmsIsDrying(); + private: AmsType m_ams_type = AmsType::AMS; std::string m_ams_id; @@ -185,6 +255,14 @@ private: int m_humidity_level = 5; // AmsType::AMS int m_humidity_percent = -1; // N3F N3S, the percentage, -1 means invalid. eg. 100 means 100% int m_left_dry_time = 0; + + // see is_support_remote_dry + std::optional m_dry_status; + std::optional m_dry_sub_status; + std::optional m_dry_fan1_status; + std::optional m_dry_fan2_status; + std::optional> m_dry_cannot_reasons; + std::optional m_dry_settings; }; /** From 1adfb5f242aaa8cec07ef18b7735b95b48f81dff Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 12:13:26 +0800 Subject: [PATCH 07/30] feat: add DevFilamentDryingPreset, ctrl method declarations, tray preset getter --- src/slic3r/GUI/DeviceCore/DevFilaSystem.h | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h index c507b61900..283de146db 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h @@ -104,6 +104,7 @@ public: std::string get_display_filament_type() const; std::string get_filament_type(); + std::optional get_ams_drying_preset() const; // static static wxColour decode_color(const std::string& color); @@ -332,7 +333,9 @@ public: public: // ctrls int CtrlAmsReset() const; - + int CtrlAmsStartDryingHour(int ams_id, std::string filament_type, int tag_temp, int tag_duration_hour, bool rotate_tray, int cooling_temp, bool close_power_conflict = false) const; + int CtrlAmsStopDrying(int ams_id) const; + public: static bool IsBBL_Filament(std::string tag_uid); @@ -367,4 +370,18 @@ public: static void ParseV1_0(const json& print_json, MachineObject* obj, DevFilaSystem* system, bool key_field_only); }; +struct DevFilamentDryingPreset +{ + std::string filament_id; + + std::unordered_set ams_limitations; // only use ams types in the set + std::unordered_map filament_dev_ams_drying_time_on_idle; // hour + std::unordered_map filament_dev_ams_drying_temperature_on_idle; + std::unordered_map filament_dev_ams_drying_time_on_print; // hour + std::unordered_map filament_dev_ams_drying_temperature_on_print; + float filament_dev_drying_cooling_temperature = 0.0f; + float filament_dev_drying_softening_temperature = 0.0f; + float filament_dev_ams_drying_heat_distortion_temperature = 0.0f; +}; + }// namespace Slic3r \ No newline at end of file From 9ee7e9aba40c0f96ff3397b89dcda42578f7c9e6 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 12:51:12 +0800 Subject: [PATCH 08/30] feat: add DevUtilBackend with GetFilamentDryingPreset Reads filament_dev_ams_drying_* config keys from filament presets and returns structured DevFilamentDryingPreset data. --- src/slic3r/GUI/DeviceCore/CMakeLists.txt | 2 + src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp | 76 ++++++++++++++++++++ src/slic3r/GUI/DeviceCore/DevUtilBackend.h | 26 +++++++ 3 files changed, 104 insertions(+) create mode 100644 src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp create mode 100644 src/slic3r/GUI/DeviceCore/DevUtilBackend.h diff --git a/src/slic3r/GUI/DeviceCore/CMakeLists.txt b/src/slic3r/GUI/DeviceCore/CMakeLists.txt index 57fd747228..67e7f1dc8c 100644 --- a/src/slic3r/GUI/DeviceCore/CMakeLists.txt +++ b/src/slic3r/GUI/DeviceCore/CMakeLists.txt @@ -28,6 +28,8 @@ list(APPEND SLIC3R_GUI_SOURCES GUI/DeviceCore/DevFilaSystem.h GUI/DeviceCore/DevFilaSystem.cpp GUI/DeviceCore/DevFilaSystemCtrl.cpp + GUI/DeviceCore/DevUtilBackend.h + GUI/DeviceCore/DevUtilBackend.cpp GUI/DeviceCore/DevFirmware.h GUI/DeviceCore/DevFirmware.cpp GUI/DeviceCore/DevPrintOptions.h diff --git a/src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp b/src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp new file mode 100644 index 0000000000..c3cd5fd850 --- /dev/null +++ b/src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp @@ -0,0 +1,76 @@ +#include "DevUtilBackend.h" + +#include "slic3r/GUI/GUI_App.hpp" + +#include "libslic3r/Preset.hpp" + +#include + +namespace Slic3r +{ + +static std::unordered_map s_ams_type_map = { + {"0", DevAmsType::N3F}, + {"1", DevAmsType::N3S}, +}; + +std::optional DevUtilBackend::GetFilamentDryingPreset(const std::string& fila_id) +{ + if (fila_id.empty() || !GUI::wxGetApp().preset_bundle) { + return std::nullopt; + } + + for (auto iter = GUI::wxGetApp().preset_bundle->filaments.begin(); iter != GUI::wxGetApp().preset_bundle->filaments.end(); ++iter) { + const Preset& filament_preset = *iter; + const auto& config = filament_preset.config; + if (filament_preset.filament_id == fila_id) { + DevFilamentDryingPreset info; + info.filament_id = fila_id; + try { + if (config.has("filament_dev_ams_drying_ams_limitations")) { + std::vector types = config.option("filament_dev_ams_drying_ams_limitations")->values; + for (auto type : types) { + if (s_ams_type_map.count(type) == 0) { + continue; + } + info.ams_limitations.insert(s_ams_type_map[type]); + } + } + + if (config.has("filament_dev_ams_drying_temperature")) { + info.filament_dev_ams_drying_temperature_on_idle[DevAmsType::N3F] = config.option("filament_dev_ams_drying_temperature")->get_at(0); + info.filament_dev_ams_drying_temperature_on_idle[DevAmsType::N3S] = config.option("filament_dev_ams_drying_temperature")->get_at(1); + info.filament_dev_ams_drying_temperature_on_print[DevAmsType::N3F] = config.option("filament_dev_ams_drying_temperature")->get_at(2); + info.filament_dev_ams_drying_temperature_on_print[DevAmsType::N3S] = config.option("filament_dev_ams_drying_temperature")->get_at(3); + } + + if (config.has("filament_dev_ams_drying_time")) { + info.filament_dev_ams_drying_time_on_idle[DevAmsType::N3F] = config.option("filament_dev_ams_drying_time")->get_at(0); + info.filament_dev_ams_drying_time_on_idle[DevAmsType::N3S] = config.option("filament_dev_ams_drying_time")->get_at(1); + info.filament_dev_ams_drying_time_on_print[DevAmsType::N3F] = config.option("filament_dev_ams_drying_time")->get_at(2); + info.filament_dev_ams_drying_time_on_print[DevAmsType::N3S] = config.option("filament_dev_ams_drying_time")->get_at(3); + } + + if (config.has("filament_dev_drying_softening_temperature")) { + info.filament_dev_drying_softening_temperature = config.option("filament_dev_drying_softening_temperature")->get_at(0); + } + + if (config.has("filament_dev_ams_drying_heat_distortion_temperature")) { + info.filament_dev_ams_drying_heat_distortion_temperature = config.option("filament_dev_ams_drying_heat_distortion_temperature")->get_at(0); + } + + if (config.has("filament_dev_drying_cooling_temperature")) { + info.filament_dev_drying_cooling_temperature = config.option("filament_dev_drying_cooling_temperature")->get_at(0); + } + + return info; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " exception: " << e.what(); + } + } + } + + return std::nullopt; +} + +}; // namespace Slic3r diff --git a/src/slic3r/GUI/DeviceCore/DevUtilBackend.h b/src/slic3r/GUI/DeviceCore/DevUtilBackend.h new file mode 100644 index 0000000000..ccaa12d056 --- /dev/null +++ b/src/slic3r/GUI/DeviceCore/DevUtilBackend.h @@ -0,0 +1,26 @@ +/** + * @file DevUtilBackend.h + * @brief Provides common static utility methods for backend (preset/slicing). + */ + +#pragma once +#include "DevDefs.h" +#include "DevFilaSystem.h" + +#include +#include + +namespace Slic3r +{ + +class DevUtilBackend +{ +public: + DevUtilBackend() = delete; + +public: + // for filament preset + static std::optional GetFilamentDryingPreset(const std::string& fila_id); +}; + +}; // namespace Slic3r From dd5b755f6023a0130f7971b52453409da8a99f7f Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 13:09:32 +0800 Subject: [PATCH 09/30] feat: add drying status JSON parsing and DevAms helper methods Parse dry_status, dry_sub_status, dry_fan statuses, dry_settings, and dry_cannot_reasons from printer JSON. Implement IsSupportRemoteDry, AmsIsDrying, get_ams_drying_preset. Co-Authored-By: Claude --- src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 52 +++++++++++++++++++++ src/slic3r/GUI/DeviceManager.hpp | 1 + 2 files changed, 53 insertions(+) diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 100e4c30e9..a2787577fd 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -6,6 +6,7 @@ #include "slic3r/GUI/I18N.hpp" #include "DevUtil.h" +#include "DevUtilBackend.h" using namespace nlohmann; @@ -99,6 +100,12 @@ std::string DevAmsTray::get_filament_type() } +std::optional DevAmsTray::get_ams_drying_preset() const +{ + return DevUtilBackend::GetFilamentDryingPreset(setting_id); +} + + DevAms::DevAms(const std::string& ams_id, int extruder_id, AmsType type) { m_ams_id = ams_id; @@ -189,6 +196,27 @@ DevAmsTray* DevAms::GetTray(const std::string& tray_id) const return nullptr; } +bool DevAms::IsSupportRemoteDry(const MachineObject* obj) const +{ + if (obj && obj->is_support_remote_dry) { + return SupportDrying(); + } + + return false; +} + +bool DevAms::AmsIsDrying() +{ + if (!GetDryStatus().has_value()) { + return false; + } + + return GetDryStatus().value() == DevAms::DryStatus::Checking + || GetDryStatus().value() == DevAms::DryStatus::Drying + || GetDryStatus().value() == DevAms::DryStatus::Error + || GetDryStatus().value() == DevAms::DryStatus::CannotStopHeatOutofControl; +} + DevFilaSystem::~DevFilaSystem() { for (auto it = amsList.begin(); it != amsList.end(); it++) @@ -431,6 +459,30 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS curr_ams->m_left_dry_time = (*it)["dry_time"].get(); } + // Drying status — only parse if printer supports remote drying + if (obj->is_support_remote_dry) { + if (it->contains("info")) { + const std::string& info = (*it)["info"].get(); + curr_ams->m_dry_status = (DevAms::DryStatus)DevUtil::get_flag_bits(info, 4, 4); + curr_ams->m_dry_fan1_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 18, 2); + curr_ams->m_dry_fan2_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 20, 2); + curr_ams->m_dry_sub_status = (DevAms::DrySubStatus)DevUtil::get_flag_bits(info, 22, 2); + } + + if (it->contains("dry_setting")) { + const auto& j_dry_settings = (*it)["dry_setting"]; + DevAms::DrySettings dry_settings; + DevJsonValParser::ParseVal(j_dry_settings, "dry_filament", dry_settings.dry_filament); + DevJsonValParser::ParseVal(j_dry_settings, "dry_temperature", dry_settings.dry_temp); + DevJsonValParser::ParseVal(j_dry_settings, "dry_duration", dry_settings.dry_hour); + curr_ams->m_dry_settings = dry_settings; + } + + if (it->contains("dry_sf_reason")) { + curr_ams->m_dry_cannot_reasons = DevJsonValParser::GetVal>((*it), "dry_sf_reason"); + } + } + if (it->contains("humidity")) { try diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 0a31c18502..6e4007a62b 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -254,6 +254,7 @@ public: bool is_tunnel_mqtt = false; //AmsTray vt_tray; // virtual tray + bool is_support_remote_dry = false; long ams_exist_bits = 0; long tray_exist_bits = 0; long tray_is_bbl_bits = 0; From 22d9765348ea17e140c40ebeb439f39af9824940 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 13:19:11 +0800 Subject: [PATCH 10/30] feat: add CtrlAmsStartDryingHour and CtrlAmsStopDrying commands Publish 'ams_filament_drying' JSON commands via MQTT for starting and stopping AMS filament drying. Co-Authored-By: Claude --- .../GUI/DeviceCore/DevFilaSystemCtrl.cpp | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp index f81d6a2835..acdc81dd8c 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp @@ -16,4 +16,44 @@ int DevFilaSystem::CtrlAmsReset() const return m_owner->publish_json(jj_command); } +int DevFilaSystem::CtrlAmsStartDryingHour(int ams_id, + std::string filament_type, + int tag_temp, + int tag_duration_hour, + bool rotate_tray, + int cooling_temp, + bool close_power_conflict) const +{ + json jj_command; + jj_command["print"]["command"] = "ams_filament_drying"; + jj_command["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); + jj_command["print"]["ams_id"] = ams_id; + jj_command["print"]["mode"] = static_cast(DevAms::DryCtrlMode::OnTime); + jj_command["print"]["filament"] = filament_type; + jj_command["print"]["temp"] = tag_temp; + jj_command["print"]["duration"] = tag_duration_hour; + jj_command["print"]["humidity"] = 0; + jj_command["print"]["rotate_tray"] = rotate_tray; + jj_command["print"]["cooling_temp"] = cooling_temp; + jj_command["print"]["close_power_conflict"] = close_power_conflict; + return m_owner->publish_json(jj_command); +} + +int DevFilaSystem::CtrlAmsStopDrying(int ams_id) const +{ + json jj_command; + jj_command["print"]["command"] = "ams_filament_drying"; + jj_command["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); + jj_command["print"]["ams_id"] = ams_id; + jj_command["print"]["mode"] = static_cast(DevAms::DryCtrlMode::Off); + jj_command["print"]["filament"] = ""; + jj_command["print"]["temp"] = 0; + jj_command["print"]["duration"] = 0; + jj_command["print"]["humidity"] = 0; + jj_command["print"]["rotate_tray"] = false; + jj_command["print"]["cooling_temp"] = 0; + jj_command["print"]["close_power_conflict"] = false; + return m_owner->publish_json(jj_command); +} + } \ No newline at end of file From ec913eaf302d80b4270602de76b6f1a691b1608c Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 13:27:39 +0800 Subject: [PATCH 11/30] feat: add is_support_remote_dry flag parsing from firmware Parses fun2 bit 5 from printer firmware to enable remote drying support. --- src/slic3r/GUI/DeviceManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 9d7afd13b9..2562d73b22 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -5031,6 +5031,7 @@ void MachineObject::parse_new_info(json print) // fun2 may have infinite length, use get_flag_bits_no_border if (!fun2.empty()) { is_support_print_with_emmc = get_flag_bits_no_border(fun2, 0) == 1; + is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1); } /*aux*/ From b2298855b02e01c0d9c868990af900e3161fd888 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 13:35:16 +0800 Subject: [PATCH 12/30] assets: add AMS drying control images from BambuStudio 14 images: humidity level icons, drying state images per AMS type, heating icon, guide illustration, tray status icons. --- resources/images/dev_ams_dry_ctr_disable.svg | 5 +++++ resources/images/dev_ams_dry_ctr_enable.svg | 4 ++++ .../dev_ams_dry_ctr_filament_in_chamber.png | Bin 0 -> 105573 bytes .../images/dev_ams_dry_ctr_heating_icon.svg | 3 +++ .../images/dev_ams_dry_ctr_n3f_cooling.png | Bin 0 -> 21003 bytes .../dev_ams_dry_ctr_n3f_dehumidifying.png | Bin 0 -> 22353 bytes resources/images/dev_ams_dry_ctr_n3f_error.png | Bin 0 -> 17987 bytes .../images/dev_ams_dry_ctr_n3f_heating.png | Bin 0 -> 20210 bytes .../images/dev_ams_dry_ctr_n3s_cooling.png | Bin 0 -> 10325 bytes .../dev_ams_dry_ctr_n3s_dehumidifying.png | Bin 0 -> 10546 bytes resources/images/dev_ams_dry_ctr_n3s_error.png | Bin 0 -> 7305 bytes .../images/dev_ams_dry_ctr_n3s_heating.png | Bin 0 -> 10872 bytes 12 files changed, 12 insertions(+) create mode 100644 resources/images/dev_ams_dry_ctr_disable.svg create mode 100644 resources/images/dev_ams_dry_ctr_enable.svg create mode 100644 resources/images/dev_ams_dry_ctr_filament_in_chamber.png create mode 100644 resources/images/dev_ams_dry_ctr_heating_icon.svg create mode 100644 resources/images/dev_ams_dry_ctr_n3f_cooling.png create mode 100644 resources/images/dev_ams_dry_ctr_n3f_dehumidifying.png create mode 100644 resources/images/dev_ams_dry_ctr_n3f_error.png create mode 100644 resources/images/dev_ams_dry_ctr_n3f_heating.png create mode 100644 resources/images/dev_ams_dry_ctr_n3s_cooling.png create mode 100644 resources/images/dev_ams_dry_ctr_n3s_dehumidifying.png create mode 100644 resources/images/dev_ams_dry_ctr_n3s_error.png create mode 100644 resources/images/dev_ams_dry_ctr_n3s_heating.png diff --git a/resources/images/dev_ams_dry_ctr_disable.svg b/resources/images/dev_ams_dry_ctr_disable.svg new file mode 100644 index 0000000000..f2212204e9 --- /dev/null +++ b/resources/images/dev_ams_dry_ctr_disable.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/images/dev_ams_dry_ctr_enable.svg b/resources/images/dev_ams_dry_ctr_enable.svg new file mode 100644 index 0000000000..0c8d4ca146 --- /dev/null +++ b/resources/images/dev_ams_dry_ctr_enable.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/dev_ams_dry_ctr_filament_in_chamber.png b/resources/images/dev_ams_dry_ctr_filament_in_chamber.png new file mode 100644 index 0000000000000000000000000000000000000000..1b36643c42cd12c4fe71f574df2a490c4229fab2 GIT binary patch literal 105573 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 {gfEi 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 B*+&2X literal 0 HcmV?d00001 diff --git a/resources/images/dev_ams_dry_ctr_heating_icon.svg b/resources/images/dev_ams_dry_ctr_heating_icon.svg new file mode 100644 index 0000000000..9084670fb7 --- /dev/null +++ b/resources/images/dev_ams_dry_ctr_heating_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/images/dev_ams_dry_ctr_n3f_cooling.png b/resources/images/dev_ams_dry_ctr_n3f_cooling.png new file mode 100644 index 0000000000000000000000000000000000000000..6a43425da5d2c5e828c8a5cc72f851fb4d4bdc9b GIT binary patch literal 21003 zcma%C^LHiB)4s8>$;QqmcVpY!*c;o%#x^$_+qP|+H@0n?-+az_|A6<0>OM24duF=3 zs=BM5XTs!V#gO1};Q;^ul7zU3;#d9hB|)&zU!{HWKIK;hXD6=i001DM|0iI8)O75x zn_v!#VnTq*N&Mrl3rJHz89@M`CJNzA9|{0a$d(Wh{Ot;M?gf{QzwdNdc4BDKdPo@Y zYvu@B+m}o>fWRh@faux}437vKf2rKT%6akhN2E&!GWbwr-8FPOa+|Cbcuhv~60)9J zNLJk-EYZ>dG#p&?lwUMUtO?8Gyz2RD*QJNDZjyUoH91MdiC~#WX<1omS=Z;|#%tH^ z4@t5Z5j2VaSAj0h^VPbKBJ2Ik6f|m5mn0)Ao5{<>WPfsXWw(BGRIEya`Re+*Aq}i+ ztf)Bu*|gz)Dn}8M2ZWnKe~vJoNM)_aJbY*D@_g;@?|*)Oe`jG}5I}U?=?W8+-M3Ex zPn0YWLUgsTuvpnxS$W~ze6;Pjo#bv(AQk(Kmi_a_a!iJO-4PDrG2qRWKcLlVv(t-( zgF}3GcbA2gm3-^q(bsk-On@)1>iXX!9v)svUEQs7CO%)2^ZAN({P4FJ$fK}!s+yHo z4d~;B@)BK8A*W!#rsZGbnb(c?aa{DE6Fn7`r zGTo2f#^A4v9fFdYtu+5A)K%rEJk`{|8$N$^FT7q+$13NU(0L(FiKK z^XD<%=F?ykMNA***Sv8G@IAJGDol`Gy}h5opq>7`fmXrI z^Z<30m619XdbQp8IYR-4E-o}XZ!<1E)6t$I$iQCU=zKm7eY_bVPVJ#@ z?)4@GOSi#%g#(0@gbjrq%6uh4e2@je_d-f15?uT8w__bj2WlqJ+(@`ls;jhGZY*^_ zUlu=o(ViQ~zaB*q`QT0ex`Nfi0@FPI8C@rjeJ7tzoVgvlFaQM&!u=KK04dFuOOJmk^OXkSiK`Ht234U1fe_EMR7X;tRb z9(;kavO>{xEp%O`l=1q*S;G>!D!yiL{Go0)M_sES&g*}>J2{7DW@gzz2v{3;^HttX zNaQ!AUmu2uHJ1KKH|_v^WHG-1U>~=A8leIZ3Y4f@!E^C^jnYKL8GozHxN-M(2?Yh? zzKi4};M5Xp4baHP(6Xh@>6@5{naXavai8`1cKGRs=B6AA@CyD~dWds-Z>^zho1g=g zb0m8i!Qz7!;rbE&O1a4M`}_OaDx>zEa#KCynLw7HeIF7#3o}P$l%*|C-(&ED3My6i`KDi) zHOUG4sWVbeProWNUMDx?Ymdl~Fw~{t=MtIXeWryaRI|k&-XsL)_7H0h6~3&^h3V-( z3q}0z$0U+_(PT1V_>bTR>0nM!^)5a>U1qkdAY~8gZsoSphe!x5-P27AjW+K_zBwM3 z3A;ypcfuqsrLA6EJ5~-ty5{voi{4YVMqGF2%MC9GA|{eUWCgSc@ASVGw$RJ_JXfmJ z&D!S`0M%-Bds=xWL^N>xEvFA>VeLvE?Oj)8y~yy?LwJ>DfJJFp7$Hmxf}@MQ-DA&Q zFZyzU&%LV8Ju3Ol-&nvqM;yrWfccPFymH6Sn;@jGG}+-x3#i%(n=n%-S_*e8E}t4X zLf0jT>8?*$(#62w?WjE}x11*&q3RJ#kSbaPT$+SmUS1k&sB2CR(rh!cMUbh&j=k|= zlO5s%ZUlUY0^IH^wcASTo6z)L(o((@ON&(wnjZqYj8XTwLHq)kbT>5+c2lJX>}0R zuFqYag}t9VE;l-aqj+wHcmDG+(P$|uU-SD9?n}>Jg%Oh<{W-;$Vdgx9J${fY<8d_? zw!CcEJXvtvYHm7YDOZbe#XZ!jLsJG658HH;wLx%Hg3Icz(y)WTxdGMlB0de?F0|ck zsjFVioY&P>3st(CwN}fupqm08|Nnc@a1q~q2|EiLn>Xsp>I7%izDJu8c0D_vtpLIm z(QsxNT;;O$AHS8#tC}E+F%vTj`@eq5fXvj>)Q{4Y)mW>Zo_g0e&dQF4 zISf$cmtjPhN;BZG+vFEzd20X#tbF?LHsp5}Z!bGvFFNAIWh7W$EGeco5I0a51U694 zk&yN+Y@5jDSV1@dKg-&#*shAp0WUk~un-45a6#$z$R4i#L%QX0Hr4@*HGm#%-J1S1 zFV3?kh$f{5(BYHupwzTpHhC2Z-^hy;hkN z2QP}~1W$(%wh$SVosWjHn)UNa^$UOe#hbTVQ24_^vs-f)@TeJm@jnOu$Jz2=^Un}L zSe1_O*D}WUTU?Ied|zZtrawMEuP~U@HjtqnA};yOC0{|f9|^ISkb0PcyoZP;x9sMN zfr}4N%_0H;C?BAPmD8vl2p_vF{n#E>>bzaDUt2(xC>PQ~_8rMjYErYUPN#d^)ZtZ& zI8D;TY3#O>8BPo8pHNMyXq*C(TvN6m=5`QmvAjfWOKq950R4kEaKfoeqpNq4)`n)z zLqI(E?7Tc%-8j>sD1^n(Vr9XA^4g>Vj>6RU=u0v38XFtg zz28rDb)~|7k0?OAOz^|m9QHi5@sOU=|M;}I9d$gG2IH<>aNQF2_>2kj2;d}&4o0?N zgasU7CYzZd<_-V>#Og?dATEc23Hn1gNa|eq#!kNjdj2@Bg9mwsSDu?$hBa5FEw}wndP%Vf<)$Q-k!kYK*@xbSIV#p@Tze5v$puE;tR3Gpc$8kA) z-8#)@fzO?ljjCmNm^sC0T_{h?Nn^9*Nm@5mO>6dJ+>|j?oI3(t&m-Ba*g0*_B1w=t zS}GX?rDknOCKKL%kKYMGz|SYLJ7T?-EMpp=5yq7AAdyaT!Y+7kz=Tf60)5o)QZp`=&VP?MOnPLSn*#xYCAUOAKYuwo7^{l+FKJhQMkb3I!fnTqSu_wKv&e!Cum!w$ey(?ZOZJ;2>PIFHICNR1$n>n zI;Mx7dWfxSOht|uRp2<|>r*}T&T}3Hi{BO^g|~>3RUT~xUtROg1?>VSb*j@7nYuN( z4(pD!nGReFXi#deoyJQzj3m3QS=lI)$J0+ka5IDe&$a0M+^>cH;KrkQo-|y5t%hDh zy>b1_0NzE9yp`1vZIrc32_u+8Fmx@NqKy<(lAl?gOiqrDZk*jK&N``vwd z1D8`CO>P~_GYYK}J*!s(Mjsqi=Sd~64T-F*^m3R$$`94;XNKcU$Ws;MQ6&DiY=}Lp zP2OR8k6|!&oLN+19!0gr&8~OeMTVtgH3;r{c9f|xoV`zPE30xzj{pIxfZ^P~to)oG zQlSB=>CGRzdibB9^Wix z*VuWITh=k4Rm>&F-9Sx+tZ!$(3HYBJuGYK|Tx<1IE9l~aewy&zKK<_QqI|kM;P7}j z%Q?*SkY#E9D~pD@h!P?$y_Pt;a-$^CI_=uxv8hEpA)#Wgn5}n+)Pb{!M>2!*Rbr7TZV0gi>i|%v&puu6T!oG(Dszix}Q4jh>6X-R0Qb zaG$05;bg}4qx>efj{484KXPYm*2E=`CeM}9@aReJ7p$A14*B#?np#Xr-~koNr-Q`C{C(Ap_lK(2ZpXY@zd7N%{}X) zJc_KR{-1Ov6?c>8t4%&+S)e9^)r}4hg;$y|T5@0b(Gve2b*Rm58RxJ^v2aT=5EoMd z_BEt$nOkK^p_vQf?c<)qrUZg&#-;D7g`}D7scA?dTNmBGyXoOfEbJ{|1p-%GVhax9 zAs%qvC6|RM9?txBE-GW(lC$}cZ0CvhkuZr)c8{kOX9Tq(s^ikjGSIw@6sjz~5vb&# zS=Orbe3@!zq>`A?wWFx@Oi3?n;OUuIoZLkB-TI9QtTmwz+7UU61oR~L>@2r^X|BYj zHI;TG%c_z4E_*QYMy>ni)6|= zblnx8p`U$RJFE4lAs*GTl`Q5kh;=PzzdQAQ&E*ruWIid>_R+cD^U&o?x?EI{vybQYbwm!R*g{&03&@IEdVPx~D5xzdI? z3^}*>s`x~(eu=jUeQg~#;;L!ZahN+0&|PQqZyLv7|MxZz58DMZaKJ&GB1Rq=Ve0cx zxCfjvo%99eNwX}Ik^Ys3PK{f4;IfPMgb~-=&9ZOvDlELCYTA-`QOjXsglD*O6!>fb zz~M71z-Q%-F9?V>-ihnafe?0tdpu*)N1Y|h4gj{{Q}`8~XRNZnIe-3LM!OCTt^8CT*dG)=y*Uk+(+Y`Eo_jx387w~@f8^y= z;BZe-#oAEO3qPp#gup<@=+v)Hh(7YFnMp$f(+(1dOUcDFER>YuqSd(U?mI+_GR2B? z?g&TcSUFNxtB*1f>^cv6hxu@qufGR)m6}kR@zju4&hO?hdi}DX3~iW5V`o14i&zVN zlHL`s!F`Ag9&H8L^Rng|QLW>1iHy6^Ra7dGpY}Z;APUSdRd;pSnd_RL`Ln7-ZJOh& z`p591OtmVwVCp5I%9A{-Xm)zk>CucJYG{P&$GY3ZOO3RcdS%DM%BCUh2kXL)&S1rC zqf*Q+^H{b}0+s+AZ54HR;TiEP*nvOKuUVP2YMo{!KET{z9l(5aOV+?fr9h_3is)mEsO9@{}!uo3jR@!CAt{cSq= zB_oWf>m7lC_f?RIw$(UZZ$7JMHYCTvFt5F@IXj&X--}tL-PutHbJ6{5t8lt9CT3A~ zB*dP)p4>=2TfPDqE`i7JdJ=KQseP1ZnHW0^HQ>fAY7ysO`}h0BPd12$&uZrf261%+ zWj{@F$kIW_^U<6-lL;Z)Kl)o>o&?dAp$qyVLLP=h*I%IpD}jn4Z-8WTWC~_ddHYev zoWsjN4m3*n<+=L#x#l2~Uam#13j_v?^Y4VH1>o|`EU@9ovOgQl?t4-*ZTh+p7Hsm> zyNAovksklVDQ6IECAgx3S+mtZTAQr4JN|$4HYMco!sZUusmxJ6kOaT(hZOBQ|lU zA8Lqjfwj_>vtm{qcZti7isO}+_iyPtU7zm9hYl-%y=3VeMZDb$_c#+4I=n^OL}jMD zrizM5V9T>#>uK**a$=%cnuiPbhyCSsmxAWt_m|Xc5WX-)rxX4qMWS>eM}~Lq$IpJ%{Ff zS#w!8n}3~^@h&okfo(Oz01$=3SN)@0)m4431KGrV%z$tyDnc@Bot zyYM$ge25S-QCSCJ50~wCZwkUmh6XdO4^W;pNZ5aqlLXV$UjJ`oWnzztcN9^p?9^Z| za8{+50X|>^P%UpaV;-)0@7#p2g4S6ItTNduVhP2;5(Si;OHHa*oky$>qHR+|e`>q)&hKj?4(Bw`$_sU^F|Y6@T+0G*x-!nv*9ki_~E7_V{M}&mgOSB77EUtAvv~Z3h&}y8cn`do227@JTaC;-(@pf zDC05(M<<4K^fuexZFp~>Ac>n0rqlE>j}q{f)jp4x_k#kl98WC`=7MWoc}1hbC=F)^ zFOi2VkTi>JAM9w!#J-7HNZiTUSrROyumk*z!HLyr9;%7XY-(;=MvDG}Vd(HwC_2rw zbd)r9QR&C;joa@5V_4B``jo8m+A12l5akpU%hmb;X7*ZH&e1)r;9F04mpz^#)ZoDc zP@D$#5L;}8>!Ytb9@4#H2e+6{I{xD{jMon{2Tt{R$5f@3!^+QR(TNYToskl_C!3lB z9?go>7#wu)&P|#THc9gMo3>7I&KGOT6X=WQ|D7q3+KKGRI%R$q`Bcy+j9qX-*&fd(LY*y*BH;3f5w3AH+MR_P4_Zu5wy?56Q}4 zlfz4(4#vX^CfaiNNLcu*N=+>DW*2gAcJ_r>!PB*WV1w;VPaHGPL~(@VbUlkw=^)}q1`h7_WQx5 zE-^wwwwa#|D>vlxqX>gq#q)2I*FU(S21oy|P?F2?w1fSVYzc1QaG-WO8X-$!R^1WO zZ{sT+M;SoBJ7}nkTFKO|x|B0u9jq6tfgQ?#!CVf-%8z#$uulFLnpB|1Zd)iU9D)M= z&b6yyGLAksGLDV~fTh}#db~%Llk6ePvLW?x1zgxsK)KUk-ISE~<6JCdbP|0@5e8^> z+V8`Iag5t1Utds_NhXu5A_4#WuzdA2%$zzTxPY%5jt@!=qsyqV%tk^=11;k}XrD!a zbX#Z4oOiQRKrhmh6Q?;Dt2)(43=}4V zYcT`iScd-GaX+_VaIQX+_;0h+`fT&iMItRg9E4O|JDaaEt^~>8lyRop5-Pj+T>iZf z+kGp`bIT?=M}WV+yDitN-=#&;y0^i{tl=<#zG~0}-N860TYWr(uRzU;7%tV$u+&!7 zpaD0JGX4A;K9pTt0$Cd5NN>H-h;Y6BP zohfLrLx{GIdpW-=;PNoyC-_#Bq)GZ{5)pi&s^<%Z4om{>;pEg$e(z3~C9Ltbi`LU< z{?WvPB_j;4xi0|Qg4yV>1k&1)9f%^KKj)*iL4b8Fi;u;huRnKS09 zY^&LF2&Pq*mToI%_Mg115ln{M$kGxGzC$9rv6eQszW^8qoS8kzyDZI#IopD8qUL?# z#WI5suT!|^RtqUw}lzDhED-H9>-X!^!!fWVo>fu9+Jsa>+T2H5DG zqzlvCo$D+tQy_Hx4zowQKk%BFmOj{i?GK4n9C5TC&`s4wBWNk&4fMW^=l%#miy9xL zaFXJFszTqtZiHoA+Ql}wirh~=t2d}tN}vrwpuW2 z@Ftl^Q|c>fWlobP-<2|0Q_Y?h)GLDRjAnU;d>P3y%~_3oFqN`wXps#AlbvTX?|Y_P z!8aYOtav2rw_yOEGY3d4RflApNoz_To0`kIS*fYKLUuMiRv5s}-v}2ST z+oxY3dl|n7>@fyX?x|-OzQsb3;8jJ%4^d_7h~JJ=&sNTK?R$^rfen8)8;i=I zH7jUZn+5QkCyxsw8^REWx}=P19frS)Ar=);&vI>Q%6nE z69tS$G9aktoMlwp0CQvnp)l4zj89K$G~doRmK=>9k1c4PaCZ@Bwzo-^>EIgu;^zD% zraUEDMyFC5`SEq;j&o5mZ`F;eF%ar4PZ1CwjIA0?5q+smYR{5+qaH4oaV&15Q6mzb zELIrC9U`o@bj%>ZUJY^s+%^XDNr09UYL14N&NRcO)9=uaGC#pG@#(U0Xv7GP&W&H5 zQKzArJ93$GN797P_udEI1wlUV8GWl5iLC25R}u+Yacj>0gZikDCVl{!>_0NM##p+- zEemuO=^cSDP5i<(JTH2=Pqry8XtZ5V8l!s}fSyBp;c!VHgL8{kI5Qu*@Xekg=~4!%Yk#0YDzvd^wTN#!ThnF=Qn zcG;xOi+Z?RR>!4KhW>%_c9D~d@>~>bG|F@{$&eO@Hm~K4WYfvnw!J@W zMV?kY0*@Jnl~uL=z@<5v=AR3J8xx8{A$nIIl$dSvEwUsHAZxh!c5F*NdrVSdO^wN| zxeOnc+n(g_S9STXU}e~(iZJ-*kn69o9pLrnE3;{dH@A@R9w)7*{0H8E+GacM2H9zO#g)}nWB>jJI<@N1M7psVb<@sB=kvjrt9L8&2=Oc)UG%^+C(G?`on0@- z2eegxrGwi19JYFVu*{&xENjV>sHFI5vBYnpgZ8G=UH)OIz`iC)EiUQK*bJ4s$eGZ} z{*H9Y`Y<`sXi1H-oe>lTHylioG&757*yb^gdsfKIeY;}D*CmYcwv^+4|M!#xZ7dCvg7&M{Cl4TlC2BU#SS)t`0zm@LZCVGcA7PlfaxG5qvkt~) zwyt%5yt81-er*_$373x}{7kgs^&xPD=yZ+~(#+mSYLW<*iL@MP@-(aiV0!Ih(Ww!? zet}8W?e_B}^;Nc6ffHK>zQ!<%Q!{J5Mk`V)?E%0id{``l-rnbWQ08GXdUKSCekfcE zdotvCzFIC0O%>eK=WWZzRT2>*VOjnWi#C z2cl&y|Iq2i2JhQ0TU_b|S6-Wbfh!H}mvN0*5e&pvf|!?*rasO=b3D41kMIjGSPF|H zhj9<`D6hq*eP~*SJgAzQnyz}vki^R+I=D-B%rE=Qa*}JRUXu4UjLm{P+pOJHVYH;0I3>^`%iYEop(FG@um?xo-=_}@nEvG zrkw`t9U??mO-8O#0aT22`H$XOC4!!70YkitO3+vG};tA*cEqQ&Fq zHF1a;&UvML&E-lBM$(HSCr)a$jfDPK3lNH=1Q@NkS%t`DmKYgDI4$$bTWjRJL%f!F zY-pvhY}Ls{L`_Lh%f_AJ8C{+4Z^B-lpYFC~OmwuiLWhBeru8nsxz7sX;-&SA4T*%l z!d|sT(0*|m_UBC5cSbt8P4(~fw3Y_Q(X)x*+=w8aq)=+BE@n@+YQreS_UxwPZBCQy zRw3@Ff`orkj<2N>2Y^K_&hYrdM6uD?5mA}yz7Le*J3z?iPR30Zyd20vbA%XiAZJc4 ztVU8PU@Z{au{29&Pn#kgy3H-NWndP%%yw+H2f?WSN<{NN5FNxN95j;qHscf?Lo;PW?0m`G(79G%gp-z$ z9pN*8Vvq;{nMxy=z}tq&8eyJ&KY(yu5RK7GHy=@k%MFXl5)&KZ$TtpUm6q+`*4-KY z)zSPLKtz+-a*L(7L9ZBHnRH3v26p_9aYB9U^s@9BSzkqap#Cdu2;lE7^KShzm*+9w zYB*6U9;p4PLOX9 z6d<}U!qb+vuOCosI%Sxx&(H=4q3O61p6jM*6Bkg%u#y>XXP?yA^KFy%i@QVXXK4X; zCi4d{*mQclL)<+&+0 zX1!0*6b*9wKjU+t(@@ywVP6KS-mj!W{+_SoN78bDLNc11-+Z z8ph25KE(fuiW{8UFM4fJifT8#u{_awdmqTx9k39!mz7C%gByrR{bGE|!Psir<~>g( z1-^Gj1WajFZP=QD!t3`BKgYd~01a&W1Ngc%20>;7>@1kE zmW^@n^|Brwc=NX+zH1IL&8=B zi%3eC+S=DC!7r`RZNm#S>fAPy2?@obLAjmxKy6j6Tw>S?te&4)hM4Fkuu))o$&T!{ zZZW{+zt~%s;qQ8lO^qzK@52g)4%7;2tjW3f=#G5>@4e>DQj(T_{{qFHaT&w9p>S|; zA`etQU}C`Hr%7+=ve>(7UiL1kikBlo(Aao0_gP_Hh|+g4K40{K2a;tz4UaA}0Rm#CM*}~zVhk9GY#D0+n z0?8YATDspb5&Rz^N}#vklD#6zRQq{RXP68$hIX1e{wn=X zF7@TbDkE_JYE`dyh?i|~j&#{S*(|KsBzIOiei8o&)$D}ztcrbp`mgT~nRZGFrm2{P z=uz=3sc zw%ajQ&^ebmT^^wbu>EYJD#WErfIIU^6?e5sSpeKby|tDh)DEediQzfk8oMlaPh%7% zPS4S|Y~kCcc9LeVTRi|8yGo=pxO@gTneZB8HK}r#I3XV!Pgf4?Od8gJG(EODuST6g zzuN-9SPW?_2uyqL?R{lCPdFQ8i4`3>gff%kbqWK(p&#!INRdgic{pR8uQa4h+@HtN z#7*Qwt=Zr*c5Y%@8vX7uK6OacVzv>CYzdyu@w;jr8vM0~J*+Y}w=cnP!Se@85~D<> zo6KP6QPll)b(8M662LYYWV-fQ+KV0QbQ0wrFq20)1UW@wNtXbb@)Kux{S6NkL_UDt z@Y}@jH?_hAR&hk%e1)zXN3y|`cEP;6&lBV2Kfd{_YZS-9*r*0ylWhleWRA{z!vDZ?vpp zM|a3X?X5gh4y+Mdz|jJ?15cJFdt+k&BDUj&sGw^DarpMzFKobK^_%r0*>A2j+-fZn zUcpIG(Pyn9mv`GWn`~-SLVvE{wZmO}v6+@%;KItvUIPqs*PU8eq#@p41MyPe{TZ%{ z3j8=zXhQJRN$w(USs7bgHrx(pLEazCd_EGDNbN(nAb$F;7C4N=z5zS}3AM*O%JOv= z@561qPbsDP&;&_bn*t-6-ieulLG>OCV<%XP;p(6Rn42F#0*6ElZ_{hJ=mb0fo^ejV z#~Xh;Emw=FkPzJ};qmcNkZF%jDt?|H0chr`GNS%9NUWHJj~HN+xD(xuXn;s(4UfZt zSf8&iWLP6QJ%|gtG8u6TV{Wp*OpEW1izstr&u9?My3aFS?cnuiM0%{2lSfj?HG*|@ zsT3QJ2z?VH`_39dRI3W4foj4f?QAw$pMg(aXj8SwqilpWd}ZXfANK787Z0ys7Q_)p z*L)?EvSg%4n*xcig;4)Ie8ole%CV^@9!%L%sL%M?HvK)$Jd}lntpS%IFJpc#|H;^Nx2J!5^{(jr1@cM(GynZMzte+6Clb)$AUM;Td z4@=0)kR48RpY|8*rnKYmN@>cem!AR6$GkleYebr31C3jbijPH>q+L0z)n$w-&Ce|- zqylh2uerl|mqE*Nwr` z@Z6F0QlY+_-^^!hU8E!bJSqaixSqC!QTVX)*%=g6lk>G&{qn@5OT&A@vx0}*c?bM1 zBHVPG@bFw77U<}MroG+w8u_z>4m-0B%H4kqjhKpgy9?-JsK&40$U|H*ue|#EBI1rm zy8&@v&b}yv9(lJ=^Ip>cv*RwCK7*N}h!MdE-m)+{18VPV!EZi^qJ-G63@vzSbzo+7 zCQivWplmLF?Ntq^sdVW|Kk2vxlfgmba}Eh7^0<{1gFY9IuHOzdRruieaoD=22Hz8^ z5&R7=O}rseaTs0An`jW*IX{n0(@D@5=)JEQ7AQ=2Y@~%uo{(Q3&MQuqEGCKO8w~wZ z>Z!NI^%02dHZ!&34=q9aZlnhGwQan@G9`b|HE4{?hsM&5+od)n>22L2*?2yboHJgI zJvKkMtk?J?5in_ylnn1(P{cwGmB~ex`%5WPd+enlwD)JEcmZy6d?E|wa5L1Ziq-lM zpGNHR%C!(E>Ha3ZEx@|625krLSNeEAfkC2oPiv{nX;I*?Kor~DTo;m}#hHUEB zwm+?TJf0<_1AO85yaZw>>CJP8Z44bS{x$t}R#&9AuE73g$pzyqij;gGS_N?z>eUCo zG^Ev=NUKTKX~V)e`U@|RMdah9)pX&yQ>vez|25s=D19;O$53y7aM;d2u--)$+Gv?HeFK)Iu%j*c{m*={LNxGhY zFFHPlr1@%zR1cMiqN0HZF!0VDjT}?FRnCj*8|;khQKIT~gW>g0sV9%fsguQg)we&S zRA*zn>aRaE&0gudWXkAXZDUt$9?Bv&4SdYkL_W1C8wn;Vu*fBO0AkLe4Nz=K?N+fFk8m_ztRhn)X zB3G#W`K!e)6r#yt^Kqvnr$A)sU@lA!$8CJ97cc@7Comu@Pd-*^`AnH=>&Rlf|Ka3V zKRr@5nccaOWN!cIEL1uZ<$a}ntXF3}J8MtN{W4gRK3OxMh*>5fB`^3S>S=M8*2a5b zz5enqvW}ohfIs;viUWiGci50rpIyTIN$Zv1A4h)P>xw9)#Zsw~!*-Ov&{5-ddGi6S zDkSdpM6HBke`O>KI|=^S0q|D^{=vZwT+SbJRv=>`!PidK022VhwUUx=-n!C8zPO8U zCWPBGMkyqok*0eoO%3%DX3}F~K^-V(QC3|)$i`us7>~I+JD(A-{!)Wm>f;S{#I%^e zTaLy;bC=aAu{1mSJ28z$2W<4O5_#<@xNkp4D3dod+dO?75r_AZj<|_w*p5)hfsrbz z6jcw4W~|BO`9Y9K%Bk1*Px5(@ceFcTz27!{Dkuhdm>eE=dR*>@E8N~2JsVV79h!*;;fR^hj*Q~Wb^$Iu{Ofh8a~_#q(ayZLKQzViGQ0VSaTJ`2=TUqbA1-p68)rF|PCoZ}g8-B^+1};|@XtU%=rbpSqY;h!Nw=S*KKoBWh4ntLxi4_az;cllM~r#bj=Liptt#38D$}NVnfuKl1aj@#M&xO8^`GS2fe9>zOp!e8U`XZB?{ysv`~2#ujdlT(@jU8_{~{%%&vRjzhc<&RJSL zuOfzVDuv)X{M{>iM1SO^M_Kjz%bJmrW4j&Rq-OO5!IV4BI9H_$zeOc4`ibx;ZKq-- zpP0kJWWuSS8X^)MRU;-I--l-#EL3C!lYH{~RaAj`VFhfTO9KMw@N?hB z2F|u25(?54Z*sI1YkrA8g22kPdN?k}lX}j#xzQ_+dnZB}NYU%2_09b+wlum_3CZT@ zrA#nQ7u=^??7W%{%Of@?pc=fJV7M2}%KS_}saHgXN;$#jVzm6?nd+KQg<0B!W~w8) z|G}bNwIi~ntz+TTjukY`=e|eufnezzi9)YICrU~2%MXXY2cjw{<=T5eYqENs-$B z#)MCb6b8FjfY{XL!Ev?~Z5SOhp+uUoOEV|ShHzLv6F+gCVbS5P3>>PLR;T3gm%6&R& z%g;;_jGz#)3-H7on_~6_2#XtQ$nL#y=Ix`Dj~5DyYYGdT^I&F~ksDVGg*2l)?Y zr`0_?iPd;0-Ttgnta}CI*{PKQW2m24R;|`4T5yinVrU1rVN_QR3YhQ_p^$j!v+M*) zt1GL;4KWh^t-)6>*KT%@fVM`vzg4Dm^ZLdRc-mwnR=?n832h+P5`Kqa(yRAk_P{zF zB(u;kgK>_wgO8ejOSKY!^fU8}m0;()d;yDse!gjKo1m)Xu;G}7Hln^qv?VPoS_xu` zr{rZl9)6Qb!lv3mL`gn%X6n`Qw{#e6H87Ixg&tJrNfN z3v?1#40J~Q@XKQEbYBn?Pm77;Yz!RA{kY(F^daRm3e~7d zRRRB2Mq9YrVlDj|=qC$kQ;?@NL}XV>|333@P3HbuYmA^ zg|*=m8I=pN!Rt*iUp-!Ym!<1ociM~lzO*a9w*0K;TXijXh;rI?nmtpoU>^BrciP>o zu@k>r^ic(XrcX+i)vXGRfIRie_EA~hFlVMjoMOVnQK1sPAjw{qHd!MDmotgy&7y+8 zJo^RNmW73+@$FCTJZ(%_%%uLU!t16Je_X7gvZs9+1v(%t5*s({{xFF_NNZ5>zwh-i zt+@iPI|Hy9EGs$d!>Rk1c8V`+Wdh+%NRuV^$>XfkeZAeB=m$gbjgZX#6R&lZ7FH)My=pm_jQyxZfW)jG=exqNG6sfcPFI5f-& zPV|FttdCWx8tJ4;^D0SVD67YJnRcP?3+r8az~*Z;Ni`?paAMZ-ZEHCs zJ_Qbnna4&2SVkvk=9@#kZt!)}wI)=jV%+Q_418;0uO)?-)2V$jXeHKXP1lt4KfPh8 zu9EAuJwMYy%=z$y#hgOjPo^$5tpv0~-8@|LdQO26ii(tw|7_U)WhC?b9f%N;PZfj? z{xH-ZFv5^F@f(bHgKadT2=x2Q5t~+r{r%&0)m@gs0TveE-5QAdzD5nUdH){21dfO7 z3%+*L4j)IT0b_*bn=3;OuQX5{Ydwiyhn>A=f9Wb=a31>Y2L$5#I^Y2gIIV}|>&2F0 zy(-d2QV(^N7KFY7MIL+x!N5o#J8?Y#Grz;m_Rh-wPeFwFBg{1A`=$&ngIO z4JMzevCaDc)HVcCix50tR1H;3B>@G5&!%V@`?qygk49g>zgDA=8aF!qpG;7cM~;6D z1C;5PZ74+vyT#GKisv7l)2L1!;jDOcvFfq+s`17(!@N{SYpWqWw<{m;p+u4665yr# z8TF>QDL_(uHBnhJPl6D=+~WBdc%MSX ztelsIgz$7%i&sd~!OypPYaSt`FlwkV=p}B=Bo? zST+-OiX7uK({i$XuCvak@;vJsa8{a28u!Te?-XHu6CIJ(7*A4yp_|Q zZ{yGIkt}9$zfk=tn$Fed3`sHlBshcRtkJ^LJxqoFrLpnK-L@XirdM+2@Z02iNfMvf zT5|26mF_A6At~1#Jn@hl_@!4qKAMwKYs2emr0!`Z^ioRZEpvchSXBc{)2o)~hI;PM zzk&eJTLY^wq?fTPl$Nm)b!0x~)9G+!pN8R-%@STW_oqLC5feb*TUOKz8!uRsdZd~B z+m!1Nrc7x};xJD(W6Jk@D0@aLaX9-5#q{`5*1chR^Nf6G9}z_*YVw7*AYa@wT$t)m z{nayq8?)05_=bAM?SV}(?&08Pm`rVz6>!9yY8D8h1XJRl6aMJxb@=EhLL{5AMupz( zq+8OQh_5!8bMo-vaNt9*XDGQULMu`sD+HZx158($NIg0Cti8U#x+xT_#rh2Ihuim; zBdo5cft0p*s)?iTYF&ek$5JYm&u1|F-d45Ujj36;$WQptU6*1SSgAkDxtmgq#WlT7 zN{WiVxGzA6%dwIt)8OXFB zl3w*g3&@EE}GSH4j)Ty@G!{d5fbHcK+T*)$jgg%_iSztlzf92vu=E)hz8onle=(;jE(y9P7a0zBarrx|+z9qt znzL?Mq*dO3&FANhLF-?EFG*$#qVfgMN1dAu7yskMB@BMP5y>y>F(c&YW?|saYaKW6 z;MA~Iep7n7H&(*ZH8_tj>yEsmNliMWHQq~po(ZqZXwbJ*N^Lwlgfk%3L+E>&Azsk? z@K>6$Er#O{CRmJ4zD{yd-|f~P8X0US2LoEGlncVs*=USrc%C~L^8Hi@8Y1%gEopi< zacWZMomrpg|GxlZ5uEP3fC&uu=cOInnS|Rfv480SP-kl0alEFcrp^5Pkw+hWbnUuz z>*z$08Jhy@hEw8N+)c7j(f;E4zWWTRspXwynp$sr*6csPX?KpR1vA>W9Dl>S)#aUG zZlg{c+8qNEDXvDNk4s7|*-W{zbZs?@8WgcSbIEjW!Gr-#iY5(U*-bG>QM7F-@MYPf z=EepZoiL@OW5s&3*_IO3wpsJ3m>Qz4S&ek=+~u@m=Pu(@+w8QudWRu*q7m9++T^Mm ztzEVSJqGw>bJv-$t3>_XrN7*MTTkx!(HeR9mhz1AqKKwNzVEheTWRy=O(FZ9vbpRN zmZv~2e9qPx0O-gR;hXH5_n$Ny#|^S&mitV(qTdXDThI?6U6ZTe#z~YgdHLiPv_&H=|oNzWXRBn ztnY3HeMS~@s8CyeoH)QHS!oi&%5~BdYxf4$u9;jTmGfP+->;`h<_?hP1AiOwd-%rv zy?xb@4bQV_sHw1pm3<>xw1L9h7E^>`C#}jB~)Ek;0@)LVLJy zg(fz^H9;dwHOp~dbltsF6tmyY!2e(T`0|4fJ~+q^IuT_?0{xzQ?ipOYdi5$MT0X^t zW(_~Qc-kW*4)B9%^IF^EEWPGnAM7)SbbQa`b!W4F=&u_RZp<`a;BDxYj&{nX!wr@t zDhbd}U65wqlw;ac0p(I9%Gaf7JSYlK%p#C>GD9-U{@bR z*v$E@uoxTK(U65`w42S}2@lvHIrg1vsQQij_5Y|f3=p=`h@_`h+ASG4XF&GQP#Icq5 z0{h)y*NnqL4^3>|=lJ1;xd5Iaxf*iex^^1U!f`76_p|HQN$1x^9QPLAKd|}xo}C;? z>*#Gm3D39PZc{Spm?}Qcu6u5lKQ{N&T$?tK0NDAO!N=Uek8eHx_~VaJA~j{k16}35 z`|kTCKP|oLs;jPJ?S2Cfke>KHwM_TvUf23NzCG0T)1}b{5#XvO7W-`c+Q=sbDAGs( zk;W6{&;-VMag+SI0ZlMC#yv9WalG5720riReg+@^%=t{6iJwytwm>$QU}Di;n54oa znxB?J8fa}z6*bK+utc&g>tuotEIC%CmBlHf`p)JF@ZWF?)~$O+s_YOcfRr_x&1SR( zy5<_0Fxw~}Yg|fgQ}U%@wFpUSn@zx)&=%V)ef-}4;K$cEA~ECR;J>M)=me7u8#X+} zzWbd#V6be{C7$L$#>{RPeDRq4!2qruTU@DS&Idt_I(O%96Y4YOE&vKGr}6fXxwavs zR60Y$gZr6(D>Fl%qKP0wd&V-vG!>ioIbq8II9;_gJSRU|TiYa6Rhu&y^Eo4cf&~B# z539iRq9-Doj00B!1v=8)!S-@fzdYq6$)Il=3UVgKCnU`kmPtz1c(;Bx?}w^Bv~+%U z!+QCCqx>1v)e9UW`37K2-m&TWk zP1MgmxhBJC0ZCX>9+;JD&Ek-IXxr8;^10TQ7I8T+@raoo1Yz?I{B6wg`Y=4kGRW`U z%9GSTvj4r25_#2fLIPdI`tMi2``z#U8y-~O;sLw(P}ujl9Ljz!Y3GuCwf0eFSzbHu zi=itCCV@np40~=R&E_m??$l;itgPFH7Rh=I6owKjHWqH%HF>6J zMBN5?j$NZ^%jDZooo^=uKLom`K)2VQ62NDqT1~AP)Zb@pp%0RRFtMah?L)#8Sc4uVLF*0Hv3Y3rcjsS1sknVGIkiJ6Srb!gS?+qO#63UVzE3u@`h zhJ+p8x4fYVZ3WWmJWZr&LDtm9AMm5mpi!4SO8I_Gi%86K4W!v)7qpF)tmB@sO)|4# zA`jSezK=ICx%MC>a#$n*x+CBF-uG^Q>s#OY{g#%N&+-8NsQT|-4PW||2)p=15NGpW zDRa*r#ArvIM9B114>ZNZle7~t<;^|{AWFRPGvfc6o(e$c(V0g2=hb7| zAV2<|LH;EM_-;z%uu1~-nEddEKitnxAG`eW%fHNn@P9DiubPg4n~tTQF0<~CT!DC4 zj@0L;u=9+kpdv3U93d%ADG6zX}8H(CkR>7eNy~!4T))fzfEpg$B%FEasQS9K17Ke z5lMhPjofq3J=^%{eeZb3J1%1r_-2j2yBfU~G3Uy3ppO%G@%Nn#p)XCEfu`F%KWwVt zNn%Z%JkOmw&n}#p6(B?7W>TgPdiZe-uJN%=yQwMD@gs7Fg4#K zTGp?a2?2~dw(G=!uo=wttcTTOiuBCWPf=@IJ1t(kSkhlB7UZV-)uyU?k%pJQeUqZ= zTYn#+xA6&Mo=dEqCj?DPwcT6>8Q{0^_xj(gy|+*zM@>$+IQp+E9Djde>C&a&<-yy` zz&eX4s$Z!UBcG0}bG7)vECQFjgH|+g^l}*2SAc6zq#+UmFQ7SPvcMxE9>#go9_BgU*{vKVc32IS!4jr9@s+Nr3)qh>gEr zciCl^-OGdeONhT?`*hiLDzyMVoy1*&>TSkt@NMYI4drRcEX?)5iGWM1CKRH0{b6;O zS=UUCYyoeMpO;$idYTv?qvtnnw0Y`YxZDvk^#YsR0&TBxZzs9sfAQlVSbHbt`7t61 z(0>DA4SyFu-SwXLyyp*jkpC$Ip{EweYds%|?;bbttM?0>#F{AeRtWJ*5Y}x=UzW6K z)`-$pXlbsc|U{a(+q;&XQ246#L6Emo<1G$qgcFbvGwJe;1YyO8j

Go?Hy*pRjRwPM2{$(9t2VCi?HjqFh;^?VRl-S4{tPb9ml;3mdv9t2?q@w zr02V&y_z0w+L9@4E6Dg9KjAmO%a3oe22YY(jw4BceuU)iyYIe}pYCK0e;ot#3;dX? z@42b42Mfc+JJXs+G+8M57DUzMrfUp`(CSd+Vq@{T?wJXrWu(8HE}4XN%cYx)U7rov zZf3pnly+`m-mUU4yzi}vd4Bv#0`wy+tl{r?*Sp^JW7dfOoi*e~4o1?&x5YuPJ~|`> zxi#NSo2Rm9XIUq_qN|X4(oz|_p&ipG$GT;`v(W+3^s;VQ)UBneG!*7Jo9nm!^rt`F zM~NKQ;!z?;O<244@Y5Ih@&4)J?)t0?GXlKGKEtw0ve2S6(KVC@^BM~upcVAWm8&Gr z9X;ESvWmnRfE~*{VZnzjn>W$u*qBTh;Yugk!pQ=hkNtB#?l*YbKPS28Ge;7jzXrk@ z{(Y<|{}SOi{2euah(tl|0v*|H3UWN-Jq7y4jn7Fp7bLgj8}kBM90Q+2WgAF1tuL3( zv1D3FBmT|jM(+7P@usgQ=K0K&1n94!aQywF=wHV`YtwD6Mt6aZ&M*MFn(h_50xF2I zmgW|E>80(Ks57ghRP4eII3E0O-}YU@-(b(RG}N-^wUt{6;`y#~OS-bur z{*D73fL5Suzc=iI_W*pD-7wX$jygVx{dCQA>F#1lIBkoJXFldn805FJxqjELe)X$i zN@T`M0`y}<;J-5f|AvA24*abkcY&@N8o<>;7Z>PQZB+?1UExK|bKbAS;Ql@z`YTU9 z`Q$^C$O$D0(2p^>?6S)~z^3(IFu;58w=`~Gwp@XpvTJ+0Ku60c039o$!aRq0ubo?X zQV{c;pA+-^1eOHo$EdLX{$~u@-(#RJ1ki<4v%a>**C%b=H`Z52d8v|f0K53^EiHdD zGBWaUX3vY1$O$b8(2rrc`s%9}G06WjYwwQ^4-co{lS4F~4&W=WvEU(u{~ly5|7D&4 z{tx@#dnl1aC;|F$A#Zxqn>rZ4?_v#pE`xXpgM5Nb^9qCijhi-YYKA87>gsx`v$OLX zZ1&%F|NZwT1Y07B%oJI(W{rRL*=L_}?z!joP$G#Wl1L(nB$7xXi6oLpB8eoDNFs?O xl1L(nB$7xXi6oLpB8eoDNFs?Ol1L&({y!p!z&`WCKfC|{002ovPDHLkV1fmAar6KH literal 0 HcmV?d00001 diff --git a/resources/images/dev_ams_dry_ctr_n3f_dehumidifying.png b/resources/images/dev_ams_dry_ctr_n3f_dehumidifying.png new file mode 100644 index 0000000000000000000000000000000000000000..92064da370318fc95a1f4523f9f5ee046e334a43 GIT binary patch literal 22353 zcma%CRa+cgvz@`67n0zX;KAM92^QR)K?Zkca0{N`t|7R)yF&;t=-}?|9KL^WF81C} z_r>n+>guXmwN^)}D9K==5upJ908BYqN!9oD@qL7%AidAdsXO%V3#y~6jtc;Q{^5TE z2uRN)cs~hrQI!z~R8Eo{ydNM~iYbZ#05x&w&nAcffab28q?o!V@T3E^*+RpOw)3{- z%J0hO#wMGVPCZN(<#*(EbbdZ+u5olK0`78`-E|IBOt)Z_o@NCO&r#`dQ44Ctt2)!7G!B@8Tm#wx#KLc zXnQ*;6^*z0@~g^s=S}4cOFEUP@-`xRa2rp~)SG*YzCTa!;|(Px6sT8pv&Z6f=> zO_D@?ki{J7!TV*}!CkQp?g*Iv`_p+};RVfOFsaIrkee@dw+yIz`4OLk5Ya(|*EwA` z^LkzKkt7~+knCZ$$l0|Nw)lYX1`ihS>34^uVJhO^esKe&#&#(!Z3|x6lz1Co9Jv9ya^un&jF{lwOR>ng(KMkZ$9l6`jh>S%z`xtwj z1IjU$!tz!|{(2sslPhT$PbDfcvNLA%dg+A$^o_F%Z?AmSs9x-E`*Obm=dWucpYXTDF%u-7;GggqwIX=U-i zTae3>Gt!-pw$B?81oMrUrge5b!E7q8H*$n+?r(4Qm2iU1m3ZFtEr~Er2_PIv3n1~c z9e!UAcsf5;R#nB~H%edfyyxJ5m9e1mW$GJlZjR(;>Qyv>YkmjeI!bipEl5p&-1#{i z0Uy~_jJq4778BABi%8nyP`Fi~;n;i71;*k-j=+xP3sbSGGxxw$_%2en(5=4W<$vo^sTfNdeP)yOG~6>%8GXlL z(nB?bzIGGt$Jl_aBHa$^>-l=Dv5zYhx2}L5*nK&PsIs!M{bH?!mj5nC&ckQw%S^!CCu7!WvqOHA?%VC*wnr0 zTPSΠ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 literal 0 HcmV?d00001 diff --git a/resources/images/dev_ams_dry_ctr_n3s_cooling.png b/resources/images/dev_ams_dry_ctr_n3s_cooling.png new file mode 100644 index 0000000000000000000000000000000000000000..f663c57c5fa01f340eb6fe6191c0e9feb02cec98 GIT binary patch literal 10325 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 IOBx0KAH8yGZvX%Q literal 0 HcmV?d00001 diff --git a/resources/images/dev_ams_dry_ctr_n3s_dehumidifying.png b/resources/images/dev_ams_dry_ctr_n3s_dehumidifying.png new file mode 100644 index 0000000000000000000000000000000000000000..5926a6cb5421f9600822db6438ee31013c9eac19 GIT binary patch literal 10546 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 B@E8C9 literal 0 HcmV?d00001 diff --git a/resources/images/dev_ams_dry_ctr_n3s_error.png b/resources/images/dev_ams_dry_ctr_n3s_error.png new file mode 100644 index 0000000000000000000000000000000000000000..0b3df1c3fe1af439195f2fcadf660b3497c0a036 GIT binary patch literal 7305 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 literal 0 HcmV?d00001 diff --git a/resources/images/dev_ams_dry_ctr_n3s_heating.png b/resources/images/dev_ams_dry_ctr_n3s_heating.png new file mode 100644 index 0000000000000000000000000000000000000000..3613a3aefc251adb2b04c3ab40cffbf988f77f80 GIT binary patch literal 10872 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 Date: Tue, 7 Jul 2026 14:16:04 +0800 Subject: [PATCH 13/30] feat: add AMSDryControl dialog for AMS filament drying Three-page wizard: main status/control page, guide page with filament tray status, and progress page. Supports N3F and N3S AMS types. Co-Authored-By: Claude --- src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/AMSDryControl.cpp | 1864 ++++++++++++++++++++++++++++++ src/slic3r/GUI/AMSDryControl.hpp | 253 ++++ 3 files changed, 2119 insertions(+) create mode 100644 src/slic3r/GUI/AMSDryControl.cpp create mode 100644 src/slic3r/GUI/AMSDryControl.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 969dcb693a..463d98315e 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -28,6 +28,8 @@ set(SLIC3R_GUI_SOURCES GUI/AMSMaterialsSetting.hpp GUI/AMSSetting.cpp GUI/AMSSetting.hpp + GUI/AMSDryControl.cpp + GUI/AMSDryControl.hpp GUI/AmsWidgets.cpp GUI/AmsWidgets.hpp GUI/Auxiliary.cpp diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp new file mode 100644 index 0000000000..f88e46f65c --- /dev/null +++ b/src/slic3r/GUI/AMSDryControl.cpp @@ -0,0 +1,1864 @@ +#include "AMSDryControl.hpp" +#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" +#include "GUI_App.hpp" +#include "I18N.hpp" + +#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" + +#include "slic3r/GUI/DeviceCore/DevManager.h" + +#include "slic3r/GUI/MsgDialog.hpp" + +#include "slic3r/GUI/Widgets/AnimaController.hpp" +#include "slic3r/GUI/Widgets/Label.hpp" +#include "slic3r/GUI/Widgets/ComboBox.hpp" +#include "slic3r/GUI/Widgets/ProgressBar.hpp" +#include "slic3r/GUI/DeviceCore/DevUtilBackend.h" + +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Preset.hpp" + +#include + +namespace Slic3r { namespace GUI { + +const int AMS_DRY_STATUS_IMAGE_SIZE = 96; + +static std::string get_humidity_level_img_path(int humidity_percent) +{ + const int num_levels = 5; + int hum_level; + + if (humidity_percent <= 100 / num_levels) { + hum_level = 5; + } else if (humidity_percent <= 100 / num_levels * 2) { + hum_level = 4; + } else if (humidity_percent <= 100 / num_levels * 3) { + hum_level = 3; + } else if (humidity_percent <= 100 / num_levels * 4) { + hum_level = 2; + } else { + hum_level = 1; + } + + if (wxGetApp().dark_mode()) { + return "hum_level" + std::to_string(hum_level) + "_no_num_light"; + } else { + return "hum_level" + std::to_string(hum_level) + "_no_num_light"; + } + +} + +static std::string get_dry_status_img_path(DevAmsType type, DevAms::DryStatus status, DevAms::DrySubStatus sub_status) +{ + std::string img_name = "dev_ams_dry_ctr_"; + switch (type) { + case DevAmsType::N3S: + img_name += "n3s"; + break; + case DevAmsType::N3F: + img_name += "n3f"; + break; + default: + return ""; + } + + img_name += "_"; + switch (status) { + case DevAms::DryStatus::Error: + img_name += "error"; + return img_name; + } + + switch (sub_status) { + case DevAms::DrySubStatus::Heating: + img_name += "heating"; + return img_name; + case DevAms::DrySubStatus::Dehumidify: + img_name += "dehumidifying"; + return img_name; + } + + return ""; +} + +FilamentItemPanel::FilamentItemPanel(wxWindow* parent, const wxString& text, const std::string& icon_name, wxWindowID id) + : wxPanel(parent, id) + , m_icon_name(icon_name) +{ + SetBackgroundColour(wxColour("#F7F7F7")); // Light gray background + SetMinSize(wxSize(FromDIP(64), FromDIP(106))); // Width: 64, Height: 106 + SetSize(wxSize(FromDIP(64), FromDIP(106))); // Fixed size + + // Create sizer for vertical layout + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + + // Top section with text - moved to be closer to center + wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); + top_sizer->AddStretchSpacer(5); + + m_text_label = new Label(this, text); + m_text_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour(*wxBLACK))); + m_text_label->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F7F7F7"))); + m_text_label->SetFont(Label::Body_12); + m_text_label->Wrap(FromDIP(40)); + top_sizer->Add(m_text_label, 0, wxALIGN_CENTER_HORIZONTAL); + + top_sizer->AddStretchSpacer(1); // Increased bottom spacer to push text down + sizer->Add(top_sizer, 1, wxEXPAND); + + // Bottom section with icon - vertically centered in bottom half + wxBoxSizer* bottom_sizer = new wxBoxSizer(wxVERTICAL); + bottom_sizer->AddStretchSpacer(1); + + m_icon_bitmap = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap); + m_icon_bitmap->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F7F7F7"))); + m_icon_bitmap->SetMinSize(wxSize(FromDIP(24), FromDIP(24))); + m_icon_bitmap->SetMaxSize(wxSize(FromDIP(24), FromDIP(24))); + bottom_sizer->Add(m_icon_bitmap, 0, wxALIGN_CENTER_HORIZONTAL); + + bottom_sizer->AddStretchSpacer(1); + sizer->Add(bottom_sizer, 1, wxEXPAND); + + SetSizer(sizer); + + // Bind events + Bind(wxEVT_PAINT, &FilamentItemPanel::OnPaint, this); + Bind(wxEVT_SIZE, &FilamentItemPanel::OnSize, this); + + SetIcon(icon_name); + wxGetApp().UpdateDarkUI(this); +} + +void FilamentItemPanel::SetText(const wxString& text) +{ + m_text_label->SetLabel(text); + Layout(); +} + +void FilamentItemPanel::SetIcon(const std::string& icon_name) +{ + m_icon_name = icon_name; + if (!icon_name.empty()) { + try { + m_icon = ScalableBitmap(this, icon_name, 20); + m_icon.msw_rescale(); + m_icon_bitmap->SetBitmap(m_icon.bmp()); + } catch (Exception&) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": failed to load filament icon"; + m_icon_bitmap->SetBitmap(wxNullBitmap); + } + } else { + m_icon_bitmap->SetBitmap(wxNullBitmap); + } + m_icon_bitmap->Refresh(); +} + +void FilamentItemPanel::msw_rescale() +{ + if (m_icon_bitmap && !m_icon_name.empty()) { + m_icon.msw_rescale(); + m_icon_bitmap->SetBitmap(m_icon.bmp()); + m_icon_bitmap->Refresh(); + } + Layout(); +} + +void FilamentItemPanel::OnPaint(wxPaintEvent& event) +{ + wxPaintDC dc(this); + wxSize size = GetSize(); + + // bool is_dark_mode = wxGetApp().dark_mode(); + wxColour backgroundColor = StateColor::darkModeColorFor(wxColour("#F7F7F7")); + wxColour borderColor = StateColor::darkModeColorFor(wxColour("#DBDBDB")); + + // Draw white background rectangle with rounded corners inside the thick vertical lines + dc.SetBrush(wxBrush(backgroundColor)); + dc.SetPen(wxPen(backgroundColor)); + dc.DrawRectangle(FromDIP(6), FromDIP(12), size.GetWidth() - FromDIP(12), size.GetHeight() - 2 * FromDIP(12)); + + // Draw much thicker vertical rounded rectangles on left and right (3 times thicker) + dc.SetBrush(wxBrush(wxColour(borderColor))); + dc.SetPen(wxPen(wxColour(borderColor))); + dc.DrawRoundedRectangle(FromDIP(0), FromDIP(0), FromDIP(6), size.GetHeight(), FromDIP(6)); // Left rounded rectangle + dc.DrawRoundedRectangle(size.GetWidth() - FromDIP(6), FromDIP(0), FromDIP(6), size.GetHeight(), FromDIP(6)); // Right rounded rectangle + + // Draw thin horizontal rounded rectangles on top and bottom (moved closer to center by half the distance) + dc.SetBrush(wxBrush(wxColour(borderColor))); + dc.SetPen(wxPen(wxColour(borderColor))); + dc.DrawRoundedRectangle(FromDIP(6), FromDIP(12), size.GetWidth() - FromDIP(12), FromDIP(3), FromDIP(1)); // Top rounded rectangle + dc.DrawRoundedRectangle(FromDIP(6), size.GetHeight() - FromDIP(13), size.GetWidth() - FromDIP(12), FromDIP(3), FromDIP(1)); // Bottom rounded rectangle +} + +void FilamentItemPanel::OnSize(wxSizeEvent& event) +{ + Refresh(); + event.Skip(); +} + +// class AMSFilamentPanel + +AMSFilamentPanel::AMSFilamentPanel(wxWindow* parent, const wxString& ams_name, wxWindowID id) + : wxPanel(parent, id) +{ + SetBackgroundColour(wxColour("#DBDBDB")); + + wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + + // Filament items section + m_filament_container = new wxPanel(this); + m_filament_container->SetBackgroundColour(wxColour("#F7F7F7")); + m_filament_sizer = new wxBoxSizer(wxHORIZONTAL); + m_filament_container->SetSizer(m_filament_sizer); + + // AMS name section + m_ams_name_label = new Label(this, ams_name); + m_ams_name_label->SetForegroundColour(wxColour("#858585")); + m_ams_name_label->SetFont(Label::Body_14); + m_ams_name_label->SetBackgroundColour(wxColour("#DBDBDB")); + + main_sizer->Add(m_filament_container, 1, wxEXPAND | wxALL, 0); + main_sizer->Add(m_ams_name_label, 0, wxALIGN_LEFT | wxALL, FromDIP(5)); + + SetSizer(main_sizer); + wxGetApp().UpdateDarkUI(this); +} + +void AMSFilamentPanel::AddFilamentItem(FilamentItemPanel* panel) +{ + if (panel) { + panel->Reparent(m_filament_container); + m_filament_sizer->Add(panel, 0, wxALL, FromDIP(5)); + m_filament_items.push_back(panel); + Layout(); + } +} + +void AMSFilamentPanel::AddFilamentItem(const wxString& text, const std::string& icon_name) +{ + FilamentItemPanel* item = new FilamentItemPanel(m_filament_container, text, icon_name); + m_filament_items.push_back(item); + m_filament_sizer->Add(item, 0, wxALL, FromDIP(5)); + Layout(); +} + +void AMSFilamentPanel::SetAmsName(const wxString& ams_name) +{ + m_ams_name_label->SetLabel(ams_name); + Layout(); +} + +void AMSFilamentPanel::Clear() +{ + m_filament_sizer->Clear(true); + m_filament_items.clear(); +} + +void AMSFilamentPanel::msw_rescale() +{ + for (auto item : m_filament_items) { + item->msw_rescale(); + } + Layout(); +} + + +AMSDryCtrWin::AMSDryCtrWin(wxWindow *parent) + :DPIDialog(parent, wxID_ANY, _L("AMS Dryness Control"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) +{ + create(); +} + +AMSDryCtrWin::~AMSDryCtrWin() +{ + if (m_progress_timer) { + delete m_progress_timer; + } +} + + +wxScrolledWindow* AMSDryCtrWin::create_preview_scrolled_window(wxWindow* parent) +{ + wxScrolledWindow* panel = new wxScrolledWindow(parent, wxID_ANY); + panel->SetScrollRate(10, 0); + panel->SetSize(AMS_ITEMS_PANEL_SIZE); + panel->SetMinSize(AMS_ITEMS_PANEL_SIZE); + panel->SetBackgroundColour(AMS_CONTROL_DEF_BLOCK_BK_COLOUR); + + return panel; +} + +wxBoxSizer* AMSDryCtrWin::create_humidity_status_section(wxPanel* parent) +{ + wxBoxSizer* image_sizer = new wxBoxSizer(wxVERTICAL); + + m_humidity_img = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap); + image_sizer->Add(m_humidity_img, 1, wxALIGN_CENTER_HORIZONTAL, 0); + + m_humidity_img->SetBitmap(wxNullBitmap); + + // Create a horizontal sizer for description and icon + wxBoxSizer* desc_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_image_description_icon = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap); + m_image_description_icon->SetBackgroundColour(*wxWHITE); + m_image_description_icon->SetMinSize(wxSize(FromDIP(20), FromDIP(20))); + m_image_description_icon->SetMaxSize(wxSize(FromDIP(20), FromDIP(20))); + m_image_description_icon->Show(false); + desc_sizer->Add(m_image_description_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); + + m_image_description = new Label(parent, _L("Idle")); + m_image_description->SetFont(Label::Head_14); + m_image_description->SetForegroundColour(*wxBLACK); + m_image_description->SetBackgroundColour(*wxWHITE); + desc_sizer->Add(m_image_description, 0, wxALIGN_CENTER_VERTICAL); + + image_sizer->Add(desc_sizer, 0, wxALIGN_CENTER | wxALL, FromDIP(5)); + + return image_sizer; +} + +wxBoxSizer* AMSDryCtrWin::create_description_item(wxPanel* parent, const wxString& title, Label*& dataLabel) +{ + wxBoxSizer* item_sizer = new wxBoxSizer(wxVERTICAL); + + Label* titleLabel = new Label(parent, title); + titleLabel->SetForegroundColour(*wxBLACK); + titleLabel->SetFont(Label::Body_16); + + dataLabel = new Label(parent, wxT("--")); + dataLabel->SetForegroundColour(*wxBLACK); + dataLabel->SetFont(Label::Body_14); + + item_sizer->AddStretchSpacer(); + item_sizer->Add(titleLabel, 0, wxALIGN_CENTER | wxALL, FromDIP(2)); + item_sizer->Add(dataLabel, 0, wxALIGN_CENTER | wxALL, FromDIP(2)); + item_sizer->AddStretchSpacer(); + + return item_sizer; +} + +wxBoxSizer* AMSDryCtrWin::create_status_descriptions_section(wxPanel* parent) +{ + wxBoxSizer* desc_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxBoxSizer* hum_desc_sizer = create_description_item(parent, _L("Humidity"), m_humidity_data_label); + desc_sizer->Add(hum_desc_sizer, 1, wxEXPAND, FromDIP(1)); + + wxStaticLine* vert_separator = new wxStaticLine(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL); + desc_sizer->Add(vert_separator, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5)); + + wxBoxSizer* temp_desc_sizer = create_description_item(parent, _L("Temperature"), m_temperature_data_label); + desc_sizer->Add(temp_desc_sizer, 1, wxEXPAND, FromDIP(1)); + + m_time_descrition_container = new wxBoxSizer(wxHORIZONTAL); + wxStaticLine* vert_separator2 = new wxStaticLine(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL); + m_time_descrition_container->Add(vert_separator2, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5)); + + wxBoxSizer* time_desc_sizer = create_description_item(parent, _L("Left Time"), m_time_data_label); + m_time_descrition_container->Add(time_desc_sizer, 1, wxEXPAND, FromDIP(1)); + + desc_sizer->Add(m_time_descrition_container, 1, wxEXPAND, 0); + // m_time_descrition_container->Show(false); + + return desc_sizer; +} + +wxBoxSizer* AMSDryCtrWin::create_left_panel(wxPanel* parent) +{ + wxBoxSizer* left_sizer = new wxBoxSizer(wxVERTICAL); + + wxBoxSizer* image_section = create_humidity_status_section(parent); + left_sizer->Add(image_section, 1, wxEXPAND | wxALL, FromDIP(5)); + + wxBoxSizer* descriptions_section = create_status_descriptions_section(parent); + left_sizer->Add(descriptions_section, 0, wxEXPAND | wxALL, FromDIP(20)); + + return left_sizer; +} + +Button* AMSDryCtrWin::create_button(wxPanel* parent, const wxString& title, + const wxColour& background_color, const wxColour& border_color, const wxColour& text_color) +{ + Button* button = new Button(parent, title); + + // Create state colors for background + StateColor bg_color( + std::pair(AMS_CONTROL_DISABLE_COLOUR, StateColor::Disabled), + std::pair(background_color.ChangeLightness(80), StateColor::Pressed), + std::pair(background_color.ChangeLightness(120), StateColor::Hovered), + std::pair(background_color, StateColor::Normal) + ); + + // Create state colors for border + StateColor bd_color( + std::pair(AMS_CONTROL_WHITE_COLOUR, StateColor::Disabled), + std::pair(border_color, StateColor::Enabled) + ); + + button->SetBackgroundColor(bg_color); + button->SetBorderColor(bd_color); + button->SetTextColor(text_color); + button->SetFont(Label::Body_14); + + // Auto-size button based on text content with padding + wxSize best_size = button->GetBestSize(); + int padding_width = FromDIP(4); + int padding_height = FromDIP(2); + wxSize final_size(best_size.GetWidth() + padding_width, best_size.GetHeight() + padding_height); + button->SetMinSize(final_size); + + return button; +} + +wxBoxSizer* AMSDryCtrWin::create_normal_state_panel(wxPanel* parent) +{ + wxBoxSizer* normal_state_sizer = new wxBoxSizer(wxVERTICAL); + + Label* description_label = new Label(parent, _L("Filament Drying Settings")); + description_label->SetForegroundColour(*wxBLACK); + description_label->SetFont(Label::Head_14); + normal_state_sizer->Add(description_label, 0, wxALL, FromDIP(5)); + + // Part 2: ComboBox for material selection + wxBoxSizer* combo_sizer = new wxBoxSizer(wxHORIZONTAL); + m_trays_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(-1, FromDIP(26)), 0, nullptr, wxCB_READONLY); + m_trays_combo->SetFont(Label::Body_14); + + combo_sizer->Add(m_trays_combo, 1, wxEXPAND); + normal_state_sizer->Add(combo_sizer, 0, wxEXPAND | wxALL, FromDIP(5)); + + m_trays_combo->Bind(wxEVT_COMBOBOX, &AMSDryCtrWin::OnFilamentSelectionChanged, this); + + // part 3 time and temperature + // Temperature part + wxBoxSizer* temp_sizer = new wxBoxSizer(wxHORIZONTAL); + m_temperature_input = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1)); + m_temperature_input->SetMaxLength(3); // Limit to 3 digits + + m_temperature_input->Bind(wxEVT_CHAR, [this](wxKeyEvent& event) { + int keycode = event.GetKeyCode(); + if (keycode >= '0' && keycode <= '9') { + event.Skip(); + } else if (keycode == WXK_BACK || keycode == WXK_DELETE || keycode == WXK_LEFT || keycode == WXK_RIGHT) { + event.Skip(); + } + }); + + m_temperature_input->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { + m_next_button->Disable(); + m_start_button->Disable(); + }); + + m_temperature_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + m_temperature_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK)); + + Label* temp_unit_label = new Label(parent, wxString::FromUTF8("℃")); + temp_unit_label->SetForegroundColour(*wxBLACK); + temp_sizer->Add(m_temperature_input, 1, wxRIGHT, FromDIP(1)); + temp_sizer->Add(temp_unit_label, 0, wxALIGN_CENTER_VERTICAL); + + // Time part + wxBoxSizer* time_sizer = new wxBoxSizer(wxHORIZONTAL); + m_time_input = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(100), -1)); + m_time_input->SetMaxLength(3); // Limit to 3 digits + + m_time_input->Bind(wxEVT_CHAR, [this](wxKeyEvent& event) { + int keycode = event.GetKeyCode(); + if (keycode >= '0' && keycode <= '9') { + event.Skip(); + } else if (keycode == WXK_BACK || keycode == WXK_DELETE || keycode == WXK_LEFT || keycode == WXK_RIGHT) { + event.Skip(); + } + }); + + m_time_input->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { + m_next_button->Disable(); + m_start_button->Disable(); + }); + + m_time_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + m_time_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK)); + + Label* time_unit_label = new Label(parent, "H"); + time_unit_label->SetForegroundColour(*wxBLACK); + time_sizer->Add(m_time_input, 1, wxRIGHT, FromDIP(1)); + time_sizer->Add(time_unit_label, 0, wxALIGN_CENTER_VERTICAL); + + wxBoxSizer* input_sizer = new wxBoxSizer(wxHORIZONTAL); + input_sizer->Add(temp_sizer, 1, wxRIGHT, FromDIP(10)); + input_sizer->Add(time_sizer, 1, 0); + normal_state_sizer->Add(input_sizer, 0, wxEXPAND | wxALL, FromDIP(5)); + + // Part 4: Abnormal description/message area + m_normal_description = new Label(parent, ""); + m_normal_description->SetForegroundColour(wxColour("#F09A17")); + m_normal_description->SetFont(Label::Body_12); + normal_state_sizer->Add(m_normal_description, 0, wxALL, FromDIP(5)); + + // Part 5: Start button + m_next_button = create_button( + parent, + _L("Start"), + AMS_CONTROL_BRAND_COLOUR, // Background color - green + AMS_CONTROL_BRAND_COLOUR, // Border color - green + wxColour("#FFFFFE") // Text color - white + ); + + m_next_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { + m_main_simplebook->SetSelection(1); + }); + + normal_state_sizer->Add(m_next_button, 0, wxALL, FromDIP(5)); + m_next_button->Disable(); + + m_stop_button = create_button( + parent, + _L("Stop"), + wxColour(255, 0, 0), // Background color - red + wxColour(255, 0, 0), // Border color - red + wxColour("#FFFFFE") // Text color - white + ); + + m_stop_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { + auto fila_system = get_fila_system(); + if (!fila_system) { + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Invalid FilaSystem Pointer"; + return; + } + + fila_system->CtrlAmsStopDrying(std::stoi(m_ams_info.m_ams_id)); + // Temporarily show stopping state, then restore after 2 seconds + if (m_stop_button) { + m_stop_button->SetLabel(_L("Stopping")); + update_button_size(m_stop_button); // Adjust button size for longer text + m_stop_button->Disable(); + m_stop_button->Layout(); + m_stop_button->GetParent()->Layout(); // Relayout parent container + m_stop_button->Refresh(); + + m_stop_button_restore_deadline.reset(); + m_stop_button_restore_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + } + }); + + m_stop_button->Show(false); + normal_state_sizer->Add(m_stop_button, 0, wxALL, FromDIP(5)); + + return normal_state_sizer; +} + +wxBoxSizer* AMSDryCtrWin::create_cannot_dry_panel(wxPanel* parent) +{ + wxBoxSizer* abnormal_sizer = new wxBoxSizer(wxVERTICAL); + + Label* description_label = new Label(parent, _L("Unable to dry temporarily due to ...")); + description_label->SetForegroundColour(*wxBLACK); + description_label->SetFont(Label::Head_16); + description_label->Wrap(FromDIP(300)); + abnormal_sizer->Add(description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10)); + + // Add a description label for the abnormal state + m_cannot_dry_description_label = new Label(parent, ("")); + m_cannot_dry_description_label->SetForegroundColour(*wxBLACK); + m_cannot_dry_description_label->SetFont(Label::Body_14); + m_cannot_dry_description_label->Wrap(FromDIP(250)); // Wrap text to fit within panel + + abnormal_sizer->Add(m_cannot_dry_description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10)); + + // Unload button shown only when ConsumableAtAmsOutlet reason is active + m_unload_button = create_button( + parent, + _L("Unload"), + AMS_CONTROL_BRAND_COLOUR, // Background color - light gray + AMS_CONTROL_BRAND_COLOUR, // Border color - gray + *wxWHITE // Text color - white + ); + + m_unload_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { + auto fila_system = get_fila_system(); + if (!fila_system) { + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::unload_button: Invalid FilaSystem Pointer"; + return; + } + + MachineObject* obj = fila_system->GetOwner(); + if (!obj) { + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::unload_button: Invalid MachineObject Pointer"; + return; + } + + obj->command_ams_change_filament(false, m_ams_info.m_ams_id, "255"); + + m_unload_button->Disable(); + m_unload_button_restore_deadline.reset(); + m_unload_button_restore_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + }); + + m_unload_button->Show(false); + abnormal_sizer->Add(m_unload_button, 0, wxALL, FromDIP(5)); + + return abnormal_sizer; +} + +wxBoxSizer* AMSDryCtrWin::create_drying_error_panel(wxPanel* parent) +{ + wxBoxSizer* err_sizer = new wxBoxSizer(wxVERTICAL); + Label* description_label = new Label(parent, _L("Drying Error")); + description_label->SetForegroundColour(*wxRED); + description_label->SetFont(Label::Body_14); // Wrap text to fit within panel + err_sizer->Add(description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10)); + + Label* additional_description_label = new Label(parent, _L("Please check the Assistant for troubleshooting")); + additional_description_label->SetForegroundColour(*wxBLACK); + additional_description_label->SetFont(Label::Body_14); // Wrap text to fit within panel + err_sizer->Add(additional_description_label, 0, wxALIGN_CENTER_VERTICAL, FromDIP(10)); + + return err_sizer; +} + +wxBoxSizer* AMSDryCtrWin::create_right_panel(wxPanel* parent) +{ + wxBoxSizer* right_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_normal_state_sizer = create_normal_state_panel(parent); + m_cannot_dry_sizer = create_cannot_dry_panel(parent); + m_dry_error_sizer = create_drying_error_panel(parent); + + right_sizer->Add(m_normal_state_sizer, 1, wxEXPAND); + right_sizer->Add(m_cannot_dry_sizer, 1, wxEXPAND); + + m_cannot_dry_sizer->Show(false); + m_dry_error_sizer->Show(false); + + return right_sizer; +} + +wxBoxSizer* AMSDryCtrWin::create_main_content_section(wxPanel* parent) +{ + wxBoxSizer* content_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxBoxSizer* left_panel = create_left_panel(parent); + content_sizer->Add(left_panel, 2, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); + left_panel->SetMinSize(wxSize(FromDIP(250), -1)); + + wxBoxSizer* right_panel = create_right_panel(parent); + content_sizer->Add(right_panel, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); + right_panel->SetMinSize(wxSize(FromDIP(250), -1)); + + return content_sizer; +} + +wxBoxSizer* AMSDryCtrWin::create_main_page_sizer(wxPanel* parent) +{ + wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + + wxBoxSizer* content_section = create_main_content_section(parent); + main_sizer->Add(content_section, 1, wxEXPAND | wxALL, FromDIP(5)); + + return main_sizer; +} + +wxBoxSizer* AMSDryCtrWin::create_guide_info_filament(wxPanel* parent) +{ + wxBoxSizer* filament_section = new wxBoxSizer(wxHORIZONTAL); + + m_ams_filament_panel = new AMSFilamentPanel(parent, ""); + + filament_section->Add(m_ams_filament_panel, 0, wxEXPAND | wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(5)); + + return filament_section; +} + +wxBoxSizer* AMSDryCtrWin::create_guide_info_section(wxPanel* parent) +{ + wxBoxSizer* info_section = new wxBoxSizer(wxVERTICAL); + + // Part 1: Title + m_guide_title_label = new Label(parent, _L("Please remove and store the filament (as shown).")); + m_guide_title_label->SetForegroundColour(*wxBLACK); + m_guide_title_label->SetFont(Label::Head_18); + info_section->Add(m_guide_title_label, 0, wxEXPAND | wxALL, FromDIP(5)); + + // Part 2: Description + m_guide_description_label = new Label(parent, _L("The AMS can rotate the filament which is properly stored, providing better drying results.")); + m_guide_description_label->SetForegroundColour(*wxBLACK); + m_guide_description_label->SetFont(Label::Body_14); + m_guide_description_label->Wrap(FromDIP(300)); // Wrap text to fit within panel + info_section->Add(m_guide_description_label, 0, wxEXPAND | wxALL, FromDIP(5)); + + // Part 3: filament panel + wxBoxSizer* fila_section = create_guide_info_filament(parent); + info_section->Add(fila_section, 0, wxEXPAND | wxALL, FromDIP(5)); + + // Part 4: Circular toggle with description + wxBoxSizer* toggle_section = new wxBoxSizer(wxHORIZONTAL); + + m_rotate_spool_toggle = new wxCheckBox(parent, wxID_ANY, ""); + m_rotate_spool_toggle->SetValue(false); + + m_rotate_spool_toggle->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { + bool is_checked = event.IsChecked(); + // Add toggle behavior logic here + }); + + toggle_section->Add(m_rotate_spool_toggle, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(10)); + + // Toggle description + Label* toggle_description = new Label(parent, _L("Rotate spool when drying"), LB_AUTO_WRAP); + toggle_description->SetForegroundColour(*wxBLACK); + toggle_description->SetFont(Label::Body_12); + toggle_section->Add(toggle_description, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND, 0); + + info_section->Add(toggle_section, 0, wxEXPAND | wxALL, FromDIP(5)); + + return info_section; +} + +wxBoxSizer* AMSDryCtrWin::create_guide_right_section(wxPanel* parent) +{ + wxBoxSizer* right_section = new wxBoxSizer(wxVERTICAL); + + // Upper part: Image section + m_image_placeholder = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap); + m_guide_image = ScalableBitmap(parent, "dev_ams_dry_ctr_filament_in_chamber", 256); + m_image_placeholder->SetBitmap(m_guide_image.bmp()); + + wxBoxSizer* image_container = new wxBoxSizer(wxHORIZONTAL); + image_container->AddStretchSpacer(1); + image_container->Add(m_image_placeholder, 0, wxALL, FromDIP(5)); + + right_section->Add(image_container, 0, wxEXPAND | wxALL, FromDIP(5)); + + wxBoxSizer* buttons_container = new wxBoxSizer(wxHORIZONTAL); + buttons_container->AddStretchSpacer(1); + + m_back_button = create_button( + parent, + wxString::FromUTF8(_CTX_utf8(L_CONTEXT("Back", "amsdrying"), "amsdrying")), + wxColour("#F8F8F8"), // Background color - light gray + wxColour("#D0D0D0"), // Border color - gray + *wxBLACK // Text color - black + ); + + m_back_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { + m_main_simplebook->SetSelection(0); + }); + + buttons_container->Add(m_back_button, 0, wxALL, FromDIP(5)); + + m_start_button = create_button( + parent, + _L("Start"), + AMS_CONTROL_BRAND_COLOUR, // Background color - green + AMS_CONTROL_BRAND_COLOUR, // Border color - green + wxColour("#FFFFFE") // Text color - white + ); + + m_start_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { + m_main_simplebook->SetSelection(2); + m_progress_gauge->SetValue(0); + + m_progress_timer->Start(70); + m_progress_value = 0; + start_sending_drying_command(); + }); + + buttons_container->Add(m_start_button, 0, wxALL, FromDIP(5)); + + right_section->Add(buttons_container, 0, wxEXPAND | wxALL, FromDIP(5)); + + return right_section; +} + +wxBoxSizer* AMSDryCtrWin::create_guide_page_sizer(wxPanel* parent) +{ + wxBoxSizer* guide_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxBoxSizer* info_section = create_guide_info_section(parent); + guide_sizer->Add(info_section, 1, wxEXPAND | wxALL, FromDIP(10)); + + wxBoxSizer* right_section = create_guide_right_section(parent); + guide_sizer->Add(right_section, 0, wxEXPAND | wxALL, FromDIP(10)); + + return guide_sizer; +} + +void AMSDryCtrWin::start_sending_drying_command() +{ + auto fila_system = get_fila_system(); + if (!fila_system) { + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Invalid FilaSystem Pointer"; + return; + } + + if (m_temperature_input->GetValue().IsEmpty() || m_time_input->GetValue().IsEmpty()) { + // Show error message to user + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Time or temperature is empty"; + return; + } + + long temperature, time; + if (!m_temperature_input->GetValue().ToLong(&temperature) || + !m_time_input->GetValue().ToLong(&time)) { + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::start_sending_drying_command: Failed to convert temperature or time"; + return; + } + + int tray_index; + tray_index = m_trays_combo->GetSelection(); + if (m_tray_ids.empty() || tray_index < 0 || tray_index >= m_tray_ids.size()) { + BOOST_LOG_TRIVIAL(warning) << "AMSDryCtrWin::start_sending_drying_command: Invalid tray_index " << tray_index + << ", m_tray_ids.size=" << m_tray_ids.size(); + return; + } + + int cooling_temp = 50; + std::optional preset = DevUtilBackend::GetFilamentDryingPreset(m_tray_ids[tray_index].filament_id); + if (preset.has_value()) { + cooling_temp = static_cast(preset.value().filament_dev_drying_softening_temperature); + } + + fila_system->CtrlAmsStartDryingHour(std::stoi(m_ams_info.m_ams_id), m_tray_ids[tray_index].filament_type, + temperature, time, m_rotate_spool_toggle->GetValue(), cooling_temp, false); + + m_dry_setting.m_filament_names[m_ams_info.m_ams_id] = m_tray_ids[tray_index].filament_name; + m_dry_setting.m_filament_type[m_ams_info.m_ams_id] = m_tray_ids[tray_index].filament_type; + m_dry_setting.m_dry_temp[m_ams_info.m_ams_id] = temperature; + m_dry_setting.m_dry_time[m_ams_info.m_ams_id] = time; +} + +void AMSDryCtrWin::OnProgressTimer(wxTimerEvent& event) +{ + m_progress_value += 1; + + if (m_progress_value <= 100) + m_progress_gauge->SetValue(m_progress_value); + + if (m_progress_value == 1) { // First tick, reset message index + m_progress_message_index = 0; + if (!m_progress_text.empty()) { + m_progress_title->SetLabel(m_progress_text[0]); + } + } + else if (m_progress_value % 15 == 0) { // Approximately every second + m_progress_message_index++; + if (m_progress_message_index < m_progress_text.size()) { + m_progress_title->SetLabel(m_progress_text[m_progress_message_index]); + m_progress_title->Refresh(); + } + } + + if (m_progress_value >= 100 && !is_dry_ctr_idle()) { + m_progress_value = 0; + m_progress_timer->Stop(); + m_main_simplebook->SetSelection(0); + } +} + +void AMSDryCtrWin::restore_stop_button_if_deadline_passed() +{ + if (m_stop_button_restore_deadline.has_value()) { + if (std::chrono::steady_clock::now() >= m_stop_button_restore_deadline.value()) { + if (m_stop_button) { + m_stop_button->SetLabel(_L("Stop")); + update_button_size(m_stop_button); // Adjust button size back to original + m_stop_button->Enable(); + m_stop_button->Layout(); + m_stop_button->GetParent()->Layout(); + m_stop_button->Refresh(); + } + m_stop_button_restore_deadline.reset(); + } + } +} + +void AMSDryCtrWin::restore_unload_button_if_deadline_passed() +{ + if (m_unload_button_restore_deadline.has_value()) { + if (std::chrono::steady_clock::now() >= m_unload_button_restore_deadline.value()) { + if (m_unload_button) { + m_unload_button->Enable(); + } + m_unload_button_restore_deadline.reset(); + } + } +} + +void AMSDryCtrWin::update_button_size(Button* button) +{ + if (!button) return; + + // Reset MinSize first to avoid accumulation of size + button->SetMinSize(wxSize(-1, -1)); + + // Recalculate button size based on current text and DPI settings + wxSize best_size = button->GetBestSize(); + int padding_width = FromDIP(4); + int padding_height = FromDIP(2); + wxSize final_size(best_size.GetWidth() + padding_width, best_size.GetHeight() + padding_height); + button->SetMinSize(final_size); +} + +void AMSDryCtrWin::OnShow(wxShowEvent& event) +{ + if (event.IsShown()) { + wxGetApp().UpdateDlgDarkUI(this); + msw_rescale(); + } + event.Skip(); +} + +void AMSDryCtrWin::OnClose(wxCloseEvent& event) +{ + if (m_progress_timer && m_progress_timer->IsRunning()) { + m_progress_timer->Stop(); + } + + if (m_progress_gauge) { + m_progress_gauge->SetValue(0); + } + + m_progress_value = 0; + m_progress_message_index = 0; + + if (m_main_simplebook) { + m_main_simplebook->SetSelection(0); + } + + if (m_progress_title && !m_progress_text.empty()) { + m_progress_title->SetLabel(m_progress_text[0]); + } + + // Clean up and restore stop button state + if (m_stop_button) { + m_stop_button->SetLabel(_L("Stop")); + update_button_size(m_stop_button); // Adjust button size back to original + m_stop_button->Enable(); + m_stop_button->Layout(); + m_stop_button->GetParent()->Layout(); + m_stop_button->Refresh(); + } + m_stop_button_restore_deadline.reset(); + + // Clean up unload button state + if (m_unload_button) { + m_unload_button->Enable(); + } + m_unload_button_restore_deadline.reset(); + + event.Skip(); +} + +wxBoxSizer* AMSDryCtrWin::create_progress_page_sizer(wxPanel* parent) +{ + wxBoxSizer* progress_sizer = new wxBoxSizer(wxVERTICAL); + + m_progress_title = new Label(parent, m_progress_text[0]); + m_progress_title->SetForegroundColour(*wxBLACK); + m_progress_title->SetFont(Label::Body_16); + + progress_sizer->Add(0, 0, 1, wxEXPAND, 0); // Spacer + progress_sizer->Add(m_progress_title, 0, wxALIGN_CENTER | wxALL, FromDIP(30)); + + m_progress_gauge = new ProgressBar(parent, wxID_ANY, 100, wxDefaultPosition, wxSize(FromDIP(300), FromDIP(8))); + m_progress_gauge->SetValue(0); + m_progress_gauge->SetHeight(FromDIP(8)); + + progress_sizer->Add(m_progress_gauge, 0, wxALIGN_CENTER | wxALL, FromDIP(10)); + progress_sizer->Add(0, 0, 1, wxEXPAND, 0); + + return progress_sizer; +} + +void AMSDryCtrWin::create() +{ + // set title icon + SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + this->SetDoubleBuffered(true); + std::string icon_path = (boost::format("%1%/images/BambuStudioTitle.ico") % resources_dir()).str(); + SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO)); + + SetSize(wxSize(FromDIP(700), FromDIP(500))); + SetMinSize(wxSize(FromDIP(700), FromDIP(500))); + SetMaxSize(wxSize(FromDIP(700), FromDIP(500))); + + m_main_simplebook = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); + + // Create main page + m_original_page = new wxPanel(m_main_simplebook, wxID_ANY); + m_original_page->SetBackgroundColour(*wxWHITE); + wxBoxSizer* main_sizer = create_main_page_sizer(m_original_page); + m_original_page->SetSizer(main_sizer); + + m_main_simplebook->AddPage(m_original_page, "Main Page"); + + m_guide_page = new wxPanel(m_main_simplebook, wxID_ANY); + m_guide_page->SetBackgroundColour(*wxWHITE); + wxBoxSizer* guide_sizer = create_guide_page_sizer(m_guide_page); + m_guide_page->SetSizer(guide_sizer); + m_main_simplebook->AddPage(m_guide_page, "Guide Page"); + + // Create progress page + m_progress_page = new wxPanel(m_main_simplebook, wxID_ANY); + m_progress_page->SetBackgroundColour(*wxWHITE); + wxBoxSizer* progress_sizer = create_progress_page_sizer(m_progress_page); + m_progress_page->SetSizer(progress_sizer); + m_main_simplebook->AddPage(m_progress_page, "Progress Page"); + + m_progress_timer = new wxTimer(this, wxID_ANY); + Bind(wxEVT_TIMER, &AMSDryCtrWin::OnProgressTimer, this, m_progress_timer->GetId()); + + Bind(wxEVT_CLOSE_WINDOW, &AMSDryCtrWin::OnClose, this); + + wxBoxSizer* top_level_sizer = new wxBoxSizer(wxVERTICAL); + top_level_sizer->Add(m_main_simplebook, 1, wxEXPAND | wxALL, FromDIP(10)); + + Bind(wxEVT_SHOW, &AMSDryCtrWin::OnShow, this); + + SetSizer(top_level_sizer); + Layout(); + Refresh(); + wxGetApp().UpdateDlgDarkUI(this); +} + +void AMSDryCtrWin::on_dpi_changed(const wxRect &suggested_rect) +{ + msw_rescale(); +} + +void AMSDryCtrWin::msw_rescale() +{ + if (!IsShown()) { + return; + } + + if (m_ams_filament_panel) {m_ams_filament_panel->msw_rescale();} + if (m_trays_combo) {m_trays_combo->Rescale(); m_trays_combo->Layout(); m_trays_combo->SetFont(Label::Body_14), m_trays_combo->Refresh();} + if (m_humidity_img && m_humidity_image.bmp().IsOk()) {m_humidity_image.msw_rescale(); m_humidity_img->SetBitmap(m_humidity_image.bmp());} + if (m_image_placeholder && m_guide_image.bmp().IsOk()) {m_guide_image.msw_rescale();m_image_placeholder->SetBitmap(m_guide_image.bmp());} + if (m_image_description_icon && m_description_icon_bitmap.bmp().IsOk()) {m_description_icon_bitmap.msw_rescale(); m_image_description_icon->SetBitmap(m_description_icon_bitmap.bmp());} + + // Rescale and update button sizes based on text content + if (m_next_button) { + m_next_button->Rescale(); + update_button_size(m_next_button); + m_next_button->Layout(); + m_next_button->Refresh(); + } + if (m_stop_button) { + m_stop_button->Rescale(); + update_button_size(m_stop_button); + m_stop_button->Layout(); + m_stop_button->Refresh(); + } + if (m_start_button) { + m_start_button->Rescale(); + update_button_size(m_start_button); + m_start_button->Layout(); + m_start_button->Refresh(); + } + if (m_back_button) { + m_back_button->Rescale(); + update_button_size(m_back_button); + m_back_button->Layout(); + m_back_button->Refresh(); + } + if (m_unload_button) { + m_unload_button->Rescale(); + update_button_size(m_unload_button); + m_unload_button->Layout(); + m_unload_button->Refresh(); + } + + if (m_guide_page) {m_guide_page->Layout();} + if (m_original_page) {m_original_page->Layout();} + + Fit(); + Layout(); + Refresh(); +} + +void AMSDryCtrWin::set_ams_id(const std::string& ams_id) +{ + m_ams_info.m_ams_id = ams_id; + m_is_ams_changed = true; +} + +void AMSDryCtrWin::update_img_description(DevAms::DryStatus status, DevAms::DrySubStatus sub_status) +{ + if (status == DevAms::DryStatus::Off || status == DevAms::DryStatus::Cooling) { + m_image_description->SetLabel(_L("Idle")); + m_image_description_icon->Show(false); + return; + } + + // Determine label text for non-idle states + wxString label_text; + if (status == DevAms::DryStatus::Error) { + label_text = _L("Drying"); + } else if (sub_status == DevAms::DrySubStatus::Heating) { + label_text = _L("Drying-Heating"); + } else if (sub_status == DevAms::DrySubStatus::Dehumidify) { + label_text = _L("Drying-Dehumidifying"); + } else { + m_image_description_icon->Show(false); + return; + } + + m_image_description->SetLabel(label_text); + + try { + m_description_icon_bitmap = ScalableBitmap(this, "dev_ams_dry_ctr_heating_icon", 20); + m_description_icon_bitmap.msw_rescale(); + m_image_description_icon->SetBitmap(m_description_icon_bitmap.bmp()); + m_image_description_icon->Show(true); + } catch (Exception&) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Error loading drying icon"; + m_image_description_icon->Show(false); + } +} + +int AMSDryCtrWin::update_image(DevAmsType model, DevAms::DryStatus status, DevAms::DrySubStatus sub_status, int humidity_percent) +{ + if (model == m_ams_info.m_model && status == m_ams_info.m_dry_status + && sub_status == m_ams_info.m_dry_sub_status && humidity_percent == m_ams_info.m_humidity_percent) { + return 0; + } + + std::string img_path; + + if (status == DevAms::DryStatus::Off && sub_status == DevAms::DrySubStatus::Off) { + img_path = get_humidity_level_img_path(humidity_percent); + } else { + img_path = get_dry_status_img_path(model, status, sub_status); + } + + if (img_path.empty()) { + return 0; + } + + m_humidity_image = ScalableBitmap(this, img_path, AMS_DRY_STATUS_IMAGE_SIZE); + m_humidity_img->SetBitmap(m_humidity_image.bmp()); + + update_img_description(status, sub_status); + + return 1; +} + +bool AMSDryCtrWin::check_values_changed(DevAms* dev_ams) +{ + bool changed = false; + + if (m_ams_info.m_model != dev_ams->GetAmsType()) { + m_ams_info.m_model = dev_ams->GetAmsType(); + changed = true; + } + + if (dev_ams->GetDryStatus().has_value() && m_ams_info.m_dry_status != dev_ams->GetDryStatus()) { + m_ams_info.m_dry_status = dev_ams->GetDryStatus().value(); + changed = true; + } + + if (dev_ams->GetDrySubStatus().has_value() && m_ams_info.m_dry_sub_status != dev_ams->GetDrySubStatus()) { + m_ams_info.m_dry_sub_status = dev_ams->GetDrySubStatus().value(); + changed = true; + } + + if (m_ams_info.m_humidity_percent != dev_ams->GetHumidityPercent()) { + m_ams_info.m_humidity_percent = dev_ams->GetHumidityPercent(); + changed = true; + } + + return changed; +} + +void AMSDryCtrWin::update_normal_description(DevAms* dev_ams) +{ + if (m_tray_ids.empty() || m_trays_combo->GetSelection() >= m_tray_ids.size()) { return; } + + FilamentBaseInfo info = m_tray_ids[m_trays_combo->GetSelection()]; + wxString warning_text; + bool can_enable_button = true; + long temp_val = 0, time_val = 0; + m_temperature_input->GetValue().ToLong(&temp_val); + m_time_input->GetValue().ToLong(&time_val); + std::optional preset = DevUtilBackend::GetFilamentDryingPreset(info.filament_id); + auto total_dry = preset.has_value() ? preset.value().ams_limitations : std::unordered_set(); + + struct AmsTempLimit { DevAmsType type; int min_temp; int max_temp; const char* name; }; + static const AmsTempLimit ams_limits[] = { + { DevAmsType::N3F, 45, 65, "AMS2" }, + { DevAmsType::N3S, 45, 85, "AMS-S" } + }; + + for (const auto& lim : ams_limits) { + if (dev_ams->GetAmsType() == lim.type) { + if (temp_val > lim.max_temp) { + wxString msg = wxString(lim.name) + _L(" maximum drying temperature is ") + wxString::Format(wxT("%d"), lim.max_temp) + wxString::FromUTF8("°C."); + warning_text += msg + "\n"; + can_enable_button = false; + } else if (temp_val < lim.min_temp) { + wxString msg = wxString(lim.name) + _L(" minimum drying temperature is ") + wxString::Format(wxT("%d"), lim.min_temp) + wxString::FromUTF8("°C."); + warning_text += msg + "\n"; + can_enable_button = false; + } + if (total_dry.find(lim.type) == total_dry.end()) { + warning_text += _L("This filament may not be completely dried.") + "\n\n"; + } + break; + } + } + + if (m_printer_status.m_is_printing && temp_val > m_ams_info.m_recommand_dry_temp) { + warning_text += _L("This AMS is currently printing. To ensure print quality, the drying temperature cannot exceed the recommended drying temperature.") + "\n"; + can_enable_button = false; + } else if (preset.has_value()) { + // Only check heat distortion temperature when AMS has filament inserted; + // empty AMS only needs to respect the device hardware temperature limit. + bool has_filament_inserted = false; + for (const auto& tray_pair : dev_ams->GetTrays()) { + if (tray_pair.second && tray_pair.second->is_exists) { + has_filament_inserted = true; + break; + } + } + if (has_filament_inserted) { + auto limit_temperature = preset.value().filament_dev_ams_drying_heat_distortion_temperature; + if (temp_val > limit_temperature) { + warning_text += _L("The temperature shall not exceed the filament's heat distortion temperature") + "(" + + wxString::Format(wxT("%d"), static_cast(limit_temperature)) + wxString::FromUTF8("°C)\n"); + can_enable_button = false; + } + } + } + + if (time_val < 1) { + warning_text += _L("Minimum time value cannot be less than 1.") + "\n"; + can_enable_button = false; + } else if (time_val > 24) { + warning_text += _L("Maximum time value cannot be greater than 24.") + "\n"; + can_enable_button = false; + } + + m_next_button->Enable(can_enable_button); + m_normal_description->SetLabel(warning_text); + m_normal_description->Wrap(FromDIP(250)); + m_normal_description->GetParent()->Layout(); +} + +void AMSDryCtrWin::update_normal_state(DevAms* dev_ams) +{ + if (is_dry_ctr_idle(dev_ams)) { + m_next_button->Show(true); + m_normal_description->Show(true); + m_trays_combo->Enable(); + m_temperature_input->SetEditable(true); + m_time_input->SetEditable(true); + + m_stop_button->Hide(); + m_dry_error_sizer->Show(false); + } else if (is_dry_ctr_err(dev_ams)) { + m_dry_error_sizer->Show(true); + m_next_button->Hide(); + m_normal_description->Hide(); + + m_trays_combo->Hide(); + m_temperature_input->Hide(); + m_time_input->Hide(); + m_stop_button->Show(true); + } else { + m_next_button->Hide(); + m_normal_description->Hide(); + + m_trays_combo->Disable(); + m_temperature_input->SetEditable(false); + m_time_input->SetEditable(false); + m_stop_button->Show(true); + m_dry_error_sizer->Show(false); + } + + if (m_temperature_input->GetValue().IsEmpty() || m_time_input->GetValue().IsEmpty()) { + m_next_button->Disable(); + m_start_button->Disable(); + } else { + m_next_button->Enable(); + } + + update_normal_description(dev_ams); +} + +void AMSDryCtrWin::OnFilamentSelectionChanged(wxCommandEvent& event) +{ + int selectionIndex = event.GetSelection(); + wxString selectedFilament = event.GetString(); + + auto fila_system = get_fila_system(); + if (!fila_system) { + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::OnFilamentSelectionChanged: Invalid FilaSystem Pointer"; + return; + } + + DevAms* dev_ams = fila_system->GetAmsById(m_ams_info.m_ams_id); + if (!dev_ams) { + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::OnFilamentSelectionChanged: Invalid AMS id"; + return; + } + + std::optional preset = DevUtilBackend::GetFilamentDryingPreset(m_tray_ids[selectionIndex].filament_id); + if (preset.has_value()) { + DevFilamentDryingPreset info = preset.value(); + if (m_printer_status.m_is_printing) { + m_temperature_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_temperature_on_print[dev_ams->GetAmsType()]))); + m_time_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_time_on_print[dev_ams->GetAmsType()]))); + } else { + m_temperature_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_temperature_on_idle[dev_ams->GetAmsType()]))); + m_time_input->SetValue(std::to_string(static_cast(info.filament_dev_ams_drying_time_on_idle[dev_ams->GetAmsType()]))); + } + } + + update_filament_guide_info(dev_ams); +} + +wxString get_cannot_reason_text(DevAms::CannotDryReason reason) +{ + wxString cannot_reason_text; + switch (reason) + { + case DevAms::CannotDryReason::InsufficientPower: + cannot_reason_text = "*" + _L("Insufficient power") + "\n"; + cannot_reason_text += _L(" Too many AMS drying simultaneously. Please plug in the power or stop other drying processes before starting.") + "\n"; + break; + case DevAms::CannotDryReason::AmsBusy: + cannot_reason_text = "*" + _L("AMS is busy") + "\n"; + cannot_reason_text += _L(" AMS is calibrating | reading RFID | loading/unloading material, please wait.") + "\n"; + break; + case DevAms::CannotDryReason::ConsumableAtAmsOutlet: + cannot_reason_text = "*" + _L("Filament in AMS outlet") + "\n"; + cannot_reason_text += _L(" The high drying temperature may cause AMS blockage, please unload first."); + break; + case DevAms::CannotDryReason::InitiatingAmsDrying: + cannot_reason_text = "*" + _L("Initiating AMS drying") + "\n"; + break; + case DevAms::CannotDryReason::NotSupportedIn2dMode: + cannot_reason_text = "*" + _L("Not supported in 2D mode") + "\n"; + break; + case DevAms::CannotDryReason::DryingInProgress: + cannot_reason_text = "*" + _L("Task in progress") + "\n"; + cannot_reason_text += _L(" The AMS might be in use during Task.") + "\n"; + break; + case DevAms::CannotDryReason::Upgrading: + cannot_reason_text = "*" + _L("Upgrading") + "\n"; + cannot_reason_text += _L(" Firmware update in progress, please wait...") + "\n"; + break; + case DevAms::CannotDryReason::InsufficientPowerNeedPluginPower: + cannot_reason_text = "*" + _L("Insufficient power") + "\n"; + cannot_reason_text += _L(" Please plug in the power and then use the drying function.") + "\n"; + break; + case DevAms::CannotDryReason::FilamentAtAmsOutletManualUnload: + cannot_reason_text = "*" + _L("Filament in AMS outlet") + "\n"; + cannot_reason_text += _L(" The high drying temperature may cause AMS blockage. Please unload the filament manually before proceeding.") + "\n"; + break; + default: + cannot_reason_text = "*" + _L("System is busy") + "\n"; + cannot_reason_text += _L(" Initiating other drying processes, please wait a few seconds...") + "\n"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": unknown cannot dry reason"; + break; + } + return cannot_reason_text; +} + +wxString organize_cannot_reasons_text(std::vector& reasons) +{ + wxString cannot_reasons_text; + if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::DryingInProgress) != reasons.end()) { + cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::DryingInProgress); + } else if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::AmsBusy) != reasons.end()) { + cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::AmsBusy); + } else if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::ConsumableAtAmsOutlet) != reasons.end()) { + cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::ConsumableAtAmsOutlet); + } else if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::FilamentAtAmsOutletManualUnload) != reasons.end()) { + cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::FilamentAtAmsOutletManualUnload); + } + + if (std::find(reasons.begin(), reasons.end(), DevAms::CannotDryReason::InsufficientPower) != reasons.end()) { + cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::InsufficientPower); + } + + for (auto reason : reasons) { + if (reason == DevAms::CannotDryReason::InsufficientPowerNeedPluginPower) { + cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::InsufficientPowerNeedPluginPower); + } else if (reason == DevAms::CannotDryReason::NotSupportedIn2dMode) { + cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::NotSupportedIn2dMode); + } else if (reason == DevAms::CannotDryReason::InitiatingAmsDrying) { + cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::InitiatingAmsDrying); + } else if (reason == DevAms::CannotDryReason::Upgrading) { + cannot_reasons_text += get_cannot_reason_text(DevAms::CannotDryReason::Upgrading); + } + } + + if (cannot_reasons_text.empty()) { + cannot_reasons_text += "*" + _L("System is busy") + "\n"; + cannot_reasons_text += _L(" Initiating other drying processes, please wait a few seconds...") + "\n"; + } + + return cannot_reasons_text; +} + +int AMSDryCtrWin::update_state(DevAms* dev_ams) +{ + std::vector cannot_reasons = dev_ams->GetCannotDryReason().has_value()? + dev_ams->GetCannotDryReason().value(): std::vector(); + + if (cannot_reasons.size() == 0 || + (cannot_reasons.size() == 1 && cannot_reasons[0] == DevAms::CannotDryReason::DryingInProgress)) { + m_normal_state_sizer->Show(true); + m_cannot_dry_sizer->Show(false); + m_unload_button->Show(false); + update_normal_state(dev_ams); + } else if (cannot_reasons.size() > 0) { + m_normal_state_sizer->Show(false); + m_cannot_dry_sizer->Show(true); + + m_cannot_dry_description_label->SetLabel(organize_cannot_reasons_text(cannot_reasons)); + m_cannot_dry_description_label->Wrap(FromDIP(300)); + + // Show unload button when ConsumableAtAmsOutlet reason is present + bool has_consumable_at_outlet = std::find(cannot_reasons.begin(), cannot_reasons.end(), + DevAms::CannotDryReason::ConsumableAtAmsOutlet) != cannot_reasons.end(); + m_unload_button->Show(has_consumable_at_outlet); + } + + restore_stop_button_if_deadline_passed(); + restore_unload_button_if_deadline_passed(); + + return 1; +} + +int AMSDryCtrWin::update_ams_change(DevAms* dev_ams) +{ + if (!is_ams_changed(dev_ams)) { + return 0; + } + + m_ams_info.m_ams_id = dev_ams->GetAmsId(); + if (dev_ams->GetAmsType() == DevAmsType::N3F) { + m_temperature_input->SetHint("45-65" + wxString::FromUTF8("°C")); + } else if (dev_ams->GetAmsType() == DevAmsType::N3S) { + m_temperature_input->SetHint("45-85" + wxString::FromUTF8("°C")); + } + + m_time_input->SetHint("1-24 h"); + + return 0; +} + +int AMSDryCtrWin::update_dryness_status(DevAms* dev_ams) +{ + int updated = 0; + + if (m_ams_info.m_humidity_percent != dev_ams->GetHumidityPercent()) { + updated += 1; + m_ams_info.m_humidity_percent = dev_ams->GetHumidityPercent(); + m_humidity_data_label->SetLabel(std::to_string(m_ams_info.m_humidity_percent) + "%"); + } + + if (m_ams_info.m_temperature != dev_ams->GetCurrentTemperature()) { + updated += 1; + m_ams_info.m_temperature = dev_ams->GetCurrentTemperature(); + m_temperature_data_label->SetLabel(std::to_string(m_ams_info.m_temperature) + wxString::FromUTF8("°C")); + } + + if (is_dry_ctr_idle(dev_ams)) { + m_time_descrition_container->Show(false); + m_original_page->Layout(); + } else { + if (is_dry_status_changed(dev_ams)) { + m_time_descrition_container->Show(true); + m_original_page->Layout(); + } + + if (m_ams_info.m_left_dry_time != dev_ams->GetLeftDryTime()) { + updated += 1; + m_ams_info.m_left_dry_time = dev_ams->GetLeftDryTime(); + m_time_data_label->SetLabel(wxString::Format("%02d : %02d", + m_ams_info.m_left_dry_time / 60, m_ams_info.m_left_dry_time % 60)); + } + } + + return updated; +} + +bool AMSDryCtrWin::is_tray_changed(DevAms* dev_ams) +{ + return false; +} + +void AMSDryCtrWin::update_filament_guide_info(DevAms* dev_ams) +{ + if (!IsShown()) { + return; + } + + m_guide_page->Freeze(); + m_ams_filament_panel->Clear(); + m_ams_filament_panel->SetAmsName(dev_ams->GetDisplayName()); + + // Get the temperature input value + long input_temp = 0; + bool valid_temp = !m_temperature_input->GetValue().IsEmpty() && + m_temperature_input->GetValue().ToLong(&input_temp); + bool can_start = true; + + int slot_count = 0, empty_count = 0; + for (auto& tray_pair : dev_ams->GetTrays()) { + if (!tray_pair.second) { + continue; + } + ++slot_count; + if (!tray_pair.second->is_exists) { + m_ams_filament_panel->AddFilamentItem("/", "dev_ams_dry_ctr_enable"); + ++empty_count; + continue; + } + auto preset_opt = tray_pair.second->get_ams_drying_preset(); + wxString filament_type = tray_pair.second->get_display_filament_type(); + DevFilamentDryingPreset preset; + if (filament_type.IsEmpty()) { + auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00"); + preset = fallback_preset.value(); + filament_type = "?"; + } else if (preset_opt.has_value()) { + preset = preset_opt.value(); + } else { + auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00"); + preset = fallback_preset.value(); + } + std::string icon_path = "dev_ams_dry_ctr_enable"; + int distortion_temp = static_cast(preset.filament_dev_ams_drying_heat_distortion_temperature); + if (valid_temp && distortion_temp < input_temp) { + icon_path = "dev_ams_dry_ctr_disable"; + can_start = false; + } + m_ams_filament_panel->AddFilamentItem(filament_type, icon_path); + } + if (slot_count == 0) { + can_start = false; + } + + if (can_start) { + m_guide_title_label->SetLabel(_L("For better drying results, remove the filament and allow it to rotate.")); + m_guide_description_label->SetLabel(_L("The AMS will automatically rotate the stored filament slots to enhance the drying performance.") + + "\n" + _L("Alternatively, you can dry the filament without removing it.") + + "\n" + "*" + _L("Unknown filaments will be treated as PLA.")); + m_guide_description_label->Wrap(FromDIP(300)); + } else { + m_guide_title_label->SetLabel(_L("Please store the filament marked with an exclamation mark.")); + m_guide_description_label->SetLabel(_L("Filament left in the feeder during drying may soften because the drying temperature exceeds the softening point of materials like PLA and TPU.") + + "\n" + "*" + _L("Unknown filaments will be treated as PLA.")); + m_guide_description_label->Wrap(FromDIP(300)); + } + + m_start_button->Enable(can_start); + m_guide_page->Layout(); + m_guide_page->Thaw(); +} + +int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj) +{ + const bool ams_changed = is_ams_changed(dev_ams); + bool rebuilt_filament_list = false; + + // Build `m_tray_ids` and the combobox items from the preset bundle. + // This list is also used as a lookup table in non-idle state (type -> name), + // so it must be available even when the combobox is disabled. + auto rebuild_filament_list = [&]() -> bool { + m_trays_combo->Clear(); + m_tray_ids.clear(); + + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle || !obj) { + return false; + } + + std::set filament_id_set; + auto & filaments = preset_bundle->filaments; + + // Get nozzle diameter using the same method as AMSMaterialsSetting::Popup + std::ostringstream stream; + int extruder_id = obj->GetFilaSystem()->GetExtruderIdByAmsId(m_ams_info.m_ams_id); + if (!obj->GetExtderSystem()->GetExtderById(extruder_id)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " get extruder id failed"; + extruder_id = 0; + } + stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(extruder_id); + std::string nozzle_diameter_str = stream.str(); + std::set printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle( + DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str); + + for (auto filament_it = filaments.begin(); filament_it != filaments.end(); ++filament_it) { + Preset& preset = *filament_it; + // Filter by system preset: root preset and (system preset or user preset is supported) + if (filaments.get_preset_base(*filament_it) != &preset || (!filament_it->is_system && !obj->is_support_user_preset)) { + continue; + } + + ConfigOption * printer_opt = filament_it->config.option("compatible_printers"); + ConfigOptionStrings *printer_strs = dynamic_cast(printer_opt); + if (!printer_strs) continue; + + for (auto printer_str : printer_strs->values) { + if (printer_names.find(printer_str) != printer_names.end()) { + if (filament_id_set.find(filament_it->filament_id) != filament_id_set.end()) { + continue; + } + + filament_id_set.insert(filament_it->filament_id); + auto filament_alias = filament_it->alias; + if (!filament_alias.empty()) { + auto opt_info = preset_bundle->get_filament_by_filament_id(filament_it->filament_id); + if (opt_info.has_value()) { + auto real_info = opt_info.value(); + real_info.filament_name = filament_alias; + m_tray_ids.push_back(std::move(real_info)); + m_trays_combo->Append(wxString::FromUTF8(filament_alias)); + } + } + } + } + } + + if (m_tray_ids.empty()) { + BOOST_LOG_TRIVIAL(warning) << "AMSDryCtrWin::update_filament_list: No valid filaments found"; + return false; + } + + return true; + }; + + if (ams_changed || m_tray_ids.empty() || m_trays_combo->GetCount() == 0) { + // Disable start button during rebuild + if (m_start_button) { + m_start_button->Disable(); + } + + rebuilt_filament_list = rebuild_filament_list(); + } + + if (!is_dry_ctr_idle(dev_ams)) { + const std::string& ams_id = dev_ams->GetAmsId(); + const auto settings_opt = dev_ams->GetDrySettings(); + + // Set the combobox display label by the filament "type" string. + // Priority: last saved selection for this AMS (if the type matches) -> fallback to current `m_tray_ids`. + auto set_filament_label_by_type = [&](const std::string& filament_type) { + if (filament_type.empty()) { + return; + } + + auto it_name = m_dry_setting.m_filament_names.find(ams_id); + auto it_type = m_dry_setting.m_filament_type.find(ams_id); + if (it_name != m_dry_setting.m_filament_names.end() && it_type != m_dry_setting.m_filament_type.end() + && it_type->second == filament_type) { + m_trays_combo->SetLabel(wxString::FromUTF8(it_name->second)); + return; + } + + const auto it = std::find_if(m_tray_ids.begin(), m_tray_ids.end(), [&](const FilamentBaseInfo& tray) { + return tray.filament_type == filament_type; + }); + if (it != m_tray_ids.end()) { + m_trays_combo->SetLabel(wxString::FromUTF8(it->filament_name)); + } + }; + + if (settings_opt.has_value()) { + const auto& settings = settings_opt.value(); + m_temperature_input->SetValue(std::to_string(settings.dry_temp)); + m_time_input->SetValue(std::to_string(settings.dry_hour)); + set_filament_label_by_type(settings.dry_filament); + } else { + auto it_name = m_dry_setting.m_filament_names.find(ams_id); + if (it_name != m_dry_setting.m_filament_names.end()) { + m_trays_combo->SetLabel(wxString::FromUTF8(it_name->second)); + m_temperature_input->SetValue(std::to_string(m_dry_setting.m_dry_temp[ams_id])); + m_time_input->SetValue(std::to_string(m_dry_setting.m_dry_time[ams_id])); + } + } + + return 0; + } + + // Idle state: avoid re-selecting / firing selection change unless the list was rebuilt. + if (!rebuilt_filament_list && !ams_changed && is_dry_ctr_idle()) { + return 0; + } + + if (m_tray_ids.empty()) { + return 0; + } + + // Select recommended drying temperature and default filament + float min_dry_temp = std::numeric_limits::max(); + std::string default_filament_id = "GFA00"; + bool has_ready = false; + const auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00"); + for (const auto& tray_pair : dev_ams->GetTrays()) { + if (!tray_pair.second || !tray_pair.second->is_tray_info_ready()) continue; + has_ready = true; + wxString filament_type = tray_pair.second->get_display_filament_type(); + Slic3r::DevFilamentDryingPreset preset; + if (filament_type.IsEmpty()) { + // no filament type, is PLA + if (!fallback_preset) continue; + preset = fallback_preset.value(); + } else { + auto preset_opt = tray_pair.second->get_ams_drying_preset(); + if (preset_opt.has_value()) { + preset = preset_opt.value(); + } else { + // no preset,is PLA + if (!fallback_preset) continue; + preset = fallback_preset.value(); + } + } + float cur_temp = m_printer_status.m_is_printing + ? std::min({preset.filament_dev_ams_drying_temperature_on_print[dev_ams->GetAmsType()], + preset.filament_dev_drying_softening_temperature, preset.filament_dev_ams_drying_heat_distortion_temperature}) + : preset.filament_dev_ams_drying_temperature_on_idle[dev_ams->GetAmsType()]; + if (cur_temp < min_dry_temp) { + min_dry_temp = cur_temp; + default_filament_id = preset.filament_id; + } + } + // if no tray is ready, use PLA as default + if (!has_ready) { + if (fallback_preset.has_value()) { + Slic3r::DevFilamentDryingPreset preset = fallback_preset.value(); + float cur_temp = m_printer_status.m_is_printing + ? std::min({preset.filament_dev_ams_drying_temperature_on_print[dev_ams->GetAmsType()], + preset.filament_dev_drying_softening_temperature, preset.filament_dev_ams_drying_heat_distortion_temperature}) + : preset.filament_dev_ams_drying_temperature_on_idle[dev_ams->GetAmsType()]; + min_dry_temp = cur_temp; + default_filament_id = preset.filament_id; + } + } + m_ams_info.m_recommand_dry_temp = min_dry_temp; + + // Set default selection + unsigned int default_index = 0; + for (unsigned int i = 0; i < m_tray_ids.size(); i++) { + if (m_tray_ids[i].filament_id == default_filament_id) { + default_index = i; + break; + } + } + m_trays_combo->SetSelection(default_index); + wxCommandEvent evt(wxEVT_COMBOBOX, m_trays_combo->GetId()); + evt.SetInt(default_index); + OnFilamentSelectionChanged(evt); + return 1; +} + +std::shared_ptr AMSDryCtrWin::get_fila_system() const +{ + std::shared_ptr fila_system = m_fila_system.lock(); + if (!fila_system) { + const_cast(this)->Close(); + } + return fila_system; +} + +void AMSDryCtrWin::update_printer_state(MachineObject* obj) +{ + m_printer_status.m_is_printing = obj->is_in_printing() + && obj->GetExtderSystem()->GetCurrentAmsId() == m_ams_info.m_ams_id; +} + +void AMSDryCtrWin::update(std::shared_ptr fila_system, MachineObject* obj) +{ + if (!fila_system || !obj) { + return; + } + + update_printer_state(obj); + m_fila_system = fila_system; + + DevAms* dev_ams = fila_system->GetAmsById(m_ams_info.m_ams_id); + if (!dev_ams) { + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::update: Invalid AMS id"; + m_ams_info.m_ams_id = ""; + Close(); + return; + } + + if (!dev_ams->IsSupportRemoteDry(fila_system->GetOwner())) { + BOOST_LOG_TRIVIAL(info) << "AMSDryCtrWin::update: Selected AMS does not support remote drying"; + Close(); + return; + } + + + update_ams_change(dev_ams); + + update_image(dev_ams->GetAmsType(), + dev_ams->GetDryStatus().has_value()? dev_ams->GetDryStatus().value(): DevAms::DryStatus::Off, + dev_ams->GetDrySubStatus().has_value()? dev_ams->GetDrySubStatus().value(): DevAms::DrySubStatus::Off, + dev_ams->GetHumidityPercent()); + + update_state(dev_ams); + update_dryness_status(dev_ams); + + update_filament_list(dev_ams, obj); + update_filament_guide_info(dev_ams); + + m_is_ams_changed = false; + + check_values_changed(dev_ams); + + Layout(); + Refresh(); +} + +bool AMSDryCtrWin::is_ams_changed(DevAms* dev_ams) +{ + return m_ams_info.m_ams_id != dev_ams->GetAmsId() || m_is_ams_changed; +} + +bool AMSDryCtrWin::is_dry_status_changed(DevAms* dev_ams) +{ + if (!dev_ams->GetDryStatus().has_value() || !dev_ams->GetDrySubStatus().has_value()) { + return true; + } + + return dev_ams->GetDryStatus().value() != m_ams_info.m_dry_status + || dev_ams->GetDrySubStatus().value() != m_ams_info.m_dry_sub_status; +} + +bool AMSDryCtrWin::is_dry_ctr_idle(DevAms* dev_ams) +{ + if (!dev_ams->GetDryStatus().has_value() || !dev_ams->GetDrySubStatus().has_value()) { + return true; + } + + return dev_ams->GetDryStatus().value() == DevAms::DryStatus::Off + || dev_ams->GetDryStatus().value() == DevAms::DryStatus::Cooling; +} + +bool AMSDryCtrWin::is_dry_ctr_idle() +{ + return m_ams_info.m_dry_status == DevAms::DryStatus::Off || m_ams_info.m_dry_status == DevAms::DryStatus::Cooling; +} + +bool AMSDryCtrWin::is_dry_ctr_err(DevAms* dev_ams) +{ + if (!dev_ams->GetDryStatus().has_value() || !dev_ams->GetDrySubStatus().has_value()) { + return false; + } + + return dev_ams->GetDryStatus().value() == DevAms::DryStatus::Error; +} + +} // GUI +} // Slic3r diff --git a/src/slic3r/GUI/AMSDryControl.hpp b/src/slic3r/GUI/AMSDryControl.hpp new file mode 100644 index 0000000000..43e177eeb1 --- /dev/null +++ b/src/slic3r/GUI/AMSDryControl.hpp @@ -0,0 +1,253 @@ +#pragma once +#include "GUI_ObjectLayers.hpp" +#include "slic3r/GUI/Widgets/AMSItem.hpp" +#include "slic3r/GUI/Widgets/Label.hpp" +#include "slic3r/GUI/Widgets/PopupWindow.hpp" + +#include "slic3r/GUI/wxExtensions.hpp" +#include "slic3r/GUI/DeviceCore/DevFilaSystem.h" +#include "slic3r/GUI/I18N.hpp" + +#include +#include + + +//Previous defintions +class wxGrid; + +namespace Slic3r { + +namespace GUI { + + +enum class DryCtrState { + IDLE, + DRY_WHEN_PRINT, + UNKNOWN +}; + +enum class DryCtrDev { + N3S, + N3F, + UNKNOWN +}; + +struct DryingPreset { + DryCtrState state; + DryCtrDev dev; + int dry_temp; + int dry_time; +}; + +class FilamentItemPanel : public wxPanel +{ +public: + FilamentItemPanel(wxWindow* parent, const wxString& text, const std::string& icon_name = "", + wxWindowID id = wxID_ANY); + + void SetText(const wxString& text); + void SetIcon(const std::string& icon_name); + void msw_rescale(); + +private: + void OnPaint(wxPaintEvent& event); + void OnSize(wxSizeEvent& event); + + Label* m_text_label; + wxStaticBitmap* m_icon_bitmap; + int m_target_size; + std::string m_icon_name; + ScalableBitmap m_icon; +}; + +class AMSFilamentPanel : public wxPanel +{ + wxBoxSizer* m_filament_sizer; + Label* m_ams_name_label; + int m_border_radius; + wxPanel* m_filament_container{nullptr}; + std::vector m_filament_items; + +public: + AMSFilamentPanel(wxWindow* parent, const wxString& ams_name, wxWindowID id = wxID_ANY); + + void AddFilamentItem(const wxString& text, const std::string& icon_name); + void AddFilamentItem(FilamentItemPanel* panel); + void SetAmsName(const wxString& ams_name); + void Clear(); + void msw_rescale(); +private: + void OnPaint(wxPaintEvent& event); +}; + + +class AMSDryCtrWin : public DPIDialog +{ +public: + AMSDryCtrWin(wxWindow *parent); + ~AMSDryCtrWin(); + + void msw_rescale(); + void update(std::shared_ptr fila_system, MachineObject* obj); + void set_ams_id(const std::string& ams_id); + +protected: + void on_dpi_changed(const wxRect &suggested_rect) override; + +private: + wxSimplebook* m_main_simplebook{nullptr}; + wxPanel* m_original_page{nullptr}; + + wxWindow* m_amswin{nullptr}; + wxBoxSizer* m_sizer_ams_items{nullptr}; + wxScrolledWindow* m_panel_prv_left {nullptr}; + wxScrolledWindow* m_panel_prv_right{nullptr}; + wxBoxSizer* m_sizer_prv_left{nullptr}; + wxBoxSizer* m_sizer_prv_right{nullptr}; + + // left panel related members + ScalableBitmap m_humidity_image; + wxStaticBitmap* m_humidity_img{nullptr}; + Label* m_image_description{nullptr}; + wxStaticBitmap* m_image_description_icon{nullptr}; + ScalableBitmap m_description_icon_bitmap; + + Label* m_humidity_data_label = nullptr; + Label* m_temperature_data_label = nullptr; + Label* m_time_data_label = nullptr; + wxBoxSizer* m_time_descrition_container = nullptr; + + // right panel related members + wxBoxSizer* m_normal_state_sizer{nullptr}; + wxBoxSizer* m_cannot_dry_sizer{nullptr}; + wxBoxSizer* m_dry_error_sizer{nullptr}; + Label* m_cannot_dry_description_label = nullptr; + + // right panel normal state + ComboBox* m_trays_combo; + std::vector m_tray_ids; + wxTextCtrl* m_temperature_input; + wxTextCtrl* m_time_input; + Label* m_normal_description; + Button* m_start_button{nullptr}; + Button* m_next_button{nullptr}; + Button* m_stop_button{nullptr}; + Button* m_back_button{nullptr}; + Button* m_unload_button{nullptr}; + + // guide page description + Label* m_guide_title_label{nullptr}; + Label* m_guide_description_label{nullptr}; + + wxCheckBox* m_rotate_spool_toggle{nullptr}; + + wxPanel* m_progress_page; + ProgressBar* m_progress_gauge; + wxTimer* m_progress_timer; + + std::optional m_stop_button_restore_deadline; + std::optional m_unload_button_restore_deadline; + + Label* m_progress_title; + int m_progress_value; + int m_progress_message_index; + std::vector m_progress_text = { + _L("Starting: Checking adapter connection"), + _L("Starting: Checking filament status"), + _L("Starting: Checking drying presets"), + _L("Starting: Checking filament location"), + _L("Starting: Checking air intake"), + _L("Starting: Checking air vent") + }; + + // Guide page + wxPanel* m_guide_page{nullptr}; + AMSFilamentPanel* m_ams_filament_panel{nullptr}; + std::map m_filament_items; + wxStaticBitmap* m_image_placeholder{nullptr}; + ScalableBitmap m_guide_image; + + + bool m_is_ams_changed = false; + std::weak_ptr m_fila_system; + struct { + std::string m_ams_id; + DevAmsType m_model = DevAmsType::EXT_SPOOL; + DevAms::DryStatus m_dry_status = DevAms::DryStatus::Off; + DevAms::DrySubStatus m_dry_sub_status = DevAms::DrySubStatus::Off; + int m_humidity_percent; + int m_temperature; + int m_left_dry_time; + float m_recommand_dry_temp; + } m_ams_info; + + struct { + std::unordered_map m_filament_names; + std::unordered_map m_filament_type; + std::unordered_map m_dry_temp; + std::unordered_map m_dry_time; + } m_dry_setting; + + struct { + bool m_is_printing = false; + } m_printer_status; + +private: + void create(); + wxBoxSizer* create_guide_page_sizer(wxPanel* parent); + wxBoxSizer* create_main_content_section(wxPanel* parent); + wxBoxSizer* create_guide_info_filament(wxPanel* parent); + wxBoxSizer* create_guide_info_section(wxPanel* parent); + wxBoxSizer* create_guide_right_section(wxPanel* parent); + wxBoxSizer* create_main_page_sizer(wxPanel* parent); + wxBoxSizer* create_left_panel(wxPanel* parent); + wxBoxSizer* create_humidity_status_section(wxPanel* parent); + wxBoxSizer* create_description_item(wxPanel* parent, const wxString& title, Label*& dataLabel); + wxBoxSizer* create_status_descriptions_section(wxPanel* parent); + + wxBoxSizer* create_right_panel(wxPanel* parent); + wxBoxSizer* create_normal_state_panel(wxPanel* parent); + wxBoxSizer* create_cannot_dry_panel(wxPanel* parent); + wxBoxSizer* create_drying_error_panel(wxPanel* parent); + Button* create_button(wxPanel* parent, const wxString& title, + const wxColour& background_color, const wxColour& border_color, const wxColour& text_color); + + wxBoxSizer* create_progress_page_sizer(wxPanel* parent); + void OnProgressTimer(wxTimerEvent& event); + void OnClose(wxCloseEvent& event); + void OnShow(wxShowEvent& event); + + void OnFilamentSelectionChanged(wxCommandEvent& event); + + + wxScrolledWindow* create_preview_scrolled_window(wxWindow* parent); + + bool check_values_changed(DevAms* dev_ams); + int update_image(DevAmsType type, DevAms::DryStatus status, DevAms::DrySubStatus sub_status, int humidity_percent); + void update_img_description(DevAms::DryStatus status, DevAms::DrySubStatus sub_status); + void update_normal_description(DevAms* dev_ams); + int update_state(DevAms* dev_ams); + int update_dryness_status(DevAms* dev_ams); + int update_ams_change(DevAms* dev_ams); + int update_filament_list(DevAms* dev_ams, MachineObject* obj); + void update_filament_guide_info(DevAms* dev_ams); + void update_normal_state(DevAms* dev_ams); + void update_printer_state(MachineObject* obj); + + std::shared_ptr get_fila_system() const; + void start_sending_drying_command(); + void restore_stop_button_if_deadline_passed(); + void restore_unload_button_if_deadline_passed(); + void update_button_size(Button* button); + + bool is_dry_status_changed(DevAms* dev_ams); + bool is_dry_ctr_idle(DevAms* dev_ams); + bool is_ams_changed(DevAms* dev_ams); + bool is_dry_ctr_idle(); + bool is_tray_changed(DevAms* dev_ams); + bool is_dry_ctr_err(DevAms* dev_ams); + +}; + +} // GUI +} // Slic3r From 08b08e09fc55ee3c4a8921707d88827691368d1e Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 14:23:05 +0800 Subject: [PATCH 14/30] fix: use OrcaSlicerTitle.ico instead of BambuStudioTitle.ico --- src/slic3r/GUI/AMSDryControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp index f88e46f65c..f8ced3ea39 100644 --- a/src/slic3r/GUI/AMSDryControl.cpp +++ b/src/slic3r/GUI/AMSDryControl.cpp @@ -977,7 +977,7 @@ void AMSDryCtrWin::create() // set title icon SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); this->SetDoubleBuffered(true); - std::string icon_path = (boost::format("%1%/images/BambuStudioTitle.ico") % resources_dir()).str(); + std::string icon_path = (boost::format("%1%/images/OrcaSlicerTitle.ico") % resources_dir()).str(); SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO)); SetSize(wxSize(FromDIP(700), FromDIP(500))); From b2827cb9fa21dff9904525d53ce4ed7926c1b5d1 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 14:36:52 +0800 Subject: [PATCH 15/30] feat: wire AMS humidity click to open AMSDryControl dialog For N3F and N3S AMS types, clicking the humidity indicator now opens the full drying control dialog instead of the popup. --- src/slic3r/GUI/Widgets/AMSControl.cpp | 36 +++++++++++++++++++++++++++ src/slic3r/GUI/Widgets/AMSControl.hpp | 2 ++ 2 files changed, 38 insertions(+) diff --git a/src/slic3r/GUI/Widgets/AMSControl.cpp b/src/slic3r/GUI/Widgets/AMSControl.cpp index a3913dc4e4..ea62d18d92 100644 --- a/src/slic3r/GUI/Widgets/AMSControl.cpp +++ b/src/slic3r/GUI/Widgets/AMSControl.cpp @@ -6,6 +6,7 @@ #include "slic3r/GUI/MsgDialog.hpp" #include "slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h" +#include "slic3r/GUI/AMSDryControl.hpp" #include "slic3r/GUI/DeviceCore/DevManager.h" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" @@ -254,6 +255,24 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons if (humidity_value > 0 && humidity_value <= 5) { m_Humidity_tip_popup.set_humidity_level(humidity_value); } m_Humidity_tip_popup.Popup(); } + else if (info->ams_type == AMSModel::N3F_AMS || info->ams_type == AMSModel::N3S_AMS) + { + // Open full drying control dialog for N3F/N3S AMS + if (!m_dry_ctr_win) { + m_dry_ctr_win = new AMSDryCtrWin(this); + } + m_dry_ctr_win->set_ams_id(info->ams_id); + + Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (dev) { + MachineObject *obj = dev->get_selected_machine(); + if (obj && obj->GetFilaSystem()) { + std::shared_ptr fila_ptr(obj->GetFilaSystem(), [](DevFilaSystem*){}); + m_dry_ctr_win->update(fila_ptr, obj); + } + } + m_dry_ctr_win->ShowModal(); + } else { m_percent_humidity_dry_popup->Update(info); @@ -493,6 +512,10 @@ void AMSControl::msw_rescale() m_percent_humidity_dry_popup->msw_rescale(); } + if (m_dry_ctr_win){ + m_dry_ctr_win->msw_rescale(); + } + m_Humidity_tip_popup.msw_rescale(); Layout(); @@ -970,6 +993,19 @@ void AMSControl::UpdateAms(const std::string &series_name, } } + /*update AMS dry control dialog*/ + if (m_dry_ctr_win && m_dry_ctr_win->IsShown()) + { + Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + if (dev) { + MachineObject *obj = dev->get_selected_machine(); + if (obj && obj->GetFilaSystem()) { + std::shared_ptr fila_ptr(obj->GetFilaSystem(), [](DevFilaSystem*){}); + m_dry_ctr_win->update(fila_ptr, obj); + } + } + } + /*update ams extruder*/ if (m_extruder->updateNozzleNum(m_total_ext_count, series_name)) { diff --git a/src/slic3r/GUI/Widgets/AMSControl.hpp b/src/slic3r/GUI/Widgets/AMSControl.hpp index 31c6b86feb..93d9acee67 100644 --- a/src/slic3r/GUI/Widgets/AMSControl.hpp +++ b/src/slic3r/GUI/Widgets/AMSControl.hpp @@ -21,6 +21,7 @@ namespace Slic3r { namespace GUI { //Previous definitions class uiAmsPercentHumidityDryPopup; +class AMSDryCtrWin; class AMSControl : public wxSimplebook { @@ -118,6 +119,7 @@ protected: AmsHumidityTipPopup m_Humidity_tip_popup; uiAmsPercentHumidityDryPopup* m_percent_humidity_dry_popup; + AMSDryCtrWin* m_dry_ctr_win{nullptr}; std::string m_last_ams_id = ""; std::string m_last_tray_id = ""; From 044a113d54946c55e39b6d67c940e61402733948 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 14:47:33 +0800 Subject: [PATCH 16/30] feat: update error dialog stop-drying to use CtrlAmsStopDrying Keeps command_ams_drying_stop() fallback for backward compatibility. --- src/slic3r/GUI/DeviceErrorDialog.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/slic3r/GUI/DeviceErrorDialog.cpp b/src/slic3r/GUI/DeviceErrorDialog.cpp index 353250361f..41d54e0ecd 100644 --- a/src/slic3r/GUI/DeviceErrorDialog.cpp +++ b/src/slic3r/GUI/DeviceErrorDialog.cpp @@ -5,6 +5,7 @@ #include "GUI_App.hpp" #include "MainFrame.hpp" #include "ReleaseNote.hpp" +#include "DeviceCore/DevFilaSystem.h" namespace Slic3r { namespace GUI @@ -449,6 +450,11 @@ void DeviceErrorDialog::on_button_click(ActionButton btn_id) break; } case DeviceErrorDialog::STOP_DRYING: { + // Use the canonical CtrlAmsStopDrying path + if (m_obj && m_obj->GetFilaSystem()) { + m_obj->GetFilaSystem()->CtrlAmsStopDrying(0); + } + // Fallback to the old command for backward compatibility m_obj->command_ams_drying_stop(); break; } From 2524669139a1324e7e51567c81ff673b2a09c121 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 14:47:52 +0800 Subject: [PATCH 17/30] Revert "feat: update error dialog stop-drying to use CtrlAmsStopDrying" This reverts commit 044a113d54946c55e39b6d67c940e61402733948. --- src/slic3r/GUI/DeviceErrorDialog.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/slic3r/GUI/DeviceErrorDialog.cpp b/src/slic3r/GUI/DeviceErrorDialog.cpp index 41d54e0ecd..353250361f 100644 --- a/src/slic3r/GUI/DeviceErrorDialog.cpp +++ b/src/slic3r/GUI/DeviceErrorDialog.cpp @@ -5,7 +5,6 @@ #include "GUI_App.hpp" #include "MainFrame.hpp" #include "ReleaseNote.hpp" -#include "DeviceCore/DevFilaSystem.h" namespace Slic3r { namespace GUI @@ -450,11 +449,6 @@ void DeviceErrorDialog::on_button_click(ActionButton btn_id) break; } case DeviceErrorDialog::STOP_DRYING: { - // Use the canonical CtrlAmsStopDrying path - if (m_obj && m_obj->GetFilaSystem()) { - m_obj->GetFilaSystem()->CtrlAmsStopDrying(0); - } - // Fallback to the old command for backward compatibility m_obj->command_ams_drying_stop(); break; } From 668654da5f57c9876c6f82ec5a74206121b7f527 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 14:58:51 +0800 Subject: [PATCH 18/30] fix: use dark humidity icons in dark mode --- src/slic3r/GUI/AMSDryControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp index f8ced3ea39..7e727abbdf 100644 --- a/src/slic3r/GUI/AMSDryControl.cpp +++ b/src/slic3r/GUI/AMSDryControl.cpp @@ -42,7 +42,7 @@ static std::string get_humidity_level_img_path(int humidity_percent) } if (wxGetApp().dark_mode()) { - return "hum_level" + std::to_string(hum_level) + "_no_num_light"; + return "hum_level" + std::to_string(hum_level) + "_no_num_dark"; } else { return "hum_level" + std::to_string(hum_level) + "_no_num_light"; } From 23799e02100f69f37fbc97ef78ffd7d0c4bd74e6 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 15:20:26 +0800 Subject: [PATCH 19/30] chore: remove superpowers planning docs --- .../2026-07-07-ams-drying-control-plan.md | 1046 ----------------- .../2026-07-07-ams-drying-control-design.md | 325 ----- 2 files changed, 1371 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-07-ams-drying-control-plan.md delete mode 100644 docs/superpowers/specs/2026-07-07-ams-drying-control-design.md diff --git a/docs/superpowers/plans/2026-07-07-ams-drying-control-plan.md b/docs/superpowers/plans/2026-07-07-ams-drying-control-plan.md deleted file mode 100644 index 6cd673c5a6..0000000000 --- a/docs/superpowers/plans/2026-07-07-ams-drying-control-plan.md +++ /dev/null @@ -1,1046 +0,0 @@ -# AMS Drying Control — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Port the AMS filament drying control feature from BambuStudio into OrcaSlicer, enabling users to start, monitor, and stop filament drying via the AMS unit from the slicer UI. - -**Architecture:** Add drying status enums/structs to the `DevAms` data model, extend the JSON parser to populate them, add `CtrlAmsStartDryingHour`/`CtrlAmsStopDrying` commands to `DevFilaSystem`, create a `DevUtilBackend` utility for filament drying preset lookup, and build the `AMSDryCtrWin` dialog. Wire the dialog to open when clicking the AMS humidity indicator for N3F/N3S AMS types. - -**Tech Stack:** C++17, wxWidgets, nlohmann/json, Boost - -## Global Constraints - -- **N3F/N3S only:** Standard AMS and AMS Lite do not have heating hardware; the dialog is not shown for them -- **Backward compatibility:** Existing `command_ams_drying_stop()` is preserved; new commands are additive -- **Cross-platform:** wxWidgets UI must work on Windows, macOS, Linux -- **Dark mode:** Dialog must support dark mode via `wxGetApp().dark_mode()` and `StateColor::darkModeColorFor()` -- **Profile compatibility:** Existing profiles with `filament_dev_ams_drying_*` keys (Qidi) continue to work; the feature reads the same keys -- **No printer-agent changes needed:** All commands go through existing `publish_json()` → `NetworkAgent` path -- **Reference:** `D:\projects\BambuStudio` — match its behavior as much as possible; when in doubt, check the BambuStudio source - ---- - -### Task 1: Global `DevAmsType` enum in `DevDefs.h` - -**Files:** -- Modify: `src/slic3r/GUI/DeviceCore/DevDefs.h` -- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystem.h` (replace local `AmsType` enum with typedef) -- Modify: `src/slic3r/GUI/DeviceCore/DevMapping.cpp` (rename `DevAms::DUMMY` → `DevAms::EXT_SPOOL`) - -**Interfaces:** -- Produces: Global `enum DevAmsType { EXT_SPOOL=0, AMS=1, AMS_LITE=2, N3F=3, N3S=4 }` in `DevDefs.h` -- Produces: `using AmsType = DevAmsType;` inside `DevAms` class (replaces old local enum) -- Note: Old `DevAms::DUMMY` becomes `DevAms::EXT_SPOOL`; all existing references update accordingly - -- [ ] **Step 1: Add global `DevAmsType` enum to `DevDefs.h`** - -In `src/slic3r/GUI/DeviceCore/DevDefs.h`, add before the closing `namespace Slic3r` or after existing enums: - -```cpp -enum DevAmsType : int -{ - EXT_SPOOL = 0, // EXT - AMS = 1, // AMS1 - AMS_LITE = 2, // AMS-Lite - N3F = 3, // N3F, AMS 2PRO - N3S = 4, // N3S, AMS HT -}; -``` - -- [ ] **Step 2: Replace `DevAms::AmsType` local enum with typedef** - -In `src/slic3r/GUI/DeviceCore/DevFilaSystem.h`, find the `DevAms` class and replace the local enum: - -```cpp -// Before (delete this): - enum AmsType : int - { - DUMMY = 0, - AMS = 1, // AMS - AMS_LITE = 2, // AMS-Lite - N3F = 3, // N3F - N3S = 4, // N3S - }; - -// After (replace with): - using AmsType = DevAmsType; -``` - -- [ ] **Step 3: Rename `DevAms::DUMMY` → `DevAms::EXT_SPOOL` in `DevMapping.cpp`** - -In `src/slic3r/GUI/DeviceCore/DevMapping.cpp` line 189: - -```cpp -// Before: -_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::DUMMY, tray, info); -// After: -_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info); -``` - -- [ ] **Step 4: Rename `DUMMY` → `EXT_SPOOL` in `DevFilaSystem.cpp`** - -In `src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp` line 114: - -```cpp -// Before: -assert(DUMMY < type && m_ams_type <= N3S); -// After: -assert(EXT_SPOOL < type && m_ams_type <= N3S); -``` - -- [ ] **Step 5: Verify the build compiles** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -Expected: build succeeds, no `DUMMY` or `AmsType` related compile errors. - -- [ ] **Step 6: Commit** - -```bash -git add src/slic3r/GUI/DeviceCore/DevDefs.h src/slic3r/GUI/DeviceCore/DevFilaSystem.h src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp src/slic3r/GUI/DeviceCore/DevMapping.cpp -git commit -m "refactor: promote DevAmsType to global enum, rename DUMMY to EXT_SPOOL - -Matches BambuStudio's DevAmsType naming convention." -``` - ---- - -### Task 2: Drying enums and structs in `DevAms` - -**Files:** -- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystem.h` - -**Interfaces:** -- Produces: `DevAms::DryCtrlMode`, `DevAms::DryStatus`, `DevAms::DrySubStatus`, `DevAms::DryFanStatus`, `DevAms::CannotDryReason` enums -- Produces: `DevAms::DrySettings` struct -- Produces: Getters: `GetDryStatus()`, `GetDrySubStatus()`, `GetFan1Status()`, `GetFan2Status()`, `GetCannotDryReason()`, `GetDrySettings()` -- Produces: `IsSupportRemoteDry(const MachineObject*)`, `AmsIsDrying()` -- Produces: `SupportDrying()` — already exists as `m_ams_type > AMS_LITE`; update to check N3F/N3S -- Produces: Members: `m_dry_status`, `m_dry_sub_status`, `m_dry_fan1_status`, `m_dry_fan2_status`, `m_dry_cannot_reasons`, `m_dry_settings` - -**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevFilaSystem.h` lines 119–262 - -- [ ] **Step 1: Add enums inside `DevAms` class** - -In `src/slic3r/GUI/DeviceCore/DevFilaSystem.h`, inside the `DevAms` class (after the `using AmsType = DevAmsType;` line, before the `public:` constructor section), add: - -```cpp -public: - - enum class DryCtrlMode : int - { - Off = 0, - OnTime = 1, - OnHumidity = 2, - }; - - enum class DryStatus : char - { - Off = 0, - Checking = 1, - Drying = 2, - Cooling = 3, - Stopping = 4, - Error = 5, - CannotStopHeatOutofControl = 6, - PrdTesting = 7, - }; - - enum class DrySubStatus - { - Off = 0, - Heating = 1, - Dehumidify = 2, - }; - - enum class DryFanStatus : char - { - Off = 0, - On = 1, - }; - - enum class CannotDryReason : int - { - TaskOccupied = 0, - InsufficientPower = 1, - AmsBusy = 2, - ConsumableAtAmsOutlet = 3, - InitiatingAmsDrying = 4, - NotSupportedIn2dMode = 5, - DryingInProgress = 6, - Upgrading = 7, - InsufficientPowerNeedPluginPower = 8, - FilamentAtAmsOutletManualUnload = 10, - }; - - struct DrySettings - { - std::string dry_filament; - int dry_temp = -1; // -1 means invalid - int dry_hour = -1; // -1 means invalid, hours - }; -``` - -- [ ] **Step 2: Add new getters to `DevAms`** - -After the existing `GetLeftDryTime()` getter, add: - -```cpp - // remote drying control - bool IsSupportRemoteDry(const MachineObject* obj) const; - std::optional GetDryStatus() const { return m_dry_status; }; - std::optional GetDrySubStatus() const { return m_dry_sub_status; } - std::optional GetFan1Status() const { return m_dry_fan1_status; } - std::optional GetFan2Status() const { return m_dry_fan2_status; } - std::optional> GetCannotDryReason() const { return m_dry_cannot_reasons; } - std::optional GetDrySettings() const { return m_dry_settings; }; - - bool AmsIsDrying(); -``` - -- [ ] **Step 3: Add new private members to `DevAms`** - -In the private section of `DevAms`, after `m_left_dry_time`, add: - -```cpp - // see is_support_remote_dry - std::optional m_dry_status; - std::optional m_dry_sub_status; - std::optional m_dry_fan1_status; - std::optional m_dry_fan2_status; - std::optional> m_dry_cannot_reasons; - std::optional m_dry_settings; -``` - -Note: In BambuStudio these members are `public:` (prefixed with `public:` before `m_dry_status`). Keep them private and use getters. The friends (`DevFilaSystemParser`) can access them. - -Also remove the duplicate `GetAmsType()` which currently returns `m_ams_type` — since `m_ams_type` is now `DevAmsType`, the getter still works. - -- [ ] **Step 4: Add forward declaration for `DevFilamentDryingPreset`** - -At the top of the file, after the existing forward declarations: - -```cpp -struct DevFilamentDryingPreset; -``` - -- [ ] **Step 5: Verify the header compiles** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -Expected: build succeeds. If there are compilation errors related to `GetAmsType()` return type, check that `AmsType` typedef resolves correctly. - -- [ ] **Step 6: Commit** - -```bash -git add src/slic3r/GUI/DeviceCore/DevFilaSystem.h -git commit -m "feat: add drying enums, structs, getters to DevAms - -Adds DryCtrlMode, DryStatus, DrySubStatus, DryFanStatus, -CannotDryReason enums and DrySettings struct for AMS drying control." -``` - ---- - -### Task 3: `DevFilamentDryingPreset` struct and `DevFilaSystem` ctrl declarations - -**Files:** -- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystem.h` - -**Interfaces:** -- Produces: `struct DevFilamentDryingPreset` at namespace scope -- Produces: `DevFilaSystem::CtrlAmsStartDryingHour(int ams_id, std::string filament_type, int tag_temp, int tag_duration_hour, bool rotate_tray, int cooling_temp, bool close_power_conflict = false) const` -- Produces: `DevFilaSystem::CtrlAmsStopDrying(int ams_id) const` -- Produces: `DevAmsTray::get_ams_drying_preset()` method - -**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevFilaSystem.h` lines 350–362 - -- [ ] **Step 1: Add `DevFilamentDryingPreset` struct at namespace scope** - -In `src/slic3r/GUI/DeviceCore/DevFilaSystem.h`, add after the `DevFilaSystemParser` class (at the very end of the file, before `}// namespace Slic3r`): - -```cpp -struct DevFilamentDryingPreset -{ - std::string filament_id; - - std::unordered_set ams_limitations; // only use ams types in the set - std::unordered_map filament_dev_ams_drying_time_on_idle; // hour - std::unordered_map filament_dev_ams_drying_temperature_on_idle; - std::unordered_map filament_dev_ams_drying_time_on_print; // hour - std::unordered_map filament_dev_ams_drying_temperature_on_print; - float filament_dev_drying_cooling_temperature = 0.0f; - float filament_dev_drying_softening_temperature = 0.0f; - float filament_dev_ams_drying_heat_distortion_temperature = 0.0f; -}; -``` - -- [ ] **Step 2: Add control method declarations to `DevFilaSystem`** - -In the `DevFilaSystem` class, after the existing `CtrlAmsReset()` declaration: - -```cpp - // crtl - int CtrlAmsStartDryingHour(int ams_id, std::string filament_type, int tag_temp, int tag_duration_hour, bool rotate_tray, int cooling_temp, bool close_power_conflict = false) const; - int CtrlAmsStopDrying(int ams_id) const; -``` - -- [ ] **Step 3: Add `get_ams_drying_preset()` to `DevAmsTray`** - -In the `DevAmsTray` class, add after the `get_filament_type()` method: - -```cpp - std::optional get_ams_drying_preset() const; -``` - -Also add `#include ` and `#include ` to the includes if not already present. -Add `#include ` if not already present. - -- [ ] **Step 4: Verify build** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -- [ ] **Step 5: Commit** - -```bash -git add src/slic3r/GUI/DeviceCore/DevFilaSystem.h -git commit -m "feat: add DevFilamentDryingPreset, ctrl method declarations, tray preset getter" -``` - ---- - -### Task 4: `DevUtilBackend` utility class - -**Files:** -- Create: `src/slic3r/GUI/DeviceCore/DevUtilBackend.h` -- Create: `src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp` -- Modify: `src/slic3r/GUI/DeviceCore/CMakeLists.txt` - -**Interfaces:** -- Produces: `DevUtilBackend::GetFilamentDryingPreset(const std::string& fila_id)` → `std::optional` - -**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevUtilBackend.h`, `DevUtilBackend.cpp` - -- [ ] **Step 1: Create `DevUtilBackend.h`** - -Create `src/slic3r/GUI/DeviceCore/DevUtilBackend.h`: - -```cpp -/** - * @file DevUtilBackend.h - * @brief Provides common static utility methods for backend (preset/slicing). - */ - -#pragma once -#include "DevDefs.h" -#include "DevFilaSystem.h" - -#include -#include - -namespace Slic3r -{ - -class DevUtilBackend -{ -public: - DevUtilBackend() = delete; - -public: - // for filament preset - static std::optional GetFilamentDryingPreset(const std::string& fila_id); -}; - -}; // namespace Slic3r -``` - -- [ ] **Step 2: Create `DevUtilBackend.cpp`** - -Create `src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp`: - -```cpp -#include "DevUtilBackend.h" - -#include "slic3r/GUI/GUI_App.hpp" - -#include "libslic3r/Preset.hpp" - -#include - -namespace Slic3r -{ - -static std::unordered_map s_ams_type_map = { - {"0", DevAmsType::N3F}, - {"1", DevAmsType::N3S}, -}; - -std::optional DevUtilBackend::GetFilamentDryingPreset(const std::string& fila_id) -{ - if (fila_id.empty() || !GUI::wxGetApp().preset_bundle) { - return std::nullopt; - } - - for (auto iter = GUI::wxGetApp().preset_bundle->filaments.begin(); iter != GUI::wxGetApp().preset_bundle->filaments.end(); ++iter) { - const Preset& filament_preset = *iter; - const auto& config = filament_preset.config; - if (filament_preset.filament_id == fila_id) { - DevFilamentDryingPreset info; - info.filament_id = fila_id; - try { - if (config.has("filament_dev_ams_drying_ams_limitations")) { - std::vector types = config.option("filament_dev_ams_drying_ams_limitations")->values; - for (auto type : types) { - if (s_ams_type_map.count(type) == 0) { - continue; - } - info.ams_limitations.insert(s_ams_type_map[type]); - } - } - - if (config.has("filament_dev_ams_drying_temperature")) { - info.filament_dev_ams_drying_temperature_on_idle[DevAmsType::N3F] = config.option("filament_dev_ams_drying_temperature")->get_at(0); - info.filament_dev_ams_drying_temperature_on_idle[DevAmsType::N3S] = config.option("filament_dev_ams_drying_temperature")->get_at(1); - info.filament_dev_ams_drying_temperature_on_print[DevAmsType::N3F] = config.option("filament_dev_ams_drying_temperature")->get_at(2); - info.filament_dev_ams_drying_temperature_on_print[DevAmsType::N3S] = config.option("filament_dev_ams_drying_temperature")->get_at(3); - } - - if (config.has("filament_dev_ams_drying_time")) { - info.filament_dev_ams_drying_time_on_idle[DevAmsType::N3F] = config.option("filament_dev_ams_drying_time")->get_at(0); - info.filament_dev_ams_drying_time_on_idle[DevAmsType::N3S] = config.option("filament_dev_ams_drying_time")->get_at(1); - info.filament_dev_ams_drying_time_on_print[DevAmsType::N3F] = config.option("filament_dev_ams_drying_time")->get_at(2); - info.filament_dev_ams_drying_time_on_print[DevAmsType::N3S] = config.option("filament_dev_ams_drying_time")->get_at(3); - } - - if (config.has("filament_dev_drying_softening_temperature")) { - info.filament_dev_drying_softening_temperature = config.option("filament_dev_drying_softening_temperature")->get_at(0); - } - - if (config.has("filament_dev_ams_drying_heat_distortion_temperature")) { - info.filament_dev_ams_drying_heat_distortion_temperature = config.option("filament_dev_ams_drying_heat_distortion_temperature")->get_at(0); - } - - if (config.has("filament_dev_drying_cooling_temperature")) { - info.filament_dev_drying_cooling_temperature = config.option("filament_dev_drying_cooling_temperature")->get_at(0); - } - - return info; - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " exception: " << e.what(); - } - } - } - - return std::nullopt; -} - -}; // namespace Slic3r -``` - -- [ ] **Step 3: Register new files in CMakeLists.txt** - -In `src/slic3r/GUI/DeviceCore/CMakeLists.txt`, add the new files. After the existing `DevFilaSystemCtrl.cpp` line: - -```cmake - GUI/DeviceCore/DevUtilBackend.h - GUI/DeviceCore/DevUtilBackend.cpp -``` - -- [ ] **Step 4: Verify build** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -Expected: build succeeds. If `ConfigOptionStrings` or `ConfigOptionFloats` are not found, check the includes — they're in `libslic3r/Preset.hpp` and `libslic3r/PrintConfig.hpp`. - -- [ ] **Step 5: Commit** - -```bash -git add src/slic3r/GUI/DeviceCore/DevUtilBackend.h src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp src/slic3r/GUI/DeviceCore/CMakeLists.txt -git commit -m "feat: add DevUtilBackend with GetFilamentDryingPreset - -Reads filament_dev_ams_drying_* config keys from filament presets -and returns structured DevFilamentDryingPreset data." -``` - ---- - -### Task 5: JSON parsing for drying status fields - -**Files:** -- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp` - -**Interfaces:** -- Consumes: `DevAms::DryStatus`, `DevAms::DrySubStatus`, `DevAms::DryFanStatus`, `DevAms::CannotDryReason`, `DevAms::DrySettings` -- Parses from JSON: `dry_time`, info flag bits (dry_status/fan1/fan2/sub_status), `dry_setting`, `dry_sf_reason` - -**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevFilaSystem.cpp` lines 690–712 - -- [ ] **Step 1: Locate the AMS parsing section in `DevFilaSystemParser::ParseV1_0()`** - -First, read `DevFilaSystem.cpp` to find where `m_left_dry_time` is parsed (look for "dry_time") and where the AMS info section is. The parsing function is `DevFilaSystemParser::ParseV1_0`. - -- [ ] **Step 2: Add drying status field parsing** - -After the existing `m_left_dry_time` parsing line (which parses `"dry_time"` from JSON), add the drying status parsing. The exact location will be in the AMS loop where `curr_ams` is populated. - -After `DevJsonValParser::ParseVal(j_ams, "dry_time", curr_ams->m_left_dry_time);` (or the equivalent line), add: - -```cpp - // Drying status — only parse if printer supports remote drying - if (obj->is_support_remote_dry) { - if (j_ams.contains("info")) { - const std::string& info = j_ams["info"].get(); - curr_ams->m_dry_status = (DevAms::DryStatus)DevUtil::get_flag_bits(info, 4, 4); - curr_ams->m_dry_fan1_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 18, 2); - curr_ams->m_dry_fan2_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 20, 2); - curr_ams->m_dry_sub_status = (DevAms::DrySubStatus)DevUtil::get_flag_bits(info, 22, 2); - } - - if (j_ams.contains("dry_setting")) { - const auto& j_dry_settings = j_ams["dry_setting"]; - DevAms::DrySettings dry_settings; - DevJsonValParser::ParseVal(j_dry_settings, "dry_filament", dry_settings.dry_filament); - DevJsonValParser::ParseVal(j_dry_settings, "dry_temperature", dry_settings.dry_temp); - DevJsonValParser::ParseVal(j_dry_settings, "dry_duration", dry_settings.dry_hour); - curr_ams->m_dry_settings = dry_settings; - } - - if (j_ams.contains("dry_sf_reason")) { - curr_ams->m_dry_cannot_reasons = DevJsonValParser::GetVal>(j_ams, "dry_sf_reason"); - } - } -``` - -Note: Check if `DevUtil::get_flag_bits` and `DevJsonValParser` exist in OrcaSlicer. If not, these utilities must be added. Check BambuStudio's `DevUtil.h` for `get_flag_bits` and `DevJsonValParser` for the parsing helper. If these are missing, add minimal versions or inline the parsing. - -Check specifically whether `DevUtil.h` in OrcaSlicer has `get_flag_bits`. The `DevUtil.h` exists in the CMakeLists. Check its contents — it should have a static `get_flag_bits(std::string, int, int)` method. If not present, port it from BambuStudio. - -- [ ] **Step 3: Implement `IsSupportRemoteDry()` and `AmsIsDrying()`** - -In the same file, add the implementations after the existing `DevAms` methods: - -```cpp -bool DevAms::IsSupportRemoteDry(const MachineObject* obj) const -{ - if (obj && obj->is_support_remote_dry) { - return SupportDrying(); - } - return false; -} - -bool DevAms::AmsIsDrying() -{ - if (!GetDryStatus().has_value()) { - return false; - } - - return GetDryStatus().value() == DevAms::DryStatus::Checking - || GetDryStatus().value() == DevAms::DryStatus::Drying - || GetDryStatus().value() == DevAms::DryStatus::Error - || GetDryStatus().value() == DevAms::DryStatus::CannotStopHeatOutofControl; -} -``` - -- [ ] **Step 4: Implement `DevAmsTray::get_ams_drying_preset()`** - -```cpp -std::optional DevAmsTray::get_ams_drying_preset() const -{ - return DevUtilBackend::GetFilamentDryingPreset(setting_id); -} -``` - -- [ ] **Step 5: Ensure `#include "DevUtilBackend.h"` is present in `DevFilaSystem.cpp`** - -Add at the top if not present. - -- [ ] **Step 6: Verify build** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -- [ ] **Step 7: Commit** - -```bash -git add src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp -git commit -m "feat: add drying status JSON parsing and DevAms helper methods - -Parse dry_status, dry_sub_status, dry_fan statuses, -dry_settings, and dry_cannot_reasons from printer JSON. -Implement IsSupportRemoteDry, AmsIsDrying, get_ams_drying_preset." -``` - ---- - -### Task 6: `is_support_remote_dry` flag on `MachineObject` - -**Files:** -- Modify: `src/slic3r/GUI/DeviceManager.hpp` (add field) -- Modify: `src/slic3r/GUI/DeviceManager.cpp` (parse the flag) - -**Interfaces:** -- Produces: `MachineObject::is_support_remote_dry` (bool) - -**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceManager.hpp` line 566, `DeviceManager.cpp` line 4403 - -- [ ] **Step 1: Add `is_support_remote_dry` field to `MachineObject`** - -In `src/slic3r/GUI/DeviceManager.hpp`, find the section with other `is_support_*` flags (near `is_support_ams_humidity`) and add: - -```cpp - bool is_support_remote_dry = false; -``` - -- [ ] **Step 2: Parse `is_support_remote_dry` from firmware flags** - -In `src/slic3r/GUI/DeviceManager.cpp`, find the `parse_version_func()` or `parse_new_info()` method where other `is_support_*` flags are set. Look for the `is_support_ams_humidity` parsing pattern. - -Add parsing for `is_support_remote_dry` from the `fun2` field. Check the BambuStudio reference: it uses `get_flag_bits_no_border(fun2, 5) == 1` for bit 5. The exact location depends on how OrcaSlicer parses firmware capability bits. - -If a `fun2` parsing block exists, add: -```cpp - is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1); -``` - -If `fun2` parsing or `get_flag_bits_no_border` don't exist yet in OrcaSlicer, check how similar flags like `is_support_ams_humidity` are parsed and follow the same pattern. If the needed helper methods are missing, port them from BambuStudio's `DeviceManager.cpp`. - -- [ ] **Step 3: Verify build** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -- [ ] **Step 4: Commit** - -```bash -git add src/slic3r/GUI/DeviceManager.hpp src/slic3r/GUI/DeviceManager.cpp -git commit -m "feat: add is_support_remote_dry flag to MachineObject - -Parsed from firmware fun2 bit 5. Controls whether drying -status fields are parsed and drying commands are available." -``` - ---- - -### Task 7: Start/Stop drying commands in `DevFilaSystemCtrl.cpp` - -**Files:** -- Modify: `src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp` - -**Interfaces:** -- Consumes: `MachineObject::m_sequence_id`, `MachineObject::publish_json()` -- Produces: `DevFilaSystem::CtrlAmsStartDryingHour(...)`, `DevFilaSystem::CtrlAmsStopDrying(int)` - -**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\DeviceCore\DevFilaSystemCtrl.cpp` - -- [ ] **Step 1: Add `CtrlAmsStartDryingHour` and `CtrlAmsStopDrying`** - -In `src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp`, add after the existing `CtrlAmsReset()` method: - -```cpp -int DevFilaSystem::CtrlAmsStartDryingHour(int ams_id, - std::string filament_type, - int tag_temp, - int tag_duration_hour, - bool rotate_tray, - int cooling_temp, - bool close_power_conflict) const -{ - json jj_command; - jj_command["print"]["command"] = "ams_filament_drying"; - jj_command["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); - jj_command["print"]["ams_id"] = ams_id; - jj_command["print"]["mode"] = DevAms::DryCtrlMode::OnTime; - jj_command["print"]["filament"] = filament_type; - jj_command["print"]["temp"] = tag_temp; - jj_command["print"]["duration"] = tag_duration_hour; - jj_command["print"]["humidity"] = 0; - jj_command["print"]["rotate_tray"] = rotate_tray; - jj_command["print"]["cooling_temp"] = cooling_temp; - jj_command["print"]["close_power_conflict"] = close_power_conflict; - return m_owner->publish_json(jj_command); -} - -int DevFilaSystem::CtrlAmsStopDrying(int ams_id) const -{ - json jj_command; - jj_command["print"]["command"] = "ams_filament_drying"; - jj_command["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); - jj_command["print"]["ams_id"] = ams_id; - jj_command["print"]["mode"] = DevAms::DryCtrlMode::Off; - jj_command["print"]["filament"] = ""; - jj_command["print"]["temp"] = 0; - jj_command["print"]["duration"] = 0; - jj_command["print"]["humidity"] = 0; - jj_command["print"]["rotate_tray"] = false; - jj_command["print"]["cooling_temp"] = 0; - jj_command["print"]["close_power_conflict"] = false; - return m_owner->publish_json(jj_command); -} -``` - -Note: The `DevAms::DryCtrlMode` enum class uses `DevAms::DryCtrlMode::OnTime` in C++. When serialized to JSON (`jj_command["print"]["mode"] = DevAms::DryCtrlMode::OnTime`), nlohmann/json will convert the enum class to its underlying int (1). This matches the BambuStudio behavior. If the implicit conversion doesn't compile, add a `static_cast()` wrapper. - -- [ ] **Step 2: Verify build** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -- [ ] **Step 3: Commit** - -```bash -git add src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp -git commit -m "feat: add CtrlAmsStartDryingHour and CtrlAmsStopDrying commands - -Publish 'ams_filament_drying' JSON commands via MQTT for -starting and stopping AMS filament drying." -``` - ---- - -### Task 8: Copy image assets from BambuStudio - -**Files:** -- Copy 14 image files from `D:\projects\BambuStudio\resources\images\` to `resources\images\` - -**Assets to copy:** - -```powershell -Copy-Item "D:\projects\BambuStudio\resources\images\hum_level1_no_num_light.svg" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\hum_level2_no_num_light.svg" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\hum_level3_no_num_light.svg" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\hum_level4_no_num_light.svg" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\hum_level5_no_num_light.svg" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3f_heating.png" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3f_dehumidifying.png" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3f_error.png" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3f_cooling.png" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3s_heating.png" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3s_dehumidifying.png" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3s_error.png" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_n3s_cooling.png" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_heating_icon.svg" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_filament_in_chamber.png" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_enable.svg" -Destination "resources\images\" -Copy-Item "D:\projects\BambuStudio\resources\images\dev_ams_dry_ctr_disable.svg" -Destination "resources\images\" -``` - -- [ ] **Step 1: Copy each asset** - -Run each Copy-Item command. If any file is missing from the source, note it — the dialog will degrade gracefully with null bitmaps. - -- [ ] **Step 2: Verify files exist** - -```powershell -Get-ChildItem resources\images\dev_ams_dry_ctr_*, resources\images\hum_level* | Select Name -``` - -Expected: all 17 files listed. - -- [ ] **Step 3: Commit** - -```bash -git add resources/images/dev_ams_dry_ctr_* resources/images/hum_level* -git commit -m "assets: add AMS drying control images from BambuStudio - -14 images: humidity level icons, drying state images per AMS type, -heating icon, guide illustration, tray status icons." -``` - ---- - -### Task 9: `AMSDryControl` UI dialog - -**Files:** -- Create: `src/slic3r/GUI/AMSDryControl.hpp` -- Create: `src/slic3r/GUI/AMSDryControl.cpp` - -**Interfaces:** -- Produces: `AMSDryCtrWin` dialog class extending `DPIDialog` -- Produces: `FilamentItemPanel`, `AMSFilamentPanel` helper widget classes -- Consumes: `DevFilaSystem`, `MachineObject`, `DevAms`, `DevUtilBackend::GetFilamentDryingPreset()` - -**Reference:** `D:\projects\BambuStudio\src\slic3r\GUI\AMSDryControl.hpp` and `AMSDryControl.cpp` - -- [ ] **Step 1: Create `AMSDryControl.hpp`** - -Create `src/slic3r/GUI/AMSDryControl.hpp`. Port the full header from BambuStudio `AMSDryControl.hpp`, adapting includes for OrcaSlicer paths: - -Key adaptations: -- Change `#include "GUI_ObjectLayers.hpp"` to appropriate OrcaSlicer equivalent (check if it exists; if not, it may not be needed or can be replaced with `wx/wx.h`) -- Keep includes for `slic3r/GUI/Widgets/AMSItem.hpp`, `Widgets/Label.hpp`, `Widgets/PopupWindow.hpp`, `wxExtensions.hpp`, `DeviceCore/DevFilaSystem.h` -- Keep the full class declarations for `FilamentItemPanel`, `AMSFilamentPanel`, `AMSDryCtrWin` - -The complete header from `D:\projects\BambuStudio\src\slic3r\GUI\AMSDryControl.hpp` (lines 1–252) should be ported with path adjustments. Due to the file's length (~250 lines), copy the full content from the reference and adapt. - -- [ ] **Step 2: Create `AMSDryControl.cpp`** - -Create `src/slic3r/GUI/AMSDryControl.cpp`. Port from `D:\projects\BambuStudio\src\slic3r\GUI\AMSDryControl.cpp` (lines 1–1860). Key adaptations: - -Replace these BambuStudio includes: -```cpp -#include "AMSDryControl.hpp" -#include "DeviceCore/DevFilaSystem.h" -#include "GUI_App.hpp" -#include "I18N.hpp" -#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" -#include "slic3r/GUI/DeviceCore/DevUpgrade.h" -#include "slic3r/GUI/DeviceCore/DevManager.h" -#include "slic3r/GUI/MsgDialog.hpp" -#include "slic3r/GUI/Widgets/AnimaController.hpp" -#include "slic3r/GUI/Widgets/Label.hpp" -#include "slic3r/GUI/Widgets/ComboBox.hpp" -#include "slic3r/GUI/Widgets/ProgressBar.hpp" -#include "DeviceCore/DevUtilBackend.h" -``` - -Adapt includes for OrcaSlicer paths. Check which of these already exist: -- `GUI_App.hpp` → existing -- `I18N.hpp` → existing -- `DeviceCore/DevFilaSystem.h` → existing -- `Widgets/Label.hpp`, `Widgets/ComboBox.hpp`, `Widgets/ProgressBar.hpp` → check existence in OrcaSlicer - -Since this file is ~1860 lines, port it in full from the BambuStudio reference, adapting only the include paths and namespace references as needed for OrcaSlicer. The implementation logic should be identical to BambuStudio. - -Key parts to verify during port: -- `AMS_ITEMS_PANEL_SIZE`, `AMS_CONTROL_DEF_BLOCK_BK_COLOUR`, `AMS_CONTROL_BRAND_COLOUR`, `AMS_CONTROL_DISABLE_COLOUR`, `AMS_CONTROL_WHITE_COLOUR` — these constants are defined in `AMSItem.hpp`. Verify they exist in OrcaSlicer's version. -- `_L()` and `_CTX_utf8()` — I18N macros; verify they work the same in OrcaSlicer -- `StateColor` — widget state color class; verify it exists in OrcaSlicer -- `ScalableBitmap` — verify it exists in OrcaSlicer -- `ComboBox` — OrcaSlicer custom combobox; verify path -- `ProgressBar` — OrcaSlicer custom progress bar; verify path -- `FromDIP()` — DPI scaling helper; should exist in OrcaSlicer -- `encode_path()` — path encoding utility; should exist -- `resources_dir()` — resources directory getter; verify it exists -- `bool is_support_user_preset` on `MachineObject` — verify this field exists - -- [ ] **Step 3: Register `AMSDryControl` in CMakeLists** - -In `src/slic3r/CMakeLists.txt`, add after the `AMSSetting` entries (around line 30): - -```cmake - GUI/AMSDryControl.cpp - GUI/AMSDryControl.hpp -``` - -- [ ] **Step 4: Verify build** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -Fix any compilation errors from missing includes or type mismatches between OrcaSlicer and BambuStudio APIs. - -- [ ] **Step 5: Commit** - -```bash -git add src/slic3r/GUI/AMSDryControl.hpp src/slic3r/GUI/AMSDryControl.cpp -git commit -m "feat: add AMSDryControl dialog for AMS filament drying - -Three-page wizard: main status/control page, guide page with -filament tray status, and progress page. Supports N3F and N3S AMS types." -``` - ---- - -### Task 10: Wire humidity click to open `AMSDryCtrWin` - -**Files:** -- Modify: `src/slic3r/GUI/Widgets/AMSControl.hpp` -- Modify: `src/slic3r/GUI/Widgets/AMSControl.cpp` - -**Interfaces:** -- Consumes: `AMSDryCtrWin` dialog class -- Modifies: `EVT_AMS_SHOW_HUMIDITY_TIPS` handler — for N3F/N3S AMS types, opens `AMSDryCtrWin` instead of the popup - -- [ ] **Step 1: Add member and include to `AMSControl.hpp`** - -In `src/slic3r/GUI/Widgets/AMSControl.hpp`: - -Add forward declaration: -```cpp -class AMSDryCtrWin; -``` - -Add member variable near the existing `m_percent_humidity_dry_popup`: -```cpp - AMSDryCtrWin* m_dry_ctr_win{nullptr}; -``` - -- [ ] **Step 2: Add include to `AMSControl.cpp`** - -```cpp -#include "slic3r/GUI/AMSDryControl.hpp" -``` - -- [ ] **Step 3: Modify the `EVT_AMS_SHOW_HUMIDITY_TIPS` handler** - -In the handler at around line 243, modify the `else` branch (for non-GENERIC_AMS types) to check if the AMS type is N3F/N3S and open the `AMSDryCtrWin` instead of the popup: - -```cpp - Bind(EVT_AMS_SHOW_HUMIDITY_TIPS, [this](wxCommandEvent& evt) { - uiAmsHumidityInfo *info = (uiAmsHumidityInfo *) evt.GetClientData(); - if (info) - { - if (info->ams_type == AMSModel::GENERIC_AMS) - { - wxPoint img_pos = ClientToScreen(wxPoint(0, 0)); - wxPoint popup_pos(img_pos.x - m_Humidity_tip_popup.GetSize().GetWidth() + FromDIP(150), img_pos.y - FromDIP(80)); - m_Humidity_tip_popup.Position(popup_pos, wxSize(0, 0)); - - int humidity_value = info->humidity_display_idx; - if (humidity_value > 0 && humidity_value <= 5) { m_Humidity_tip_popup.set_humidity_level(humidity_value); } - m_Humidity_tip_popup.Popup(); - } - else if (info->ams_type == AMSModel::N3F_AMS || info->ams_type == AMSModel::N3S_AMS) - { - // Open full drying control dialog for N3F/N3S AMS - if (!m_dry_ctr_win) { - m_dry_ctr_win = new AMSDryCtrWin(this); - } - m_dry_ctr_win->set_ams_id(info->ams_id); - // Get fila system and machine object via the parent chain - // The dialog will be updated in parse_object() - m_dry_ctr_win->ShowModal(); - } - else - { - m_percent_humidity_dry_popup->Update(info); - - wxPoint img_pos = ClientToScreen(wxPoint(0, 0)); - wxPoint popup_pos(img_pos.x - m_percent_humidity_dry_popup->GetSize().GetWidth() + FromDIP(150), img_pos.y - FromDIP(80)); - m_percent_humidity_dry_popup->Move(popup_pos); - m_percent_humidity_dry_popup->ShowModal(); - } - } - - delete info; - }); -``` - -Note: The `AMSDryCtrWin` needs `DevFilaSystem` and `MachineObject` references for its `update()` method. The dialog's `update()` should be called before `ShowModal()`. The `MachineObject*` can be obtained from the parent widget chain or stored as a member of `AMSControl`. Check how `AMSControl` currently accesses `MachineObject` (likely through `m_obj` or similar). Pass it to the dialog's `update()` call. - -- [ ] **Step 4: Add periodic update for the dialog in `parse_object()`** - -In the `parse_object()` method (around line 954), find the existing humidity popup update block and extend it to also update the dry control dialog: - -```cpp - /*update AMS dry control dialog*/ - if (m_dry_ctr_win && m_dry_ctr_win->IsShown()) - { - // Get the MachineObject and DevFilaSystem from the current device - // m_dry_ctr_win->update(obj->GetFilaSystem_ptr(), obj); - } -``` - -Note: The exact code depends on how `AMSControl` accesses `MachineObject*`. Check existing patterns — it likely has access through `m_obj` or a similar member. The update will need both the `DevFilaSystem` (as `shared_ptr`) and `MachineObject*`. - -- [ ] **Step 5: Verify build** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -- [ ] **Step 6: Commit** - -```bash -git add src/slic3r/GUI/Widgets/AMSControl.hpp src/slic3r/GUI/Widgets/AMSControl.cpp -git commit -m "feat: wire AMS humidity click to open AMSDryControl dialog - -For N3F and N3S AMS types, clicking the humidity indicator -now opens the full drying control dialog instead of the popup." -``` - ---- - -### Task 11: Update error dialog stop-drying call - -**Files:** -- Modify: `src/slic3r/GUI/DeviceErrorDialog.cpp` - -**Interfaces:** -- Consumes: `DevFilaSystem::CtrlAmsStopDrying(int)` - -- [ ] **Step 1: Update `STOP_DRYING` case in `DeviceErrorDialog.cpp`** - -In `src/slic3r/GUI/DeviceErrorDialog.cpp` around line 452, update the stop drying action: - -```cpp - case DeviceErrorDialog::STOP_DRYING: { - // Use the canonical CtrlAmsStopDrying path - if (m_obj && m_obj->GetFilaSystem()) { - // Get the AMS ID — check how the dialog knows which AMS is involved - // For now, stop all AMS units or use the first one - m_obj->GetFilaSystem()->CtrlAmsStopDrying(0); - } - // Fallback to the old command for backward compatibility - m_obj->command_ams_drying_stop(); - break; - } -``` - -Note: The exact AMS ID to pass depends on the error context. Check how BambuStudio handles this — it may use a specific AMS ID from the error data, or stop all drying. The fallback to `command_ams_drying_stop()` ensures backward compatibility if `CtrlAmsStopDrying` fails or the fila system isn't available. - -- [ ] **Step 2: Verify build** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -- [ ] **Step 3: Commit** - -```bash -git add src/slic3r/GUI/DeviceErrorDialog.cpp -git commit -m "feat: update error dialog stop-drying to use CtrlAmsStopDrying - -Keeps command_ams_drying_stop() fallback for backward compatibility." -``` - ---- - -### Task 12: Build, verify, and final integration test - -**Files:** All modified files - -- [ ] **Step 1: Full clean build** - -```powershell -cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m -``` - -Expected: Zero errors, zero warnings related to the new code. - -- [ ] **Step 2: Verify no regressions in existing AMS features** - -- Confirm the app launches -- Confirm AMS panel renders correctly for printers without N3F/N3S -- Confirm humidity popup still works for standard AMS and AMS Lite -- Confirm existing `command_ams_drying_stop()` still functions - -- [ ] **Step 3: Manual UI smoke test (requires N3F/N3S printer)** - -If a printer with N3F or N3S AMS is available: -1. Connect to the printer -2. Click AMS humidity indicator → verify `AMSDryCtrWin` opens -3. Verify humidity/temperature readings match printer data -4. Verify filament combobox is populated -5. Verify temp/time auto-fill when selecting a filament -6. Verify validation warnings for invalid temperatures -7. Verify the dialog closes cleanly - -- [ ] **Step 4: Commit any final fixes** - -```bash -git add -A -git commit -m "fix: final integration fixes for AMS drying control" -``` diff --git a/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md b/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md deleted file mode 100644 index cecbf7852f..0000000000 --- a/docs/superpowers/specs/2026-07-07-ams-drying-control-design.md +++ /dev/null @@ -1,325 +0,0 @@ -# AMS Drying Control — Design Spec - -**Date:** 2026-07-07 -**Source:** Port from BambuStudio `AMSDryControl` feature -**Reference:** `D:\projects\BambuStudio` — match its behavior as much as possible; when in doubt, check the BambuStudio source -**Branch:** `dev/ams-heat` - -## Overview - -Port the AMS filament drying control feature from BambuStudio into OrcaSlicer. This allows users to start, monitor, and stop AMS-based filament drying directly from the slicer UI. Currently OrcaSlicer can monitor drying state and stop drying, but cannot initiate it. This feature adds the missing start-drying command, the full drying control dialog, and the drying preset lookup infrastructure. - -**Target AMS models:** N3F (AMS 2 Pro, max 65°C) and N3S (AMS HT, max 85°C) — the only AMS units with heating hardware. - -## Architecture - -The feature spans three layers, matching the existing OrcaSlicer `DeviceCore/` pattern: - -``` -┌─────────────────────────────────────────────────────┐ -│ UI Layer │ -│ AMSDryControl.hpp/.cpp (new dialog) │ -│ AMSControl.cpp (hook: click → dialog) │ -│ DeviceErrorDialog.cpp (stop drying on error) │ -├─────────────────────────────────────────────────────┤ -│ Command/Control Layer │ -│ DevFilaSystemCtrl.cpp (start/stop drying) │ -│ DevUtilBackend.h/.cpp (preset lookup) │ -├─────────────────────────────────────────────────────┤ -│ Data Model Layer │ -│ DevFilaSystem.h/.cpp (enums, structs, parse) │ -│ DevDefs.h (DevAmsType enum) │ -└─────────────────────────────────────────────────────┘ -│ Communication Layer (already exists) │ -│ MachineObject::publish_json() → MQTT → printer │ -└─────────────────────────────────────────────────────┘ -``` - -### New files (4 source + headers) - -| File | Purpose | -|------|---------| -| `src/slic3r/GUI/AMSDryControl.hpp` | AMS drying control dialog class declarations | -| `src/slic3r/GUI/AMSDryControl.cpp` | Dialog implementation (~1800 lines) | -| `src/slic3r/GUI/DeviceCore/DevFilaSystemCtrl.cpp` | `CtrlAmsStartDryingHour()` and `CtrlAmsStopDrying()` | -| `src/slic3r/GUI/DeviceCore/DevUtilBackend.h` | Static utility class declaring `GetFilamentDryingPreset()` | -| `src/slic3r/GUI/DeviceCore/DevUtilBackend.cpp` | Reads filament drying presets from printer config | - -### Modified files - -| File | Changes | -|------|---------| -| `DevFilaSystem.h` | Add drying enums/structs to `DevAms`; add `DevFilamentDryingPreset` struct; add ctrl method declarations to `DevFilaSystem`; add `get_ams_drying_preset()` to `DevAmsTray` | -| `DevFilaSystem.cpp` | Parse dry status fields in `DevFilaSystemParser`; implement `IsSupportRemoteDry()`, `AmsIsDrying()`, `get_ams_drying_preset()` | -| `DevDefs.h` | Add `DevAmsType` as a global enum | -| `AMSControl.cpp` | Wire humidity indicator click to open `AMSDryCtrWin` for N3F/N3S AMS | -| `DeviceErrorDialog.cpp` | Update stop-drying to use `DevFilaSystem::CtrlAmsStopDrying()` | -| `DeviceCore/CMakeLists.txt` | Register new source files | - -### New image assets (14) - -| Asset | Purpose | -|-------|---------| -| `hum_level1_no_num_light.svg` – `hum_level5_no_num_light.svg` | 5 humidity level icons | -| `dev_ams_dry_ctr_n3f_heating.png`, `_dehumidifying.png`, `_error.png`, `_cooling.png` | 4 N3F drying state images | -| `dev_ams_dry_ctr_n3s_heating.png`, `_dehumidifying.png`, `_error.png`, `_cooling.png` | 4 N3S drying state images | -| `dev_ams_dry_ctr_heating_icon.svg` | Animated heating indicator | -| `dev_ams_dry_ctr_filament_in_chamber.png` | Guide page illustration | -| `dev_ams_dry_ctr_enable.svg`, `dev_ams_dry_ctr_disable.svg` | Filament tray status icons | - -Additionally, OrcaSlicer already has `ams_drying.svg` and `ams_is_drying.svg`. - -## Data Model - -### `DevDefs.h` — Global `DevAmsType` enum - -Promoted from `DevAms::AmsType` (currently local to the class) to a global enum in `DevDefs.h` so `DevFilamentDryingPreset` and `DevUtilBackend` can reference it without depending on `DevAms`. `DevAms::AmsType` is updated to be a typedef for `DevAmsType`, and all existing references to `DevAms::AmsType` remain valid (it's the same type): - -```cpp -// DevDefs.h — global enum matching BambuStudio's DevAmsType naming -enum DevAmsType : int { - EXT_SPOOL = 0, - AMS = 1, - AMS_LITE = 2, - N3F = 3, // AMS 2 Pro - N3S = 4, // AMS HT -}; - -// DevFilaSystem.h — DevAms class updated: -// Replace "enum AmsType { DUMMY=0, AMS=1, AMS_LITE=2, N3F=3, N3S=4 }" -// with "using AmsType = DevAmsType;" -// Existing references to DevAms::DUMMY become DevAms::EXT_SPOOL (same value, different name). -``` - -### `DevAms` class additions (in `DevFilaSystem.h`) - -**New enums:** - -| Enum | Values | -|------|--------| -| `DryCtrlMode` | `Off=0, OnTime=1, OnHumidity=2` | -| `DryStatus` | `Off=0, Checking=1, Drying=2, Cooling=3, Stopping=4, Error=5, CannotStopHeatOutofControl=6, PrdTesting=7` | -| `DrySubStatus` | `Off=0, Heating=1, Dehumidify=2` | -| `DryFanStatus` | `Off=0, On=1` | -| `CannotDryReason` | `TaskOccupied=0, InsufficientPower=1, AmsBusy=2, ConsumableAtAmsOutlet=3, InitiatingAmsDrying=4, NotSupportedIn2dMode=5, DryingInProgress=6, Upgrading=7, InsufficientPowerNeedPluginPower=8, FilamentAtAmsOutletManualUnload=10` | - -**New struct:** - -```cpp -struct DrySettings { - std::string dry_filament; - int dry_temp = -1; // -1 means invalid - int dry_hour = -1; // hours -}; -``` - -**New getters on `DevAms`:** - -- `std::optional GetDryStatus() const` -- `std::optional GetDrySubStatus() const` -- `std::optional GetFan1Status() const` -- `std::optional GetFan2Status() const` -- `std::optional> GetCannotDryReason() const` -- `std::optional GetDrySettings() const` -- `bool IsSupportRemoteDry(const MachineObject* obj) const` -- `bool AmsIsDrying()` - -**New private members:** `m_dry_status`, `m_dry_sub_status`, `m_dry_fan1_status`, `m_dry_fan2_status`, `m_dry_cannot_reasons`, `m_dry_settings` — all `std::optional<>`. - -### `DevFilamentDryingPreset` struct (namespace scope in `DevFilaSystem.h`) - -```cpp -struct DevFilamentDryingPreset { - std::string filament_id; - std::unordered_set ams_limitations; - std::unordered_map filament_dev_ams_drying_time_on_idle; // hours - std::unordered_map filament_dev_ams_drying_temperature_on_idle; - std::unordered_map filament_dev_ams_drying_time_on_print; // hours - std::unordered_map filament_dev_ams_drying_temperature_on_print; - float filament_dev_drying_cooling_temperature; - float filament_dev_drying_softening_temperature; - float filament_dev_ams_drying_heat_distortion_temperature; -}; -``` - -Populated from filament preset config keys (`filament_dev_ams_drying_temperature`, `filament_dev_ams_drying_time`, `filament_dev_drying_softening_temperature`, etc.). - -### `DevAmsTray` addition - -```cpp -std::optional get_ams_drying_preset() const; -``` - -Delegates to `DevUtilBackend::GetFilamentDryingPreset(setting_id)`. - -### JSON parsing - -`DevFilaSystemParser::ParseV1_0()` extended to parse the following fields from the printer's AMS status JSON: - -| JSON field | DevAms member | -|------------|---------------| -| `dry_status` | `m_dry_status` (`DryStatus` enum) | -| `dry_sub_status` | `m_dry_sub_status` (`DrySubStatus` enum) | -| `dry_fan1_status` | `m_dry_fan1_status` | -| `dry_fan2_status` | `m_dry_fan2_status` | -| `dry_cannot_reasons` | `m_dry_cannot_reasons` (vector of `CannotDryReason`) | -| `dry_settings` | `m_dry_settings` (`DrySettings` struct) | - -## Commands - -### `DevFilaSystem::CtrlAmsStartDryingHour()` - -Publishes JSON command `"ams_filament_drying"` via `MachineObject::publish_json()`: - -```json -{ - "print": { - "command": "ams_filament_drying", - "sequence_id": "", - "ams_id": , - "mode": 1, - "filament": "", - "temp": , - "duration": , - "humidity": 0, - "rotate_tray": , - "cooling_temp": , - "close_power_conflict": false - } -} -``` - -### `DevFilaSystem::CtrlAmsStopDrying()` - -Same command with `mode: 0` (Off). - -### Existing `MachineObject::command_ams_drying_stop()` - -Kept for backward compatibility. The error dialog will be updated to call `CtrlAmsStopDrying()` instead, which is the preferred path. - -## Backend Utility - -### `DevUtilBackend` (new, `DeviceCore/DevUtilBackend.h/.cpp`) - -Static utility class. Only method needed for this feature: - -```cpp -static std::optional GetFilamentDryingPreset(const std::string& fila_id); -``` - -Iterates `wxGetApp().preset_bundle->filaments` to find the matching filament_id, then reads config keys into a `DevFilamentDryingPreset`. Uses a static map `{"0" → N3F, "1" → N3S}` for the `ams_limitations` string-to-enum conversion. - -## UI Dialog - -### `AMSDryCtrWin` (extends `DPIDialog`) - -Three pages in a `wxSimplebook`: - -#### Page 0: Main Page - -Split left/right layout: - -**Left panel — Status display:** -- Large humidity/drying state image: humidity level icon when idle; heating/dehumidifying/error animation when active -- Status label with animated heating icon ("Idle", "Drying-Heating", "Drying-Dehumidifying") -- Three stat labels: Humidity (%), Temperature (°C), Left Time (HH:MM). Left Time hidden when idle. - -**Right panel — Controls.** Three sub-panels shown/hidden based on `DryStatus` and `CannotDryReason`: - -| Sub-panel | When shown | Contents | -|-----------|------------|----------| -| Normal state | `DryStatus` is Off or Cooling | Filament type combobox → Temperature input (numeric, validated to AMS limits) → Time input (hours, 1–24) → Validation warning text → **Start** button | -| Cannot dry | `CannotDryReason` is non-empty (excluding only `DryingInProgress`) | Formatted reason text + **Unload** button (only for `ConsumableAtAmsOutlet` reason) | -| Drying error | `DryStatus` is Error | Error message + **Stop** button | -| Drying active | `DryStatus` is Checking/Drying/Stopping | Read-only temp/time display + **Stop** button | - -**Temperature validation rules:** -- Hardware limits: N3F 45–65°C, N3S 45–85°C -- Heat distortion check: temp must not exceed `filament_dev_ams_drying_heat_distortion_temperature` when filament is present -- Printing mode: temp must not exceed recommended drying temp when this AMS is actively feeding a print -- Time: 1–24 hours - -#### Page 1: Guide Page - -Shown after clicking Start on Page 0, before sending the command: -- Instructional text about removing heat-sensitive filament -- `AMSFilamentPanel` showing each tray's filament type with ✅/⚠ icons based on whether drying temp exceeds that filament's softening point -- "Rotate spool when drying" checkbox -- **Back** and **Start** buttons - -#### Page 2: Progress Page - -Shown while waiting for printer to confirm drying has started: -- Animated progress bar (cycles 0–99% until printer state changes) -- Cycling status messages -- Auto-transitions back to Page 0 when printer reports drying active - -### Helper classes (in `AMSDryControl.hpp`) - -- **`FilamentItemPanel`** — Renders a single filament item with icon and text, with custom rounded-rectangle border painting -- **`AMSFilamentPanel`** — Container for multiple `FilamentItemPanel` instances, labeled with AMS name -- **`DryingPreset` struct, `DryCtrState` enum, `DryCtrDev` enum** — Local types for tracking preset state - -### Dialog lifecycle - -- Created on first click of the AMS humidity indicator -- Updated on each `AMSControl::parse_object()` cycle via `update(fila_system, obj)` -- Auto-closes if: AMS is removed from the system, AMS no longer supports remote drying, or AMS ID becomes invalid -- Closed manually via close button or dialog destruction -- On close: stops progress timer, resets to main page, restores button states - -## Integration - -### Entry point: AMS humidity click → dialog - -In `AMSControl.cpp`, the `EVT_AMS_SHOW_HUMIDITY_TIPS` handler currently shows either `AmsHumidityTipPopup` or `uiAmsPercentHumidityDryPopup`. For N3F and N3S AMS types only, replace this with opening `AMSDryCtrWin`: - -1. Lazily create `AMSDryCtrWin*` member on `AMSControl` (like `m_percent_humidity_dry_popup` today) -2. Call `set_ams_id()` and `update(fila_system, obj)` before showing -3. Show the dialog modally via `ShowModal()` (matching BambuStudio behavior) - -### Periodic updates - -`AMSControl::parse_object()` updates AMS state on a timer. Extend it to: -1. If `m_dry_ctr_win` is shown, call `update()` to refresh readings -2. Update the dialog's AMS ID if the user switches AMS view - -### Error dialog - -`DeviceErrorDialog.cpp` line 452 calls `m_obj->command_ams_drying_stop()`. Update to use `obj->GetFilaSystem()->CtrlAmsStopDrying(ams_id)` for the correct AMS ID. - -### CMakeLists - -- `src/slic3r/GUI/DeviceCore/CMakeLists.txt`: add `DevFilaSystemCtrl.cpp`, `DevUtilBackend.h`, `DevUtilBackend.cpp` -- GUI sources list: add `AMSDryControl.hpp`, `AMSDryControl.cpp` - -## Testing - -### Manual verification - -1. **Prerequisite:** Connect to a printer with N3F or N3S AMS hardware -2. Click AMS humidity indicator → verify dialog opens with correct humidity/temperature readings -3. Select a filament from the combobox → verify temp/time auto-fill from preset -4. Enter valid temp/time → verify Start button enables -5. Enter invalid temp (e.g., 100°C for N3F) → verify warning appears and button disables -6. Click Start → verify guide page shows filament tray status -7. Click Start on guide page → verify progress page appears, command is sent -8. Verify printer begins drying → dialog shows active drying state with countdown -9. Click Stop → verify drying stops -10. Test "cannot dry" states (e.g., during print, filament at outlet) - -### Code-level checks - -- Verify `CtrlAmsStartDryingHour` publishes correct JSON structure -- Verify `CtrlAmsStopDrying` publishes stop command -- Verify `GetFilamentDryingPreset` returns correct values for known filament IDs -- Verify dark mode rendering of all dialog pages - -## Constraints - -- **N3F/N3S only:** Standard AMS and AMS Lite do not have heating hardware; the dialog is not shown for them -- **Backward compatibility:** Existing `command_ams_drying_stop()` is preserved; new commands are additive -- **Cross-platform:** wxWidgets UI must work on Windows, macOS, Linux -- **Dark mode:** Dialog must support dark mode via `wxGetApp().dark_mode()` and `StateColor::darkModeColorFor()` -- **Profile compatibility:** Existing profiles with `filament_dev_ams_drying_*` keys (Qidi) continue to work; the feature reads the same keys -- **No printer-agent changes needed:** All commands go through existing `publish_json()` → `NetworkAgent` path, which already handles MQTT for connected printers From 0e20f607122144ce18175655ea99ec5cf331f82c Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 15:25:32 +0800 Subject: [PATCH 20/30] fix: adapt SVG colors to OrcaSlicer palette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dev_ams_dry_ctr_enable.svg: Bambu green #00AE42 → Orca teal #009688 - dev_ams_dry_ctr_heating_icon.svg: red #E14747 → amber #F09A17 (matches ams_is_drying.svg) --- resources/images/dev_ams_dry_ctr_enable.svg | 2 +- resources/images/dev_ams_dry_ctr_heating_icon.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/images/dev_ams_dry_ctr_enable.svg b/resources/images/dev_ams_dry_ctr_enable.svg index 0c8d4ca146..31eb482d17 100644 --- a/resources/images/dev_ams_dry_ctr_enable.svg +++ b/resources/images/dev_ams_dry_ctr_enable.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/images/dev_ams_dry_ctr_heating_icon.svg b/resources/images/dev_ams_dry_ctr_heating_icon.svg index 9084670fb7..614b0c9a92 100644 --- a/resources/images/dev_ams_dry_ctr_heating_icon.svg +++ b/resources/images/dev_ams_dry_ctr_heating_icon.svg @@ -1,3 +1,3 @@ - + From f713c7ae4937d9f85f6d5d8fbf05e07b257f56c1 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 16:34:35 +0800 Subject: [PATCH 21/30] Make sure codes uses same structure/naming/position as BambuStudio for easier code comparasion & porting in the future --- src/libslic3r/Preset.cpp | 20 ++++++ src/libslic3r/Preset.hpp | 3 + src/slic3r/GUI/AMSDryControl.cpp | 2 +- src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 69 +++++++++--------- src/slic3r/GUI/DeviceCore/DevFilaSystem.h | 7 +- src/slic3r/GUI/DeviceManager.hpp | 2 +- src/slic3r/GUI/StatusPanel.cpp | 1 + src/slic3r/GUI/Widgets/AMSControl.cpp | 80 +++++++++++---------- src/slic3r/GUI/Widgets/AMSControl.hpp | 5 +- 9 files changed, 111 insertions(+), 78 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 98a5a4fcb6..6d08ca9f2d 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -3781,6 +3781,26 @@ void PresetCollection::set_custom_preset_alias(Preset &preset) set_printer_hold_alias(preset.alias, preset); } +std::string PresetCollection::get_preset_alias(Preset &preset, bool force) +{ + if (!preset.alias.empty()) + return preset.alias; + else + set_custom_preset_alias(preset); + + if (!preset.alias.empty() || !force) + return preset.alias; + + std::string alias_name; + std::string preset_name = preset.name; + size_t end_pos = preset_name.find_first_of("@"); + if (end_pos != std::string::npos) { + alias_name = preset_name.substr(0, end_pos); + boost::trim_right(alias_name); + } + return alias_name; +} + void PresetCollection::set_printer_hold_alias(const std::string &alias, Preset &preset, bool remove) { auto compatible_printers = dynamic_cast(preset.config.option("compatible_printers")); diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index bbbdc6c5ff..cd3c244fd9 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -803,6 +803,9 @@ public: std::string path_from_name(const std::string &new_name, bool detach = false) const; std::string path_for_preset(const Preset & preset) const; + // Get the alias of a preset, setting it if it's empty + std::string get_preset_alias(Preset &preset, bool force = false); + size_t num_default_presets() { return m_num_default_presets; } protected: diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp index 7e727abbdf..98b35b5809 100644 --- a/src/slic3r/GUI/AMSDryControl.cpp +++ b/src/slic3r/GUI/AMSDryControl.cpp @@ -1615,7 +1615,7 @@ int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj) } filament_id_set.insert(filament_it->filament_id); - auto filament_alias = filament_it->alias; + auto filament_alias = filaments.get_preset_alias(*filament_it, true); if (!filament_alias.empty()) { auto opt_info = preset_bundle->get_filament_by_filament_id(filament_it->filament_id); if (opt_info.has_value()) { diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index a2787577fd..c54b8b8964 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -99,8 +99,7 @@ std::string DevAmsTray::get_filament_type() return m_fila_type; } - -std::optional DevAmsTray::get_ams_drying_preset() const +std::optional DevAmsTray::get_ams_drying_preset() const { return DevUtilBackend::GetFilamentDryingPreset(setting_id); } @@ -454,32 +453,17 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS ; } - if (it->contains("dry_time") && (*it)["dry_time"].is_number()) + + if (it->contains("temp")) { - curr_ams->m_left_dry_time = (*it)["dry_time"].get(); - } - - // Drying status — only parse if printer supports remote drying - if (obj->is_support_remote_dry) { - if (it->contains("info")) { - const std::string& info = (*it)["info"].get(); - curr_ams->m_dry_status = (DevAms::DryStatus)DevUtil::get_flag_bits(info, 4, 4); - curr_ams->m_dry_fan1_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 18, 2); - curr_ams->m_dry_fan2_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 20, 2); - curr_ams->m_dry_sub_status = (DevAms::DrySubStatus)DevUtil::get_flag_bits(info, 22, 2); + std::string temp = (*it)["temp"].get(); + try + { + curr_ams->m_current_temperature = DevUtil::string_to_float(temp); } - - if (it->contains("dry_setting")) { - const auto& j_dry_settings = (*it)["dry_setting"]; - DevAms::DrySettings dry_settings; - DevJsonValParser::ParseVal(j_dry_settings, "dry_filament", dry_settings.dry_filament); - DevJsonValParser::ParseVal(j_dry_settings, "dry_temperature", dry_settings.dry_temp); - DevJsonValParser::ParseVal(j_dry_settings, "dry_duration", dry_settings.dry_hour); - curr_ams->m_dry_settings = dry_settings; - } - - if (it->contains("dry_sf_reason")) { - curr_ams->m_dry_cannot_reasons = DevJsonValParser::GetVal>((*it), "dry_sf_reason"); + catch (...) + { + curr_ams->m_current_temperature = INVALID_AMS_TEMPERATURE; } } @@ -509,17 +493,32 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS } } - - if (it->contains("temp")) + if (it->contains("dry_time") && (*it)["dry_time"].is_number()) { - std::string temp = (*it)["temp"].get(); - try - { - curr_ams->m_current_temperature = DevUtil::string_to_float(temp); + curr_ams->m_left_dry_time = (*it)["dry_time"].get(); + } + + // Drying status — only parse if printer supports remote drying + if (obj->is_support_remote_dry) { + if (it->contains("info")) { + const std::string& info = (*it)["info"].get(); + curr_ams->m_dry_status = (DevAms::DryStatus)DevUtil::get_flag_bits(info, 4, 4); + curr_ams->m_dry_fan1_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 18, 2); + curr_ams->m_dry_fan2_status = (DevAms::DryFanStatus)DevUtil::get_flag_bits(info, 20, 2); + curr_ams->m_dry_sub_status = (DevAms::DrySubStatus)DevUtil::get_flag_bits(info, 22, 2); } - catch (...) - { - curr_ams->m_current_temperature = INVALID_AMS_TEMPERATURE; + + if (it->contains("dry_setting")) { + const auto& j_dry_settings = (*it)["dry_setting"]; + DevAms::DrySettings dry_settings; + DevJsonValParser::ParseVal(j_dry_settings, "dry_filament", dry_settings.dry_filament); + DevJsonValParser::ParseVal(j_dry_settings, "dry_temperature", dry_settings.dry_temp); + DevJsonValParser::ParseVal(j_dry_settings, "dry_duration", dry_settings.dry_hour); + curr_ams->m_dry_settings = dry_settings; + } + + if (it->contains("dry_sf_reason")) { + curr_ams->m_dry_cannot_reasons = DevJsonValParser::GetVal>((*it), "dry_sf_reason"); } } diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h index 283de146db..23f8d23b50 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.h @@ -104,10 +104,11 @@ public: std::string get_display_filament_type() const; std::string get_filament_type(); - std::optional get_ams_drying_preset() const; // static static wxColour decode_color(const std::string& color); + + std::optional get_ams_drying_preset() const; }; /** @@ -228,7 +229,7 @@ public: int GetHumidityLevel() const { return m_humidity_level; } int GetHumidityPercent() const { return m_humidity_percent; } - bool SupportDrying() const { return m_ams_type == N3F || m_ams_type == N3S; } + bool SupportDrying() const { return m_ams_type == DevAmsType::N3F || m_ams_type == DevAmsType::N3S; } int GetLeftDryTime() const { return m_left_dry_time; } // remote drying control @@ -333,6 +334,8 @@ public: public: // ctrls int CtrlAmsReset() const; + + // crtl int CtrlAmsStartDryingHour(int ams_id, std::string filament_type, int tag_temp, int tag_duration_hour, bool rotate_tray, int cooling_temp, bool close_power_conflict = false) const; int CtrlAmsStopDrying(int ams_id) const; diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 6e4007a62b..a0d8f97ea6 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -254,7 +254,6 @@ public: bool is_tunnel_mqtt = false; //AmsTray vt_tray; // virtual tray - bool is_support_remote_dry = false; long ams_exist_bits = 0; long tray_exist_bits = 0; long tray_is_bbl_bits = 0; @@ -621,6 +620,7 @@ public: // fun2 bool is_support_print_with_emmc{false}; + bool is_support_remote_dry = false; bool installed_upgrade_kit{false}; int bed_temperature_limit = -1; diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 5cbf3e2a83..6b1cb8ebd2 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -3334,6 +3334,7 @@ void StatusPanel::update_ams(MachineObject *obj) // must select a current can m_ams_control->UpdateAms(obj->get_printer_series_str(), obj->printer_type, ams_info, ext_info, *obj->GetExtderSystem(), obj->get_dev_id(), false); + m_ams_control->UpdateAmsDryControl(obj); last_tray_exist_bits = obj->tray_exist_bits; last_ams_exist_bits = obj->ams_exist_bits; diff --git a/src/slic3r/GUI/Widgets/AMSControl.cpp b/src/slic3r/GUI/Widgets/AMSControl.cpp index ea62d18d92..24e35cda12 100644 --- a/src/slic3r/GUI/Widgets/AMSControl.cpp +++ b/src/slic3r/GUI/Widgets/AMSControl.cpp @@ -6,7 +6,6 @@ #include "slic3r/GUI/MsgDialog.hpp" #include "slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h" -#include "slic3r/GUI/AMSDryControl.hpp" #include "slic3r/GUI/DeviceCore/DevManager.h" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" @@ -30,6 +29,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons , m_Humidity_tip_popup(AmsHumidityTipPopup(this)) , m_percent_humidity_dry_popup(new uiAmsPercentHumidityDryPopup(this)) , m_ams_introduce_popup(AmsIntroducePopup(this)) + , m_ams_dry_ctr_win(new AMSDryCtrWin(this)) { Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (dev) { @@ -245,6 +245,12 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons uiAmsHumidityInfo *info = (uiAmsHumidityInfo *) evt.GetClientData(); if (info) { + Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + MachineObject *obj = nullptr; + if (dev) { + obj = dev->get_selected_machine(); + } + if (info->ams_type == AMSModel::GENERIC_AMS) { wxPoint img_pos = ClientToScreen(wxPoint(0, 0)); @@ -254,27 +260,14 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons int humidity_value = info->humidity_display_idx; if (humidity_value > 0 && humidity_value <= 5) { m_Humidity_tip_popup.set_humidity_level(humidity_value); } m_Humidity_tip_popup.Popup(); - } - else if (info->ams_type == AMSModel::N3F_AMS || info->ams_type == AMSModel::N3S_AMS) - { - // Open full drying control dialog for N3F/N3S AMS - if (!m_dry_ctr_win) { - m_dry_ctr_win = new AMSDryCtrWin(this); - } - m_dry_ctr_win->set_ams_id(info->ams_id); + } else if (obj && obj->is_support_remote_dry && (info->ams_type == AMSModel::N3F_AMS || info->ams_type == AMSModel::N3S_AMS)){ + m_ams_dry_ctr_win->set_ams_id(info->ams_id); - Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (dev) { - MachineObject *obj = dev->get_selected_machine(); - if (obj && obj->GetFilaSystem()) { - std::shared_ptr fila_ptr(obj->GetFilaSystem(), [](DevFilaSystem*){}); - m_dry_ctr_win->update(fila_ptr, obj); - } - } - m_dry_ctr_win->ShowModal(); - } - else - { + wxPoint img_pos = ClientToScreen(wxPoint(0, 0)); + wxPoint popup_pos(img_pos.x - m_ams_dry_ctr_win->GetSize().GetWidth() + FromDIP(150), img_pos.y - FromDIP(80)); + m_ams_dry_ctr_win->Move(popup_pos); + m_ams_dry_ctr_win->ShowModal(); + } else { m_percent_humidity_dry_popup->Update(info); wxPoint img_pos = ClientToScreen(wxPoint(0, 0)); @@ -294,7 +287,12 @@ void AMSControl::on_retry() post_event(wxCommandEvent(EVT_AMS_RETRY)); } -AMSControl::~AMSControl() {} +AMSControl::~AMSControl() +{ + if (m_ams_dry_ctr_win) { + delete m_ams_dry_ctr_win; + } +} std::string AMSControl::GetCurentAms() { return m_current_ams; @@ -512,8 +510,8 @@ void AMSControl::msw_rescale() m_percent_humidity_dry_popup->msw_rescale(); } - if (m_dry_ctr_win){ - m_dry_ctr_win->msw_rescale(); + if (m_ams_dry_ctr_win) { + m_ams_dry_ctr_win->msw_rescale(); } m_Humidity_tip_popup.msw_rescale(); @@ -839,6 +837,27 @@ void AMSControl::show_vams_kn_value(bool show) //m_vams_lib->show_kn_value(show); } +void AMSControl::UpdateAmsDryControl(MachineObject* obj) +{ + if (!m_ams_dry_ctr_win->IsShown()) { + return; + } + + if (!obj || !obj->GetFilaSystem()) { + m_ams_dry_ctr_win->Close(); + return; + } + + std::weak_ptr weak_fila_system = obj->GetFilaSystem(); + + if (auto locaked_fila_system = weak_fila_system.lock()) { + m_ams_dry_ctr_win->update(locaked_fila_system, obj); + } else { + m_ams_dry_ctr_win->Close(); + return; + } +} + std::vector AMSControl::GenerateSimulateData() { auto caninfo0_0 = Caninfo{ "0", (""), *wxRED, AMSCanType::AMS_CAN_TYPE_VIRTUAL }; auto caninfo0_1 = Caninfo{ "1", (""), *wxGREEN, AMSCanType::AMS_CAN_TYPE_VIRTUAL }; @@ -993,19 +1012,6 @@ void AMSControl::UpdateAms(const std::string &series_name, } } - /*update AMS dry control dialog*/ - if (m_dry_ctr_win && m_dry_ctr_win->IsShown()) - { - Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (dev) { - MachineObject *obj = dev->get_selected_machine(); - if (obj && obj->GetFilaSystem()) { - std::shared_ptr fila_ptr(obj->GetFilaSystem(), [](DevFilaSystem*){}); - m_dry_ctr_win->update(fila_ptr, obj); - } - } - } - /*update ams extruder*/ if (m_extruder->updateNozzleNum(m_total_ext_count, series_name)) { diff --git a/src/slic3r/GUI/Widgets/AMSControl.hpp b/src/slic3r/GUI/Widgets/AMSControl.hpp index 93d9acee67..ff727fd77c 100644 --- a/src/slic3r/GUI/Widgets/AMSControl.hpp +++ b/src/slic3r/GUI/Widgets/AMSControl.hpp @@ -15,13 +15,13 @@ #include #include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" +#include "slic3r/GUI/AMSDryControl.hpp" namespace Slic3r { namespace GUI { //Previous definitions class uiAmsPercentHumidityDryPopup; -class AMSDryCtrWin; class AMSControl : public wxSimplebook { @@ -119,7 +119,7 @@ protected: AmsHumidityTipPopup m_Humidity_tip_popup; uiAmsPercentHumidityDryPopup* m_percent_humidity_dry_popup; - AMSDryCtrWin* m_dry_ctr_win{nullptr}; + AMSDryCtrWin* m_ams_dry_ctr_win; std::string m_last_ams_id = ""; std::string m_last_tray_id = ""; @@ -158,6 +158,7 @@ public: void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type); void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type); void ClearAms(); + void UpdateAmsDryControl(MachineObject* obj); void UpdateAms(const std::string &series_name, const std::string &printer_type, std::vector ams_info, From 922af972c70c534d630e01d9f9958dd5995f6e8e Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 17:29:10 +0800 Subject: [PATCH 22/30] refactor: convert m_fila_system from raw pointer to shared_ptr Matches BambuStudio commit c8f70c6ca. DevFilaSystem* m_fila_system becomes std::shared_ptr, enabling proper shared ownership for the AMS drying control feature without no-op deleter hacks. - DeviceManager.hpp: m_fila_system and GetFilaSystem() use shared_ptr - DeviceManager.cpp: std::make_shared allocation, remove manual delete, add .get() to ParseV1_0 calls - MoonrakerPrinterAgent.cpp: add .get() to GetFilaSystem() in ParseV1_0 --- src/slic3r/GUI/DeviceManager.cpp | 6 ++---- src/slic3r/GUI/DeviceManager.hpp | 4 ++-- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 2562d73b22..f8d7c970b2 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -566,7 +566,7 @@ MachineObject::MachineObject(DeviceManager* manager, NetworkAgent* agent, std::s m_extder_system = new DevExtderSystem(this); m_extension_tool = DevExtensionTool::Create(this); m_nozzle_system = new DevNozzleSystem(this); - m_fila_system = new DevFilaSystem(this); + m_fila_system = std::make_shared(this); m_hms_system = new DevHMS(this); m_config = new DevConfig(this); @@ -612,8 +612,6 @@ MachineObject::~MachineObject() delete m_ctrl; m_ctrl = nullptr; - delete m_fila_system; - m_fila_system = nullptr; delete m_hms_system; m_hms_system = nullptr; @@ -3723,7 +3721,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ update_printer_preset_name(); update_filament_list(); if (jj.contains("ams")) { - DevFilaSystemParser::ParseV1_0(jj, this, m_fila_system, key_field_only); + DevFilaSystemParser::ParseV1_0(jj, this, m_fila_system.get(), key_field_only); } /* vitrual tray*/ diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index a0d8f97ea6..fae8d60415 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -114,7 +114,7 @@ private: std::shared_ptr m_extension_tool; DevExtderSystem* m_extder_system; DevNozzleSystem* m_nozzle_system; - DevFilaSystem* m_fila_system; + std::shared_ptr m_fila_system; DevFan* m_fan; DevBed * m_bed; DevStorage* m_storage; @@ -329,7 +329,7 @@ public: DevNozzleSystem* GetNozzleSystem() const { return m_nozzle_system;} - DevFilaSystem* GetFilaSystem() const { return m_fila_system;} + std::shared_ptr GetFilaSystem() const { return m_fila_system;} bool HasAms() const; DevLamp* GetLamp() const { return m_lamp; } diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index f1d892134b..b0ea653dd3 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -536,7 +536,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, print_json["ams"] = ams_json; // Call the parser to populate DevFilaSystem - DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem(), false); + DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false); BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays"; // Set printer_type so update_sync_status() can match it against the preset's printer type. From f58a1f350a077715e3590db1cecd7d95ff4b1c4d Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 17:45:08 +0800 Subject: [PATCH 23/30] feat: add drying-while-printing warning in SelectMachine dialog Port from BambuStudio commit c8f70c6ca. Shows a warning when AMS is actively drying during print job preparation, alerting users that the drying temperature will be lowered during printing. --- src/slic3r/GUI/SelectMachine.cpp | 29 +++++++++++++++++++++++++++++ src/slic3r/GUI/SelectMachine.hpp | 6 ++++++ 2 files changed, 35 insertions(+) diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 09314d3b49..3330f527ca 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -506,6 +506,14 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) //m_change_filament_times_sizer->Add(m_img_change_filament_times, 0, wxTOP, FromDIP(2)); m_change_filament_times_sizer->Add(m_txt_change_filament_times, 0, wxTOP, 0); + m_warn_when_drying_sizer = new wxBoxSizer(wxHORIZONTAL); + m_txt_warn_when_drying = new Label(m_scroll_area, wxEmptyString); + m_txt_warn_when_drying->SetFont(::Label::Body_13); + m_txt_warn_when_drying->SetForegroundColour(wxColour("#F09A17")); + m_txt_warn_when_drying->SetBackgroundColour(*wxWHITE); + m_txt_warn_when_drying->SetLabel(_L("To ensure print quality, the drying temperature will be lowered during printing.")); + m_warn_when_drying_sizer->Add(m_txt_warn_when_drying, 0, wxTOP, FromDIP(2)); + /*Advanced Options*/ wxBoxSizer* sizer_split_options = new wxBoxSizer(wxHORIZONTAL); auto m_split_options_line = new wxPanel(m_scroll_area, wxID_ANY); @@ -716,6 +724,8 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) m_scroll_sizer->Add(m_change_filament_times_sizer, 0,wxLEFT|wxRIGHT, FromDIP(15)); // m_scroll_sizer->Add(m_link_edit_nozzle, 0, wxLEFT|wxRIGHT, FromDIP(15)); m_scroll_sizer->Add(suggestion_sizer, 0, wxLEFT|wxRIGHT|wxEXPAND, FromDIP(15)); + m_scroll_sizer->Add(0, 0, 0, wxTOP, FromDIP(10)); + m_scroll_sizer->Add(m_warn_when_drying_sizer, 0, wxLEFT|wxRIGHT, FromDIP(15)); m_scroll_sizer->Add(sizer_split_options, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(15)); m_scroll_sizer->Add(0, 0, 0, wxTOP, FromDIP(10)); m_scroll_sizer->Add(m_options_other, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(15)); @@ -1443,6 +1453,18 @@ int SelectMachineDialog::convert_filament_map_nozzle_id_to_task_nozzle_id(int no } } +bool SelectMachineDialog::is_ams_drying(MachineObject* obj) +{ + const auto& ams_list = obj->GetFilaSystem()->GetAmsList(); + for (auto ams = ams_list.begin(); ams != ams_list.end(); ams++) { + if (ams->second->AmsIsDrying()) { + return true; + } + } + + return false; +} + void SelectMachineDialog::prepare(int print_plate_idx) { m_print_plate_idx = print_plate_idx; @@ -3285,6 +3307,12 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_) m_check_ext_change_assist->Enable(false); } + if (is_ams_drying(obj_)) { + m_warn_when_drying_sizer->Show(true); + } else { + m_warn_when_drying_sizer->Show(false); + } + /*reading done*/ if (wxGetApp().app_config) { if (obj_->upgrade_force_upgrade) { @@ -3774,6 +3802,7 @@ void SelectMachineDialog::set_default() m_mapping_sugs_sizer->Show(false); m_change_filament_times_sizer->Show(false); m_txt_change_filament_times->Show(false); + m_warn_when_drying_sizer->Show(false); // rset status bar m_status_bar->reset(); diff --git a/src/slic3r/GUI/SelectMachine.hpp b/src/slic3r/GUI/SelectMachine.hpp index ab88240ec0..c6c8ed458b 100644 --- a/src/slic3r/GUI/SelectMachine.hpp +++ b/src/slic3r/GUI/SelectMachine.hpp @@ -28,6 +28,7 @@ #include "boost/bimap/bimap.hpp" #include "AmsMappingPopup.hpp" +#include "GUI_ObjectLayers.hpp" #include "ReleaseNote.hpp" #include "GUI_Utils.hpp" #include "wxExtensions.hpp" @@ -354,6 +355,7 @@ protected: wxBoxSizer* m_sizer_autorefill{ nullptr }; wxBoxSizer* m_mapping_sugs_sizer{ nullptr }; wxBoxSizer* m_change_filament_times_sizer{ nullptr }; + wxBoxSizer* m_warn_when_drying_sizer{ nullptr }; Button* m_button_ensure{ nullptr }; wxStaticBitmap * m_rename_button{nullptr}; wxStaticBitmap* m_staticbitmap{ nullptr }; @@ -387,6 +389,8 @@ protected: CheckBox* m_check_ext_change_assist{ nullptr }; Label* m_label_ext_change_assist{ nullptr }; + Label* m_txt_warn_when_drying{ nullptr }; + PrinterInfoBox* m_printer_box { nullptr}; PrinterMsgPanel * m_text_printer_msg{nullptr}; Label* m_text_printer_msg_tips{ nullptr }; @@ -511,6 +515,8 @@ public: bool is_nozzle_type_match(DevExtderSystem data, wxString& error_message) const; int convert_filament_map_nozzle_id_to_task_nozzle_id(int nozzle_id) const; + bool is_ams_drying(MachineObject* obj); + PrintFromType get_print_type() {return m_print_type;}; wxString format_steel_name(NozzleType type); PrintDialogStatus get_status() { return m_print_status; } From 1b93f35565a33e86830f81c47dedf540013b0761 Mon Sep 17 00:00:00 2001 From: "changyu.chen" Date: Wed, 14 Jan 2026 10:17:23 +0800 Subject: [PATCH 24/30] FIX: print warning when ams drying jira: [STUDIO-16505] Change-Id: I47b98643a7d941c594eb4d90c08f48a7880c947e (cherry picked from commit bdc833d49440c73a56cf10b723fb02d33d1c3a98) --- src/slic3r/GUI/SelectMachine.cpp | 31 ++++++++++++++++++++++++++++++- src/slic3r/GUI/SelectMachine.hpp | 1 + 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 3330f527ca..5efbe7a620 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -1465,6 +1465,35 @@ bool SelectMachineDialog::is_ams_drying(MachineObject* obj) return false; } +bool SelectMachineDialog::is_selected_ams_drying(MachineObject* obj) +{ + if (!obj) return false; + + // If a UI material is selected, only when that material is mapped to an AMS + // and that AMS is currently drying. + for (const auto &kv : m_materialList) { + Material *mat = kv.second; + if (!mat || !mat->item) continue; + if (!mat->item->m_selected) continue; + + // find mapping entry for this material id + for (const FilamentInfo &f : m_ams_mapping_result) { + if (f.id != mat->id) continue; + + if (f.ams_id.empty()) return false; + + auto fila_system = obj->GetFilaSystem(); + if (!fila_system) return false; + DevAms* dev_ams = fila_system->GetAmsById(f.ams_id); + return (dev_ams && dev_ams->AmsIsDrying()); + } + + return false; + } + + return false; +} + void SelectMachineDialog::prepare(int print_plate_idx) { m_print_plate_idx = print_plate_idx; @@ -3307,7 +3336,7 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_) m_check_ext_change_assist->Enable(false); } - if (is_ams_drying(obj_)) { + if (is_selected_ams_drying(obj_)) { m_warn_when_drying_sizer->Show(true); } else { m_warn_when_drying_sizer->Show(false); diff --git a/src/slic3r/GUI/SelectMachine.hpp b/src/slic3r/GUI/SelectMachine.hpp index c6c8ed458b..deb40237ab 100644 --- a/src/slic3r/GUI/SelectMachine.hpp +++ b/src/slic3r/GUI/SelectMachine.hpp @@ -516,6 +516,7 @@ public: int convert_filament_map_nozzle_id_to_task_nozzle_id(int nozzle_id) const; bool is_ams_drying(MachineObject* obj); + bool is_selected_ams_drying(MachineObject* obj); PrintFromType get_print_type() {return m_print_type;}; wxString format_steel_name(NozzleType type); From c2f5a87e243aa433d3b9d143d50dead1644f93d4 Mon Sep 17 00:00:00 2001 From: "changyu.chen" Date: Fri, 16 Jan 2026 18:43:59 +0800 Subject: [PATCH 25/30] FIX: print warning when ams is drying jira: [STUDIO-16558] Change-Id: I6313c4c02dce6853d48fd4b175a161bf116865a9 (cherry picked from commit 49609394cd92b7043f4f65debf585cbc37f382bd) --- src/slic3r/GUI/SelectMachine.cpp | 54 +++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 5efbe7a620..d5ea29046b 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -3336,10 +3336,14 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_) m_check_ext_change_assist->Enable(false); } - if (is_selected_ams_drying(obj_)) { - m_warn_when_drying_sizer->Show(true); - } else { - m_warn_when_drying_sizer->Show(false); + { + const bool show_warn_when_drying = is_selected_ams_drying(obj_); + const bool is_currently_shown = (m_txt_warn_when_drying != nullptr) ? m_txt_warn_when_drying->IsShown() : false; + if (is_currently_shown != show_warn_when_drying) { + m_warn_when_drying_sizer->Show(show_warn_when_drying); + Layout(); + Fit(); + } } /*reading done*/ @@ -3941,6 +3945,10 @@ void SelectMachineDialog::reset_and_sync_ams_list() m_filaments_map = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_real_filament_maps(project_config); } + bool selected_any = false; + MaterialItem *first_enabled = nullptr; + int first_enabled_id = -1; + for (auto i = 0; i < extruders.size(); i++) { auto extruder = extruders[i] - 1; auto colour = wxGetApp().preset_bundle->project_config.opt_string("filament_colour", (unsigned int) extruder); @@ -3970,7 +3978,20 @@ void SelectMachineDialog::reset_and_sync_ams_list() item = new MaterialItem(m_filament_panel, colour_rgb, _L(display_materials[extruder])); m_sizer_ams_mapping->Add(item, 0, wxALL, FromDIP(5)); } + + if (!item) continue; + item->SetToolTip(m_ams_tooltip); + + if (!selected_any && extruder == m_current_filament_id && item->m_enable) { + item->on_selected(); + selected_any = true; + } + if (!first_enabled && item->m_enable) { + first_enabled = item; + first_enabled_id = extruder; + } + item->Bind(wxEVT_LEFT_UP, [this, item, materials, extruder](wxMouseEvent &e) {}); item->Bind(wxEVT_LEFT_DOWN, [this, item, materials, extruder](wxMouseEvent &e) { if (!item->m_enable) {return;} @@ -4049,6 +4070,11 @@ void SelectMachineDialog::reset_and_sync_ams_list() } } + if (!selected_any && first_enabled) { + m_current_filament_id = first_enabled_id; + first_enabled->on_selected(); + } + if (use_double_extruder) { m_filament_left_panel->Show(); @@ -4459,6 +4485,10 @@ void SelectMachineDialog::set_default_from_sdcard() m_materialList.clear(); m_filaments.clear(); + bool selected_any = false; + MaterialItem *first_enabled = nullptr; + int first_enabled_id = -1; + for (auto i = 0; i < m_required_data_plate_data_list[m_print_plate_idx]->slice_filaments_info.size(); i++) { FilamentInfo fo = m_required_data_plate_data_list[m_print_plate_idx]->slice_filaments_info[i]; @@ -4481,6 +4511,17 @@ void SelectMachineDialog::set_default_from_sdcard() m_sizer_ams_mapping->Add(item, 0, wxALL, FromDIP(5)); } + if (!item) continue; + + if (!selected_any && fo.id == m_current_filament_id && item->m_enable) { + item->on_selected(); + selected_any = true; + } + if (!first_enabled && item->m_enable) { + first_enabled = item; + first_enabled_id = fo.id; + } + item->Bind(wxEVT_LEFT_UP, [this, item, materials](wxMouseEvent& e) {}); item->Bind(wxEVT_LEFT_DOWN, [this, obj_, item, materials, diameters_count, fo](wxMouseEvent& e) { if (!item->m_enable) {return;} @@ -4557,6 +4598,11 @@ void SelectMachineDialog::set_default_from_sdcard() wxSize screenSize = wxGetDisplaySize(); auto dialogSize = this->GetSize(); + if (!selected_any && first_enabled) { + m_current_filament_id = first_enabled_id; + first_enabled->on_selected(); + } + reset_ams_material(); // basic info From ae5d9c26fb47963947a6679d6e262359ccead46e Mon Sep 17 00:00:00 2001 From: "weizhen.xie" Date: Mon, 24 Nov 2025 11:36:49 +0800 Subject: [PATCH 26/30] ENH: Add filament ams/chamber parameters Jira: None Change-Id: I4c0a2d5b7e1c503fc9af9eb656fc6325e99af1de (cherry picked from commit ce88e7794cb8786fae85603617757628a963e807) --- src/libslic3r/Preset.cpp | 6 +++++- src/libslic3r/PrintConfig.cpp | 24 ++++++++++++++++++++++++ src/libslic3r/PrintConfig.hpp | 9 +++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 6d08ca9f2d..dbdd48a0df 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1348,7 +1348,11 @@ static std::vector s_Preset_filament_options {/*"filament_colour", "filament_long_retractions_when_cut","filament_retraction_distances_when_cut", "idle_temperature", //BBS filament change length while the extruder color "filament_change_length","filament_flush_volumetric_speed","filament_flush_temp", "filament_cooling_before_tower", - "long_retractions_when_ec", "retraction_distances_when_ec" + "long_retractions_when_ec", "retraction_distances_when_ec", + //ams chamber + "filament_dev_ams_drying_ams_limitations", "filament_dev_ams_drying_temperature", "filament_dev_ams_drying_time", "filament_dev_ams_drying_heat_distortion_temperature", + "filament_dev_chamber_drying_bed_temperature", "filament_dev_chamber_drying_time", + "filament_dev_drying_softening_temperature", "filament_dev_drying_cooling_temperature" }; static std::vector s_Preset_machine_limits_options { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index dda24e4e5b..c053b3f8fa 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7307,6 +7307,30 @@ void PrintConfigDef::init_fff_params() def->min = 0; def->set_default_value(new ConfigOptionPercent(85)); + def = this->add("filament_dev_ams_drying_ams_limitations", coStrings); + def->set_default_value(new ConfigOptionStrings{""}); + + def = this->add("filament_dev_ams_drying_temperature", coFloats); + def->set_default_value(new ConfigOptionFloats{0}); + + def = this->add("filament_dev_ams_drying_time", coFloats); + def->set_default_value(new ConfigOptionFloats{0}); + + def = this->add("filament_dev_ams_drying_heat_distortion_temperature", coFloats); + def->set_default_value(new ConfigOptionFloats{0}); + + def = this->add("filament_dev_chamber_drying_bed_temperature", coFloats); + def->set_default_value(new ConfigOptionFloats{0}); + + def = this->add("filament_dev_chamber_drying_time", coFloats); + def->set_default_value(new ConfigOptionFloats{0}); + + def = this->add("filament_dev_drying_softening_temperature", coFloats); + def->set_default_value(new ConfigOptionFloats{0}); + + def = this->add("filament_dev_drying_cooling_temperature", coFloats); + def->set_default_value(new ConfigOptionFloats{0}); + // Declare retract values for filament profile, overriding the printer's extruder profile. for (auto& opt_key : filament_extruder_override_keys) { const std::string filament_prefix = "filament_"; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 3b8b809c78..2948f24417 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1490,6 +1490,15 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionStrings, small_area_infill_flow_compensation_model)) ((ConfigOptionBool, has_scarf_joint_seam)) + //ams chamber + ((ConfigOptionStrings, filament_dev_ams_drying_ams_limitations)) + ((ConfigOptionFloats, filament_dev_ams_drying_temperature)) + ((ConfigOptionFloats, filament_dev_ams_drying_time)) + ((ConfigOptionFloats, filament_dev_ams_drying_heat_distortion_temperature)) + ((ConfigOptionFloats, filament_dev_chamber_drying_bed_temperature)) + ((ConfigOptionFloats, filament_dev_chamber_drying_time)) + ((ConfigOptionFloats, filament_dev_drying_softening_temperature)) + ((ConfigOptionFloats, filament_dev_drying_cooling_temperature)) ) // This object is mapped to Perl as Slic3r::Config::Print. From 4227a192cd35e4e18db2ea3e590d1dab625b9d17 Mon Sep 17 00:00:00 2001 From: shsst Date: Wed, 10 Dec 2025 21:07:39 +0800 Subject: [PATCH 27/30] ENH:[Process/Filament] MOD: filament_ams parameters Profile Edited by lianhu.xiong Change-Id: I2f540d0afd6ba368100bc88433cdf2024a1c7d3c (cherry picked from commit 316d8461476d65a85de8dba41a861877c358dba5) --- .../BBL/filament/Bambu ASA-Aero @base.json | 4 +-- .../BBL/filament/Bambu ASA-CF @base.json | 3 ++ .../BBL/filament/Bambu PAHT-CF @base.json | 3 ++ .../BBL/filament/Bambu PET-CF @base.json | 21 +++++++++++++ .../Bambu Support For PA PET @base.json | 18 +++++++++++ .../filament/Bambu Support For PLA @base.json | 15 +++++++-- .../Bambu Support For PLA-PETG @base.json | 18 +++++++++-- .../BBL/filament/Bambu Support G @base.json | 21 +++++++++++-- .../filament/Bambu Support for ABS @base.json | 9 ++++-- .../BBL/filament/Generic PA @base.json | 15 +++++++++ .../BBL/filament/Generic PCTG @base.json | 28 +++++++++++++++++ .../BBL/filament/Generic PPS @base.json | 23 +++++++++++++- .../Polymaker/Fiberon PET-CF @base.json | 27 ++++++++++++++-- .../BBL/filament/fdm_filament_abs.json | 21 +++++++++++++ .../BBL/filament/fdm_filament_asa.json | 21 +++++++++++++ .../BBL/filament/fdm_filament_bvoh.json | 28 +++++++++++++++++ .../BBL/filament/fdm_filament_eva.json | 28 +++++++++++++++++ .../BBL/filament/fdm_filament_hips.json | 24 ++++++++++++-- .../BBL/filament/fdm_filament_pa.json | 24 ++++++++++++++ .../BBL/filament/fdm_filament_pc.json | 21 +++++++++++++ .../BBL/filament/fdm_filament_pctg.json | 31 +++++++++++++++++++ .../BBL/filament/fdm_filament_pe.json | 25 +++++++++++++++ .../BBL/filament/fdm_filament_pet.json | 28 +++++++++++++++++ .../BBL/filament/fdm_filament_pha.json | 25 +++++++++++++++ .../BBL/filament/fdm_filament_pla.json | 31 +++++++++++++++++-- .../BBL/filament/fdm_filament_pp.json | 28 +++++++++++++++++ .../BBL/filament/fdm_filament_ppa.json | 28 +++++++++++++++++ .../BBL/filament/fdm_filament_pps.json | 25 +++++++++++++++ .../BBL/filament/fdm_filament_pva.json | 21 +++++++++++++ .../BBL/filament/fdm_filament_tpu.json | 24 ++++++++++++++ 30 files changed, 614 insertions(+), 24 deletions(-) diff --git a/resources/profiles/BBL/filament/Bambu ASA-Aero @base.json b/resources/profiles/BBL/filament/Bambu ASA-Aero @base.json index 2dcf633a26..1a36118d7f 100644 --- a/resources/profiles/BBL/filament/Bambu ASA-Aero @base.json +++ b/resources/profiles/BBL/filament/Bambu ASA-Aero @base.json @@ -42,8 +42,8 @@ "filament_z_hop_types": [ "Normal Lift" ], - "filament_scarf_seam_type": [ - "none" + "filament_dev_drying_cooling_temperature": [ + "70" ], "impact_strength_z": [ "3.4" diff --git a/resources/profiles/BBL/filament/Bambu ASA-CF @base.json b/resources/profiles/BBL/filament/Bambu ASA-CF @base.json index 9c425dba90..69624032bb 100644 --- a/resources/profiles/BBL/filament/Bambu ASA-CF @base.json +++ b/resources/profiles/BBL/filament/Bambu ASA-CF @base.json @@ -32,6 +32,9 @@ "filament_vendor": [ "Bambu Lab" ], + "filament_dev_drying_cooling_temperature": [ + "95" + ], "hot_plate_temp": [ "100" ], diff --git a/resources/profiles/BBL/filament/Bambu PAHT-CF @base.json b/resources/profiles/BBL/filament/Bambu PAHT-CF @base.json index 54122f3bbf..28b1391f84 100644 --- a/resources/profiles/BBL/filament/Bambu PAHT-CF @base.json +++ b/resources/profiles/BBL/filament/Bambu PAHT-CF @base.json @@ -33,6 +33,9 @@ "full_fan_speed_layer": [ "2" ], + "filament_dev_chamber_drying_time": [ + "16" + ], "impact_strength_z": [ "13.3" ], diff --git a/resources/profiles/BBL/filament/Bambu PET-CF @base.json b/resources/profiles/BBL/filament/Bambu PET-CF @base.json index b982cd1361..ff8b40070f 100644 --- a/resources/profiles/BBL/filament/Bambu PET-CF @base.json +++ b/resources/profiles/BBL/filament/Bambu PET-CF @base.json @@ -45,6 +45,27 @@ "filament_adhesiveness_category": [ "800" ], + "filament_dev_chamber_drying_bed_temperature": [ + "90" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "165" + ], + "filament_dev_drying_cooling_temperature": [ + "150" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "80" + ], + "filament_dev_drying_softening_temperature": [ + "150" + ], "hot_plate_temp": [ "100" ], diff --git a/resources/profiles/BBL/filament/Bambu Support For PA PET @base.json b/resources/profiles/BBL/filament/Bambu Support For PA PET @base.json index a622bff105..d199cdb0a0 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PA PET @base.json +++ b/resources/profiles/BBL/filament/Bambu Support For PA PET @base.json @@ -24,6 +24,24 @@ "filament_adhesiveness_category": [ "703" ], + "filament_dev_chamber_drying_time": [ + "16" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "90" + ], + "filament_dev_drying_cooling_temperature": [ + "75" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "80" + ], + "filament_dev_drying_softening_temperature": [ + "85" + ], "nozzle_temperature": [ "280" ], diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA @base.json b/resources/profiles/BBL/filament/Bambu Support For PLA @base.json index 10e4983ebf..9bfdef5bf9 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA @base.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA @base.json @@ -24,11 +24,20 @@ "filament_vendor": [ "Bambu Lab" ], - "filament_scarf_seam_type": [ - "none" - ], "filament_adhesiveness_category": [ "702" + ], + "filament_dev_ams_drying_temperature": [ + "60", + "60", + "50", + "50" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "50" + ], + "filament_dev_drying_softening_temperature": [ + "55" ], "slow_down_layer_time": [ "8" diff --git a/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @base.json b/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @base.json index 0f1b87e83f..38443f5be3 100644 --- a/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @base.json +++ b/resources/profiles/BBL/filament/Bambu Support For PLA-PETG @base.json @@ -27,14 +27,26 @@ "filament_max_volumetric_speed": [ "6" ], - "filament_scarf_seam_type": [ - "none" - ], "filament_vendor": [ "Bambu Lab" ], "filament_adhesiveness_category": [ "705" + ], + "filament_dev_chamber_drying_bed_temperature": [ + "80" + ], + "filament_dev_ams_drying_temperature": [ + "60", + "60", + "50", + "50" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "50" + ], + "filament_dev_drying_softening_temperature": [ + "55" ], "hot_plate_temp": [ "60" diff --git a/resources/profiles/BBL/filament/Bambu Support G @base.json b/resources/profiles/BBL/filament/Bambu Support G @base.json index c903f92d07..9cb01972b8 100644 --- a/resources/profiles/BBL/filament/Bambu Support G @base.json +++ b/resources/profiles/BBL/filament/Bambu Support G @base.json @@ -21,11 +21,26 @@ "filament_vendor": [ "Bambu Lab" ], - "filament_scarf_seam_type": [ - "none" - ], "filament_adhesiveness_category": [ "701" + ], + "filament_dev_chamber_drying_time": [ + "16" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "90" + ], + "filament_dev_drying_cooling_temperature": [ + "75" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "80" + ], + "filament_dev_drying_softening_temperature": [ + "85" ], "nozzle_temperature": [ "280" diff --git a/resources/profiles/BBL/filament/Bambu Support for ABS @base.json b/resources/profiles/BBL/filament/Bambu Support for ABS @base.json index 35e13e67a0..bc8867db2a 100644 --- a/resources/profiles/BBL/filament/Bambu Support for ABS @base.json +++ b/resources/profiles/BBL/filament/Bambu Support for ABS @base.json @@ -27,11 +27,14 @@ "filament_vendor": [ "Bambu Lab" ], - "filament_scarf_seam_type": [ - "none" - ], "filament_adhesiveness_category": [ "706" + ], + "filament_dev_ams_drying_time": [ + "12", + "4", + "12", + "4" ], "nozzle_temperature_range_high": [ "270" diff --git a/resources/profiles/BBL/filament/Generic PA @base.json b/resources/profiles/BBL/filament/Generic PA @base.json index 5947f5a859..fe819487b0 100644 --- a/resources/profiles/BBL/filament/Generic PA @base.json +++ b/resources/profiles/BBL/filament/Generic PA @base.json @@ -21,6 +21,21 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "80" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "90" + ], + "filament_dev_drying_cooling_temperature": [ + "65" + ], + "filament_dev_drying_softening_temperature": [ + "85" + ], "nozzle_temperature": [ "260" ], diff --git a/resources/profiles/BBL/filament/Generic PCTG @base.json b/resources/profiles/BBL/filament/Generic PCTG @base.json index d0ec60848f..e8b7c1ce0a 100644 --- a/resources/profiles/BBL/filament/Generic PCTG @base.json +++ b/resources/profiles/BBL/filament/Generic PCTG @base.json @@ -38,6 +38,34 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_dev_chamber_drying_bed_temperature": [ + "80" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1", + "0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "75" + ], + "filament_dev_drying_cooling_temperature": [ + "55" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "65", + "55", + "55" + ], + "filament_dev_drying_softening_temperature": [ + "60" + ], "hot_plate_temp": [ "70" ], diff --git a/resources/profiles/BBL/filament/Generic PPS @base.json b/resources/profiles/BBL/filament/Generic PPS @base.json index e8da776dfc..882fea94e0 100644 --- a/resources/profiles/BBL/filament/Generic PPS @base.json +++ b/resources/profiles/BBL/filament/Generic PPS @base.json @@ -4,5 +4,26 @@ "inherits": "fdm_filament_pps", "from": "system", "filament_id": "GFT97", - "instantiation": "false" + "instantiation": "false", + "filament_dev_chamber_drying_bed_temperature": [ + "90" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "90" + ], + "filament_dev_drying_cooling_temperature": [ + "80" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "80" + ], + "filament_dev_drying_softening_temperature": [ + "85" + ] } diff --git a/resources/profiles/BBL/filament/Polymaker/Fiberon PET-CF @base.json b/resources/profiles/BBL/filament/Polymaker/Fiberon PET-CF @base.json index 921ce55dd6..365ebb2fd7 100644 --- a/resources/profiles/BBL/filament/Polymaker/Fiberon PET-CF @base.json +++ b/resources/profiles/BBL/filament/Polymaker/Fiberon PET-CF @base.json @@ -41,6 +41,30 @@ "filament_vendor": [ "Polymaker" ], + "filament_adhesiveness_category": [ + "800" + ], + "filament_dev_chamber_drying_bed_temperature": [ + "90" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "165" + ], + "filament_dev_drying_cooling_temperature": [ + "150" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "80" + ], + "filament_dev_drying_softening_temperature": [ + "150" + ], "hot_plate_temp": [ "70" ], @@ -83,9 +107,6 @@ "textured_plate_temp_initial_layer": [ "70" ], - "filament_adhesiveness_category": [ - "800" - ], "filament_start_gcode": [ "; filament start gcode\n{if (bed_temperature[current_extruder] >80)||(bed_temperature_initial_layer[current_extruder] >80)}M106 P3 S255\n{elsif (bed_temperature[current_extruder] >60)||(bed_temperature_initial_layer[current_extruder] >60)}M106 P3 S180\n{endif}\n\n{if activate_air_filtration[current_extruder] && support_air_filtration}\nM106 P3 S{during_print_exhaust_fan_speed_num[current_extruder]} \n{endif}" ] diff --git a/resources/profiles/BBL/filament/fdm_filament_abs.json b/resources/profiles/BBL/filament/fdm_filament_abs.json index d05b005db9..4eb47ce0bd 100644 --- a/resources/profiles/BBL/filament/fdm_filament_abs.json +++ b/resources/profiles/BBL/filament/fdm_filament_abs.json @@ -43,6 +43,27 @@ "filament_adhesiveness_category": [ "200" ], + "filament_dev_ams_drying_time": [ + "12", + "8.0", + "12", + "8.0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "90" + ], + "filament_dev_drying_cooling_temperature": [ + "75" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "75" + ], + "filament_dev_drying_softening_temperature": [ + "80" + ], "hot_plate_temp": [ "90" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_asa.json b/resources/profiles/BBL/filament/fdm_filament_asa.json index de053b5e14..b5ae3ad8b5 100644 --- a/resources/profiles/BBL/filament/fdm_filament_asa.json +++ b/resources/profiles/BBL/filament/fdm_filament_asa.json @@ -43,6 +43,27 @@ "filament_adhesiveness_category": [ "200" ], + "filament_dev_ams_drying_time": [ + "12", + "8.0", + "12", + "8.0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "100" + ], + "filament_dev_drying_cooling_temperature": [ + "85" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "80" + ], + "filament_dev_drying_softening_temperature": [ + "85" + ], "hot_plate_temp": [ "90" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_bvoh.json b/resources/profiles/BBL/filament/fdm_filament_bvoh.json index 1a766a5a71..ee15cc5e30 100644 --- a/resources/profiles/BBL/filament/fdm_filament_bvoh.json +++ b/resources/profiles/BBL/filament/fdm_filament_bvoh.json @@ -43,6 +43,34 @@ "filament_type": [ "BVOH" ], + "filament_dev_chamber_drying_bed_temperature": [ + "80" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1", + "0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "65" + ], + "filament_dev_drying_cooling_temperature": [ + "40" + ], + "filament_dev_ams_drying_temperature": [ + "60", + "60", + "45", + "45" + ], + "filament_dev_drying_softening_temperature": [ + "50" + ], "hot_plate_temp": [ "55" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_eva.json b/resources/profiles/BBL/filament/fdm_filament_eva.json index 283606c21e..a3f9ab0011 100644 --- a/resources/profiles/BBL/filament/fdm_filament_eva.json +++ b/resources/profiles/BBL/filament/fdm_filament_eva.json @@ -7,6 +7,34 @@ "filament_type": [ "EVA" ], + "filament_dev_chamber_drying_bed_temperature": [ + "70" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1", + "0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "45" + ], + "filament_dev_drying_cooling_temperature": [ + "40" + ], + "filament_dev_ams_drying_temperature": [ + "45", + "45", + "45", + "45" + ], + "filament_dev_drying_softening_temperature": [ + "50" + ], "supertack_plate_temp": [ "0" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_hips.json b/resources/profiles/BBL/filament/fdm_filament_hips.json index 25c0acbb6f..7016a866f6 100644 --- a/resources/profiles/BBL/filament/fdm_filament_hips.json +++ b/resources/profiles/BBL/filament/fdm_filament_hips.json @@ -4,9 +4,6 @@ "inherits": "fdm_filament_common", "from": "system", "instantiation": "false", - "additional_cooling_fan_speed": [ - "0" - ], "cool_plate_temp": [ "0" ], @@ -42,6 +39,27 @@ ], "filament_adhesiveness_category": [ "798" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "90" + ], + "filament_dev_drying_cooling_temperature": [ + "75" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "75" + ], + "filament_dev_drying_softening_temperature": [ + "80" ], "hot_plate_temp": [ "90" diff --git a/resources/profiles/BBL/filament/fdm_filament_pa.json b/resources/profiles/BBL/filament/fdm_filament_pa.json index 629ef70885..61aeed66ce 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pa.json +++ b/resources/profiles/BBL/filament/fdm_filament_pa.json @@ -43,6 +43,30 @@ "filament_adhesiveness_category": [ "400" ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_chamber_drying_time": [ + "18" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "165" + ], + "filament_dev_drying_cooling_temperature": [ + "150" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "85", + "65", + "85" + ], + "filament_dev_drying_softening_temperature": [ + "150" + ], "hot_plate_temp": [ "100" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_pc.json b/resources/profiles/BBL/filament/fdm_filament_pc.json index 11dcf5ea46..a73a56b440 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pc.json +++ b/resources/profiles/BBL/filament/fdm_filament_pc.json @@ -40,6 +40,27 @@ "filament_adhesiveness_category": [ "500" ], + "filament_dev_ams_drying_time": [ + "12", + "8.0", + "12", + "8.0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "105" + ], + "filament_dev_drying_cooling_temperature": [ + "90" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "80", + "65", + "80" + ], + "filament_dev_drying_softening_temperature": [ + "90" + ], "hot_plate_temp": [ "110" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_pctg.json b/resources/profiles/BBL/filament/fdm_filament_pctg.json index f735c26f48..01810a2ddc 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pctg.json +++ b/resources/profiles/BBL/filament/fdm_filament_pctg.json @@ -28,6 +28,37 @@ "filament_type": [ "PCTG" ], + "filament_dev_chamber_drying_bed_temperature": [ + "80" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1", + "0" + ], + "filament_dev_chamber_drying_time": [ + "12" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "75" + ], + "filament_dev_drying_cooling_temperature": [ + "55" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "65", + "55", + "55" + ], + "filament_dev_drying_softening_temperature": [ + "60" + ], "hot_plate_temp": [ "80" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_pe.json b/resources/profiles/BBL/filament/fdm_filament_pe.json index 51549b2d8d..9235c3301e 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pe.json +++ b/resources/profiles/BBL/filament/fdm_filament_pe.json @@ -40,6 +40,31 @@ "filament_type": [ "PE" ], + "filament_dev_chamber_drying_bed_temperature": [ + "70" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1", + "0" + ], + "filament_dev_drying_cooling_temperature": [ + "50" + ], + "filament_dev_ams_drying_temperature": [ + "45", + "45", + "45", + "45" + ], + "filament_dev_drying_softening_temperature": [ + "50" + ], "hot_plate_temp": [ "55" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_pet.json b/resources/profiles/BBL/filament/fdm_filament_pet.json index 8fde5f2faa..23a8f73fe4 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pet.json +++ b/resources/profiles/BBL/filament/fdm_filament_pet.json @@ -31,6 +31,34 @@ "filament_adhesiveness_category": [ "300" ], + "filament_dev_chamber_drying_bed_temperature": [ + "80" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1", + "0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "75" + ], + "filament_dev_drying_cooling_temperature": [ + "55" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "65", + "55", + "55" + ], + "filament_dev_drying_softening_temperature": [ + "60" + ], "hot_plate_temp": [ "80" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_pha.json b/resources/profiles/BBL/filament/fdm_filament_pha.json index b9ac18587d..2bd348e2de 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pha.json +++ b/resources/profiles/BBL/filament/fdm_filament_pha.json @@ -40,6 +40,31 @@ "filament_type": [ "PHA" ], + "filament_dev_chamber_drying_bed_temperature": [ + "70" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1", + "0" + ], + "filament_dev_drying_cooling_temperature": [ + "45" + ], + "filament_dev_ams_drying_temperature": [ + "45", + "45", + "45", + "45" + ], + "filament_dev_drying_softening_temperature": [ + "50" + ], "hot_plate_temp": [ "55" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_pla.json b/resources/profiles/BBL/filament/fdm_filament_pla.json index 3c6713b86d..f1e1c9a244 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pla.json +++ b/resources/profiles/BBL/filament/fdm_filament_pla.json @@ -37,14 +37,39 @@ "filament_max_volumetric_speed": [ "12" ], - "filament_scarf_seam_type": [ - "none" - ], "filament_scarf_gap": [ "15%" ], "filament_adhesiveness_category": [ "100" + ], + "filament_dev_chamber_drying_bed_temperature": [ + "70" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1", + "0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "45" + ], + "filament_dev_drying_cooling_temperature": [ + "45" + ], + "filament_dev_ams_drying_temperature": [ + "45", + "45", + "45", + "45" + ], + "filament_dev_drying_softening_temperature": [ + "50" ], "hot_plate_temp": [ "55" diff --git a/resources/profiles/BBL/filament/fdm_filament_pp.json b/resources/profiles/BBL/filament/fdm_filament_pp.json index dd390e0a82..2d8c3772ba 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pp.json +++ b/resources/profiles/BBL/filament/fdm_filament_pp.json @@ -43,6 +43,34 @@ "filament_adhesiveness_category": [ "902" ], + "filament_dev_chamber_drying_bed_temperature": [ + "70" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1", + "0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "60" + ], + "filament_dev_drying_cooling_temperature": [ + "50" + ], + "filament_dev_ams_drying_temperature": [ + "60", + "60", + "50", + "50" + ], + "filament_dev_drying_softening_temperature": [ + "55" + ], "hot_plate_temp": [ "55" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_ppa.json b/resources/profiles/BBL/filament/fdm_filament_ppa.json index bb06a73c76..3bba318bc5 100644 --- a/resources/profiles/BBL/filament/fdm_filament_ppa.json +++ b/resources/profiles/BBL/filament/fdm_filament_ppa.json @@ -46,6 +46,34 @@ "filament_vendor": [ "Bambu Lab" ], + "filament_dev_chamber_drying_bed_temperature": [ + "100" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [], + "filament_dev_chamber_drying_time": [ + "18" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "165" + ], + "filament_dev_drying_cooling_temperature": [ + "150" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "85", + "65", + "85" + ], + "filament_dev_drying_softening_temperature": [ + "150" + ], "hot_plate_temp": [ "100" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_pps.json b/resources/profiles/BBL/filament/fdm_filament_pps.json index a19a5aca2b..d1c6a6aed2 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pps.json +++ b/resources/profiles/BBL/filament/fdm_filament_pps.json @@ -43,6 +43,31 @@ "filament_adhesiveness_category": [ "801" ], + "filament_dev_chamber_drying_bed_temperature": [ + "100" + ], + "filament_dev_ams_drying_time": [ + "12", + "12", + "12", + "12" + ], + "filament_dev_ams_drying_ams_limitations": [], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "165" + ], + "filament_dev_drying_cooling_temperature": [ + "150" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "85", + "65", + "85" + ], + "filament_dev_drying_softening_temperature": [ + "150" + ], "hot_plate_temp": [ "110" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_pva.json b/resources/profiles/BBL/filament/fdm_filament_pva.json index 98ca7e63b9..55d83517ce 100644 --- a/resources/profiles/BBL/filament/fdm_filament_pva.json +++ b/resources/profiles/BBL/filament/fdm_filament_pva.json @@ -49,6 +49,27 @@ "filament_adhesiveness_category": [ "704" ], + "filament_dev_ams_drying_time": [ + "12", + "18", + "12", + "18" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "75" + ], + "filament_dev_drying_cooling_temperature": [ + "40" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "85", + "65", + "70" + ], + "filament_dev_drying_softening_temperature": [ + "75" + ], "hot_plate_temp": [ "55" ], diff --git a/resources/profiles/BBL/filament/fdm_filament_tpu.json b/resources/profiles/BBL/filament/fdm_filament_tpu.json index f6ab1d3edf..d2ef5db84e 100644 --- a/resources/profiles/BBL/filament/fdm_filament_tpu.json +++ b/resources/profiles/BBL/filament/fdm_filament_tpu.json @@ -46,6 +46,30 @@ "filament_adhesiveness_category": [ "600" ], + "filament_dev_ams_drying_time": [ + "12", + "18", + "12", + "18" + ], + "filament_dev_chamber_drying_time": [ + "16" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "45" + ], + "filament_dev_drying_cooling_temperature": [ + "40" + ], + "filament_dev_ams_drying_temperature": [ + "65", + "75", + "45", + "45" + ], + "filament_dev_drying_softening_temperature": [ + "50" + ], "hot_plate_temp": [ "35" ], From 7f67946dbf4eb74283e22ad8bb4e29607bce5412 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 22:02:10 +0800 Subject: [PATCH 28/30] Fix incorrect length modification when loading device-related parameters (cherry picked from commit bambulab/BambuStudio@ebf57372da54e7515d3a469b0dbe468ff16e9aed) Co-Authored-By: weizhen.xie --- src/libslic3r/Preset.cpp | 2 ++ src/libslic3r/PrintConfig.cpp | 11 +++++++++++ src/libslic3r/PrintConfig.hpp | 2 ++ 3 files changed, 15 insertions(+) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index dbdd48a0df..077814130d 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -470,6 +470,8 @@ void Preset::normalize(DynamicPrintConfig &config) continue; if (filament_options_with_variant.find(key) != filament_options_with_variant.end()) continue; + if (filament_dev_options.find(key) != filament_dev_options.end()) + continue; auto *opt = config.option(key, false); /*assert(opt != nullptr); assert(opt->is_vector());*/ diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c053b3f8fa..7c116dce3e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -8538,6 +8538,17 @@ std::set printer_options_with_variant_2 = { std::set empty_options; +std::set filament_dev_options = { + "filament_dev_ams_drying_ams_limitations", + "filament_dev_ams_drying_temperature", + "filament_dev_ams_drying_time", + "filament_dev_ams_drying_heat_distortion_temperature", + "filament_dev_chamber_drying_bed_temperature", + "filament_dev_chamber_drying_time", + "filament_dev_drying_softening_temperature", + "filament_dev_drying_cooling_temperature" +}; + DynamicPrintConfig DynamicPrintConfig::full_print_config() { return DynamicPrintConfig((const PrintRegionConfig&)FullPrintConfig::defaults()); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 2948f24417..6ea1f9db1c 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -698,6 +698,8 @@ extern std::set printer_options_with_variant_1; extern std::set printer_options_with_variant_2; extern std::set empty_options; +extern std::set filament_dev_options; + extern void compute_filament_override_value(const std::string& opt_key, const ConfigOption *opt_old_machine, const ConfigOption *opt_new_machine, const ConfigOption *opt_new_filament, const DynamicPrintConfig& new_full_config, t_config_option_keys& diff_keys, DynamicPrintConfig& filament_overrides, std::vector& f_maps); From 696a94a1a6c76a4bec04072b735b8f8af83726f1 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 22:29:28 +0800 Subject: [PATCH 29/30] Update color --- src/slic3r/GUI/AMSDryControl.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp index 98b35b5809..9efe7faa87 100644 --- a/src/slic3r/GUI/AMSDryControl.cpp +++ b/src/slic3r/GUI/AMSDryControl.cpp @@ -86,7 +86,7 @@ FilamentItemPanel::FilamentItemPanel(wxWindow* parent, const wxString& text, con : wxPanel(parent, id) , m_icon_name(icon_name) { - SetBackgroundColour(wxColour("#F7F7F7")); // Light gray background + SetBackgroundColour(wxColour("#F0F0F1")); // Light gray background SetMinSize(wxSize(FromDIP(64), FromDIP(106))); // Width: 64, Height: 106 SetSize(wxSize(FromDIP(64), FromDIP(106))); // Fixed size @@ -99,7 +99,7 @@ FilamentItemPanel::FilamentItemPanel(wxWindow* parent, const wxString& text, con m_text_label = new Label(this, text); m_text_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour(*wxBLACK))); - m_text_label->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F7F7F7"))); + m_text_label->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F0F0F1"))); m_text_label->SetFont(Label::Body_12); m_text_label->Wrap(FromDIP(40)); top_sizer->Add(m_text_label, 0, wxALIGN_CENTER_HORIZONTAL); @@ -112,7 +112,7 @@ FilamentItemPanel::FilamentItemPanel(wxWindow* parent, const wxString& text, con bottom_sizer->AddStretchSpacer(1); m_icon_bitmap = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap); - m_icon_bitmap->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F7F7F7"))); + m_icon_bitmap->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F0F0F1"))); m_icon_bitmap->SetMinSize(wxSize(FromDIP(24), FromDIP(24))); m_icon_bitmap->SetMaxSize(wxSize(FromDIP(24), FromDIP(24))); bottom_sizer->Add(m_icon_bitmap, 0, wxALIGN_CENTER_HORIZONTAL); @@ -170,7 +170,7 @@ void FilamentItemPanel::OnPaint(wxPaintEvent& event) wxSize size = GetSize(); // bool is_dark_mode = wxGetApp().dark_mode(); - wxColour backgroundColor = StateColor::darkModeColorFor(wxColour("#F7F7F7")); + wxColour backgroundColor = StateColor::darkModeColorFor(wxColour("#F0F0F1")); wxColour borderColor = StateColor::darkModeColorFor(wxColour("#DBDBDB")); // Draw white background rectangle with rounded corners inside the thick vertical lines @@ -208,7 +208,7 @@ AMSFilamentPanel::AMSFilamentPanel(wxWindow* parent, const wxString& ams_name, w // Filament items section m_filament_container = new wxPanel(this); - m_filament_container->SetBackgroundColour(wxColour("#F7F7F7")); + m_filament_container->SetBackgroundColour(wxColour("#F0F0F1")); m_filament_sizer = new wxBoxSizer(wxHORIZONTAL); m_filament_container->SetSizer(m_filament_sizer); From 40fd22530870ed65828231c669d76d8c189f3f4f Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 7 Jul 2026 23:16:04 +0800 Subject: [PATCH 30/30] Make sure new strings are translated --- localization/i18n/list.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index d968cd5322..d63686b1da 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -274,4 +274,6 @@ src/slic3r/GUI/DownloaderFileGet.cpp src/slic3r/GUI/FileArchiveDialog.cpp src/slic3r/GUI/PrinterCloudAuthDialog.cpp src/slic3r/GUI/PrinterWebViewHandler.cpp +src/slic3r/GUI/AMSDryControl.cpp +src/slic3r/GUI/AMSDryControl.hpp src/libslic3r/PresetBundle.cpp