Browse Source

Merge pull request #3223 from maziggy/1.2.5.7

**Bambuddy 1.2.5.7**

**What this is**

A feature release on top of 1.2.5.6. 25 new features, four changes, 40 fixes and four dependency advisories closed. 29 community PRs were merged, and 28 people are credited.

The headline is **post-print outcome confirmation**. Until now a print that completed counted as a success, even if the part was warped or the wrong colour. A print can now ask whether it came out good, and the answer can come from the web UI, the printer card, a notification, or a 👍 / 👎 on the Telegram message. The File Manager gets the most new features: a Column view, larger previews with zoom and fullscreen, image previews, PDF thumbnails made on the server, notes, links and photos on library files, and **Combine to 3MF** for putting several STLs on one plate. Inventory gains material numbers and a managed supplier list. SSO logins can keep Bambuddy groups in step with the identity provider. Other applications can now sign people in with their Bambuddy account and send messages through your notification channels. Bambuddy itself can now show announcements from the maintainers.

Several of the fixes are notifications that had a toggle but never fired: Low Filament, Reorder Alert and Stock Break Alert all send now. Read **Before you update** below: some alerts will fire right after the upgrade.

New tables and columns are added automatically on both SQLite and PostgreSQL. There are no breaking API changes, but HMS fault `severity` values change; see below.

If you are coming from 1.2.5.5 or earlier, read the 1.2.5.6 release notes too. If you are coming from 1.2.5 or earlier, read the 1.2.5 notes first; all of their upgrade callouts apply to you as well.

**Before you update**

These change behaviour you may rely on. None of them needs action unless it applies to your setup.

- **Notifications that never fired now do.** Low Filament (#2913), Reorder Alert and Stock Break Alert (#2955) had toggles on every provider, but nothing ever sent them. If you switched Low Filament on in the past, expect one alert for each assigned spool that is already low right after updating, and again after each restart while it stays low. Stock alerts fire once per SKU when it reaches its reorder point or will run out before a reorder could arrive.

- **HMS faults use the printer's own level (#2728).** The status response, WebSocket, Camera Wall and MQTT relay carry the new `severity` values, so anything that reads `severity` will see different numbers for the same fault. Printer-error notifications now also go out for `hms[]` faults that have a description, which almost never happened before, so you may get more of them.

- **The queue starts jobs in the order the queue page shows (#3200).** Jobs pinned to a printer and jobs queued for "Any <model>" used to be ordered separately, and which one won a printer depended on the database. If your queue mixes both, jobs may now start in a different order than you are used to, namely the one on screen.

- **Spoolman: each spool's own size is used (#3194).** Editing **Label Weight** now changes only that spool, not its filament. **Cost per kg** is converted at the spool's size, both ways. For 1000 g spools nothing changes. Spools that an earlier version reset with **Reset usage to 0** had their remaining weight overwritten in Spoolman and need re-weighing once (#2906).

- **Announcements from the maintainers.** Bambuddy now fetches one signed file from the public `maziggy/bambuddy-notifications` repo on GitHub, at startup and every 6 hours. Nothing about your install is sent. Administrators see them by default. **Settings → General → Updates** can show them to every user, or turn them off entirely, in which case nothing is fetched.

- **PostgreSQL: the default connection pool is now 80 instead of 100.** A stock PostgreSQL allows 97, so every install without its own `DB_POOL_SIZE` / `DB_MAX_OVERFLOW` logged a warning at startup. If you raised the server's `max_connections` and want the old ceiling, set `DB_MAX_OVERFLOW=80`.

- **Telegram reactions need a bot of their own (#3129).** If you choose **Reaction (👍 / 👎)** as a Telegram provider's verdict mode, Bambuddy polls that bot for reactions. Another app polling the same bot, such as Home Assistant's Telegram integration, stops receiving its messages. In a group, the bot must be an admin to see reactions.

- **A printer added by discovery with the wrong model keeps it.** The P-series code table was shifted, so a discovered P1S was saved as a P1P, a P1P as a P1S, and an X1E as a P2S. New printers are saved correctly. For one already saved wrong, pick the right model under **Model** in **Edit** from the printer card's menu.

- **Native installs get one new Python dependency**, `pypdfium2`, for PDF thumbnails. The update script and the manual `pip install -r requirements.txt` below install it; it bundles its own binary, so no system package is needed.

---

**How to update**

**Docker**

```bash
docker compose pull
docker compose up -d
```

**Native install - recommended path**

```bash
sudo BRANCH=main /opt/bambuddy/install/update.sh
```

**Native install - manual path**

```bash
sudo systemctl stop bambuddy
cd /opt/bambuddy
sudo -u bambuddy git fetch --prune --tags --force origin
sudo -u bambuddy git checkout main
sudo -u bambuddy git reset --hard origin/main
sudo /opt/bambuddy/venv/bin/pip install -r requirements.txt
cd frontend && sudo npm i
sudo systemctl start bambuddy
```

**Windows install**

Download `bambuddy-1.2.5.7-windows-x64-setup.exe` from this release page (or the unversioned `bambuddy-windows-x64-setup.exe` alias). Existing Windows installs upgrade in place via the in-app Install Update flow.

---

**New**

**Print outcome:**

- **Post-print outcome confirmation** (#1898, requested by @FedericoPuntelli, contributed by @Thomansky in #3047) - turn on **Ask for Outcome** in the print dialog, or as a default under Settings → Workflow, and Bambuddy asks "How did your print come out?" with the finish photo and **Good** / **Reject**. You can answer in the web UI, on the printer card, or from a notification: ntfy and Telegram get buttons, every other channel a link. A reject takes an optional reason and offers **Print again**. Unanswered prints get an **outcome?** badge and an **Unconfirmed** filter on the Archives page.
- **Answer the outcome prompt with a Telegram reaction** (#3046, requested and contributed by @Thomansky in #3129) - a 👍 or 👎 on the message records the verdict. The phone only talks to Telegram, so it works away from home and needs no external URL. See **Before you update**.

**File Manager:**

- **Column view** (#3020, requested and contributed by @Thomansky in #3190) - a third view mode next to Grid and List, as in the macOS Finder: one column per folder level, with full keyboard navigation and the same actions as the list view.
- **Larger previews with zoom and fullscreen, and image previews** (#2976, requested and contributed by @Thomansky in #2990) - PDF, spreadsheet and 3D previews share one large window with a fullscreen button. PDFs zoom with Ctrl/⌘ + wheel, pinch or keys. PNG, JPG, GIF, WebP and BMP get a preview with zoom and pan. Double-click a file to open its preview.
- **PDF thumbnails as soon as the file arrives** (#2976) - page one is rendered on the server on upload, from ZIPs and from external folders. **Generate Thumbnails** backfills existing PDFs.
- **External link, notes and photos on library files** (#3077, requested by @SergioFuchs, contributed by @Thomansky in #3128) - **File details** opens the file's facts, an editable notes field, a link and a photo gallery.
- **Combine several STLs, or several copies of one, onto one plate** (#2999, requested by @Markus98, contributed by @adman234 in #3162) - select STLs, click **Combine to 3MF**, set the copies, and Bambuddy saves one 3MF with every object on one plate, optionally opening the Slice dialog with auto-arrange on.

**Inventory and spools:**

- **Material numbers** (#2870, requested and contributed by @Thomansky in #2994) - your own number per product, such as `15` for Bambu Lab PLA Basic, filled in automatically for new spools of the same product, with a column, filter, Bulk Edit and a **By Material Number** statistics widget.
- **Suppliers as a managed list** (#2988, requested and contributed by @Thomansky in #2996) - assign any number of suppliers to a spool, each with an article number and a quoted price per kg, mark where it was bought, and see consumption and cost **By Supplier** in Statistics.
- **Find a spool by its label number, and assign it to a slot from the spool** (#2978, requested and contributed by @pd81 in #2998) - `#3` finds only spool 3, and a spool without a slot has an **Assign Spool** button, which is also where scanning its QR code lands.
- **Choose what goes on a spool label, preview it, and save labels as PNG** (#2981, requested by @apizz) - a checkbox per line, a live preview, and PNG output at 203, 300 or 600 dpi for label printer software.
- **Ambient drying can wait for sustained humidity** (#2518, requested by @ryansouza, contributed by @M2ABRAMSTANK in #2895) - opening the AMS lid no longer starts a drying cycle of up to 12 hours. Off by default; 15 minutes when switched on.

**Queue and printing:**

- **Queue cards show the filament each job will print with** (#3132, requested and contributed by @bgrr74 in #3184) - colour swatches and names, the mapped slot and spool, and a yellow warning when a mapped slot has been emptied since.
- **Colour swatches for printer slots in the Print / Schedule dialog** (#3159, requested by @frantiseklorenc) - each slot shows its actual colour and hex, so a real colour mismatch can be told apart from two names for the same colour.
- **A batch can record the external order it fulfils** - `external_source` and `external_ref` on `POST /queue/batches`, unique together, so an integration that retries can't queue the same order twice.

**Notifications and camera:**

- **Camera snapshots reach more notifications and more providers** (#3089, requested and contributed by @bbbenji in #3199) - Plate Not Empty and AI Failure Detection carry a photo; Home Assistant, Bark and Slack-format webhooks get photos too, with an **Attach Photo** switch per provider.
- **Other applications can send messages through your notification channels** - a per-provider **Messages from connected apps** switch, off by default, and an API key with the new **Send Notifications** permission.
- **The streaming overlay can show the printer model** (#3080, requested and contributed by @adamspicedev in #3099 and #3134).
- **A second streaming overlay design, for portrait and landscape sources** (#3177, requested and contributed by @adamspicedev in #3183) - **Artwork: Version 2**; Classic stays the default.

**Accounts, integrations and the app itself:**

- **SSO group sync** (#3107, requested and contributed by @willuhmjs in #3122) - each OIDC provider gains a **Group Claim** and a **Group Mapping**, and the mapped Bambuddy groups follow the identity provider on every login, the same rule as LDAP group mapping.
- **Connected apps** - other applications can sign people in with their Bambuddy account (OAuth 2.0 authorization code with PKCE), registered under Settings → API Keys → Connected Apps. Requires authentication to be enabled.
- **Announcements from the Bambuddy maintainers, inside Bambuddy** - security fixes, breaking changes, releases and calls for testers, signed and fetched from GitHub. See **Before you update**.
- **An app shown in the sidebar can match Bambuddy's theme, and open a Bambuddy page in place** - the framed page is told the theme, and can ask Bambuddy to open one of its own pages. Both only for the link's own origin.
- **The API-key printer status carries layers, the job id, HMS faults and the serial** (#2919, requested by @simplytoast1) - additive; existing fields are unchanged.

**Changed**

- **The Slice dialog uses more of a large screen** - up to 1536 px wide, with a left column that grows with it.
- **A STEP preview says it is converting, and for how long** (#2976) - a large STEP file can take over a minute in the browser, and now shows a running counter instead of a spinner that looked stuck.
- **Leftover Web Push code and the unused `pywebpush` dependency are gone** (#3171).
- **The frontend build no longer warns about `path` and `crypto` for the STEP previewer** (#2976).

**Fixes**

**AMS slots, K profiles and drying:**

- AMS slots that lost their K profile after a printer restart stayed on the default K, and queued jobs printed with it (#3219). Bambuddy now restores a lost selection while the printer is idle and checks again right before a queued job is sent.
- A slot that read empty for one status update lost its spool assignment for good (#3186, reported by @Sawtaytoes).
- Orca Cloud filaments assigned to an AMS slot showed up in OrcaSlicer as Generic (#3216, reported by @mrnoisytiger).
- A drying command the AMS never started showed as an active cycle and could hold the queue forever (#2896, contributed by @M2ABRAMSTANK in #3096).
- Re-reading a slot's RFID reported success when the printer refused, and older firmware now gets the command it understands (#3206, reported by @Sawtaytoes).
- Filament printed from the external spool was deducted from an AMS spool (#3166; also #2880).

**Spools, inventory, Spoolman and SpoolBuddy:**

- In Spoolman mode, a spool's own size was ignored in favour of its filament's, so a 250 g spool was charged as if it held 1000 g (#3194, reported by @worried-networking).
- In Spoolman mode, weighing ignored the vendor's empty spool weight (#3195, reported by @worried-networking).
- A Spoolman spool's own empty spool weight was never saved (#2908, contributed by @ojimpo in #3011).
- "Reset usage to 0" on a Spoolman spool set it back to full (#2906, contributed by @ojimpo in #2939).
- A new Bambu Lab roll in Spoolman mode was linked to another product line of the same colour (#2907, contributed by @ojimpo in #2944).
- SpoolBuddy showed multi-colour and effect spools as one flat colour (#3033, reported by @Sawtaytoes).
- "Print labels…" ignored the spools you had ticked (#2980, reported by @apizz).

**Queue and dispatch:**

- A printer that refused uploads emptied the print queue: one P2S failed 43 jobs in ten minutes (#3210, reported by @Leander-Vh). Those jobs now stay queued and the printer is retried with a growing wait.
- The queue did not start jobs in the order shown when pinned and "Any <model>" jobs were mixed (#3200, reported by @bgrr74).
- A queued file whose only plate isn't plate 1 hung the printer (#2947, contributed by @sgiffhorn in #2951).

**Notifications and HMS:**

- The Reorder Alert and Stock Break Alert notifications were never sent (#2955, contributed by @ojimpo in #3196), and their toggles could never be turned on (#2945, regression tests contributed by @ojimpo in #2956).
- The Low Filament notification never fired (#2913, contributed by @ojimpo in #2940).
- Progress milestones sent 75% at the start of a print, and then never sent 25% or 50% (#3211, reported by @BurgerKerman).
- The `{finish_photo_url}` link in a notification failed when authentication was on.
- HMS faults now show Bambu's own description and the right level, and faults without published text no longer vanish (#2728, reported by @gzimbric).
- A print the printer's AI camera stopped for spaghetti was archived with no failure reason (#2946, contributed by @ojimpo in #2954).

**Camera and stream overlay:**

- A built-in camera could show the same old picture for hours while reporting a healthy stream (#3218, reported by @adamspicedev; also #3189, reported by @Sawtaytoes).
- A live camera view stopped for good after about half an hour on X1, H2 and P2 printers.
- The stream overlay's camera stayed frozen until its browser source was refreshed (#3205, contributed by @adamspicedev in #3213).
- Stream overlay and Cam Wall links stopped working after a reload, and signed the browser out (#3204, contributed by @adamspicedev in #3212).

**Login and directory:**

- LDAP group mapping found no groups on lldap and OpenLDAP, and StartTLS never worked with Active Directory or Samba AD (#3197, reported by @TOFM).
- Remember Me disappeared from the login page when only SSO sign-in was allowed (#2784, contributed by @vuthanhtrung2010 in #3117).

**File Manager and interface:**

- PDF previews failed on any browser older than Chrome 145 or Firefox 144 (#2976).
- The File Manager stopped 64 px short of the bottom of the window (#3215, reported by @koder-guy).
- Number fields could not be cleared and retyped (#3182, reported by @Carter3DP).
- Settings tabs lost their icons when a label was long (#3191, contributed by @Thomansky in #3192).
- Reading a 3MF's details loaded all of its geometry into memory.

**Install, platform and database:**

- Installing or updating ran out of memory on 2 GB machines, such as a standard Proxmox LXC or a 2 GB Raspberry Pi (#3181, reported by @PhilippeP62).
- PostgreSQL installs warned at every start that the connection pool may exceed the server. See **Before you update**.
- Turning off "Check printer firmware" did not stop every firmware lookup.
- Saving an empty value for a number setting through the API broke the Settings page until the database was fixed by hand. An affected install now recovers on its own.
- A P1P, P1S or X1E added by discovery was saved as the wrong printer.
- A connection check that overran in the support bundle discarded everything it had found (#3164).

**Security (dependencies)**

- `PyJWT` to 2.15.1 and `urllib3` to 2.8.0. Bambuddy's session tokens and SSO sign-in were not open to the PyJWT issues; the fixes for malformed tokens and key sets do reach SSO sign-in.
- `dompurify` to 3.4.16, for a low-severity advisory in a mode Bambuddy does not use.
- `js-yaml` to 5.4.2 and `brace-expansion` to 5.0.12, both development-only; neither is in the shipped image.

**Merged community PRs in this release**

- #2895 by @M2ABRAMSTANK - sustained humidity for ambient drying.
- #2939, #2940, #2944, #2954, #2956, #3011, #3196 by @ojimpo - Spoolman reset, empty-weight and product-line fixes, Low Filament and stock alerts, AI spaghetti failure reason.
- #2951 by @sgiffhorn - queued files whose only plate isn't plate 1.
- #2990, #2994, #2996, #3047, #3128, #3129, #3190, #3192 by @Thomansky - File Manager previews, material numbers, suppliers, outcome confirmation, library file details, Telegram reactions, Column view, Settings tabs.
- #2998 by @pd81 - spool ID search and assignment from the spool.
- #3096 by @M2ABRAMSTANK - parked AMS drying commands.
- #3099, #3134, #3183, #3212, #3213 by @adamspicedev - printer model and Version 2 design for the stream overlay, overlay links and camera recovery.
- #3117 by @vuthanhtrung2010 - Remember Me with SSO-only login.
- #3122 by @willuhmjs - SSO group sync.
- #3162 by @adman234 - Combine to 3MF.
- #3184 by @bgrr74 - filament on queue cards.
- #3199 by @bbbenji - camera snapshots for more notifications.

**Thanks**

To everyone who requested, reported, captured logs, or contributed: @adamspicedev, @adman234, @apizz, @bbbenji, @bgrr74, @BurgerKerman, @Carter3DP, @FedericoPuntelli, @frantiseklorenc, @gzimbric, @koder-guy, @Leander-Vh, @M2ABRAMSTANK, @Markus98, @mrnoisytiger, @ojimpo, @pd81, @PhilippeP62, @ryansouza, @Sawtaytoes, @SergioFuchs, @sgiffhorn, @simplytoast1, @Thomansky, @TOFM, @vuthanhtrung2010, @willuhmjs, @worried-networking.

---
**Sponsors**

Bambuddy is sustainable thanks to people who put their money where their use is. If this release saved you time or kept your farm running, the project runs on recurring contributions - there's no paid tier, no telemetry, no upsell, just sustainable maintenance.

- GitHub Sponsors (recurring, 5 tiers from $5/mo to $300/mo) - https://github.com/sponsors/maziggy
- Ko-fi (one-time or recurring) - https://ko-fi.com/maziggy
MartinNYHC 12 hours ago
parent
commit
b3e45f53a0
100 changed files with 17746 additions and 1739 deletions
  1. 14 0
      .env.example
  2. 7 3
      .github/MAINTAINERS.md
  3. 2 0
      .gitignore
  4. 22 0
      CHANGELOG.md
  5. 182 7
      backend/app/api/routes/_spoolman_helpers.py
  6. 65 0
      backend/app/api/routes/announcements.py
  7. 4 0
      backend/app/api/routes/api_keys.py
  8. 270 1
      backend/app/api/routes/archives.py
  9. 7 2
      backend/app/api/routes/auth.py
  10. 104 4
      backend/app/api/routes/camera.py
  11. 5 0
      backend/app/api/routes/camwall.py
  12. 405 0
      backend/app/api/routes/connected_apps.py
  13. 39 5
      backend/app/api/routes/firmware.py
  14. 487 18
      backend/app/api/routes/inventory.py
  15. 194 93
      backend/app/api/routes/labels.py
  16. 472 43
      backend/app/api/routes/library.py
  17. 76 3
      backend/app/api/routes/mfa.py
  18. 9 0
      backend/app/api/routes/notification_templates.py
  19. 119 4
      backend/app/api/routes/notifications.py
  20. 5 0
      backend/app/api/routes/pipeline_runs.py
  21. 48 43
      backend/app/api/routes/print_queue.py
  22. 136 22
      backend/app/api/routes/printers.py
  23. 21 4
      backend/app/api/routes/projects.py
  24. 133 85
      backend/app/api/routes/settings.py
  25. 13 9
      backend/app/api/routes/spoolbuddy.py
  26. 10 3
      backend/app/api/routes/spoolman.py
  27. 219 34
      backend/app/api/routes/spoolman_inventory.py
  28. 27 0
      backend/app/api/routes/users.py
  29. 60 17
      backend/app/api/routes/webhook.py
  30. 66 24
      backend/app/core/auth.py
  31. 4 2
      backend/app/core/config.py
  32. 475 3
      backend/app/core/database.py
  33. 69 3
      backend/app/core/oidc_env.py
  34. 10 0
      backend/app/core/websocket.py
  35. 7799 0
      backend/app/data/hms_catalog.json
  36. 29 0
      backend/app/data/hms_catalog_extra.json
  37. 688 94
      backend/app/main.py
  38. 10 0
      backend/app/models/__init__.py
  39. 51 0
      backend/app/models/announcement.py
  40. 4 0
      backend/app/models/api_key.py
  41. 35 0
      backend/app/models/archive.py
  42. 27 0
      backend/app/models/auth_ephemeral.py
  43. 48 0
      backend/app/models/connected_app.py
  44. 6 0
      backend/app/models/library.py
  45. 54 0
      backend/app/models/notification.py
  46. 16 2
      backend/app/models/notification_template.py
  47. 13 0
      backend/app/models/oidc_provider.py
  48. 10 1
      backend/app/models/print_batch.py
  49. 4 0
      backend/app/models/print_log.py
  50. 3 0
      backend/app/models/print_queue.py
  51. 4 0
      backend/app/models/scheduled_drying.py
  52. 6 0
      backend/app/models/slot_preset.py
  53. 15 0
      backend/app/models/spool.py
  54. 133 0
      backend/app/models/supplier.py
  55. 3 0
      backend/app/schemas/api_key.py
  56. 14 0
      backend/app/schemas/archive.py
  57. 118 0
      backend/app/schemas/auth.py
  58. 109 0
      backend/app/schemas/connected_app.py
  59. 56 1
      backend/app/schemas/library.py
  60. 65 1
      backend/app/schemas/notification.py
  61. 101 3
      backend/app/schemas/notification_template.py
  62. 1 0
      backend/app/schemas/print_log.py
  63. 21 1
      backend/app/schemas/print_queue.py
  64. 31 6
      backend/app/schemas/printer.py
  65. 1 0
      backend/app/schemas/scheduled_drying.py
  66. 40 0
      backend/app/schemas/settings.py
  67. 58 0
      backend/app/schemas/spool.py
  68. 112 0
      backend/app/schemas/supplier.py
  69. 476 0
      backend/app/services/announcements.py
  70. 50 2
      backend/app/services/archive.py
  71. 8 1
      backend/app/services/bambu_ftp.py
  72. 209 33
      backend/app/services/bambu_mqtt.py
  73. 11 2
      backend/app/services/camera_profiles.py
  74. 158 0
      backend/app/services/color_catalog_lookup.py
  75. 19 3
      backend/app/services/diagnostic_snapshot.py
  76. 10 0
      backend/app/services/drying_preflight.py
  77. 60 10
      backend/app/services/email_service.py
  78. 42 28
      backend/app/services/external_camera.py
  79. 26 0
      backend/app/services/failure_analysis.py
  80. 25 7
      backend/app/services/filament_deficit.py
  81. 1 1
      backend/app/services/firmware_check.py
  82. 10 0
      backend/app/services/github_backup.py
  83. 11 0
      backend/app/services/github_restore.py
  84. 76 904
      backend/app/services/hms_errors.py
  85. 300 0
      backend/app/services/kprofile_drift.py
  86. 228 69
      backend/app/services/label_renderer.py
  87. 144 14
      backend/app/services/ldap_service.py
  88. 3 1
      backend/app/services/library_trash.py
  89. 4 2
      backend/app/services/log_reader.py
  90. 75 0
      backend/app/services/material_number.py
  91. 361 0
      backend/app/services/mesh_combine.py
  92. 620 67
      backend/app/services/notification_service.py
  93. 10 3
      backend/app/services/obico_actions.py
  94. 7 3
      backend/app/services/obico_detection.py
  95. 153 0
      backend/app/services/oidc_group_sync.py
  96. 72 0
      backend/app/services/pdf_thumbnail.py
  97. 6 32
      backend/app/services/print_batch.py
  98. 229 0
      backend/app/services/print_confirmation.py
  99. 860 16
      backend/app/services/print_scheduler.py
  100. 18 0
      backend/app/services/printer_diagnostic.py

+ 14 - 0
.env.example

@@ -96,6 +96,8 @@ LOG_TO_FILE=true
 # BAMBUDDY_OIDC_AUTO_LINK_EXISTING=false
 # BAMBUDDY_OIDC_EMAIL_CLAIM=email
 # BAMBUDDY_OIDC_REQUIRE_EMAIL_VERIFIED=true
+# BAMBUDDY_OIDC_GROUP_CLAIM=groups
+# BAMBUDDY_OIDC_GROUP_MAPPING=
 # BAMBUDDY_OIDC_ICON_URL=
 # BAMBUDDY_OIDC_AUTOLOGIN=false
 # BAMBUDDY_OIDC_DEFAULT_GROUP=
@@ -112,6 +114,18 @@ LOG_TO_FILE=true
 # FIRST boot that means no provider is created at all and no SSO button appears
 # -- create the group first. Removing the variable clears the group again.
 #
+# GROUP_CLAIM / GROUP_MAPPING enable OIDC group sync (#3107). GROUP_CLAIM names
+# the ID token claim holding the user's IdP groups (default "groups"). GROUP_MAPPING
+# is a JSON object mapping IdP group values to Bambuddy group NAMES, e.g.
+#   BAMBUDDY_OIDC_GROUP_MAPPING={"fablab-staff":"Operators","students":"Viewers"}
+# On every SSO login the mapped groups are synced from the claim: groups the
+# mapping names are replaced by IdP truth, anything else on the user is a manual
+# assignment and survives. Only mapping VALUES are checked against the database:
+# like DEFAULT_GROUP, a name matching no group refuses the whole provider config
+# (logged, app still starts). Blank/unset mapping = sync off, the default. The
+# claim accepts a JSON array (Keycloak, Authentik) or a space/comma-separated
+# string (Logto, legacy); matching is case-insensitive on the IdP side.
+#
 # AUTO_LINK_EXISTING binds an OIDC identity to an existing local account with
 # the same email address. With EMAIL_CLAIM=email it is refused unless
 # REQUIRE_EMAIL_VERIFIED=true, because an identity provider that does not

+ 7 - 3
.github/MAINTAINERS.md

@@ -118,13 +118,17 @@ Example:
 
 1. Update version in `pyproject.toml`
 2. Update `CHANGELOG.md`
-3. Create a PR with these changes
-4. After merge, tag the release:
+3. If Bambu Studio has updated its HMS files since the last release, run
+   `python scripts/generate_hms_catalog.py /path/to/BambuStudio` and review the
+   diff of `backend/app/data/hms_catalog.json` (added codes, changed texts, codes
+   that lost their text)
+4. Create a PR with these changes
+5. After merge, tag the release:
    ```bash
    git tag v0.1.x
    git push origin v0.1.x
    ```
-5. Run `docker-publish.sh` to publish Docker image
+6. Run `docker-publish.sh` to publish Docker image
 
 ## Dependabot (Optional)
 

+ 2 - 0
.gitignore

@@ -103,3 +103,5 @@ test_pipeline_run_1.3mf
 .coverage
 .coverage.*
 htmlcov/
+
+feature_votes.sh

File diff suppressed because it is too large
+ 22 - 0
CHANGELOG.md


+ 182 - 7
backend/app/api/routes/_spoolman_helpers.py

@@ -32,6 +32,7 @@ class MappedSpoolFields(TypedDict):
     effect_type: None
     label_weight: int | None
     core_weight: int | None
+    core_weight_is_inherited: bool
     core_weight_catalog_id: None
     weight_used: float | None
     weight_used_baseline: float | None
@@ -54,6 +55,10 @@ class MappedSpoolFields(TypedDict):
     created_at: str | None  # None when Spoolman spool has no registered timestamp
     updated_at: str | None
     cost_per_kg: float | None
+    # Spoolman's native filament.article_number, surfaced as the internal
+    # material number (#2870). Read-only in Spoolman mode — the number is
+    # filament-level there and maintained in Spoolman itself.
+    material_number: str | None
     storage_location: str | None
     location_id: int | None
     k_profiles: list[Any]
@@ -139,6 +144,98 @@ def _safe_optional_float(value: object) -> float | None:
     return None
 
 
+# Used only when Spoolman has no empty-spool weight at any level.
+SPOOLMAN_FALLBACK_TARE = 250.0
+
+
+def spoolman_tare(spool: dict) -> tuple[float, str]:
+    """The empty-spool weight of a Spoolman spool, and where it came from.
+
+    Spoolman resolves the tare as the spool's own ``spool_weight``, then the
+    filament's ``spool_weight``, then the vendor's ``empty_spool_weight``, and
+    its own ``/measure`` endpoint follows that order. Skipping the vendor
+    level made a spool whose tare lives only on its vendor weigh against
+    250 g instead (#3195). Every Spoolman-mode tare in Bambuddy goes through
+    here so the weigh endpoints and the displayed core weight cannot drift
+    apart again.
+
+    Returns ``(grams, source)`` with source one of ``"spool"``,
+    ``"filament"``, ``"vendor"`` or ``"fallback"``. 0 is a real tare, not a
+    missing one; a value that is not a finite number counts as missing.
+    """
+    filament = spool.get("filament") or {}
+    vendor = filament.get("vendor") or {}
+    for source, raw in (
+        ("spool", spool.get("spool_weight")),
+        ("filament", filament.get("spool_weight")),
+        ("vendor", vendor.get("empty_spool_weight")),
+    ):
+        value = _safe_optional_float(raw)
+        if value is not None:
+            return value, source
+    return SPOOLMAN_FALLBACK_TARE, "fallback"
+
+
+def spoolman_net_weight(spool: dict) -> float | None:
+    """The net filament weight of a full Spoolman spool (its label weight).
+
+    Spoolman keeps it on the spool as ``initial_weight`` and falls back to the
+    filament's catalogue ``weight``, so one filament can have spools of
+    different sizes. Reading only the filament made a 250 g spool of a 1000 g
+    filament look four times its size (#3194). Like Spoolman, an
+    ``initial_weight`` of 0 counts as unset.
+
+    Returns None when neither level has a finite number (a bool is not
+    one); the filament's value is otherwise returned as stored, 0 included,
+    so callers keep their own handling of a filament without a weight.
+    """
+    initial = _weight_or_none(spool.get("initial_weight"))
+    if initial is not None and initial > 0:
+        return initial
+    filament = spool.get("filament")
+    return _weight_or_none(filament.get("weight")) if isinstance(filament, dict) else None
+
+
+def _weight_or_none(value: object) -> float | None:
+    # A bool is an int in Python; True must not read as a 1 g spool.
+    return None if isinstance(value, bool) else _safe_optional_float(value)
+
+
+def spoolman_price_to_cost_per_kg(price: object, net_weight: float) -> float | None:
+    """A Spoolman spool price as a price per kilogram.
+
+    Spoolman's ``price`` on a spool is what that spool cost, not a rate; the
+    spool form edits a rate, so the two are converted at the spool's net
+    weight. They agree only for 1000 g spools (#3194). Rounded to 4 places so
+    a round trip through the form doesn't drift by float noise.
+    """
+    value = _safe_optional_float(price)
+    if value is None or net_weight <= 0:
+        return None
+    return round(value * 1000.0 / net_weight, 4)
+
+
+def cost_per_kg_to_spoolman_price(cost_per_kg: float, net_weight: float) -> float:
+    """Inverse of spoolman_price_to_cost_per_kg: the price of a spool of ``net_weight`` grams."""
+    return round(cost_per_kg * net_weight / 1000.0, 4)
+
+
+def spoolman_price_weight(spool: dict) -> float:
+    """The weight a Spoolman spool's price is converted at.
+
+    The spool's exact net weight, not the whole-gram label weight, so a
+    250.7 g spool is not re-priced by a no-op edit. A spool with no usable
+    weight keeps the old 1:1 reading of its price as a per-kg rate.
+    """
+    net = spoolman_net_weight(spool)
+    return net if net is not None and net > 0 else 1000.0
+
+
+def spoolman_cost_per_kg(spool: dict) -> float | None:
+    """The spool form's cost per kg for a Spoolman spool (#3194)."""
+    return spoolman_price_to_cost_per_kg(spool.get("price"), spoolman_price_weight(spool))
+
+
 def _extract_extra_str(extra: dict, key: str) -> str:
     """Extract a JSON-encoded string from a Spoolman extra dict.
 
@@ -158,6 +255,51 @@ def _extract_extra_str(extra: dict, key: str) -> str:
     return decoded if isinstance(decoded, str) else ""
 
 
+# Spool.extra key holding the consumed-counter baseline: the value Spoolman's
+# used_weight had when the user last pressed "Reset usage to 0". Displayed
+# consumed is used_weight minus this, which is what internal mode has always
+# done with its own weight_used_baseline column (#1644). See #2906.
+BAMBU_WEIGHT_USED_BASELINE_KEY = "bambu_weight_used_baseline"
+
+
+def _extract_extra_float(extra: dict, key: str) -> float | None:
+    """Extract a JSON-encoded number from a Spoolman extra dict.
+
+    Same storage convention as :func:`_extract_extra_str` — Spoolman keeps
+    extra values as JSON text, so 250.0 is stored as ``"250.0"``. Returns
+    ``None`` for missing keys, non-finite values, or anything that does not
+    decode to a number, so the caller can tell "no baseline recorded" from
+    "a baseline of zero".
+    """
+    raw = extra.get(key)
+    if raw is None:
+        return None
+    if isinstance(raw, (int, float)) and not isinstance(raw, bool):
+        value = float(raw)
+    elif isinstance(raw, str):
+        try:
+            decoded = json.loads(raw)
+        except (json.JSONDecodeError, ValueError):
+            return None
+        if isinstance(decoded, str):
+            # Spoolman's extra fields default to field_type "text", which only
+            # accepts values that decode to a str, so numbers written through
+            # it arrive as '"263.0"' rather than '263.0'. Both spellings have
+            # to read back the same, or a baseline written by the reset would
+            # be invisible to the code that subtracts it.
+            try:
+                value = float(decoded)
+            except ValueError:
+                return None
+        elif isinstance(decoded, (int, float)) and not isinstance(decoded, bool):
+            value = float(decoded)
+        else:
+            return None
+    else:
+        return None
+    return value if math.isfinite(value) else None
+
+
 def parse_spoolman_multi_colors(filament: dict) -> list[str]:
     """Spoolman's ``multi_color_hexes`` as a list of bare 6/8-char hex tokens.
 
@@ -240,7 +382,7 @@ def _map_spoolman_spool(spool: dict) -> MappedSpoolFields:
     extra_stops = parse_spoolman_multi_colors(filament)
     extra_colors: str | None = ",".join(extra_stops) if extra_stops else None
 
-    label_weight: int = _safe_int(filament.get("weight"), 1000)
+    label_weight: int = _safe_int(spoolman_net_weight(spool), 1000)
     real_used_weight: float = _safe_float(spool.get("used_weight"), 0.0)
     # Parity with internal mode (#1390): the InventorySpool shape lets the
     # frontend compute `remaining = label_weight - weight_used` and
@@ -251,14 +393,37 @@ def _map_spoolman_spool(spool: dict) -> MappedSpoolFields:
     # When remaining_weight is unset (legacy spools, or filament linked but
     # never primed), fall back to the old behaviour: weight_used =
     # used_weight, baseline = 0.
+    #
+    # A "Reset usage to 0" records the then-current used_weight under
+    # BAMBU_WEIGHT_USED_BASELINE_KEY in spool.extra and touches nothing else,
+    # so displayed consumed is used_weight minus that. It is folded into the
+    # baseline here because the frontend's arithmetic is fixed: it always shows
+    # `weight_used - weight_used_baseline`. The reset used to PATCH Spoolman's
+    # used_weight to 0 instead, and Spoolman recomputes remaining from initial
+    # minus used, so the spool jumped back to full (#2906).
+    # `or 0.0` would collapse None and 0.0, which is the one distinction
+    # _extract_extra_float exists to preserve; spell the absent case out.
+    # Clamp the low end too: the write side already refuses a negative
+    # baseline, and without the same rule here a negative value stored by hand
+    # would be subtracted, inflating the displayed counter. min() below only
+    # bounds the other end.
+    stored = _extract_extra_float(extra, BAMBU_WEIGHT_USED_BASELINE_KEY)
+    stored_baseline = 0.0 if stored is None else max(0.0, stored)
     remaining_raw = spool.get("remaining_weight")
     if remaining_raw is not None:
         remaining_weight: float = _safe_float(remaining_raw, 0.0)
         used_weight: float = max(0.0, float(label_weight) - remaining_weight)
-        weight_used_baseline: float = max(0.0, used_weight - real_used_weight)
+        weight_used_baseline: float = max(0.0, used_weight - real_used_weight + stored_baseline)
     else:
         used_weight = real_used_weight
-        weight_used_baseline = 0.0
+        weight_used_baseline = stored_baseline  # already clamped to >= 0 above
+    # Never let the displayed counter read negative. used_weight can fall below
+    # the baseline after a reset without anyone touching it: the AMS sync
+    # writes remaining_weight from the tray percentage, and Spoolman derives
+    # used_weight from that. The counter then reads 0 until consumption climbs
+    # past the baseline again. Internal mode behaves the same way, and it is
+    # far better than before #2906, when the next sync undid the reset outright.
+    weight_used_baseline = min(weight_used_baseline, used_weight)
 
     # Archived state – Spoolman uses a boolean ``archived`` field
     archived: bool = spool.get("archived", False)
@@ -307,9 +472,12 @@ def _map_spoolman_spool(spool: dict) -> MappedSpoolFields:
         "effect_type": None,
         "brand": vendor.get("name") or None,
         "label_weight": label_weight,
-        "core_weight": _safe_int(
-            spool.get("spool_weight") if spool.get("spool_weight") is not None else filament.get("spool_weight"), 250
-        ),
+        "core_weight": _safe_int(spoolman_tare(spool)[0], 250),
+        # True when the spool has no spool_weight of its own and core_weight is
+        # the filament type's, the vendor's or the 250 g fallback. The spool form needs it
+        # to copy a spool without dropping an own tare or stamping an
+        # inherited one (#2908).
+        "core_weight_is_inherited": spool.get("spool_weight") is None,
         "core_weight_catalog_id": None,
         "weight_used": used_weight,
         "weight_used_baseline": weight_used_baseline,
@@ -339,7 +507,14 @@ def _map_spoolman_spool(spool: dict) -> MappedSpoolFields:
         "created_at": created_at,
         # Spoolman has no updated_at field; use registered timestamp as best available proxy
         "updated_at": created_at,
-        "cost_per_kg": _safe_optional_float(spool.get("price")),
+        # Spoolman's spool price is what the whole spool cost (#3194).
+        "cost_per_kg": spoolman_cost_per_kg(spool),
+        # Spoolman's filament.article_number maps 1:1 onto the internal
+        # material number (#2870): both identify the purchasable product.
+        # Trimmed for the same reason the schema validator trims the internal
+        # one — the filter chip builds its options from trimmed values and
+        # matches exactly, so a padded number would list and match nothing.
+        "material_number": ((filament.get("article_number") or "").strip() or None),
         "storage_location": spool.get("location") or None,
         "location_id": None,
         "k_profiles": [],

+ 65 - 0
backend/app/api/routes/announcements.py

@@ -0,0 +1,65 @@
+"""Announcements from the Bambuddy maintainers (services/announcements.py).
+
+Shown to administrators, and to every signed-in user when the
+``announcements_all_users`` setting is on. With authentication off whoever opens
+Bambuddy runs it, so they see them. Anyone else gets ``visible: false`` and an
+empty list rather than a 403. ``visible`` is separate from the list because the
+sidebar entry is there for whoever may see announcements, also while nothing is
+published, and hidden for everyone else.
+"""
+
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.core.auth import is_auth_enabled, require_auth_if_enabled
+from backend.app.core.database import get_db
+from backend.app.models.settings import Settings
+from backend.app.models.user import User
+from backend.app.services import announcements as service
+
+router = APIRouter(prefix="/announcements", tags=["announcements"])
+
+
+async def _may_see(db: AsyncSession, user: User | None) -> bool:
+    if not await service.is_enabled(db):
+        return False
+    if not await is_auth_enabled(db):
+        return True
+    # Authenticated by API key: a script, not a person with an inbox.
+    if user is None:
+        return False
+    if user.is_admin:
+        return True
+    all_users = (
+        await db.execute(select(Settings.value).where(Settings.key == service.ALL_USERS_KEY))
+    ).scalar_one_or_none()
+    return (all_users or "").lower() == "true"
+
+
+@router.get("")
+async def list_announcements(
+    db: AsyncSession = Depends(get_db),
+    current_user: User | None = Depends(require_auth_if_enabled),
+) -> dict:
+    """Whether this user may see announcements, and the live ones newest first,
+    with their read state."""
+    if not await _may_see(db, current_user):
+        return {"visible": False, "announcements": []}
+    return {
+        "visible": True,
+        "announcements": await service.list_for(db, current_user.id if current_user else None),
+    }
+
+
+@router.post("/{public_id}/read", status_code=status.HTTP_204_NO_CONTENT)
+async def mark_announcement_read(
+    public_id: str,
+    db: AsyncSession = Depends(get_db),
+    current_user: User | None = Depends(require_auth_if_enabled),
+) -> None:
+    if not await _may_see(db, current_user):
+        raise HTTPException(status.HTTP_404_NOT_FOUND, "Announcement not found")
+    if not await service.mark_read(db, public_id, current_user.id if current_user else None):
+        raise HTTPException(status.HTTP_404_NOT_FOUND, "Announcement not found")
+    await db.commit()

+ 4 - 0
backend/app/api/routes/api_keys.py

@@ -70,6 +70,7 @@ async def create_api_key(
         can_manage_projects=data.can_manage_projects,
         can_access_cloud=data.can_access_cloud,
         can_update_energy_cost=data.can_update_energy_cost,
+        can_send_notifications=data.can_send_notifications,
         printer_ids=data.printer_ids,
         expires_at=data.expires_at,
     )
@@ -94,6 +95,7 @@ async def create_api_key(
         can_manage_projects=api_key.can_manage_projects,
         can_access_cloud=api_key.can_access_cloud,
         can_update_energy_cost=api_key.can_update_energy_cost,
+        can_send_notifications=api_key.can_send_notifications,
         printer_ids=api_key.printer_ids,
         enabled=api_key.enabled,
         last_used=api_key.last_used,
@@ -162,6 +164,8 @@ async def update_api_key(
         api_key.can_access_cloud = data.can_access_cloud
     if data.can_update_energy_cost is not None:
         api_key.can_update_energy_cost = data.can_update_energy_cost
+    if data.can_send_notifications is not None:
+        api_key.can_send_notifications = data.can_send_notifications
     if data.printer_ids is not None:
         api_key.printer_ids = data.printer_ids
     if data.enabled is not None:

+ 270 - 1
backend/app/api/routes/archives.py

@@ -7,6 +7,7 @@ import zipfile
 from collections import defaultdict
 from datetime import date, datetime, time, timedelta, timezone
 from decimal import ROUND_HALF_UP, Decimal
+from html import escape as html_escape
 from pathlib import Path
 
 from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile
@@ -39,6 +40,12 @@ from backend.app.services.archive import ArchiveService
 from backend.app.services.bambu_ftp import ftps_handshake_blocked, list_files_result_async
 from backend.app.services.design_settings import overrides_from_config
 from backend.app.services.filament_requirements import annotate_rack_groups
+from backend.app.services.print_confirmation import (
+    is_one_tap_request,
+    is_unattended_fetch,
+    retire_confirm_token,
+    stamp_verdict,
+)
 from backend.app.services.print_storage import (
     REASON_FTP_TRANSFER_FAILED,
     REASON_FTPS_COOLOFF,
@@ -366,6 +373,14 @@ def archive_to_response(
         "cost": archive.cost,
         "photos": archive.photos,
         "failure_reason": archive.failure_reason,
+        # Post-print outcome confirmation (#1898). confirm_token stays
+        # server-side — it is a capability and never belongs in a response.
+        "user_verdict": archive.user_verdict,
+        "user_verdict_source": archive.user_verdict_source,
+        "user_verdict_at": archive.user_verdict_at,
+        # bool() because the column is nullable to match the migration; the
+        # response contract stays a strict bool either way.
+        "confirm_requested": bool(archive.confirm_requested),
         "quantity": archive.quantity,
         "energy_kwh": archive.energy_kwh,
         "energy_cost": archive.energy_cost,
@@ -1762,9 +1777,23 @@ async def update_archive(
     previous_filament_grams = archive.filament_used_grams
 
     update_payload = update_data.model_dump(exclude_unset=True)
+    # #1898: how the verdict arrived is recorded with it, never on its own.
+    verdict_source = update_payload.pop("user_verdict_source", None)
     for field, value in update_payload.items():
         setattr(archive, field, value)
 
+    # #1898: a landed verdict retires the one-tap capability token from the
+    # push notification — the links stop changing anything once someone
+    # decided, and report the recorded verdict instead. Clearing the verdict
+    # drops the provenance but leaves the token spent: it was used.
+    if "user_verdict" in update_payload:
+        if update_payload["user_verdict"] is None:
+            archive.user_verdict_source = None
+            archive.user_verdict_at = None
+        else:
+            stamp_verdict(archive, verdict_source or "api")
+            retire_confirm_token(archive)
+
     # #1444: Mirror per-run classification fields to the most recent
     # PrintLogEntry for this archive. PrintLogEntry.failure_reason is captured
     # once at print-completion time from archive.failure_reason — which is
@@ -1780,7 +1809,7 @@ async def update_archive(
     # ENTRY's grams, not the archive's, so correcting only the archive would fix
     # the card and leave every aggregate reading the old figure -- or, for a
     # print that archived without its 3MF, no figure at all.
-    mirror_fields = {"failure_reason", "status", "filament_used_grams"}
+    mirror_fields = {"failure_reason", "status", "filament_used_grams", "user_verdict"}
     to_mirror = {k: v for k, v in update_payload.items() if k in mirror_fields}
     if to_mirror:
         from backend.app.models.print_log import PrintLogEntry
@@ -3394,6 +3423,246 @@ async def delete_photo(
     return {"status": "deleted", "photos": archive.photos}
 
 
+# ============================================
+# Post-print outcome confirmation (#1898)
+# ============================================
+
+# English-only on purpose: this page is rendered by the backend for a phone
+# browser that carries no session and therefore no language preference.
+_VERDICT_LABELS = {"good": "Good part", "reject": "Rejected"}
+_VERDICT_SOURCE_PHRASES = {
+    "dialog": "in the app",
+    "link": "with a one-tap link",
+    "plate_clear": "automatically when the print plate was cleared",
+    "printer_card": "from the printer card",
+    "api": "through the API",
+    "reaction": "with a reaction in chat",
+}
+
+
+def _confirm_page(glyph: str, heading: str, body: str) -> str:
+    """The small HTML page every one-tap outcome link renders."""
+    return (
+        "<!doctype html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'>"
+        "<title>Bambuddy</title></head>"
+        "<body style='font-family: system-ui, sans-serif; background:#1a1d21; color:#fff; display:flex;"
+        " align-items:center; justify-content:center; min-height:90vh; margin:0'>"
+        f"<div style='text-align:center; padding:0 1.5rem; max-width:32rem'>"
+        f"<div style='font-size:3rem'>{glyph}</div><h2>{heading}</h2>{body}</div></body></html>"
+    )
+
+
+async def _render_already_answered_page(db: AsyncSession, archive: PrintArchive) -> str:
+    """Explain a spent one-tap link instead of calling it invalid.
+
+    The plate-clear default, the app and the other link all answer the same
+    prompt, so the button in a push notification is routinely tapped after the
+    question is settled. Report the verdict on file, when and how it landed,
+    and where to change it.
+    """
+    from backend.app.api.routes.settings import get_external_base_url
+
+    name = html_escape(archive.print_name or archive.filename or "")
+    # The verdict's own timestamp, not the moment the token was spent: a
+    # verdict changed later in the app would otherwise be dated by the older
+    # event. Pre-#1898 rows have neither, and then the page omits the date.
+    used_at = archive.user_verdict_at or archive.confirm_token_used_at
+    if used_at is not None and used_at.tzinfo is not None:
+        used_at = used_at.astimezone(timezone.utc)
+    when = used_at.strftime("%Y-%m-%d %H:%M UTC") if used_at else None
+    how = _VERDICT_SOURCE_PHRASES.get(archive.user_verdict_source or "")
+    label = _VERDICT_LABELS.get(archive.user_verdict or "")
+
+    if label:
+        recorded = f"Recorded as <strong>{label}</strong>"
+        if how:
+            recorded += f" {how}"
+        if when:
+            recorded += f" on {when}"
+        recorded += "."
+    else:
+        # The verdict was cleared again in the app; the link stays spent.
+        recorded = "This prompt was already answered and the verdict has since been cleared."
+
+    base = await get_external_base_url(db)
+    link = html_escape(f"{base}/archives?confirm={archive.id}", quote=True)
+    glyph = "&#10003;" if archive.user_verdict == "good" else "&#10007;" if archive.user_verdict else "&#8505;"
+    return _confirm_page(
+        glyph,
+        "Already answered",
+        f"<p style='color:#9ca3af'>{name}</p>"
+        f"<p style='color:#9ca3af'>{recorded}</p>"
+        f"<p><a style='color:#00ae42' href='{link}'>Open this print in Bambuddy</a> to change it.</p>",
+    )
+
+
+def _render_confirm_prompt_page(request: Request, archive: PrintArchive, verdict: str) -> str:
+    """The page a one-tap verdict link opens. It has recorded nothing yet.
+
+    GET is where link unfurlers, mail-security scanners and browser prefetchers
+    arrive, uninvited and within seconds of the message being sent, so GET
+    writes nothing at all -- the verdict is recorded by the form below, over
+    POST, which none of them issue.
+
+    The form submits itself only for a page opened from a notification button,
+    which is what keeps the operator at one tap. The marker for that
+    (``?tap=1``) is put on the Telegram inline keyboard's URLs and nowhere else
+    -- never on a URL that travels in message text -- so a mail-security
+    sandbox that renders HTML and runs JavaScript cannot press the button for
+    the operator: it only ever sees the unmarked URL out of the body. The
+    User-Agent heuristic still runs on top of that, but it is no longer the
+    only thing between a scanner and the write; it cannot be, because such a
+    sandbox sends an ordinary Chrome string.
+
+    Every other arrival -- an unmarked link somebody typed or mailed, a browser
+    with JavaScript off -- gets the same page and presses the button.
+    """
+    name = html_escape(archive.print_name or archive.filename or "")
+    label = _VERDICT_LABELS.get(verdict, verdict)
+    # No action attribute: the form posts back to the URL the page was loaded
+    # from, so it works behind a reverse proxy and on a host external_url does
+    # not name.
+    form = (
+        "<form method='post' id='confirm-form'>"
+        "<button type='submit' style='font: inherit; font-size:1.1rem; padding:0.9rem 2rem;"
+        " border:0; border-radius:0.5rem; background:#00ae42; color:#fff'>Yes, record it</button>"
+        "</form>"
+    )
+    # The SPA's CSP allows inline scripts only with the per-request nonce the
+    # security-headers middleware mints (main.py). Without one -- no middleware,
+    # an unmarked URL, or an unattended-looking caller -- the page simply waits
+    # for the button.
+    nonce = getattr(request.state, "csp_nonce", None)
+    script = ""
+    if nonce and is_one_tap_request(request.query_params) and not is_unattended_fetch(request.method, request.headers):
+        script = (
+            f"<script nonce='{html_escape(nonce, quote=True)}'>"
+            "document.getElementById('confirm-form').submit();</script>"
+        )
+    return _confirm_page(
+        "&#63;",
+        "Confirm this outcome",
+        f"<p style='color:#9ca3af'>{name}</p>"
+        f"<p style='color:#9ca3af'>Record this print as <strong>{label}</strong>?</p>"
+        f"{form}{script}",
+    )
+
+
+def _confirm_response(html: str):
+    """The one-tap pages, never cached.
+
+    A proxy holding on to "Saved" or to the prompt would answer a later tap
+    from its cache, and the prompt page is a capability URL either way.
+    """
+    from fastapi.responses import HTMLResponse
+
+    return HTMLResponse(html, headers={"Cache-Control": "no-store"})
+
+
+async def _load_confirmable_archive(db: AsyncSession, token: str, verdict: str) -> PrintArchive:
+    """Resolve a one-tap capability token, or raise the route's 400/404."""
+    if verdict not in ("good", "reject"):
+        raise HTTPException(400, "Verdict must be 'good' or 'reject'")
+
+    result = await db.execute(
+        select(PrintArchive).where(PrintArchive.confirm_token == token, PrintArchive.confirm_token.isnot(None))
+    )
+    archive = result.scalar_one_or_none()
+    if not archive:
+        raise HTTPException(404, "Confirmation link is invalid or was already used")
+    return archive
+
+
+@router.get("/confirm/{token}/{verdict}")
+async def confirm_outcome_page(
+    request: Request,
+    token: str,
+    verdict: str,
+    db: AsyncSession = Depends(get_db),
+):
+    """Open a one-tap verdict link. Reads only — the verdict is recorded by POST.
+
+    This used to be the route that recorded, and that was the defect: a GET
+    that changes state is answered by everything that walks a URL. Telegram
+    and Slack fetch the links in a message body to build a preview card, mail
+    gateways detonate them before delivery, browsers prefetch them — any one
+    of those spent the single-use token and settled the outcome before the
+    operator had read the question, always in the "good" direction because
+    good_url came first. Suppressing previews per channel does not cover the
+    proxies and scanners in between; only removing the write from GET does.
+
+    So GET now hands back the prompt page and nothing else. The page's form
+    POSTs to this same URL, and :func:`confirm_outcome_by_token` records it.
+    The human cost is zero for the path the feature is built around: a URL
+    opened from a notification button carries ``?tap=1`` and the page submits
+    itself, so that tap is still the only tap. Nothing else gets that script,
+    including a scanner that runs JavaScript -- the marker is on the buttons,
+    not in the message text a scanner reads.
+    """
+    archive = await _load_confirmable_archive(db, token, verdict)
+
+    if archive.confirm_token_used_at is not None:
+        # Answered already — by hand, by the other link, by the plate-clear
+        # default or by a reaction. Report what is on file and change nothing.
+        return _confirm_response(await _render_already_answered_page(db, archive))
+
+    return _confirm_response(_render_confirm_prompt_page(request, archive, verdict))
+
+
+@router.post("/confirm/{token}/{verdict}")
+async def confirm_outcome_by_token(
+    token: str,
+    verdict: str,
+    db: AsyncSession = Depends(get_db),
+):
+    """Record a print-outcome verdict via the capability token from a push notification.
+
+    Deliberately unauthenticated: the token IS the credential. It is a 256-bit
+    per-archive capability, minted when the confirmation prompt fires, and it
+    only ever grants writing good/reject on that one archive, exactly once.
+    "Exactly once" is enforced by ``confirm_token_used_at`` rather than by
+    dropping the token value, so a link for a print that was already answered
+    can be recognised and explained instead of looking broken.
+
+    POST is what keeps the capability the operator's: unfurlers, scanners and
+    prefetchers issue GET, and the GET route above writes nothing. The two
+    callers that reach here are the prompt page's form and the ntfy action
+    button, which performs its own request from the phone and is configured
+    with ``method=POST``. Returns a small HTML page for the phone browser.
+    """
+    archive = await _load_confirmable_archive(db, token, verdict)
+
+    if archive.confirm_token_used_at is not None:
+        return _confirm_response(await _render_already_answered_page(db, archive))
+
+    archive.user_verdict = verdict
+    stamp_verdict(archive, "link")
+    retire_confirm_token(archive)
+
+    # Same mirror as the PATCH route (#1444): verdict-aware statistics read
+    # print_log_entries, so the latest run must carry the verdict too.
+    from backend.app.models.print_log import PrintLogEntry
+
+    latest_entry = await db.scalar(
+        select(PrintLogEntry).where(PrintLogEntry.archive_id == archive.id).order_by(PrintLogEntry.id.desc()).limit(1)
+    )
+    if latest_entry is not None:
+        latest_entry.user_verdict = verdict
+
+    await db.commit()
+
+    label = "Good part" if verdict == "good" else "Rejected"
+    name = archive.print_name or archive.filename
+    return _confirm_response(
+        _confirm_page(
+            "&#10003;" if verdict == "good" else "&#10007;",
+            label,
+            f"<p style='color:#9ca3af'>{html_escape(name)}</p>"
+            "<p style='color:#9ca3af'>Saved &mdash; you can close this page.</p>",
+        )
+    )
+
+
 # ============================================
 # QR Code Endpoint
 # ============================================

+ 7 - 2
backend/app/api/routes/auth.py

@@ -483,6 +483,7 @@ async def login(raw_request: Request, request: LoginRequest, response: Response,
     user = None
     # Check if LDAP is enabled
     ldap_user = None
+    ldap_user_provisioned = False
     ldap_settings = await _get_ldap_settings(db)
     if ldap_settings:
         try:
@@ -506,10 +507,14 @@ async def login(raw_request: Request, request: LoginRequest, response: Response,
                             # User doesn't exist and auto-provision is off
                             ldap_user = None
                         else:
-                            # Auto-provision LDAP user
+                            # Auto-provision LDAP user. Provisioning already sets the
+                            # email, groups and finance defaults the sync below would,
+                            # so a new user skips it (it also logged the default-group
+                            # warning a second time, #3197).
                             user = await _provision_ldap_user(db, ldap_user, ldap_config)
+                            ldap_user_provisioned = True
 
-                    if user and ldap_user:
+                    if user and ldap_user and not ldap_user_provisioned:
                         # Update email and group mappings on each login
                         await _sync_ldap_user(db, user, ldap_user, ldap_config)
                         # Keep finance defaults idempotently in sync for LDAP users

+ 104 - 4
backend/app/api/routes/camera.py

@@ -8,6 +8,7 @@ import subprocess
 import sys
 import time
 import uuid
+import zlib
 from collections.abc import AsyncGenerator
 
 from fastapi import APIRouter, Depends, HTTPException, Request
@@ -71,6 +72,23 @@ _FFMPEG_TERM_TIMEOUT = 2.0
 # an active stream on its next pass.
 _FFMPEG_KILL_TIMEOUT = 2.0
 
+# How long an RTSP stream may keep emitting byte-identical JPEGs before the
+# ffmpeg behind it is restarted (#3218). A dead upstream can leave ffmpeg
+# repeating its last frame indefinitely: measured on a P2S, ~29 fps of one
+# frame for two hours with no socket to the printer left at all, while every
+# check that counts frames saw a healthy stream. A live camera practically
+# never repeats a frame byte for byte -- sensor noise changes every one (2262
+# of 2262 distinct on an idle X1C chamber, #3189) -- so a run this long means
+# the picture is frozen.
+_RTSP_FROZEN_SECONDS = 20.0
+# "Practically never" is not never: a dark, idle chamber can encode to the same
+# frame every time. When the first frame after such a restart is the frozen one
+# again, the camera really is showing that picture, and the stream only
+# re-checks at this much longer interval until the picture changes -- so a
+# still scene costs one reconnect every few minutes instead of every 20 s, and a
+# real freeze on it is still recovered.
+_RTSP_STILL_RECHECK_SECONDS = 300.0
+
 # Track active ffmpeg processes for cleanup
 _active_streams: dict[str, asyncio.subprocess.Process] = {}
 
@@ -635,6 +653,11 @@ async def generate_rtsp_mjpeg_stream(
     process = None
     stderr_tail: _FfmpegStderrTail | None = None
     got_any_frames = False
+    # Across sessions (#3218): the frame a session froze on, and whether the
+    # next session showed the very same picture -- a still scene, not a
+    # frozen ffmpeg.
+    frozen_crc: int | None = None
+    still_scene = False
 
     try:
         while reconnect_count <= profile.rtsp_reconnect_max:
@@ -650,7 +673,14 @@ async def generate_rtsp_mjpeg_stream(
                     ip_address,
                     stream_id,
                 )
-                await asyncio.sleep(profile.rtsp_reconnect_delay)
+                # Fast after a session that delivered frames (reconnect_count
+                # is 1 then), backing off while the printer keeps refusing.
+                await asyncio.sleep(
+                    min(
+                        profile.rtsp_reconnect_delay * 2 ** (reconnect_count - 1),
+                        profile.rtsp_reconnect_backoff_max,
+                    )
+                )
                 if disconnect_event and disconnect_event.is_set():
                     break
 
@@ -698,6 +728,14 @@ async def generate_rtsp_mjpeg_stream(
             buffer = b""
             stream_ended = False
             client_gone = False
+            session_got_frames = False
+            # A frame that differs from the one before, not any frame, is what
+            # shows the picture is live: ffmpeg can repeat its last one
+            # forever (#3218).
+            last_frame_crc: int | None = None
+            last_change = time.time()
+            identical_frames = 0
+            frozen = False
 
             while True:
                 if disconnect_event and disconnect_event.is_set():
@@ -735,12 +773,40 @@ async def generate_rtsp_mjpeg_stream(
                         frame = buffer[: end_idx + 2]
                         buffer = buffer[end_idx + 2 :]
                         got_any_frames = True
+                        session_got_frames = True
+
+                        now = time.time()
+                        frame_crc = zlib.crc32(frame)
+                        if last_frame_crc is None:
+                            # First frame of this session. The same frame the
+                            # last session froze on means the camera really
+                            # shows that picture (see _RTSP_STILL_RECHECK_SECONDS).
+                            still_scene = frozen_crc is not None and frame_crc == frozen_crc
+                            if still_scene:
+                                logger.info(
+                                    "RTSP picture unchanged after restart for %s (stream_id=%s): "
+                                    "treating it as a still scene, re-checking every %.0fs",
+                                    ip_address,
+                                    stream_id,
+                                    _RTSP_STILL_RECHECK_SECONDS,
+                                )
+                            last_frame_crc = frame_crc
+                            last_change = now
+                        elif frame_crc != last_frame_crc:
+                            last_frame_crc = frame_crc
+                            last_change = now
+                            identical_frames = 0
+                            # The picture moves: back to the normal check.
+                            still_scene = False
+                            frozen_crc = None
+                        else:
+                            identical_frames += 1
 
                         if printer_id is not None:
                             _last_frames[printer_id] = frame
-                            _last_frame_times[printer_id] = time.time()
+                            _last_frame_times[printer_id] = now
                             if stream_id:
-                                _stream_last_frame_times[stream_id] = time.time()
+                                _stream_last_frame_times[stream_id] = now
 
                         yield (
                             b"--frame\r\n"
@@ -749,6 +815,33 @@ async def generate_rtsp_mjpeg_stream(
                             b"\r\n" + frame + b"\r\n"
                         )
 
+                        if now - last_change > (_RTSP_STILL_RECHECK_SECONDS if still_scene else _RTSP_FROZEN_SECONDS):
+                            frozen = True
+                            frozen_crc = last_frame_crc
+                            break
+
+                    if frozen:
+                        if still_scene:
+                            logger.info(
+                                "RTSP still-scene re-check for %s (stream_id=%s), restarting ffmpeg",
+                                ip_address,
+                                stream_id,
+                            )
+                        else:
+                            stderr_text = await _read_ffmpeg_stderr(process)
+                            if stderr_text:
+                                logger.warning("ffmpeg stderr (stream_id=%s): %s", stream_id, stderr_text)
+                            logger.warning(
+                                "RTSP output frozen for %s (stream_id=%s): %d identical frames over %.0fs, "
+                                "restarting ffmpeg",
+                                ip_address,
+                                stream_id,
+                                identical_frames,
+                                time.time() - last_change,
+                            )
+                        stream_ended = True
+                        break
+
                 except TimeoutError:
                     stderr_text = await _read_ffmpeg_stderr(process)
                     if stderr_text:
@@ -784,6 +877,13 @@ async def generate_rtsp_mjpeg_stream(
                 break
 
             if stream_ended:
+                # The budget is for failures in a row. A session that
+                # delivered video was a success, however it ended -- a stock
+                # X1C ends every session after about a minute, which under a
+                # lifetime count stopped the live view for good after half an
+                # hour.
+                if session_got_frames:
+                    reconnect_count = 0
                 reconnect_count += 1
                 continue
 
@@ -792,7 +892,7 @@ async def generate_rtsp_mjpeg_stream(
 
         if reconnect_count > profile.rtsp_reconnect_max:
             logger.error(
-                "RTSP max reconnects (%d) reached for %s (stream_id=%s)",
+                "RTSP max consecutive reconnects (%d) reached for %s (stream_id=%s)",
                 profile.rtsp_reconnect_max,
                 ip_address,
                 stream_id,

+ 5 - 0
backend/app/api/routes/camwall.py

@@ -85,6 +85,11 @@ async def list_camwall_printers(
                             "module": e.module,
                             "severity": e.severity,
                             "actions": e.actions or [],
+                            # The wall counts a fault only when Bambu publishes
+                            # text for it or it offers actions, the same rule as
+                            # the printer card, so it needs the text (#2728).
+                            "full_code": e.full_code,
+                            "description": e.description,
                         }
                         for e in (state.hms_errors or [])
                     ],

+ 405 - 0
backend/app/api/routes/connected_apps.py

@@ -0,0 +1,405 @@
+"""Connected apps: sign users in to external applications with Bambuddy.
+
+A minimal OAuth 2.0 authorization-code flow with PKCE (S256 only):
+
+1. The app sends the user's browser to the SPA page ``/connect/authorize``.
+   The page, running with the user's normal Bambuddy session, calls
+   ``GET /connect/authorize/info`` to show who is asking and, after consent,
+   ``POST /connect/authorize`` for a code. It then sends the browser back to
+   the app's registered callback URL with that code.
+2. The app's server calls ``POST /connect/token`` with the code, its client
+   secret and the PKCE verifier, and receives the user's identity and
+   permissions.
+
+What the app never gets is the user's Bambuddy login token. What a code is
+worth is deliberately small: single use, 60 seconds, bound to one app, its
+exact callback URL and the PKCE challenge, and only redeemable with the app's
+secret.
+
+Sign-in through an app only exists while Bambuddy authentication is enabled.
+With it disabled there are no users to sign in, and both authorize and token
+answer ``auth_disabled`` so the app can fall back to its own login rather
+than let everyone in.
+"""
+
+import base64
+import functools
+import hashlib
+import hmac
+import logging
+import secrets
+from datetime import datetime, timedelta, timezone
+from typing import Annotated
+
+from fastapi import APIRouter, Depends, HTTPException, Query, Request
+from fastapi.security import HTTPAuthorizationCredentials
+from sqlalchemy import delete, select
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.core.auth import (
+    RequirePermissionIfAuthEnabled,
+    get_current_user,
+    get_password_hash,
+    get_user_by_username,
+    is_auth_enabled,
+    security,
+    verify_password,
+)
+from backend.app.core.database import get_db
+from backend.app.core.permissions import Permission
+from backend.app.models.auth_ephemeral import AuthEphemeralToken, EventType, TokenType
+from backend.app.models.connected_app import ConnectedApp, ConnectedAppGrant
+from backend.app.models.user import User
+from backend.app.schemas.connected_app import (
+    ConnectAuthorizeInfo,
+    ConnectAuthorizeRequest,
+    ConnectAuthorizeResponse,
+    ConnectedAppCreate,
+    ConnectedAppResponse,
+    ConnectedAppSecretResponse,
+    ConnectedAppUpdate,
+    ConnectedUser,
+    ConnectTokenRequest,
+    ConnectTokenResponse,
+)
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter(prefix="/connect", tags=["connected-apps"])
+
+CODE_TTL = timedelta(seconds=60)
+# Failed token exchanges tolerated per client and per IP in the rate-limit
+# window (mfa.LOCKOUT_WINDOW). A working app fails almost never; this only has
+# to stop someone guessing secrets or codes.
+MAX_FAILED_TOKEN_EXCHANGES = 20
+
+AUTH_DISABLED = "auth_disabled"
+
+
+@functools.cache
+def _dummy_secret_hash() -> str:
+    """Verified against when the client_id is unknown, so an unknown client
+    costs the same bcrypt round as a wrong secret and the two can't be told
+    apart by timing. Built on first use to keep bcrypt out of import time."""
+    return get_password_hash(secrets.token_urlsafe(32))
+
+
+def _hash_code(code: str) -> str:
+    return hashlib.sha256(code.encode()).hexdigest()
+
+
+def _s256(verifier: str) -> str:
+    digest = hashlib.sha256(verifier.encode("ascii")).digest()
+    return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
+
+
+def _new_client_credentials() -> tuple[str, str]:
+    return f"bba_{secrets.token_hex(12)}", f"bbs_{secrets.token_urlsafe(32)}"
+
+
+def _with_secret(app: ConnectedApp, client_secret: str) -> ConnectedAppSecretResponse:
+    return ConnectedAppSecretResponse(
+        **ConnectedAppResponse.model_validate(app).model_dump(), client_secret=client_secret
+    )
+
+
+async def _get_app_or_404(db: AsyncSession, app_id: int) -> ConnectedApp:
+    app = await db.get(ConnectedApp, app_id)
+    if app is None:
+        raise HTTPException(404, "Connected app not found")
+    return app
+
+
+# --------------------------------------------------------------------------
+# Admin: register and manage apps
+# --------------------------------------------------------------------------
+
+
+@router.get("/apps", response_model=list[ConnectedAppResponse])
+async def list_connected_apps(
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
+):
+    result = await db.execute(select(ConnectedApp).order_by(ConnectedApp.created_at.desc()))
+    return list(result.scalars().all())
+
+
+@router.post("/apps", response_model=ConnectedAppSecretResponse)
+async def create_connected_app(
+    data: ConnectedAppCreate,
+    db: AsyncSession = Depends(get_db),
+    current_user: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
+):
+    """Register an app. The client secret is in this response and nowhere else."""
+    if not await is_auth_enabled(db):
+        # Nobody could sign in through it, and an admin who set one up would
+        # reasonably assume the app is now protected by Bambuddy's login.
+        raise HTTPException(400, "Connected apps require authentication to be enabled")
+    client_id, client_secret = _new_client_credentials()
+    app = ConnectedApp(
+        name=data.name.strip(),
+        client_id=client_id,
+        client_secret_hash=get_password_hash(client_secret),
+        redirect_uri=data.redirect_uri,
+        enabled=True,
+        created_by_id=current_user.id if current_user else None,
+    )
+    db.add(app)
+    await db.commit()
+    await db.refresh(app)
+    logger.info("Connected app %s '%s' registered", app.id, app.name)
+    return _with_secret(app, client_secret)
+
+
+@router.patch("/apps/{app_id}", response_model=ConnectedAppResponse)
+async def update_connected_app(
+    app_id: int,
+    data: ConnectedAppUpdate,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
+):
+    app = await _get_app_or_404(db, app_id)
+    if data.name is not None:
+        app.name = data.name.strip()
+    if data.redirect_uri is not None:
+        app.redirect_uri = data.redirect_uri
+    if data.enabled is not None:
+        app.enabled = data.enabled
+    await db.commit()
+    await db.refresh(app)
+    return app
+
+
+@router.post("/apps/{app_id}/rotate-secret", response_model=ConnectedAppSecretResponse)
+async def rotate_connected_app_secret(
+    app_id: int,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
+):
+    """Replace the client secret. The old one stops working immediately."""
+    app = await _get_app_or_404(db, app_id)
+    _, client_secret = _new_client_credentials()
+    app.client_secret_hash = get_password_hash(client_secret)
+    await db.commit()
+    await db.refresh(app)
+    logger.info("Connected app %s secret rotated", app.id)
+    return _with_secret(app, client_secret)
+
+
+@router.delete("/apps/{app_id}")
+async def delete_connected_app(
+    app_id: int,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
+):
+    app = await _get_app_or_404(db, app_id)
+    await db.execute(delete(ConnectedAppGrant).where(ConnectedAppGrant.app_id == app.id))
+    # Outstanding codes would fail anyway (their app is gone); drop them now.
+    await db.execute(
+        delete(AuthEphemeralToken).where(
+            AuthEphemeralToken.token_type == TokenType.CONNECT_CODE,
+            AuthEphemeralToken.provider_id == app.id,
+        )
+    )
+    await db.delete(app)
+    await db.commit()
+    logger.info("Connected app %s deleted", app_id)
+    return {"message": "Connected app deleted"}
+
+
+# --------------------------------------------------------------------------
+# Authorize: called by the SPA with the signed-in user's session
+# --------------------------------------------------------------------------
+
+
+async def require_signed_in_user(
+    credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)] = None,
+    db: AsyncSession = Depends(get_db),
+) -> User:
+    """The user behind a Bambuddy login token. API keys are refused.
+
+    An API key would let a script sign its owner in to an app without the
+    owner ever seeing a consent screen; only a person's own session may do
+    that. ``get_current_user`` accepts JWTs only, so a ``bb_`` key fails there.
+    """
+    if not await is_auth_enabled(db):
+        raise HTTPException(409, AUTH_DISABLED)
+    return await get_current_user(credentials)
+
+
+async def _app_for_authorize(db: AsyncSession, client_id: str, redirect_uri: str) -> ConnectedApp:
+    result = await db.execute(select(ConnectedApp).where(ConnectedApp.client_id == client_id))
+    app = result.scalar_one_or_none()
+    # One message for every case: the page must not redirect anywhere unless
+    # all of these hold, and there is nothing useful to tell the user apart.
+    if app is None or not app.enabled or not hmac.compare_digest(app.redirect_uri, redirect_uri):
+        raise HTTPException(400, "Unknown app, app disabled, or callback URL does not match its registration")
+    return app
+
+
+@router.get("/authorize/info", response_model=ConnectAuthorizeInfo)
+async def authorize_info(
+    client_id: str = Query(..., max_length=64),
+    redirect_uri: str = Query(..., max_length=500),
+    db: AsyncSession = Depends(get_db),
+    user: User = Depends(require_signed_in_user),
+):
+    """What the consent screen shows. Validates the request before the page acts on it."""
+    app = await _app_for_authorize(db, client_id, redirect_uri)
+    granted = await db.execute(
+        select(ConnectedAppGrant.id).where(ConnectedAppGrant.app_id == app.id, ConnectedAppGrant.user_id == user.id)
+    )
+    return ConnectAuthorizeInfo(
+        app_name=app.name, username=user.username, already_granted=granted.scalar_one_or_none() is not None
+    )
+
+
+@router.post("/authorize", response_model=ConnectAuthorizeResponse)
+async def authorize(
+    data: ConnectAuthorizeRequest,
+    db: AsyncSession = Depends(get_db),
+    user: User = Depends(require_signed_in_user),
+):
+    """Issue a code for the signed-in user and record their consent."""
+    app = await _app_for_authorize(db, data.client_id, data.redirect_uri)
+    now = datetime.now(timezone.utc)
+
+    # Keep the table small: codes live for a minute, so anything expired is junk.
+    await db.execute(
+        delete(AuthEphemeralToken).where(
+            AuthEphemeralToken.token_type == TokenType.CONNECT_CODE,
+            AuthEphemeralToken.expires_at < now,
+        )
+    )
+
+    granted = await db.execute(
+        select(ConnectedAppGrant.id).where(ConnectedAppGrant.app_id == app.id, ConnectedAppGrant.user_id == user.id)
+    )
+    if granted.scalar_one_or_none() is None:
+        db.add(ConnectedAppGrant(app_id=app.id, user_id=user.id))
+
+    code = secrets.token_urlsafe(32)
+    db.add(
+        AuthEphemeralToken.new_connect_code(
+            code_hash=_hash_code(code),
+            username=user.username,
+            app_id=app.id,
+            code_challenge=data.code_challenge,
+            expires_at=now + CODE_TTL,
+        )
+    )
+    try:
+        await db.commit()
+    except IntegrityError:
+        # Two tabs consenting at once both inserted the grant; the other one won.
+        await db.rollback()
+        db.add(
+            AuthEphemeralToken.new_connect_code(
+                code_hash=_hash_code(code),
+                username=user.username,
+                app_id=app.id,
+                code_challenge=data.code_challenge,
+                expires_at=now + CODE_TTL,
+            )
+        )
+        await db.commit()
+
+    logger.info("Connected app %s: code issued for user %s", app.id, user.id)
+    return ConnectAuthorizeResponse(code=code, redirect_uri=app.redirect_uri)
+
+
+# --------------------------------------------------------------------------
+# Token: called by the app's server, authenticated by its client secret
+# --------------------------------------------------------------------------
+
+
+def _token_error(status_code: int, error: str) -> HTTPException:
+    return HTTPException(status_code, {"error": error})
+
+
+@router.post("/token", response_model=ConnectTokenResponse)
+async def exchange_code(
+    request: Request,
+    data: ConnectTokenRequest,
+    db: AsyncSession = Depends(get_db),
+):
+    """Swap a code for the user's identity and permissions.
+
+    Every failure after the rate-limit check counts against both the client and
+    the caller's IP. The response says only ``invalid_client`` (who is asking
+    is wrong) or ``invalid_grant`` (the code is), never which check failed.
+    """
+    from backend.app.api.routes.auth import _get_client_ip
+    from backend.app.api.routes.mfa import check_rate_limit, clear_failed_attempts, record_failed_attempt
+
+    client_ip = _get_client_ip(request)
+    await check_rate_limit(
+        db, data.client_id, event_type=EventType.CONNECT_TOKEN_CLIENT, max_attempts=MAX_FAILED_TOKEN_EXCHANGES
+    )
+    await check_rate_limit(
+        db, client_ip, event_type=EventType.CONNECT_TOKEN_IP, max_attempts=MAX_FAILED_TOKEN_EXCHANGES
+    )
+
+    async def fail(status_code: int, error: str, reason: str) -> HTTPException:
+        await record_failed_attempt(db, data.client_id, event_type=EventType.CONNECT_TOKEN_CLIENT)
+        await record_failed_attempt(db, client_ip, event_type=EventType.CONNECT_TOKEN_IP)
+        logger.warning("Connected app token exchange refused for client %s: %s", data.client_id, reason)
+        return _token_error(status_code, error)
+
+    if not await is_auth_enabled(db):
+        raise _token_error(400, AUTH_DISABLED)
+
+    result = await db.execute(select(ConnectedApp).where(ConnectedApp.client_id == data.client_id))
+    app = result.scalar_one_or_none()
+    secret_ok = verify_password(data.client_secret, app.client_secret_hash if app else _dummy_secret_hash())
+    if app is None or not secret_ok:
+        raise await fail(401, "invalid_client", "unknown client or wrong secret")
+    if not app.enabled:
+        raise await fail(401, "invalid_client", "app disabled")
+
+    # Consume first, then check: DELETE ... RETURNING makes the code single-use
+    # even under concurrent exchanges, and a code that fails a later check is
+    # spent all the same.
+    now = datetime.now(timezone.utc)
+    consumed = await db.execute(
+        delete(AuthEphemeralToken)
+        .where(
+            AuthEphemeralToken.token == _hash_code(data.code),
+            AuthEphemeralToken.token_type == TokenType.CONNECT_CODE,
+            AuthEphemeralToken.provider_id == app.id,
+            AuthEphemeralToken.expires_at > now,
+        )
+        .returning(AuthEphemeralToken.username, AuthEphemeralToken.nonce)
+    )
+    row = consumed.one_or_none()
+    await db.commit()
+    if row is None:
+        raise await fail(400, "invalid_grant", "unknown, expired, reused or foreign code")
+    username, code_challenge = row
+
+    if not hmac.compare_digest(app.redirect_uri, data.redirect_uri):
+        raise await fail(400, "invalid_grant", "callback URL mismatch")
+    if not code_challenge or not hmac.compare_digest(_s256(data.code_verifier), code_challenge):
+        raise await fail(400, "invalid_grant", "PKCE verifier mismatch")
+
+    user = await get_user_by_username(db, username)
+    if user is None or not user.is_active:
+        raise await fail(400, "invalid_grant", "user gone or disabled")
+
+    app.last_used_at = now.replace(tzinfo=None)
+    await clear_failed_attempts(db, data.client_id, event_type=EventType.CONNECT_TOKEN_CLIENT)
+    await db.commit()
+
+    logger.info("Connected app %s: user %s signed in", app.id, user.id)
+    return ConnectTokenResponse(
+        user=ConnectedUser(
+            id=user.id,
+            username=user.username,
+            email=user.email,
+            is_admin=user.is_admin,
+            groups=sorted(g.name for g in user.groups),
+            permissions=sorted(user.get_permissions()),
+        ),
+        issued_at=now,
+    )

+ 39 - 5
backend/app/api/routes/firmware.py

@@ -16,6 +16,7 @@ from backend.app.core.auth import RequirePermissionIfAuthEnabled
 from backend.app.core.database import get_db
 from backend.app.core.permissions import Permission
 from backend.app.models.printer import Printer
+from backend.app.models.settings import Settings
 from backend.app.models.user import User
 from backend.app.services.firmware_check import get_firmware_service
 from backend.app.services.firmware_update import (
@@ -70,6 +71,37 @@ class LatestFirmwareInfo(BaseModel):
     release_notes: str | None = None
 
 
+async def _checks_enabled(db: AsyncSession) -> bool:
+    """The "Check printer firmware" setting (default on).
+
+    Enforced here and not only in the UI: the printers page asks before its
+    settings have loaded, so a check switched off still went out to bambulab.com
+    once per printer on every page load.
+    """
+    value = (
+        await db.execute(select(Settings.value).where(Settings.key == "check_printer_firmware"))
+    ).scalar_one_or_none()
+    return (value or "true").lower() != "false"
+
+
+def _current_version(printer_id: int) -> str | None:
+    mqtt_client = printer_manager.get_client(printer_id)
+    if mqtt_client and mqtt_client.state:
+        return mqtt_client.state.firmware_version
+    return None
+
+
+def _not_checked(printer: Printer) -> FirmwareUpdateInfo:
+    return FirmwareUpdateInfo(
+        printer_id=printer.id,
+        printer_name=printer.name,
+        model=printer.model or "Unknown",
+        current_version=_current_version(printer.id),
+        latest_version=None,
+        update_available=False,
+    )
+
+
 @router.get("/updates", response_model=FirmwareUpdatesResponse)
 async def check_firmware_updates(
     db: AsyncSession = Depends(get_db),
@@ -90,6 +122,9 @@ async def check_firmware_updates(
     result = await db.execute(select(Printer).where(Printer.is_active.is_(True)))
     printers = result.scalars().all()
 
+    if not await _checks_enabled(db):
+        return FirmwareUpdatesResponse(updates=[_not_checked(p) for p in printers], updates_available=0)
+
     updates = []
     updates_available = 0
 
@@ -142,11 +177,10 @@ async def check_printer_firmware(
     if not printer:
         raise HTTPException(status_code=404, detail="Printer not found")
 
-    # Get current firmware version from MQTT state
-    current_version = None
-    mqtt_client = printer_manager.get_client(printer.id)
-    if mqtt_client and mqtt_client.state:
-        current_version = mqtt_client.state.firmware_version
+    if not await _checks_enabled(db):
+        return _not_checked(printer)
+
+    current_version = _current_version(printer.id)
 
     # Check for update
     model = printer.model or "Unknown"

+ 487 - 18
backend/app/api/routes/inventory.py

@@ -1,11 +1,12 @@
 import json
 import logging
+from datetime import date, datetime, time, timezone
 
 import httpx
 from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
 from fastapi.responses import Response, StreamingResponse
 from pydantic import BaseModel, Field, field_validator
-from sqlalchemy import delete, func, select
+from sqlalchemy import case, delete, func, select
 from sqlalchemy.exc import IntegrityError
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
@@ -28,9 +29,11 @@ from backend.app.models.spool_assignment import SpoolAssignment
 from backend.app.models.spool_catalog import SpoolCatalogEntry
 from backend.app.models.spool_filament_preset import SpoolFilamentPreset
 from backend.app.models.spool_k_profile import SpoolKProfile
+from backend.app.models.supplier import SpoolmanSpoolSupplier, SpoolSupplier, Supplier, supplier_name_key
 from backend.app.models.user import User
 from backend.app.schemas.location import LocationCreate, LocationResponse, LocationUpdate
 from backend.app.schemas.spool import (
+    MaterialNumberStats,
     SpoolAssignmentCreate,
     SpoolAssignmentResponse,
     SpoolBulkCreate,
@@ -45,6 +48,15 @@ from backend.app.schemas.spool import (
     normalize_extra_colors,
 )
 from backend.app.schemas.spool_usage import SpoolUsageHistoryResponse
+from backend.app.schemas.supplier import (
+    SpoolSupplierLinkInput,
+    SpoolSupplierResponse,
+    SupplierCreate,
+    SupplierResponse,
+    SupplierStats,
+    SupplierUpdate,
+)
+from backend.app.services import slot_unlink_grace
 from backend.app.services.ams_slot_presence import spool_present
 from backend.app.services.location_service import (
     DUPLICATE_LOCATION_NAME,
@@ -56,6 +68,7 @@ from backend.app.services.location_service import (
     prepare_internal_spool_payload,
     rename_location as rename_location_record,
 )
+from backend.app.services.material_number import apply_material_number_inheritance
 from backend.app.services.slicer_filament_resolver import resolve_slicer_filament
 from backend.app.services.slot_nozzle import resolve_slot_nozzle
 from backend.app.services.spool_csv import (
@@ -67,6 +80,7 @@ from backend.app.services.spool_csv import (
 )
 from backend.app.services.spool_filament_preset import resolve_spool_preset
 from backend.app.services.spoolman import SpoolmanClient, get_spoolman_client, init_spoolman_client
+from backend.app.services.supplier_links import apply_supplier_inheritance, apply_supplier_inheritance_to_batch
 from backend.app.services.tag_conflict import tag_already_linked
 from backend.app.utils.filament_ids import (
     GENERIC_FILAMENT_IDS,
@@ -83,6 +97,20 @@ _GENERIC_ID_VALUES = set(GENERIC_FILAMENT_IDS.values())
 
 router = APIRouter(prefix="/inventory", tags=["inventory"])
 
+
+def spool_response_loads():
+    """Loader options for every query that answers with a ``SpoolResponse``.
+
+    Both relationships the schema reads carry the default loader, so the ~50
+    other ``select(Spool)`` call sites across the usage tracker, AMS sync,
+    labels and backup pay nothing for them (#2988).
+    """
+    return (
+        selectinload(Spool.k_profiles),
+        selectinload(Spool.supplier_links).selectinload(SpoolSupplier.supplier),
+    )
+
+
 # Bounded read size for the CSV import body so a chunked upload with no
 # Content-Length can't stream past the cap into memory before we notice.
 _CSV_UPLOAD_CHUNK_BYTES = 64 * 1024
@@ -361,6 +389,7 @@ async def apply_spool_to_slot_via_mqtt(
         tray_sub_brands=tray_sub_brands,
         tray_type=tray_type,
         setting_id=setting_id,
+        configured_tray_info_idx=effective_tray_info_idx,
     )
 
     logger.info(
@@ -759,6 +788,182 @@ async def delete_location(
     return {"status": "deleted"}
 
 
+# ── Supplier master list (#2988) ───────────────────────────────────────────
+#
+# Inventory master data that spools reference, exactly like Locations — so it
+# lives here, gated by the inventory permissions, not behind its own
+# permission set or a Settings page. Suppliers are *where filament is bought*,
+# distinct from ``Spool.brand`` (who made it).
+
+
+DUPLICATE_SUPPLIER_NAME = "A supplier with this name already exists"
+
+
+async def _supplier_by_name(db: AsyncSession, name: str, *, exclude_id: int | None = None) -> Supplier | None:
+    """Case-insensitive name lookup behind the duplicate guard (#2988).
+
+    Matches on the stored ``name_key``, so the comparison is the Python fold
+    the CSV import also uses — ``func.lower()`` would have folded ASCII only
+    on SQLite and let an umlaut'd case variant past the check.
+    """
+    query = select(Supplier).where(Supplier.name_key == supplier_name_key(name))
+    if exclude_id is not None:
+        query = query.where(Supplier.id != exclude_id)
+    return (await db.execute(query)).scalars().first()
+
+
+async def _supplier_reference_counts(db: AsyncSession) -> dict[int, int]:
+    """Spools referencing each supplier, across BOTH inventories.
+
+    The delete guard has to see Spoolman-mode assignments too — a supplier
+    used only by Spoolman spools must not be deletable just because the
+    built-in inventory has no reference to it.
+    """
+    counts: dict[int, int] = {}
+    for model in (SpoolSupplier, SpoolmanSpoolSupplier):
+        result = await db.execute(select(model.supplier_id, func.count(model.id)).group_by(model.supplier_id))
+        for supplier_id, count in result.all():
+            counts[supplier_id] = counts.get(supplier_id, 0) + count
+    return counts
+
+
+async def _prune_orphaned_spoolman_supplier_rows(db: AsyncSession) -> int:
+    """Drop twin rows whose Spoolman spool no longer exists (#2988).
+
+    ``_purge_local_rows_for_spool`` covers the deletes Bambuddy performs, but
+    Spoolman is a separate application with its own UI: a spool deleted there
+    — or a Spoolman instance that was rebuilt or replaced — leaves
+    ``spoolman_spool_suppliers`` rows behind that keep the supplier's
+    reference count non-zero, and nothing in Bambuddy can show or remove the
+    phantom reference. Without this, that 409 is permanent.
+
+    Reconciled on the delete attempt rather than on every listing: it costs
+    one Spoolman call, and only the route that is about to refuse needs the
+    answer. Archived spools count as live — archiving is a soft delete and the
+    assignment has to survive it. Returns the number of rows removed; 0 when
+    Spoolman is off or unreachable, which leaves the 409 standing rather than
+    dropping rows on the strength of a failed lookup.
+    """
+    settings = await _load_settings_map(db)
+    if not _spoolman_is_enabled(settings):
+        return 0
+    local_ids = set((await db.execute(select(SpoolmanSpoolSupplier.spoolman_spool_id).distinct())).scalars().all())
+    if not local_ids:
+        return 0
+    client = await _ensure_spoolman_client(settings)
+    if not client:
+        return 0
+    try:
+        spools = await client.get_all_spools(allow_archived=True)
+    except Exception:
+        logger.warning("Failed to fetch Spoolman spools to reconcile supplier assignments", exc_info=True)
+        return 0
+    stale = local_ids - {s.get("id") for s in spools if isinstance(s, dict)}
+    if not stale:
+        return 0
+    await db.execute(delete(SpoolmanSpoolSupplier).where(SpoolmanSpoolSupplier.spoolman_spool_id.in_(stale)))
+    await db.commit()
+    logger.info("Dropped supplier assignments for %d Spoolman spool(s) that no longer exist", len(stale))
+    return len(stale)
+
+
+@router.get("/suppliers", response_model=list[SupplierResponse])
+async def list_suppliers(
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
+):
+    """List all suppliers with their spool-usage counts."""
+    # name_key, not name: the list should read the way it looks, so "extrudr"
+    # sorts next to "Extrudr" rather than after "Zultrat".
+    result = await db.execute(select(Supplier).order_by(Supplier.name_key))
+    suppliers = result.scalars().all()
+    counts = await _supplier_reference_counts(db)
+    responses = []
+    for supplier in suppliers:
+        response = SupplierResponse.model_validate(supplier)
+        response.spool_count = counts.get(supplier.id, 0)
+        responses.append(response)
+    return responses
+
+
+@router.post("/suppliers", response_model=SupplierResponse, status_code=201)
+async def create_supplier(
+    data: SupplierCreate,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
+):
+    """Create a supplier (mirrors create_location, duplicate name included)."""
+    if await _supplier_by_name(db, data.name):
+        raise HTTPException(status_code=409, detail=DUPLICATE_SUPPLIER_NAME)
+    supplier = Supplier(**data.model_dump())
+    db.add(supplier)
+    try:
+        await db.commit()
+    except IntegrityError as exc:
+        # The unique index behind the check above, for the concurrent case.
+        await db.rollback()
+        raise HTTPException(status_code=409, detail=DUPLICATE_SUPPLIER_NAME) from exc
+    await db.refresh(supplier)
+    await ws_manager.broadcast({"type": "inventory_changed"})
+    return SupplierResponse.model_validate(supplier)
+
+
+@router.patch("/suppliers/{supplier_id}", response_model=SupplierResponse)
+async def update_supplier(
+    supplier_id: int,
+    data: SupplierUpdate,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
+):
+    """Update a supplier."""
+    result = await db.execute(select(Supplier).where(Supplier.id == supplier_id))
+    supplier = result.scalar_one_or_none()
+    if not supplier:
+        raise HTTPException(status_code=404, detail="Supplier not found")
+
+    fields = data.model_dump(exclude_unset=True)
+    if "name" in fields and await _supplier_by_name(db, fields["name"], exclude_id=supplier_id):
+        raise HTTPException(status_code=409, detail=DUPLICATE_SUPPLIER_NAME)
+    for field, value in fields.items():
+        setattr(supplier, field, value)
+    try:
+        await db.commit()
+    except IntegrityError as exc:
+        await db.rollback()
+        raise HTTPException(status_code=409, detail=DUPLICATE_SUPPLIER_NAME) from exc
+    await db.refresh(supplier)
+    await ws_manager.broadcast({"type": "inventory_changed"})
+
+    response = SupplierResponse.model_validate(supplier)
+    response.spool_count = (await _supplier_reference_counts(db)).get(supplier.id, 0)
+    return response
+
+
+@router.delete("/suppliers/{supplier_id}")
+async def delete_supplier(
+    supplier_id: int,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
+):
+    """Delete a supplier when no spools reference it (mirrors delete_location)."""
+    result = await db.execute(select(Supplier).where(Supplier.id == supplier_id))
+    supplier = result.scalar_one_or_none()
+    if not supplier:
+        raise HTTPException(status_code=404, detail="Supplier not found")
+
+    if (await _supplier_reference_counts(db)).get(supplier_id, 0) > 0:
+        # Last chance before refusing: the reference may be a Spoolman spool
+        # that was deleted in Spoolman itself, which Bambuddy never hears about.
+        await _prune_orphaned_spoolman_supplier_rows(db)
+        if (await _supplier_reference_counts(db)).get(supplier_id, 0) > 0:
+            raise HTTPException(status_code=409, detail="Supplier has spools assigned and cannot be deleted")
+
+    await db.delete(supplier)
+    await db.commit()
+    await ws_manager.broadcast({"type": "inventory_changed"})
+    return {"status": "deleted"}
+
+
 # ── Color Catalog CRUD ─────────────────────────────────────────────────────
 
 
@@ -1180,7 +1385,7 @@ async def list_spools(
     _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
 ):
     """List all spools, excluding archived by default."""
-    query = select(Spool).options(selectinload(Spool.k_profiles))
+    query = select(Spool).options(*spool_response_loads())
     if not include_archived:
         query = query.where(Spool.archived_at.is_(None))
     query = query.order_by(Spool.material, Spool.brand, Spool.color_name)
@@ -1201,7 +1406,13 @@ async def export_spools_csv(
     """Export the active inventory as CSV (same schema the importer accepts)."""
     from datetime import datetime, timezone
 
-    query = select(Spool).where(Spool.archived_at.is_(None)).order_by(Spool.material, Spool.brand, Spool.color_name)
+    query = (
+        select(Spool)
+        # The supplier columns (#2988) read the assignments off each row.
+        .options(selectinload(Spool.supplier_links).selectinload(SpoolSupplier.supplier))
+        .where(Spool.archived_at.is_(None))
+        .order_by(Spool.material, Spool.brand, Spool.color_name)
+    )
     result = await db.execute(query)
     spools = list(result.scalars().all())
     content = serialize(spools)
@@ -1259,7 +1470,24 @@ async def import_spools_csv(
     created = 0
     for row in preview.rows:
         if row.status == "valid" and row.spool is not None:
-            db.add(Spool(**row.spool))
+            # Deliberately no material-number inheritance here (#2870), unlike
+            # the other create paths: the file is authoritative. A CSV that
+            # leaves the column blank is stating "no number", not asking for
+            # one to be guessed from whatever else is in the inventory.
+            spool = Spool(**row.spool)
+            db.add(spool)
+            # Supplier assignments resolved by name during parsing (#2988).
+            # Flush first so the spool has an id to hang the links on.
+            if row.supplier_ids:
+                await db.flush()
+                for supplier_id in row.supplier_ids:
+                    db.add(
+                        SpoolSupplier(
+                            spool_id=spool.id,
+                            supplier_id=supplier_id,
+                            is_purchase_source=supplier_id == row.purchase_supplier_id,
+                        )
+                    )
             created += 1
 
     if created:
@@ -1299,7 +1527,7 @@ async def get_spool_by_tag(
     if not normalized_tray_uuid and not normalized_tag_uid:
         raise HTTPException(400, "Provide tray_uuid and/or tag_uid")
 
-    base_query = select(Spool).options(selectinload(Spool.k_profiles))
+    base_query = select(Spool).options(*spool_response_loads())
     if not include_archived:
         base_query = base_query.where(Spool.archived_at.is_(None))
 
@@ -1324,7 +1552,7 @@ async def get_spool(
     _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
 ):
     """Get a single spool with k_profiles."""
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id == spool_id))
     spool = result.scalar_one_or_none()
     if not spool:
         raise HTTPException(404, "Spool not found")
@@ -1342,11 +1570,17 @@ async def create_spool(
         payload = await prepare_internal_spool_payload(db, spool_data.model_dump(), set(spool_data.model_fields_set))
     except ValueError as exc:
         raise HTTPException(status_code=400, detail=str(exc)) from exc
+    # A new spool of an already-numbered product inherits its material number (#2870).
+    payload = await apply_material_number_inheritance(db, payload)
     spool = Spool(**payload)
     db.add(spool)
+    await db.flush()
+    # A new spool of a product that already carries supplier assignments
+    # inherits the source list (#2988).
+    await apply_supplier_inheritance(db, spool)
     await db.commit()
     await db.refresh(spool)
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool.id))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id == spool.id))
     await ws_manager.broadcast({"type": "inventory_changed"})
     return result.scalar_one()
 
@@ -1364,13 +1598,18 @@ async def bulk_create_spools(
         payload = await prepare_internal_spool_payload(db, data.spool.model_dump(), fields_set)
     except ValueError as exc:
         raise HTTPException(status_code=400, detail=str(exc)) from exc
+    # A new spool of an already-numbered product inherits its material number (#2870).
+    payload = await apply_material_number_inheritance(db, payload)
     for _ in range(data.quantity):
         spool = Spool(**payload)
         db.add(spool)
         spools.append(spool)
+    await db.flush()
+    # Every copy gets its own link rows, from one donor lookup (#2988).
+    await apply_supplier_inheritance_to_batch(db, spools)
     await db.commit()
     ids = [s.id for s in spools]
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id.in_(ids)))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id.in_(ids)))
     await ws_manager.broadcast({"type": "inventory_changed"})
     return list(result.scalars().all())
 
@@ -1401,7 +1640,7 @@ async def update_spool(
         setattr(spool, field, value)
 
     await db.commit()
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id == spool_id))
     await ws_manager.broadcast({"type": "inventory_changed"})
     return result.scalar_one()
 
@@ -1440,7 +1679,7 @@ async def archive_spool(
 
     spool.archived_at = datetime.now(timezone.utc)
     await db.commit()
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id == spool_id))
     await ws_manager.broadcast({"type": "inventory_changed"})
     return result.scalar_one()
 
@@ -1459,7 +1698,7 @@ async def restore_spool(
 
     spool.archived_at = None
     await db.commit()
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id == spool_id))
     await ws_manager.broadcast({"type": "inventory_changed"})
     return result.scalar_one()
 
@@ -1491,7 +1730,7 @@ async def reset_spool_consumed_counter(
 
     spool.weight_used_baseline = spool.weight_used or 0
     await db.commit()
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id == spool_id))
     await ws_manager.broadcast({"type": "inventory_changed"})
     return result.scalar_one()
 
@@ -1689,6 +1928,56 @@ async def replace_k_profiles(
     return new_profiles
 
 
+@router.put("/spools/{spool_id}/suppliers", response_model=list[SpoolSupplierResponse])
+async def replace_spool_suppliers(
+    spool_id: int,
+    links: list[SpoolSupplierLinkInput],
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
+):
+    """Replace a spool's supplier assignments (#2988, batch save).
+
+    Same replace-all shape as the k-profiles endpoint. At most one assignment
+    may be the purchase source — the record of where this concrete spool was
+    actually bought; the rest read as alternative sources.
+    """
+    result = await db.execute(select(Spool).where(Spool.id == spool_id))
+    if not result.scalar_one_or_none():
+        raise HTTPException(404, "Spool not found")
+
+    supplier_ids = [link.supplier_id for link in links]
+    if len(set(supplier_ids)) != len(supplier_ids):
+        raise HTTPException(400, "Duplicate supplier in assignment list")
+    if sum(1 for link in links if link.is_purchase_source) > 1:
+        raise HTTPException(400, "Only one assignment can be the purchase source")
+    if supplier_ids:
+        found = await db.execute(select(Supplier.id).where(Supplier.id.in_(supplier_ids)))
+        missing = set(supplier_ids) - {row[0] for row in found.all()}
+        if missing:
+            raise HTTPException(404, f"Supplier(s) not found: {sorted(missing)}")
+
+    existing = await db.execute(select(SpoolSupplier).where(SpoolSupplier.spool_id == spool_id))
+    for old in existing.scalars().all():
+        await db.delete(old)
+    await db.flush()
+
+    new_links = []
+    for link in links:
+        row = SpoolSupplier(spool_id=spool_id, **link.model_dump())
+        db.add(row)
+        new_links.append(row)
+
+    await db.commit()
+    refreshed = await db.execute(
+        select(SpoolSupplier)
+        .options(selectinload(SpoolSupplier.supplier))
+        .where(SpoolSupplier.id.in_([row.id for row in new_links]))
+        .order_by(SpoolSupplier.id)
+    )
+    await ws_manager.broadcast({"type": "inventory_changed"})
+    return list(refreshed.scalars().all())
+
+
 @router.get("/spools/{spool_id}/filament-presets", response_model=list[SpoolFilamentPresetResponse])
 async def list_filament_presets(
     spool_id: int,
@@ -1769,7 +2058,7 @@ async def list_assignments(
     from backend.app.services.printer_manager import printer_manager
 
     query = select(SpoolAssignment).options(
-        selectinload(SpoolAssignment.spool).selectinload(Spool.k_profiles),
+        selectinload(SpoolAssignment.spool).options(*spool_response_loads()),
         selectinload(SpoolAssignment.printer),
     )
     if printer_id is not None:
@@ -1834,7 +2123,7 @@ async def assign_spool(
     from backend.app.services.printer_manager import printer_manager
 
     # 1. Validate spool exists and is not archived
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == data.spool_id))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id == data.spool_id))
     spool = result.scalar_one_or_none()
     if not spool:
         raise HTTPException(404, "Spool not found")
@@ -1918,6 +2207,7 @@ async def assign_spool(
     db.add(assignment)
     await db.commit()
     await db.refresh(assignment)
+    slot_unlink_grace.forget_slot(data.printer_id, data.ams_id, data.tray_id)
 
     # 4. Auto-configure AMS slot via MQTT.
     #
@@ -2013,7 +2303,7 @@ async def assign_spool(
     result = await db.execute(
         select(SpoolAssignment)
         .options(
-            selectinload(SpoolAssignment.spool).selectinload(Spool.k_profiles),
+            selectinload(SpoolAssignment.spool).options(*spool_response_loads()),
             selectinload(SpoolAssignment.printer),
         )
         .where(SpoolAssignment.id == assignment.id)
@@ -2118,7 +2408,7 @@ async def link_tag_to_spool(
     ``tag_already_linked`` 409, which names that spool so a caller can offer
     to move the tag instead of only reporting that it is taken (#3110).
     """
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id == spool_id))
     spool = result.scalar_one_or_none()
     if not spool:
         raise HTTPException(404, "Spool not found")
@@ -2199,7 +2489,7 @@ async def link_tag_to_spool(
         spool.data_origin = data.data_origin
 
     await db.commit()
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    result = await db.execute(select(Spool).options(*spool_response_loads()).where(Spool.id == spool_id))
     return result.scalar_one()
 
 
@@ -2230,6 +2520,175 @@ async def get_spool_usage_history(
     return list(result.scalars().all())
 
 
+@router.get("/stats/suppliers", response_model=list[SupplierStats])
+async def get_supplier_stats(
+    date_from: date | None = Query(None, description="Start date (inclusive), YYYY-MM-DD"),
+    date_to: date | None = Query(None, description="End date (inclusive), YYYY-MM-DD"),
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
+):
+    """Aggregate the inventory by purchase-source supplier (#2988).
+
+    Groups by the supplier a spool was actually bought from (the
+    ``is_purchase_source`` assignment), so "how much did we run through
+    supplier X" reads directly. Stock comes from active spools; consumption
+    and cost from the recorded usage history, archived spools included —
+    their consumption happened. Sorted by consumption, heaviest first, then by name.
+
+    ``date_from`` / ``date_to`` scope the usage half only, so the widget can
+    honour the dashboard timeframe like every other one on that page. Stock is
+    point-in-time by nature and is never windowed.
+    """
+    from backend.app.models.spool_usage_history import SpoolUsageHistory
+
+    purchase_link = (SpoolSupplier.spool_id == Spool.id) & SpoolSupplier.is_purchase_source.is_(True)
+    usage_window = []
+    if date_from:
+        usage_window.append(SpoolUsageHistory.created_at >= datetime.combine(date_from, time.min, tzinfo=timezone.utc))
+    if date_to:
+        usage_window.append(SpoolUsageHistory.created_at <= datetime.combine(date_to, time.max, tzinfo=timezone.utc))
+
+    remaining = Spool.label_weight - Spool.weight_used
+    inventory_rows = await db.execute(
+        select(
+            SpoolSupplier.supplier_id,
+            func.count(Spool.id),
+            # Clamp each spool before summing: an over-consumed spool counts as
+            # empty, not as negative stock that eats into its neighbours.
+            func.sum(case((remaining > 0, remaining), else_=0.0)),
+        )
+        .select_from(Spool)
+        .join(SpoolSupplier, purchase_link)
+        .where(Spool.archived_at.is_(None))
+        .group_by(SpoolSupplier.supplier_id)
+    )
+
+    usage_rows = await db.execute(
+        select(
+            SpoolSupplier.supplier_id,
+            func.sum(SpoolUsageHistory.weight_used),
+            func.sum(SpoolUsageHistory.cost),
+        )
+        .select_from(SpoolUsageHistory)
+        .join(Spool, SpoolUsageHistory.spool_id == Spool.id)
+        .join(SpoolSupplier, purchase_link)
+        .where(*usage_window)
+        .group_by(SpoolSupplier.supplier_id)
+    )
+
+    names = dict((await db.execute(select(Supplier.id, Supplier.name))).all())
+
+    stats: dict[int, SupplierStats] = {}
+    for supplier_id, count, remaining_g in inventory_rows.all():
+        stats[supplier_id] = SupplierStats(
+            supplier_id=supplier_id,
+            supplier_name=names.get(supplier_id, f"#{supplier_id}"),
+            spool_count=count,
+            remaining_g=float(remaining_g or 0),
+            consumed_g=0.0,
+            cost=0.0,
+        )
+    for supplier_id, consumed, cost in usage_rows.all():
+        entry = stats.get(supplier_id)
+        if entry is None:
+            entry = SupplierStats(
+                supplier_id=supplier_id,
+                supplier_name=names.get(supplier_id, f"#{supplier_id}"),
+                spool_count=0,
+                remaining_g=0.0,
+                consumed_g=0.0,
+                cost=0.0,
+            )
+            stats[supplier_id] = entry
+        entry.consumed_g = float(consumed or 0)
+        entry.cost = float(cost or 0)
+
+    # Name breaks ties: a narrow date range leaves most rows at 0 g consumed,
+    # and those would otherwise come back in GROUP BY order.
+    return sorted(stats.values(), key=lambda s: (-s.consumed_g, supplier_name_key(s.supplier_name)))
+
+
+@router.get("/stats/material-numbers", response_model=list[MaterialNumberStats])
+async def get_material_number_stats(
+    date_from: date | None = Query(None),
+    date_to: date | None = Query(None),
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
+):
+    """Aggregate the inventory by material number (#2870).
+
+    The material number is the internal purchasing identifier shared by all
+    spools of a product, so this is the grouping the business actually costs
+    by — unlike brand+material+colour. Two queries: active-spool counts and
+    remaining weight from the spool table, consumption and cost from the
+    recorded usage history (archived spools included — their consumption
+    happened).
+
+    ``date_from``/``date_to`` narrow the usage half only, so the widget can
+    follow the dashboard timeframe the rest of the stats page uses. Stock is
+    point-in-time by nature and stays unfiltered — "how much do I hold" has
+    no date range. Sorted by consumption, heaviest first, then by number so
+    a range where nothing was consumed still lists in a stable order.
+    """
+    from backend.app.models.spool_usage_history import SpoolUsageHistory
+
+    # material_number is normalised to NULL-or-non-empty by the schema
+    # validator, so NULL is the only "unset" state to exclude here.
+    has_number = Spool.material_number.is_not(None)
+
+    usage_filters = [has_number]
+    if date_from:
+        usage_filters.append(SpoolUsageHistory.created_at >= datetime.combine(date_from, time.min, tzinfo=timezone.utc))
+    if date_to:
+        usage_filters.append(SpoolUsageHistory.created_at <= datetime.combine(date_to, time.max, tzinfo=timezone.utc))
+
+    # Clamped PER SPOOL, like every other remaining-weight computation in the
+    # codebase: a spool whose weight_used overshot its label_weight holds 0 g,
+    # it does not subtract from the other spools sharing the number.
+    per_spool_remaining = func.coalesce(Spool.label_weight, 0) - func.coalesce(Spool.weight_used, 0)
+    inventory_rows = await db.execute(
+        select(
+            Spool.material_number,
+            func.count(Spool.id),
+            func.sum(case((per_spool_remaining > 0, per_spool_remaining), else_=0.0)),
+        )
+        .where(has_number, Spool.archived_at.is_(None))
+        .group_by(Spool.material_number)
+    )
+
+    usage_rows = await db.execute(
+        select(
+            Spool.material_number,
+            func.sum(SpoolUsageHistory.weight_used),
+            func.sum(SpoolUsageHistory.cost),
+        )
+        .join(Spool, SpoolUsageHistory.spool_id == Spool.id)
+        .where(*usage_filters)
+        .group_by(Spool.material_number)
+    )
+
+    stats: dict[str, MaterialNumberStats] = {}
+    for number, count, remaining in inventory_rows.all():
+        stats[number] = MaterialNumberStats(
+            material_number=number,
+            spool_count=count,
+            remaining_g=float(remaining or 0),
+            consumed_g=0.0,
+            cost=0.0,
+        )
+    for number, consumed, cost in usage_rows.all():
+        entry = stats.get(number)
+        if entry is None:
+            entry = MaterialNumberStats(
+                material_number=number, spool_count=0, remaining_g=0.0, consumed_g=0.0, cost=0.0
+            )
+            stats[number] = entry
+        entry.consumed_g = float(consumed or 0)
+        entry.cost = float(cost or 0)
+
+    return sorted(stats.values(), key=lambda s: (-s.consumed_g, s.material_number))
+
+
 @router.get("/usage", response_model=list[SpoolUsageHistoryResponse])
 async def get_all_usage_history(
     limit: int = 100,
@@ -2717,5 +3176,15 @@ async def create_spool_from_slot(
             "spool_id": spool.id,
         }
     )
-    result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool.id))
+    # populate_existing because `spool` is the same identity-mapped instance
+    # create_spool_from_tray built: it pre-initialises `supplier_links` to []
+    # so the flush can't lazy-load it, and SQLAlchemy will not overwrite an
+    # already-loaded collection on a plain re-select — the inherited
+    # assignments (#2988) would be in the table but missing from the response.
+    result = await db.execute(
+        select(Spool)
+        .options(*spool_response_loads())
+        .where(Spool.id == spool.id)
+        .execution_options(populate_existing=True)
+    )
     return result.scalar_one()

+ 194 - 93
backend/app/api/routes/labels.py

@@ -5,32 +5,49 @@ Two endpoints, one per inventory backend:
 - ``POST /inventory/labels``  — local-DB spools
 - ``POST /spoolman/labels``   — Spoolman-backed spools
 
-Both accept ``{spool_ids: [int], template: str, starting_position: int}`` and
-return a PDF stream.
+Both accept ``{spool_ids: [int], template: str, starting_position: int}`` plus
+the lines to print (``fields``) and the output ``format``: a PDF, or PNGs for
+label-printer software that takes images — one PNG as is, several in a ZIP.
+Each has a ``/preview`` sibling that renders one label as a PNG for the
+picker (#2981).
+
 The QR code on each label deep-links to ``/inventory?spool=<id>`` so a phone
 scan jumps straight back into Bambuddy at that spool's row.
 """
 
 from __future__ import annotations
 
+import asyncio
 import io
 import logging
-from typing import Literal
+import zipfile
+from datetime import date, datetime
+from typing import Annotated, Literal
 
 from fastapi import APIRouter, Depends, HTTPException, Request
-from fastapi.responses import StreamingResponse
+from fastapi.responses import Response, StreamingResponse
 from pydantic import BaseModel, Field, model_validator
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 
-from backend.app.api.routes._spoolman_helpers import parse_spoolman_multi_colors
+from backend.app.api.routes._spoolman_helpers import _map_spoolman_spool
 from backend.app.api.routes.settings import get_setting
 from backend.app.core.auth import RequirePermissionIfAuthEnabled
 from backend.app.core.database import get_db
 from backend.app.core.permissions import Permission
 from backend.app.models.spool import Spool
 from backend.app.models.user import User
-from backend.app.services.label_renderer import LabelData, TemplateName, get_sheet_capacity, render_labels
+from backend.app.services.label_renderer import (
+    ALL_LABEL_FIELDS,
+    DEFAULT_LABEL_FIELDS,
+    LabelData,
+    LabelField,
+    TemplateName,
+    get_sheet_capacity,
+    pdf_to_pngs,
+    render_label_preview_pdf,
+    render_labels,
+)
 from backend.app.services.spoolman import get_spoolman_client
 from backend.app.utils.http import build_content_disposition
 
@@ -38,34 +55,38 @@ logger = logging.getLogger(__name__)
 
 router = APIRouter(tags=["labels"])
 
-_VALID_TEMPLATES: tuple[TemplateName, ...] = (
-    "ams_holder_74x33",
-    "ams_holder_75x55",
-    "box_40x30",
-    "box_62x29",
-    "avery_5160",
-    "avery_l7160",
-)
-
 # Cap how many labels can be requested in one go. Sane upper bound for the
 # largest realistic batch (an Avery sheet at 30/page × ~10 pages).
 MAX_LABELS_PER_REQUEST = 500
 
+# Preview resolution. High enough that the browser's downscale stays sharp on
+# a HiDPI screen; a label is a few centimetres, so the PNG stays small.
+PREVIEW_DPI = 300
+
+
+# Spoolman's mapping rejects an id below 1, so one never reaches it.
+SpoolId = Annotated[int, Field(gt=0)]
 
-class LabelRequest(BaseModel):
-    spool_ids: list[int] = Field(..., min_length=1, max_length=MAX_LABELS_PER_REQUEST)
-    template: Literal[
-        "ams_holder_74x33",
-        "ams_holder_75x55",
-        "box_40x30",
-        "box_62x29",
-        "avery_5160",
-        "avery_l7160",
-    ]
+
+class LabelOptions(BaseModel):
+    template: TemplateName
     # Black-and-white thermal printers: drop the colour swatch (prints as a
     # muddy grey block) and widen the text column instead (#1870).
     monochrome: bool = False
+    # The lines to print (#2981). Omitted: what labels always carried.
+    fields: list[LabelField] | None = Field(default=None, max_length=len(ALL_LABEL_FIELDS))
+
+    def field_set(self) -> frozenset[LabelField]:
+        return DEFAULT_LABEL_FIELDS if self.fields is None else frozenset(self.fields)
+
+
+class LabelRequest(LabelOptions):
+    spool_ids: list[SpoolId] = Field(..., min_length=1, max_length=MAX_LABELS_PER_REQUEST)
     starting_position: int = Field(default=1, ge=1)
+    format: Literal["pdf", "png"] = "pdf"
+    # PNG only. 203 and 300 are the common thermal-printer resolutions; an
+    # image at the printer's own resolution is printed dot for dot.
+    dpi: Literal[203, 300, 600] = 300
 
     @model_validator(mode="after")
     def validate_starting_position(self) -> LabelRequest:
@@ -79,6 +100,10 @@ class LabelRequest(BaseModel):
         return self
 
 
+class LabelPreviewRequest(LabelOptions):
+    spool_id: SpoolId
+
+
 def _split_extra_colors(raw: str | None) -> list[str] | None:
     """Parse ``Spool.extra_colors`` (comma-separated hex tokens) into a list."""
     if not raw:
@@ -111,37 +136,54 @@ def _spool_to_label_data(spool: Spool, deeplink_base: str) -> LabelData:
         extra_colors=_split_extra_colors(spool.extra_colors),
         storage_location=getattr(spool, "storage_location", None),
         deeplink_url=f"{deeplink_base}/inventory?spool={spool.id}",
+        material_number=spool.material_number,
+        nozzle_temp_min=spool.nozzle_temp_min,
+        nozzle_temp_max=spool.nozzle_temp_max,
+        label_weight=spool.label_weight,
+        note=spool.note,
+        added=spool.created_at.date() if spool.created_at else None,
     )
 
 
+def _parse_date(value: str | None) -> date | None:
+    if not value:
+        return None
+    try:
+        return datetime.fromisoformat(value).date()
+    except ValueError:
+        return None
+
+
 def _spoolman_dict_to_label_data(s: dict, deeplink_base: str) -> LabelData:
     """Build LabelData from a raw Spoolman /spool response dict.
 
-    Spoolman models don't have a native 'spool name' — we derive it from the
-    embedded filament. Material and brand come from filament/vendor.
+    Goes through ``_map_spoolman_spool``, the mapping the inventory page shows,
+    so a Spoolman label carries what a built-in-inventory label does: the same
+    subtype, colour name, material number, temperature and weight.
     """
-    filament = s.get("filament") or {}
-    vendor = filament.get("vendor") or {}
-    fname = filament.get("name") or ""
-    material = filament.get("material") or ""
-    brand = vendor.get("name")
-    color_hex = filament.get("color_hex")
-    rgba = color_hex.lstrip("#") if isinstance(color_hex, str) else None
-
-    # Shared with `_map_spoolman_spool`, so the swatch printed on a label and
-    # the swatch drawn on an AMS slot card cannot read the same field two ways.
-    extra: list[str] | None = parse_spoolman_multi_colors(filament) or None
+    m = _map_spoolman_spool(s)
+    material = m["material"] or ""
+    # Same precedence as a built-in spool: a colour name the user set, then the
+    # filament (slicer) name. A synthesised colour name is only the subtype.
+    color_name = None if m["color_name_is_synthesized"] else m["color_name"]
+    name = color_name or m["slicer_filament_name"] or f"{m['brand'] or ''} {material}".strip()
 
     return LabelData(
-        spool_id=int(s.get("id", 0)),
-        name=fname or material or "Spool",
-        material=material or "",
-        brand=brand,
-        subtype=None,
-        rgba=rgba,
-        extra_colors=extra,
-        storage_location=s.get("location"),
-        deeplink_url=f"{deeplink_base}/inventory?spool={int(s.get('id', 0))}",
+        spool_id=m["id"],
+        name=name or material or "Spool",
+        material=material,
+        brand=m["brand"],
+        subtype=m["subtype"],
+        rgba=m["rgba"],
+        extra_colors=_split_extra_colors(m["extra_colors"]),
+        storage_location=m["storage_location"],
+        deeplink_url=f"{deeplink_base}/inventory?spool={m['id']}",
+        material_number=m["material_number"],
+        nozzle_temp_min=m["nozzle_temp_min"],
+        nozzle_temp_max=m["nozzle_temp_max"],
+        label_weight=m["label_weight"],
+        note=m["note"],
+        added=_parse_date(m["created_at"]),
     )
 
 
@@ -159,58 +201,78 @@ def _stream_pdf(pdf: bytes, filename: str) -> StreamingResponse:
     )
 
 
-@router.post("/inventory/labels")
-async def render_local_inventory_labels(
-    body: LabelRequest,
-    request: Request,
-    db: AsyncSession = Depends(get_db),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
-) -> StreamingResponse:
-    """Render labels for spools in the local inventory."""
-    if body.template not in _VALID_TEMPLATES:
-        raise HTTPException(400, f"Unknown template: {body.template}")
+async def _render_response(body: LabelRequest, data_list: list[LabelData], filename_stem: str) -> Response:
+    pdf = render_labels(
+        body.template,
+        data_list,
+        monochrome=body.monochrome,
+        starting_position=body.starting_position,
+        fields=body.field_set(),
+    )
+    if body.format == "pdf":
+        return _stream_pdf(pdf, f"{filename_stem}.pdf")
+
+    # Rasterising a few hundred pages is CPU work; keep it off the event loop.
+    pngs = await asyncio.to_thread(pdf_to_pngs, pdf, body.dpi)
+    if len(pngs) == 1:
+        return Response(
+            content=pngs[0],
+            media_type="image/png",
+            headers={
+                "Content-Disposition": build_content_disposition(f"{filename_stem}.png", disposition="attachment"),
+                "Cache-Control": "no-store",
+            },
+        )
+
+    # A roll template has one page per spool, so its files are named after
+    # the spool; a sheet's pages are numbered.
+    if get_sheet_capacity(body.template) is None:
+        names = [f"label-{d.spool_id}.png" for d in data_list]
+    else:
+        names = [f"sheet-{n}.png" for n in range(1, len(pngs) + 1)]
+    buf = io.BytesIO()
+    # PNG is already compressed; deflating it again only costs time.
+    with zipfile.ZipFile(buf, "w", zipfile.ZIP_STORED) as zf:
+        for name, png in zip(names, pngs, strict=True):
+            zf.writestr(name, png)
+    return Response(
+        content=buf.getvalue(),
+        media_type="application/zip",
+        headers={
+            "Content-Disposition": build_content_disposition(f"{filename_stem}.zip", disposition="attachment"),
+            "Cache-Control": "no-store",
+        },
+    )
+
+
+async def _render_preview(body: LabelPreviewRequest, data: LabelData) -> Response:
+    pdf = render_label_preview_pdf(body.template, data, monochrome=body.monochrome, fields=body.field_set())
+    pngs = await asyncio.to_thread(pdf_to_pngs, pdf, PREVIEW_DPI)
+    return Response(content=pngs[0], media_type="image/png", headers={"Cache-Control": "no-store"})
+
 
-    result = await db.execute(select(Spool).where(Spool.id.in_(body.spool_ids)))
+async def _local_label_data(spool_ids: list[int], request: Request, db: AsyncSession) -> list[LabelData]:
+    result = await db.execute(select(Spool).where(Spool.id.in_(spool_ids)))
     spools = list(result.scalars().all())
 
     found_ids = {s.id for s in spools}
-    missing = [sid for sid in body.spool_ids if sid not in found_ids]
+    missing = [sid for sid in spool_ids if sid not in found_ids]
     if missing:
         raise HTTPException(404, f"Spool(s) not found: {missing}")
 
     # Preserve caller's order so an Avery sheet print matches the on-screen list.
-    ordered = sorted(spools, key=lambda s: body.spool_ids.index(s.id))
+    ordered = sorted(spools, key=lambda s: spool_ids.index(s.id))
 
     deeplink_base = await _resolve_deeplink_base(request, db)
-    data_list = [_spool_to_label_data(s, deeplink_base) for s in ordered]
-
-    pdf = render_labels(
-        body.template,
-        data_list,
-        monochrome=body.monochrome,
-        starting_position=body.starting_position,
-    )
-    filename = f"bambuddy-labels-{body.template}.pdf"
-    return _stream_pdf(pdf, filename)
+    return [_spool_to_label_data(s, deeplink_base) for s in ordered]
 
 
-@router.post("/spoolman/labels")
-async def render_spoolman_labels(
-    body: LabelRequest,
-    request: Request,
-    db: AsyncSession = Depends(get_db),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
-) -> StreamingResponse:
-    """Render labels for spools tracked in Spoolman.
-
-    The Spoolman client doesn't expose a per-id endpoint, so this fetches the
+async def _spoolman_label_data(spool_ids: list[int], request: Request, db: AsyncSession) -> list[LabelData]:
+    """The Spoolman client doesn't expose a per-id endpoint, so this fetches the
     full spool list and filters in-memory. For typical libraries (~50 spools)
     that's negligible; for very large libraries this is the trade-off until
     Spoolman gains a bulk filter.
     """
-    if body.template not in _VALID_TEMPLATES:
-        raise HTTPException(400, f"Unknown template: {body.template}")
-
     spoolman_on = (await get_setting(db, "spoolman_enabled") or "").lower() == "true"
     if not spoolman_on:
         raise HTTPException(400, "Spoolman integration is not enabled")
@@ -226,18 +288,57 @@ async def render_spoolman_labels(
         raise HTTPException(502, "Failed to fetch spools from Spoolman") from exc
 
     by_id = {int(s.get("id", 0)): s for s in all_spools if s.get("id") is not None}
-    missing = [sid for sid in body.spool_ids if sid not in by_id]
+    missing = [sid for sid in spool_ids if sid not in by_id]
     if missing:
         raise HTTPException(404, f"Spool(s) not found in Spoolman: {missing}")
 
     deeplink_base = await _resolve_deeplink_base(request, db)
-    data_list = [_spoolman_dict_to_label_data(by_id[sid], deeplink_base) for sid in body.spool_ids]
+    return [_spoolman_dict_to_label_data(by_id[sid], deeplink_base) for sid in spool_ids]
 
-    pdf = render_labels(
-        body.template,
-        data_list,
-        monochrome=body.monochrome,
-        starting_position=body.starting_position,
-    )
-    filename = f"bambuddy-labels-spoolman-{body.template}.pdf"
-    return _stream_pdf(pdf, filename)
+
+@router.post("/inventory/labels")
+async def render_local_inventory_labels(
+    body: LabelRequest,
+    request: Request,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
+) -> Response:
+    """Render labels for spools in the local inventory."""
+    data_list = await _local_label_data(body.spool_ids, request, db)
+    return await _render_response(body, data_list, f"bambuddy-labels-{body.template}")
+
+
+@router.post("/inventory/labels/preview")
+async def preview_local_inventory_label(
+    body: LabelPreviewRequest,
+    request: Request,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
+) -> Response:
+    """One label for a local spool, as a PNG, for the picker's preview."""
+    data_list = await _local_label_data([body.spool_id], request, db)
+    return await _render_preview(body, data_list[0])
+
+
+@router.post("/spoolman/labels")
+async def render_spoolman_labels(
+    body: LabelRequest,
+    request: Request,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
+) -> Response:
+    """Render labels for spools tracked in Spoolman."""
+    data_list = await _spoolman_label_data(body.spool_ids, request, db)
+    return await _render_response(body, data_list, f"bambuddy-labels-spoolman-{body.template}")
+
+
+@router.post("/spoolman/labels/preview")
+async def preview_spoolman_label(
+    body: LabelPreviewRequest,
+    request: Request,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
+) -> Response:
+    """One label for a Spoolman spool, as a PNG, for the picker's preview."""
+    data_list = await _spoolman_label_data([body.spool_id], request, db)
+    return await _render_preview(body, data_list[0])

+ 472 - 43
backend/app/api/routes/library.py

@@ -17,7 +17,7 @@ from pathlib import Path
 
 from fastapi import APIRouter, Depends, File, HTTPException, Query, Response, UploadFile
 from fastapi.responses import FileResponse as FastAPIFileResponse
-from sqlalchemy import distinct, func, select
+from sqlalchemy import distinct, func, select, update
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
@@ -49,6 +49,8 @@ from backend.app.schemas.library import (
     BatchThumbnailResult,
     BulkDeleteRequest,
     BulkDeleteResponse,
+    ClientThumbnailResponse,
+    CombineFilesRequest,
     ExternalFolderCreate,
     FileDuplicate,
     FileListResponse,
@@ -75,7 +77,9 @@ from backend.app.services.design_settings import (
     overrides_from_config,
 )
 from backend.app.services.filament_requirements import annotate_rack_groups
+from backend.app.services.pdf_thumbnail import generate_pdf_thumbnail
 from backend.app.services.plate_thumbnail import inject_plate_thumbnails_if_missing
+from backend.app.services.print_confirmation import confirm_outcome_for_new_queue_item
 from backend.app.services.process_overrides import apply_process_overrides
 from backend.app.services.slice_output_check import (
     missing_start_gcode_message,
@@ -90,6 +94,7 @@ from backend.app.utils.filename import (
     safe_path_component,
     validate_print_filename,
 )
+from backend.app.utils.library_paths import library_photos_dir, remove_library_photos_dir
 from backend.app.utils.printer_models import is_gcode_compatible
 from backend.app.utils.safe_path import PathTraversalError, assert_under, safe_join_under
 from backend.app.utils.threemf_tools import (
@@ -653,8 +658,10 @@ async def save_3mf_bytes_to_library(
     thumbnail_path: str | None = None
     if ext == ".3mf":
         try:
+            # Off the event loop: the parser still decompresses the whole model
+            # entry, which for a combined plate is hundreds of MB (#3162).
             parser = ThreeMFParser(str(file_path))
-            raw_metadata = parser.parse()
+            raw_metadata = await asyncio.to_thread(parser.parse)
             thumb_data = raw_metadata.get("_thumbnail_data")
             thumb_ext = raw_metadata.get("_thumbnail_ext", ".png")
             if thumb_data:
@@ -808,9 +815,42 @@ def create_image_thumbnail(file_path: Path, thumbnails_dir: Path, max_size: int
 # Supported image extensions for thumbnails
 IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif"}
 
+# File types whose thumbnails are rendered client-side and uploaded back
+# (#2976). The server has no renderer for STEP (that would need OpenCascade)
+# or the spreadsheet types, so the browser posts its first preview render to
+# POST /files/{id}/preview-thumbnail instead. PDF is rendered server-side with
+# PDFium when it lands and stays here for a PDF that renderer cannot read.
+# Kept to exactly these types so the endpoint can never overwrite a
+# server-generated STL/3MF/G-code/image thumbnail.
+CLIENT_THUMBNAIL_TYPES = {"step", "stp", "pdf", "csv", "xlsx", "ods"}
+
+# Photos of the printed result (#3077): same allowlist and naming as the
+# archive photo routes. 10 MB is ample for a phone camera JPEG.
+PHOTO_EXTENSIONS = (".jpg", ".jpeg", ".png", ".webp")
+PHOTO_MEDIA_TYPES = {
+    ".jpg": "image/jpeg",
+    ".jpeg": "image/jpeg",
+    ".png": "image/png",
+    ".webp": "image/webp",
+}
+MAX_PHOTO_BYTES = 10 * 1024 * 1024
+
+# Upper bound for an uploaded client-rendered thumbnail. The FE sends a
+# 256px PNG (a few tens of KB); anything near this limit is not a thumbnail.
+MAX_CLIENT_THUMBNAIL_BYTES = 2 * 1024 * 1024
+
+# Upper bound on the *decoded* size, checked against the header before any
+# pixels are allocated: a few-KB PNG can declare 12000x7000 and still be under
+# PIL's own decompression-bomb limit, which would be ~340 MB of RGBA.
+MAX_CLIENT_THUMBNAIL_EDGE = 2048
 
-async def _backfill_external_stl_thumbnails(folder_ids: list[int]) -> None:
-    """Generate STL thumbnails for an external folder tree in the background.
+# What the endpoint stores. The grid renders at ~256px, so anything larger is
+# downscaled rather than kept.
+STORED_CLIENT_THUMBNAIL_EDGE = 512
+
+
+async def _backfill_external_thumbnails(folder_ids: list[int]) -> None:
+    """Generate STL and PDF thumbnails for an external folder tree in the background.
 
     Spawned via ``asyncio.create_task`` from ``scan_external_folder`` so the
     HTTP request can return as soon as the filesystem walk + folder/file rows
@@ -818,7 +858,9 @@ async def _backfill_external_stl_thumbnails(folder_ids: list[int]) -> None:
     the request open for many minutes (each file triggers a ``trimesh.load``
     + matplotlib render, ~1-5s each) and the FE modal times out before the
     final ``db.commit()`` runs — causing the original symptom in #1299 where
-    subdirectories never showed up because nothing got committed.
+    subdirectories never showed up because nothing got committed. PDFs are
+    faster (a PDFium page render) but a share holding hundreds of them would
+    still hold the request open, so they are rendered here too.
 
     Opens its own session because the request session is closed by the time
     this task starts running. Commits per-file so a worker restart mid-run
@@ -832,22 +874,30 @@ async def _backfill_external_stl_thumbnails(folder_ids: list[int]) -> None:
         result = await db.execute(
             LibraryFile.active().where(
                 LibraryFile.folder_id.in_(folder_ids),
-                LibraryFile.file_type == "stl",
+                LibraryFile.file_type.in_(("stl", "pdf")),
                 LibraryFile.thumbnail_path.is_(None),
             )
         )
-        stl_files = result.scalars().all()
-        if not stl_files:
+        target_files = result.scalars().all()
+        if not target_files:
             return
         logger.info(
-            "Backfilling STL thumbnails: %d file(s) across %d folder(s)",
-            len(stl_files),
+            "Backfilling STL/PDF thumbnails: %d file(s) across %d folder(s)",
+            len(target_files),
             len(folder_ids),
         )
-        for stl_file in stl_files:
-            abs_path = to_absolute_path(stl_file.file_path)
+        for target_file in target_files:
+            abs_path = to_absolute_path(target_file.file_path)
             if not abs_path or not abs_path.exists():
                 continue
+            if target_file.file_type == "pdf":
+                # generate_pdf_thumbnail never raises; an unreadable PDF
+                # returns None and keeps the browser-preview fallback.
+                thumb_path = await asyncio.to_thread(generate_pdf_thumbnail, abs_path, thumbnails_dir)
+                if thumb_path:
+                    target_file.thumbnail_path = to_relative_path(Path(thumb_path))
+                    await db.commit()
+                continue
             # Pre-skip files too small to contain even a single triangle.
             # Bulk-uploaded ZIPs of stub STLs would otherwise trigger one
             # trimesh.load() call + one debug log line per stub.
@@ -862,7 +912,7 @@ async def _backfill_external_stl_thumbnails(folder_ids: list[int]) -> None:
                 logger.debug("STL thumbnail backfill skipped %s: %s", abs_path, exc)
                 continue
             if thumb_path:
-                stl_file.thumbnail_path = to_relative_path(Path(thumb_path))
+                target_file.thumbnail_path = to_relative_path(Path(thumb_path))
                 await db.commit()
 
 
@@ -1494,6 +1544,8 @@ async def delete_folder(
 
     await delete_dependent_variants(db, doomed_file_ids)
     await release_queue_references(db, doomed_file_ids)
+    for doomed_id in doomed_file_ids:
+        remove_library_photos_dir(doomed_id)
 
     # Delete folder (cascade will handle files and subfolders)
     await db.delete(folder)
@@ -1588,6 +1640,12 @@ _SCANNABLE_EXTENSIONS = {
     ".webp",
     ".svg",
     ".md",
+    # Documents that ship alongside a job folder and now have in-app
+    # previews (#2976): drawings/datasheets and part lists.
+    ".pdf",
+    ".csv",
+    ".xlsx",
+    ".ods",
 }
 
 
@@ -1954,8 +2012,8 @@ async def scan_external_folder(
                 except Exception as e:
                     logger.debug("Failed to extract metadata from external 3mf %s: %s", filepath, e)
 
-            # STL thumbnails are deferred to a background task spawned after
-            # the scan's db.commit() — see _backfill_external_stl_thumbnails.
+            # STL and PDF thumbnails are deferred to a background task spawned
+            # after the scan's db.commit() — see _backfill_external_thumbnails.
             # Doing them inline would block the HTTP request for minutes on a
             # large NAS mount (#1299).
 
@@ -2009,6 +2067,10 @@ async def scan_external_folder(
                         abs_thumb.unlink()
                 except OSError:
                     pass
+            # The row is gone for good — external files skip the trash — so
+            # its photos go with it rather than being orphaned under an id
+            # nothing points at any more (#3077).
+            remove_library_photos_dir(db_file.id)
             await db.delete(db_file)
             removed += 1
 
@@ -2049,17 +2111,17 @@ async def scan_external_folder(
 
     await db.commit()
 
-    # Spawn STL thumbnail backfill in the background — the scan endpoint
+    # Spawn STL/PDF thumbnail backfill in the background — the scan endpoint
     # returns immediately so the FE modal closes and subdirectories are
     # visible right away; thumbnails fill in over the following seconds /
-    # minutes as the task processes each STL file. Survives FE refresh —
+    # minutes as the task processes each file. Survives FE refresh —
     # the task lives in the FastAPI event loop, not the request scope.
     # folder_cache.values() covers the root + every pre-existing subfolder
     # + every subfolder created during this scan. all_folder_ids on its own
     # would miss the newly-created ones (it's snapshotted before the walk).
     spawn_background_task(
-        _backfill_external_stl_thumbnails(list(set(folder_cache.values()))),
-        name=f"stl-backfill-folder-{folder_id}",
+        _backfill_external_thumbnails(list(set(folder_cache.values()))),
+        name=f"thumbnail-backfill-folder-{folder_id}",
     )
 
     return {"status": "success", "added": added, "removed": removed}
@@ -2229,6 +2291,9 @@ async def list_files(
                 tags=[TagSummary(id=t.id, name=t.name) for t in f.tags],
                 variant_group_id=f.variant_group_id,
                 variant_count=variant_counts.get(f.variant_group_id, 0) if f.variant_group_id else 0,
+                external_url=f.external_url,
+                has_notes=bool(f.notes),
+                photo_count=len(f.photos or []),
             )
         )
 
@@ -2362,6 +2427,11 @@ async def upload_file(
             # For image files, create a thumbnail from the image itself
             thumbnail_path = create_image_thumbnail(file_path, thumbnails_dir)
 
+        elif ext.lower() == ".pdf":
+            # Page one, rendered server-side; the browser preview's own
+            # render (POST /preview-thumbnail) remains the fallback.
+            thumbnail_path = await asyncio.to_thread(generate_pdf_thumbnail, file_path, thumbnails_dir)
+
         elif ext == ".stl":
             # Generate STL thumbnail if enabled. Same MIN_USABLE_STL_BYTES
             # pre-skip as extract_zip_file — stubs / placeholders below this
@@ -2634,6 +2704,9 @@ async def extract_zip_file(
                     elif ext.lower() in IMAGE_EXTENSIONS:
                         thumbnail_path = create_image_thumbnail(file_path, thumbnails_dir)
 
+                    elif ext.lower() == ".pdf":
+                        thumbnail_path = await asyncio.to_thread(generate_pdf_thumbnail, file_path, thumbnails_dir)
+
                     elif ext == ".stl":
                         # Generate STL thumbnail if enabled. Pre-skip files
                         # below MIN_USABLE_STL_BYTES — they can't contain
@@ -2704,29 +2777,45 @@ async def extract_zip_file(
 async def batch_generate_stl_thumbnails(
     request: BatchThumbnailRequest,
     db: AsyncSession = Depends(get_db),
-    _: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_UPDATE_ALL)),
+    auth_result: tuple[User | None, bool] = Depends(
+        require_ownership_permission(
+            Permission.LIBRARY_UPDATE_ALL,
+            Permission.LIBRARY_UPDATE_OWN,
+        )
+    ),
 ):
-    """Generate thumbnails for STL files in batch.
+    """Generate thumbnails for STL and PDF files in batch.
+
+    With library:update_all this covers every matching file; with only
+    library:update_own it is narrowed to the caller's own files, the same
+    rule as update_file. The File Manager offers the toolbar button and the
+    per-file "Generate Thumbnail" entry to update_own users, and both land
+    here.
 
-    Note: Requires library:update_all permission since this is a batch operation
-    that may affect files owned by different users.
+    PDFs are included so the ones added before server-side PDF thumbnails
+    existed can be backfilled without opening each preview. The route keeps
+    its name for API compatibility.
 
     Can generate thumbnails for:
     - Specific file IDs (file_ids)
-    - All STL files in a folder (folder_id)
-    - All STL files missing thumbnails (all_missing=True)
+    - All STL/PDF files in a folder (folder_id)
+    - All STL/PDF files missing thumbnails (all_missing=True)
     """
     thumbnails_dir = get_library_thumbnails_dir()
     results: list[BatchThumbnailResult] = []
 
     # Build query based on request
-    query = LibraryFile.active().where(LibraryFile.file_type == "stl")
+    query = LibraryFile.active().where(LibraryFile.file_type.in_(("stl", "pdf")))
+
+    user, can_modify_all = auth_result
+    if not can_modify_all:
+        query = query.where(LibraryFile.created_by_id == user.id)
 
     if request.file_ids:
         # Specific files
         query = query.where(LibraryFile.id.in_(request.file_ids))
     elif request.folder_id is not None:
-        # All STL files in a specific folder
+        # All STL/PDF files in a specific folder
         query = query.where(LibraryFile.folder_id == request.folder_id)
         if not request.all_missing:
             # If not specifically asking for missing thumbnails, get all
@@ -2734,7 +2823,7 @@ async def batch_generate_stl_thumbnails(
         else:
             query = query.where(LibraryFile.thumbnail_path.is_(None))
     elif request.all_missing:
-        # All STL files without thumbnails
+        # All STL/PDF files without thumbnails
         query = query.where(LibraryFile.thumbnail_path.is_(None))
     else:
         # No criteria specified - return empty
@@ -2746,19 +2835,19 @@ async def batch_generate_stl_thumbnails(
         )
 
     result = await db.execute(query)
-    stl_files = result.scalars().all()
+    target_files = result.scalars().all()
 
     succeeded = 0
     failed = 0
 
-    for stl_file in stl_files:
-        file_path = to_absolute_path(stl_file.file_path)
+    for target_file in target_files:
+        file_path = to_absolute_path(target_file.file_path)
 
         if not file_path or not file_path.exists():
             results.append(
                 BatchThumbnailResult(
-                    file_id=stl_file.id,
-                    filename=stl_file.filename,
+                    file_id=target_file.id,
+                    filename=target_file.filename,
                     success=False,
                     error="File not found on disk",
                 )
@@ -2767,16 +2856,19 @@ async def batch_generate_stl_thumbnails(
             continue
 
         try:
-            thumbnail_path = generate_stl_thumbnail(file_path, thumbnails_dir)
+            if target_file.file_type == "pdf":
+                thumbnail_path = await asyncio.to_thread(generate_pdf_thumbnail, file_path, thumbnails_dir)
+            else:
+                thumbnail_path = generate_stl_thumbnail(file_path, thumbnails_dir)
 
             if thumbnail_path:
                 # Update database with relative path
-                stl_file.thumbnail_path = to_relative_path(thumbnail_path)
+                target_file.thumbnail_path = to_relative_path(thumbnail_path)
                 await db.flush()
                 results.append(
                     BatchThumbnailResult(
-                        file_id=stl_file.id,
-                        filename=stl_file.filename,
+                        file_id=target_file.id,
+                        filename=target_file.filename,
                         success=True,
                     )
                 )
@@ -2784,19 +2876,19 @@ async def batch_generate_stl_thumbnails(
             else:
                 results.append(
                     BatchThumbnailResult(
-                        file_id=stl_file.id,
-                        filename=stl_file.filename,
+                        file_id=target_file.id,
+                        filename=target_file.filename,
                         success=False,
                         error="Thumbnail generation failed",
                     )
                 )
                 failed += 1
         except Exception as e:
-            logger.error("Failed to generate thumbnail for %s: %s", stl_file.filename, e)
+            logger.error("Failed to generate thumbnail for %s: %s", target_file.filename, e)
             results.append(
                 BatchThumbnailResult(
-                    file_id=stl_file.id,
-                    filename=stl_file.filename,
+                    file_id=target_file.id,
+                    filename=target_file.filename,
                     success=False,
                     error=str(e),
                 )
@@ -2806,7 +2898,7 @@ async def batch_generate_stl_thumbnails(
     await db.commit()
 
     return BatchThumbnailResponse(
-        processed=len(stl_files),
+        processed=len(target_files),
         succeeded=succeeded,
         failed=failed,
         results=results,
@@ -2828,6 +2920,90 @@ def is_sliced_file(filename: str) -> bool:
     return lower.endswith(".gcode") or ".gcode." in lower
 
 
+@router.post("/files/combine", response_model=FileUploadResponse)
+async def combine_files(
+    request: CombineFilesRequest,
+    db: AsyncSession = Depends(get_db),
+    current_user: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_UPLOAD)),
+):
+    """Combine STL library files into one multi-object 3MF.
+
+    The slicer sidecar takes one model per slice, so putting several separate
+    STLs (or several copies of one, #2999) on one plate means building that
+    file first. The result is a new library file that slices like any other
+    3MF; with auto-arrange on, the slicer lays the objects out on the bed.
+    The sources are left untouched.
+    """
+    from backend.app.services.mesh_combine import CombinePart, MeshCombineError, combine_parts_to_3mf
+
+    filename = request.filename.strip()
+    if not filename.lower().endswith(".3mf"):
+        filename = f"{filename}.3mf"
+    try:
+        validate_print_filename(filename)
+    except InvalidFilenameError as e:
+        raise HTTPException(status_code=400, detail=str(e)) from e
+
+    if request.folder_id is not None:
+        folder = (
+            await db.execute(select(LibraryFolder).where(LibraryFolder.id == request.folder_id))
+        ).scalar_one_or_none()
+        if folder is None:
+            raise HTTPException(status_code=404, detail="Folder not found")
+
+    # Same per-row visibility the slice route applies: a READ_OWN caller must
+    # not be able to pull another user's model into their own file by raw id.
+    can_read_all = current_user is None or current_user.has_permission(Permission.LIBRARY_READ_ALL.value)
+
+    # The same file listed twice is one object with the copies added up, so
+    # its mesh is loaded and stored once. Order follows first appearance.
+    copies_by_id: dict[int, int] = {}
+    for item in request.items:
+        copies_by_id[item.file_id] = copies_by_id.get(item.file_id, 0) + item.copies
+
+    rows = (await db.execute(LibraryFile.active().where(LibraryFile.id.in_(copies_by_id)))).scalars().all()
+    by_id = {row.id: row for row in rows}
+
+    # Gate every source before touching any of them on disk, so the answer for
+    # a file the caller can't see is the same 404 whatever else is in the list.
+    sources = [_ensure_library_file_visible(by_id.get(file_id), current_user, can_read_all) for file_id in copies_by_id]
+
+    parts: list[CombinePart] = []
+    for lib_file in sources:
+        if not lib_file.filename.lower().endswith(".stl"):
+            raise HTTPException(status_code=400, detail=f"Only STL files can be combined: {lib_file.filename}")
+        src_path = _resolve_source_disk_path(lib_file)
+        if src_path is None or not src_path.exists():
+            raise HTTPException(status_code=404, detail=f"Source file missing on disk: {lib_file.filename}")
+        parts.append(CombinePart(name=lib_file.filename, path=src_path, copies=copies_by_id[lib_file.id]))
+
+    try:
+        content = await asyncio.to_thread(combine_parts_to_3mf, parts)
+    except MeshCombineError as e:
+        raise HTTPException(status_code=400, detail=str(e)) from e
+
+    # The preview is embedded as Metadata/thumbnail.png by combine_parts_to_3mf,
+    # so ThreeMFParser picks it up here like any other 3MF's thumbnail. Loading
+    # the combined file back to render one would expand every copy.
+    library_file, _ = await save_3mf_bytes_to_library(
+        db,
+        file_bytes=content,
+        filename=filename,
+        folder_id=request.folder_id,
+        source_type="combined",
+        owner_id=current_user.id if current_user else None,
+    )
+
+    return FileUploadResponse(
+        id=library_file.id,
+        filename=library_file.filename,
+        file_type=library_file.file_type,
+        file_size=library_file.file_size,
+        thumbnail_path=library_file.thumbnail_path,
+        metadata=library_file.file_metadata,
+    )
+
+
 @router.post("/files/add-to-queue", response_model=AddToQueueResponse)
 async def add_files_to_queue(
     request: AddToQueueRequest,
@@ -2908,6 +3084,10 @@ async def add_files_to_queue(
     pos_result = await db.execute(select(func.coalesce(func.max(PrintQueueItem.position), 0)))
     max_position = pos_result.scalar() or 0
 
+    # There is no per-job ask-for-outcome toggle on a bulk add, so the rows take
+    # the same default the print dialog seeds itself from (#1898).
+    confirm_outcome = await confirm_outcome_for_new_queue_item(db)
+
     for file_id in request.file_ids:
         lib_file = files.get(file_id)
 
@@ -3001,6 +3181,7 @@ async def add_files_to_queue(
                 or (folder_projects.get(lib_file.folder_id) if lib_file.folder_id is not None else None),
                 position=max_position,
                 status="pending",
+                confirm_outcome=confirm_outcome,
                 # Without this the row is ownerless, and `queue:read_own` filters
                 # on `created_by_id` — so the user who queued the file could not
                 # see it in their own queue.
@@ -5058,6 +5239,9 @@ async def get_file(
         print_count=file.print_count,
         last_printed_at=file.last_printed_at,
         notes=file.notes,
+        external_url=file.external_url,
+        photos=list(file.photos or []),
+        source_url=file.source_url,
         duplicates=duplicates if duplicates else None,
         duplicate_count=duplicate_count,
         created_by_id=file.created_by_id,
@@ -5132,6 +5316,9 @@ async def update_file(
     if data.notes is not None:
         file.notes = data.notes if data.notes else None
 
+    if data.external_url is not None:
+        file.external_url = data.external_url.strip() or None
+
     await db.commit()
     await db.refresh(file)
 
@@ -5186,6 +5373,7 @@ async def delete_file(
 
         await delete_dependent_variants(db, [file.id])
         await release_queue_references(db, [file.id])
+        remove_library_photos_dir(file.id)
         await db.delete(file)
         await db.commit()
         return {"status": "success", "message": "File deleted", "trashed": False}
@@ -5328,6 +5516,242 @@ async def get_thumbnail(
     return FastAPIFileResponse(str(abs_thumb_path), media_type=media_type)
 
 
+@router.post("/files/{file_id}/preview-thumbnail", response_model=ClientThumbnailResponse)
+async def upload_preview_thumbnail(
+    file_id: int,
+    thumbnail: UploadFile = File(...),
+    db: AsyncSession = Depends(get_db),
+    auth_result: tuple[User | None, bool] = Depends(
+        require_ownership_permission(
+            Permission.LIBRARY_UPDATE_ALL,
+            Permission.LIBRARY_UPDATE_OWN,
+        )
+    ),
+):
+    """Store a client-rendered preview thumbnail for a file (#2976).
+
+    STEP, PDF and spreadsheet previews are rendered in the browser; the FE
+    posts its first render here so the grid gets a thumbnail without the
+    server needing OpenCascade. A PDF normally has its PDFium thumbnail from
+    upload already, so for PDFs this only fills the gap for a file PDFium
+    could not read. Only file types in ``CLIENT_THUMBNAIL_TYPES`` are
+    accepted, and only while the file has no thumbnail yet — a stored
+    thumbnail is never replaced by this route.
+    """
+    user, can_modify_all = auth_result
+
+    result = await db.execute(LibraryFile.active().where(LibraryFile.id == file_id))
+    file = result.scalar_one_or_none()
+
+    if not file:
+        raise HTTPException(status_code=404, detail="File not found")
+
+    # Ownership check (same shape as update_file)
+    if not can_modify_all:
+        if file.created_by_id != user.id:
+            raise HTTPException(status_code=403, detail="You can only update your own files")
+
+    if file.file_type not in CLIENT_THUMBNAIL_TYPES:
+        raise HTTPException(status_code=400, detail="File type does not accept client-rendered thumbnails")
+
+    if file.thumbnail_path:
+        return ClientThumbnailResponse(updated=False)
+
+    content = await thumbnail.read(MAX_CLIENT_THUMBNAIL_BYTES + 1)
+    if len(content) > MAX_CLIENT_THUMBNAIL_BYTES:
+        raise HTTPException(status_code=413, detail="Thumbnail too large")
+
+    # Decode and re-encode through PIL: validates the bytes are a real PNG
+    # and strips anything that isn't pixel data before it lands on disk.
+    import io
+
+    from PIL import Image, UnidentifiedImageError
+
+    try:
+        # Image.open() reads the header only. Both checks below happen before
+        # load(), so a declared-but-never-delivered canvas is refused rather
+        # than allocated. DecompressionBombError derives straight from
+        # Exception, so it has to be named explicitly — open() itself raises
+        # it once the declared size passes PIL's own limit.
+        with Image.open(io.BytesIO(content)) as source:
+            if source.format != "PNG":
+                raise HTTPException(status_code=400, detail="Thumbnail must be a PNG image")
+            if max(source.size) > MAX_CLIENT_THUMBNAIL_EDGE:
+                raise HTTPException(status_code=400, detail="Thumbnail image dimensions too large")
+            source.load()
+            img = source.convert("RGBA") if source.mode not in ("RGB", "RGBA") else source.copy()
+    except HTTPException:
+        raise
+    except (UnidentifiedImageError, OSError, ValueError, Image.DecompressionBombError) as e:
+        raise HTTPException(status_code=400, detail="Invalid thumbnail image") from e
+
+    if max(img.size) > STORED_CLIENT_THUMBNAIL_EDGE:
+        img.thumbnail((STORED_CLIENT_THUMBNAIL_EDGE, STORED_CLIENT_THUMBNAIL_EDGE), Image.Resampling.LANCZOS)
+
+    thumbnails_dir = get_library_thumbnails_dir()
+    thumb_filename = f"{uuid.uuid4().hex}.png"
+    thumb_path = thumbnails_dir / thumb_filename  # SEC-PATH-OK: thumb_filename = uuid.uuid4().hex + ".png"
+    # Outside the decode guard on purpose: a full disk or an unwritable
+    # thumbnail directory is ours, not "Invalid thumbnail image".
+    try:
+        img.save(thumb_path, "PNG", optimize=True)
+    except OSError as e:
+        logger.error("Failed to store preview thumbnail for file %s: %s", file_id, e)
+        raise HTTPException(status_code=500, detail="Failed to store thumbnail") from e
+
+    # Two previews of the same file can reach this point together; the loser
+    # of the UPDATE takes its PNG back off disk instead of orphaning it.
+    result = await db.execute(
+        update(LibraryFile)
+        .where(LibraryFile.id == file_id, LibraryFile.thumbnail_path.is_(None))
+        .values(thumbnail_path=to_relative_path(thumb_path))
+    )
+    await db.commit()
+    if result.rowcount == 0:
+        thumb_path.unlink(missing_ok=True)
+        return ClientThumbnailResponse(updated=False)
+
+    return ClientThumbnailResponse(updated=True)
+
+
+# ============ Photo Endpoints (#3077) ============
+
+
+@router.post("/files/{file_id}/photos")
+async def upload_file_photo(
+    file_id: int,
+    file: UploadFile = File(...),
+    db: AsyncSession = Depends(get_db),
+    auth_result: tuple[User | None, bool] = Depends(
+        require_ownership_permission(
+            Permission.LIBRARY_UPDATE_ALL,
+            Permission.LIBRARY_UPDATE_OWN,
+        )
+    ),
+):
+    """Attach a photo of the printed result to a library file.
+
+    Photos are Bambuddy-side metadata, so external files take them too. Same
+    shape as the archive photo upload: extension allowlist, uuid-named on
+    disk, and the ``photos`` list re-assigned so SQLAlchemy sees the change.
+    """
+    user, can_modify_all = auth_result
+
+    result = await db.execute(LibraryFile.active().where(LibraryFile.id == file_id))
+    library_file = result.scalar_one_or_none()
+
+    if not library_file:
+        raise HTTPException(status_code=404, detail="File not found")
+
+    # Ownership check (same shape as update_file)
+    if not can_modify_all:
+        if library_file.created_by_id != user.id:
+            raise HTTPException(status_code=403, detail="You can only update your own files")
+
+    if not file.filename or not file.filename.lower().endswith(PHOTO_EXTENSIONS):
+        raise HTTPException(status_code=400, detail="File must be an image (.jpg, .jpeg, .png, .webp)")
+
+    content = await file.read(MAX_PHOTO_BYTES + 1)
+    if len(content) > MAX_PHOTO_BYTES:
+        raise HTTPException(status_code=413, detail="Photo too large (max 10 MB)")
+
+    photos_dir = library_photos_dir(library_file.id)
+    photos_dir.mkdir(parents=True, exist_ok=True)
+
+    ext = Path(file.filename).suffix.lower()
+    photo_filename = f"{uuid.uuid4().hex[:8]}{ext}"
+    photo_path = photos_dir / photo_filename  # SEC-PATH-OK: photo_filename = uuid.uuid4().hex[:8] + ext
+    photo_path.write_bytes(content)
+
+    photos = list(library_file.photos or [])
+    photos.append(photo_filename)
+    library_file.photos = photos
+
+    await db.commit()
+    await db.refresh(library_file)
+
+    return {"status": "uploaded", "filename": photo_filename, "photos": library_file.photos}
+
+
+@router.get("/files/{file_id}/photos/{filename}")
+async def get_file_photo(
+    file_id: int,
+    filename: str,
+    db: AsyncSession = Depends(get_db),
+    auth_result: tuple[User | None, bool] = Depends(
+        require_media_token_ownership(
+            Permission.LIBRARY_READ_ALL,
+            Permission.LIBRARY_READ_OWN,
+        )
+    ),
+):
+    """Serve one photo. Media-token auth like the thumbnail route (#3025)."""
+    user, can_read_all = auth_result
+    result = await db.execute(LibraryFile.active().where(LibraryFile.id == file_id))
+    library_file = _ensure_library_file_visible(result.scalar_one_or_none(), user, can_read_all)
+
+    # Membership check first: names are uuid-generated on upload, so anything
+    # not in the stored list is not a photo, whatever is on disk.
+    if not library_file.photos or filename not in library_file.photos:
+        raise HTTPException(status_code=404, detail="Photo not found")
+
+    try:
+        photo_path = safe_join_under(library_photos_dir(library_file.id), filename, http=False)
+    except PathTraversalError:
+        raise HTTPException(status_code=404, detail="Photo not found") from None
+    if not photo_path.is_file():
+        raise HTTPException(status_code=404, detail="Photo not found")
+
+    media_type = PHOTO_MEDIA_TYPES.get(Path(filename).suffix.lower(), "image/jpeg")
+    return FastAPIFileResponse(str(photo_path), media_type=media_type)
+
+
+@router.delete("/files/{file_id}/photos/{filename}")
+async def delete_file_photo(
+    file_id: int,
+    filename: str,
+    db: AsyncSession = Depends(get_db),
+    auth_result: tuple[User | None, bool] = Depends(
+        require_ownership_permission(
+            Permission.LIBRARY_UPDATE_ALL,
+            Permission.LIBRARY_UPDATE_OWN,
+        )
+    ),
+):
+    """Remove a photo from a library file."""
+    user, can_modify_all = auth_result
+
+    result = await db.execute(LibraryFile.active().where(LibraryFile.id == file_id))
+    library_file = result.scalar_one_or_none()
+
+    if not library_file:
+        raise HTTPException(status_code=404, detail="File not found")
+
+    if not can_modify_all:
+        if library_file.created_by_id != user.id:
+            raise HTTPException(status_code=403, detail="You can only update your own files")
+
+    if not library_file.photos or filename not in library_file.photos:
+        raise HTTPException(status_code=404, detail="Photo not found")
+
+    try:
+        photo_path = safe_join_under(library_photos_dir(library_file.id), filename, http=False)
+    except PathTraversalError:
+        raise HTTPException(status_code=404, detail="Photo not found") from None
+    if photo_path.is_file():
+        try:
+            photo_path.unlink()
+        except OSError as e:
+            logger.warning("Failed to delete photo from disk: %s", e)
+
+    photos = [p for p in library_file.photos if p != filename]
+    library_file.photos = photos if photos else None
+
+    await db.commit()
+
+    return {"status": "deleted", "photos": library_file.photos or []}
+
+
 @router.get("/files/{file_id}/gcode")
 async def get_gcode(
     file_id: int,
@@ -5558,6 +5982,7 @@ async def bulk_delete(
         await delete_dependent_variants(db, hard_deleted_ids)
         await release_queue_references(db, hard_deleted_ids)
         for file in hard_deleted:
+            remove_library_photos_dir(file.id)
             await db.delete(file)
 
     # Delete folders (cascade will handle contents). Folders have no ownership
@@ -5580,6 +6005,10 @@ async def bulk_delete(
             tree_file_ids = await _folder_tree_file_ids(db, folder_id)
             await delete_dependent_variants(db, tree_file_ids)
             await release_queue_references(db, tree_file_ids)
+            # The cascade hard-deletes every row in the subtree, so their
+            # photos go with them — same as DELETE /folders/{id} (#3077).
+            for doomed_id in tree_file_ids:
+                remove_library_photos_dir(doomed_id)
             await db.delete(folder)
             deleted_folders += 1
 

+ 76 - 3
backend/app/api/routes/mfa.py

@@ -71,6 +71,7 @@ from backend.app.schemas.auth import (
     OIDCExchangeRequest,
     OIDCLinkResponse,
     OIDCProviderCreate,
+    OIDCProviderPublicResponse,
     OIDCProviderResponse,
     OIDCProviderUpdate,
     TOTPDisableRequest,
@@ -1320,18 +1321,24 @@ async def admin_disable_2fa(
 # ===========================================================================
 
 
-@router.get("/oidc/providers", response_model=list[OIDCProviderResponse])
+@router.get("/oidc/providers", response_model=list[OIDCProviderPublicResponse])
 async def list_oidc_providers(
     db: AsyncSession = Depends(get_db),
-) -> list[OIDCProviderResponse]:
+) -> list[OIDCProviderPublicResponse]:
     """List all enabled OIDC providers (public).
 
     The login page renders icons via /oidc/providers/{id}/icon — `icon_data`
     stays deferred so this list query never pulls the BLOB.
+
+    #3107: returns the slim public shape only. The login page needs id, name,
+    has_icon and is_autologin; the full response (scopes, claims, and now
+    group_claim / group_mapping) is served by the permission-gated
+    /oidc/providers/all below, so an unauthenticated caller cannot learn
+    which IdP group name maps to which Bambuddy group.
     """
     result = await db.execute(select(OIDCProvider).where(OIDCProvider.is_enabled.is_(True)))
     providers = result.scalars().all()
-    return [_build_provider_response(p) for p in providers]
+    return [OIDCProviderPublicResponse.model_validate(p) for p in providers]
 
 
 @router.get("/oidc/providers/all", response_model=list[OIDCProviderResponse])
@@ -1365,6 +1372,17 @@ async def create_oidc_provider(
                 detail="default_group_id references a non-existent group",
             )
 
+    # #3107 — every mapping value must reference an existing Bambuddy group,
+    # same answer default_group_id gets. Checked as a set: a mapping with three
+    # entries naming the same group is one lookup, not three.
+    if body.group_mapping:
+        missing_groups = await _missing_group_names(db, set(body.group_mapping.values()))
+        if missing_groups:
+            raise HTTPException(
+                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
+                detail=f"group_mapping references non-existent groups: {', '.join(sorted(missing_groups))}",
+            )
+
     # Fetch the icon BEFORE creating the row so a failure leaves the DB clean.
     icon_data: bytes | None = None
     icon_content_type: str | None = None
@@ -1383,6 +1401,8 @@ async def create_oidc_provider(
         auto_link_existing_accounts=body.auto_link_existing_accounts,
         email_claim=body.email_claim,
         require_email_verified=body.require_email_verified,
+        group_claim=body.group_claim,
+        group_mapping=body.group_mapping,
         icon_url=body.icon_url,
         icon_data=icon_data,
         icon_content_type=icon_content_type,
@@ -1416,6 +1436,21 @@ def _refuse_if_env_managed(provider: OIDCProvider) -> None:
         )
 
 
+async def _missing_group_names(db: AsyncSession, names: set[str]) -> set[str]:
+    """#3107 — names from a group_mapping with no matching Bambuddy group.
+
+    Exact match only, same as the sync's own ``Group.name.in_()`` lookup and
+    the env path's ``Group.name == target``: the sync resolves names exactly,
+    so admitting a case-variant here would pass validation only to have the
+    sync silently never grant it — the exact failure this check exists to
+    catch at save time, in front of the admin, instead of at login time.
+    """
+    if not names:
+        return set()
+    exact = set((await db.execute(select(Group.name).where(Group.name.in_(names)))).scalars().all())
+    return names - exact
+
+
 @router.put("/oidc/providers/{provider_id}", response_model=OIDCProviderResponse)
 async def update_oidc_provider(
     provider_id: int,
@@ -1448,6 +1483,17 @@ async def update_oidc_provider(
                 detail="default_group_id references a non-existent group",
             )
 
+    # #3107 — same existence check as the create route, on the submitted
+    # mapping only. A null group_mapping (field absent) leaves the stored one
+    # alone; an explicit {} empties it, and emptiness needs no group lookup.
+    if body.group_mapping:
+        missing_groups = await _missing_group_names(db, set(body.group_mapping.values()))
+        if missing_groups:
+            raise HTTPException(
+                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
+                detail=f"group_mapping references non-existent groups: {', '.join(sorted(missing_groups))}",
+            )
+
     dumped = body.model_dump(exclude_none=True)
 
     # Decide whether an icon refetch is needed BEFORE mutating the ORM object,
@@ -2060,6 +2106,33 @@ async def oidc_callback(
             if not user or not user.is_active:
                 return RedirectResponse(url=f"{frontend_error_url}account_inactive", status_code=302)
 
+            # #3107 — apply the provider's group mapping on every login, not
+            # just at account creation. Same managed-slice contract as the
+            # LDAP sync (#1292): only groups named in group_mapping values are
+            # touched, manual assignments elsewhere survive. A sync failure is
+            # logged inside and never blocks the login.
+            if provider.group_mapping:
+                from backend.app.services.oidc_group_sync import sync_oidc_user_groups
+
+                await sync_oidc_user_groups(
+                    db,
+                    user,
+                    group_claim=provider.group_claim,
+                    group_mapping=provider.group_mapping,
+                    claims=claims,
+                )
+                # Post-rollback guard, not token freshness: nothing below reads
+                # user.groups (the callback only puts the username on the exchange
+                # token; /oidc/exchange re-selects the user with selectinload).
+                # ALL attributes, not just groups: a sync that raised called
+                # db.rollback(), which expires every loaded object in the
+                # session — the next username=user.username below would then
+                # lazy-load and raise MissingGreenlet, landing the user on
+                # ?oidc_error=user_resolution_failed, i.e. a failed sync would
+                # block the login after all. Inside the mapping branch so
+                # sync-off installs do not pay the query.
+                await db.refresh(user)
+
             # Issue an OIDC exchange token (short-lived, single-use) stored in DB.
             # I7: Opportunistically prune expired exchange tokens to keep the table small.
             now2 = datetime.now(timezone.utc)

+ 9 - 0
backend/app/api/routes/notification_templates.py

@@ -11,6 +11,7 @@ from backend.app.models.notification_template import DEFAULT_TEMPLATES, Notifica
 from backend.app.models.user import User
 from backend.app.schemas.notification_template import (
     EVENT_VARIABLES,
+    PHOTO_CAPABLE_EVENTS,
     SAMPLE_DATA,
     EventVariablesResponse,
     NotificationTemplateResponse,
@@ -33,8 +34,12 @@ EVENT_NAMES = {
     "billing_charge_failed": "Billing Charge Failed",
     "printer_offline": "Printer Offline",
     "printer_error": "Printer Error",
+    "ai_failure_detection": "AI Failure Detection",
+    "plate_not_empty": "Plate Not Empty",
+    "plate_clear_required": "Plate Clear Required",
     "filament_low": "Filament Low",
     "maintenance_due": "Maintenance Due",
+    "first_layer_complete": "First Layer Complete",
     "test": "Test Notification",
     # Queue notifications
     "queue_job_added": "Queue Job Added",
@@ -44,6 +49,9 @@ EVENT_NAMES = {
     "queue_job_skipped": "Queue Job Skipped",
     "queue_job_failed": "Queue Job Failed",
     "queue_completed": "Queue Completed",
+    # Inventory stock alerts
+    "stock_reorder_alert": "Stock Reorder Alert",
+    "stock_break_alert": "Stock Break Alert",
     # User management
     "user_created": "Welcome Email",
     "password_reset": "Password Reset",
@@ -76,6 +84,7 @@ async def get_variables(
             event_type=event_type,
             event_name=EVENT_NAMES.get(event_type, event_type),
             variables=variables,
+            supports_photo=event_type in PHOTO_CAPABLE_EVENTS,
         )
         for event_type, variables in EVENT_VARIABLES.items()
     ]

+ 119 - 4
backend/app/api/routes/notifications.py

@@ -2,18 +2,24 @@
 
 import json
 import logging
+import time
+from collections import defaultdict, deque
 from datetime import datetime, timedelta, timezone
 
 from fastapi import APIRouter, Depends, HTTPException, Query
+from fastapi.responses import FileResponse
 from sqlalchemy import delete, desc, func, select
 from sqlalchemy.ext.asyncio import AsyncSession
 
-from backend.app.core.auth import RequirePermissionIfAuthEnabled
+from backend.app.core.auth import RequirePermissionIfAuthEnabled, ScopedCaller, require_notification_send
 from backend.app.core.database import get_db
 from backend.app.core.permissions import Permission
 from backend.app.models.notification import NotificationLog, NotificationProvider
 from backend.app.models.user import User
 from backend.app.schemas.notification import (
+    AppMessage,
+    AppMessageChannel,
+    AppMessageResult,
     NotificationLogResponse,
     NotificationLogStats,
     NotificationProviderCreate,
@@ -23,12 +29,26 @@ from backend.app.schemas.notification import (
     NotificationTestResponse,
 )
 from backend.app.services.notification_service import notification_service
+from backend.app.services.telegram_reactions import telegram_reaction_poller
+from backend.app.utils.notification_photos import find_notification_photo
 
 logger = logging.getLogger(__name__)
 
 router = APIRouter(prefix="/notifications", tags=["notifications"])
 
 
+async def _resync_reaction_poller():
+    """Start/stop Telegram reaction polls after a provider changed (#3046).
+
+    Never fails the request: the provider row is already saved, and the
+    poller catches up on the next restart at worst.
+    """
+    try:
+        await telegram_reaction_poller.sync()
+    except Exception as e:
+        logger.warning("Telegram reaction poller resync failed: %s", e)
+
+
 def _provider_to_dict(provider: NotificationProvider) -> dict:
     """Convert a NotificationProvider model to a response dictionary."""
     return {
@@ -37,6 +57,7 @@ def _provider_to_dict(provider: NotificationProvider) -> dict:
         "provider_type": provider.provider_type,
         "enabled": provider.enabled,
         "config": json.loads(provider.config) if isinstance(provider.config, str) else provider.config,
+        "attach_photo": provider.attach_photo,
         # Print lifecycle events
         "on_print_start": provider.on_print_start,
         "on_print_complete": provider.on_print_complete,
@@ -66,10 +87,16 @@ def _provider_to_dict(provider: NotificationProvider) -> dict:
         # Build plate detection
         "on_plate_not_empty": provider.on_plate_not_empty,
         "on_plate_clear_required": provider.on_plate_clear_required,
+        # Post-print outcome confirmation (#1898)
+        "on_print_confirm_request": provider.on_print_confirm_request,
+        # Rows from before #3046 hold NULL here; "buttons" is what they did.
+        "telegram_verdict_mode": provider.telegram_verdict_mode or "buttons",
         # Bed cooled
         "on_bed_cooled": provider.on_bed_cooled,
         # First layer complete
         "on_first_layer_complete": provider.on_first_layer_complete,
+        # Messages from connected apps
+        "on_app_message": bool(provider.on_app_message),
         # Inventory stock alerts. Absent here, the toggles above always read
         # back off no matter what the row holds — the same hand-maintained
         # field map the Home Assistant comment warns about.
@@ -131,6 +158,7 @@ async def create_notification_provider(
         provider_type=provider_data.provider_type.value,
         enabled=provider_data.enabled,
         config=json.dumps(provider_data.config),
+        attach_photo=provider_data.attach_photo,
         # Print lifecycle events
         on_print_start=provider_data.on_print_start,
         on_print_complete=provider_data.on_print_complete,
@@ -158,10 +186,14 @@ async def create_notification_provider(
         # Build plate detection
         on_plate_not_empty=provider_data.on_plate_not_empty,
         on_plate_clear_required=provider_data.on_plate_clear_required,
+        # Post-print outcome confirmation (#1898)
+        on_print_confirm_request=provider_data.on_print_confirm_request,
+        telegram_verdict_mode=provider_data.telegram_verdict_mode,
         # Bed cooled
         on_bed_cooled=provider_data.on_bed_cooled,
         # First layer complete
         on_first_layer_complete=provider_data.on_first_layer_complete,
+        on_app_message=provider_data.on_app_message,
         # Inventory stock alerts
         on_stock_reorder_alert=provider_data.on_stock_reorder_alert,
         on_stock_break_alert=provider_data.on_stock_break_alert,
@@ -189,6 +221,7 @@ async def create_notification_provider(
     await db.refresh(provider)
 
     logger.info("Created notification provider: %s (%s)", provider.name, provider.provider_type)
+    await _resync_reaction_poller()
 
     return _provider_to_dict(provider)
 
@@ -206,7 +239,7 @@ async def test_notification_config(
 ):
     """Test notification configuration before saving."""
     success, message = await notification_service.send_test_notification(
-        test_request.provider_type.value, test_request.config, db
+        test_request.provider_type.value, test_request.config, db, attach_photo=test_request.attach_photo
     )
 
     return NotificationTestResponse(success=success, message=message)
@@ -230,7 +263,9 @@ async def test_all_notification_providers(
 
     for provider in providers:
         config = json.loads(provider.config) if isinstance(provider.config, str) else provider.config
-        success, message = await notification_service.send_test_notification(provider.provider_type, config, db)
+        success, message = await notification_service.send_test_notification(
+            provider.provider_type, config, db, attach_photo=provider.attach_photo
+        )
 
         # Update provider status
         if success:
@@ -392,11 +427,87 @@ async def clear_notification_logs(
     return {"deleted": deleted_count, "message": f"Deleted {deleted_count} logs older than {older_than_days} days"}
 
 
+@router.get("/photos/{filename}")
+async def get_notification_photo(filename: str):
+    """Serve an ad-hoc notification snapshot to HA, Bark or Slack.
+
+    They fetch this URL themselves with no session, so the unguessable
+    filename is the credential and opens this one photo only -- see
+    backend/app/utils/notification_photos.py. Anything that isn't a live
+    photo of exactly that shape is a 404.
+    """
+    photo_path = find_notification_photo(filename)
+    if photo_path is None:
+        raise HTTPException(404, "Photo not found")
+
+    return FileResponse(path=photo_path, media_type="image/jpeg", headers={"Cache-Control": "private, no-store"})
+
+
 # ============================================================================
 # Provider Instance Routes (parameterized - must come LAST)
 # ============================================================================
 
 
+# Messages from other applications -------------------------------------------
+
+# Per caller, in memory: enough for any real app (Bambuddy Orders sends a few a
+# day), and a buggy or hostile one can't flood the channels.
+APP_MESSAGE_LIMIT = 20
+APP_MESSAGE_WINDOW_SECONDS = 60
+_app_message_times: dict[str, deque[float]] = defaultdict(deque)
+
+
+def _app_sender(caller: ScopedCaller) -> tuple[str, str]:
+    """(rate-limit key, name shown in the log) for whoever sends the message."""
+    if caller.api_key is not None:
+        return f"key:{caller.api_key.id}", caller.api_key.name
+    if caller.user is not None:
+        return f"user:{caller.user.id}", caller.user.username
+    return "anonymous", "app"
+
+
+def _check_app_message_rate(key: str) -> None:
+    times = _app_message_times[key]
+    cutoff = time.monotonic() - APP_MESSAGE_WINDOW_SECONDS
+    while times and times[0] < cutoff:
+        times.popleft()
+    if len(times) >= APP_MESSAGE_LIMIT:
+        raise HTTPException(status_code=429, detail="Too many messages; try again in a minute")
+    times.append(time.monotonic())
+
+
+@router.post("/app-message", response_model=AppMessageResult)
+async def send_app_message(
+    data: AppMessage,
+    db: AsyncSession = Depends(get_db),
+    caller: ScopedCaller = Depends(require_notification_send()),
+):
+    """Send a message through every enabled channel that has "Messages from
+    connected apps" on. For other applications, e.g. Bambuddy Orders; an API
+    key needs the "Send notifications" permission."""
+    key, sender = _app_sender(caller)
+    _check_app_message_rate(key)
+    channels = await notification_service.on_app_message(
+        db, sender=sender, title=data.title, message=data.message, url=data.url
+    )
+    return AppMessageResult(channels=channels)
+
+
+@router.get("/app-message/channels", response_model=list[AppMessageChannel])
+async def app_message_channels(
+    db: AsyncSession = Depends(get_db),
+    _: ScopedCaller = Depends(require_notification_send()),
+):
+    """The enabled channels that deliver app messages: names and types only,
+    so an app can tell its user where its messages will arrive."""
+    rows = await db.execute(
+        select(NotificationProvider)
+        .where(NotificationProvider.enabled.is_(True), NotificationProvider.on_app_message.is_(True))
+        .order_by(NotificationProvider.name)
+    )
+    return [AppMessageChannel(name=p.name, provider_type=p.provider_type) for p in rows.scalars()]
+
+
 @router.get("/{provider_id}", response_model=NotificationProviderResponse)
 async def get_notification_provider(
     provider_id: int,
@@ -442,6 +553,7 @@ async def update_notification_provider(
     await db.refresh(provider)
 
     logger.info("Updated notification provider: %s", provider.name)
+    await _resync_reaction_poller()
 
     return _provider_to_dict(provider)
 
@@ -464,6 +576,7 @@ async def delete_notification_provider(
     await db.commit()
 
     logger.info("Deleted notification provider: %s", name)
+    await _resync_reaction_poller()
 
     return {"message": f"Notification provider '{name}' deleted"}
 
@@ -482,7 +595,9 @@ async def test_notification_provider(
         raise HTTPException(status_code=404, detail="Notification provider not found")
 
     config = json.loads(provider.config) if isinstance(provider.config, str) else provider.config
-    success, message = await notification_service.send_test_notification(provider.provider_type, config, db)
+    success, message = await notification_service.send_test_notification(
+        provider.provider_type, config, db, attach_photo=provider.attach_photo
+    )
 
     # Update provider status
     if success:

+ 5 - 0
backend/app/api/routes/pipeline_runs.py

@@ -61,6 +61,7 @@ from backend.app.services.pipeline_eligibility import (
     EligibilityReport,
     check_pipeline_eligibility,
 )
+from backend.app.services.print_confirmation import confirm_outcome_for_new_queue_item
 
 logger = logging.getLogger(__name__)
 
@@ -571,6 +572,9 @@ def _make_orchestration_callable(
             if len(jobs) != copies:
                 logger.warning("pipeline_run %d expected %d jobs, found %d", run_id, copies, len(jobs))
 
+            # No per-job ask-for-outcome toggle on a pipeline run either (#1898).
+            confirm_outcome = await confirm_outcome_for_new_queue_item(session)
+
             for job, (printer_id, target_model) in zip(jobs, assignments, strict=False):
                 queue_item = PrintQueueItem(
                     printer_id=printer_id,
@@ -578,6 +582,7 @@ def _make_orchestration_callable(
                     library_file_id=slice_response.library_file_id,
                     created_by_id=creator_user_id,
                     status="pending",
+                    confirm_outcome=confirm_outcome,
                 )
                 session.add(queue_item)
                 await session.flush()

+ 48 - 43
backend/app/api/routes/print_queue.py

@@ -9,6 +9,7 @@ from pathlib import Path
 import defusedxml.ElementTree as ET
 from fastapi import APIRouter, Depends, HTTPException, Query
 from sqlalchemy import and_, func, inspect, or_, select, update
+from sqlalchemy.exc import IntegrityError
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
@@ -53,6 +54,7 @@ from backend.app.services.print_batch import (
     refresh_batch_status_for_item,
 )
 from backend.app.services.print_cost_estimate import estimate_queue_source_cost
+from backend.app.services.queue_position import lock_queue_positions, max_queue_position
 from backend.app.utils.printer_models import (
     is_gcode_compatible,
 )
@@ -471,6 +473,7 @@ def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
         "timelapse": item.timelapse,
         "use_ams": item.use_ams,
         "nozzle_offset_cali": item.nozzle_offset_cali,
+        "confirm_outcome": item.confirm_outcome,
         "preheat_override": item.preheat_override,
         "preheat_chamber_target_override": item.preheat_chamber_target_override,
         "status": item.status,
@@ -613,7 +616,12 @@ async def list_queue(
             # Cross-model candidates (#671) and their files, for the card label.
             selectinload(PrintQueueItem.variants).selectinload(PrintQueueVariant.library_file),
         )
-        .order_by(PrintQueueItem.printer_id.nulls_first(), PrintQueueItem.position)
+        # The order the scheduler dispatches in (#3200), so the first pending
+        # item for a printer is the one it will start next -- which is what the
+        # printer card's "Next in queue" shows. Sorting by printer first put
+        # every "Any <model>" job (no printer_id) ahead of a job pinned to that
+        # printer, whatever their positions.
+        .order_by(PrintQueueItem.position, PrintQueueItem.id)
     )
     if user is not None and not can_read_all:
         query = query.where(PrintQueueItem.created_by_id == user.id)
@@ -999,47 +1007,14 @@ async def add_to_queue(
         await db.flush()  # Get batch.id before creating items
         batch_id = batch.id
 
-    # Get queue scope for this printer (or for unassigned/model-based items).
-    if data.printer_id is not None:
-        queue_scope = (
-            PrintQueueItem.printer_id == data.printer_id,
-            PrintQueueItem.status == "pending",
-        )
-    else:
-        # For unassigned/model-based items, scope across all unassigned.
-        queue_scope = (
-            PrintQueueItem.printer_id.is_(None),
-            PrintQueueItem.status == "pending",
-        )
-
-    # Serialize concurrent queue inserts to the same scope (#1625-followup).
-    # The race: two concurrent ASAP inserts both compute MAX(position) before
-    # either commits; in an empty scope, both INSERT at position 1 (duplicate).
-    # In a non-empty scope, Postgres's row-level locks on the UPDATE shift
-    # serialize naturally, but the empty-scope path has no rows to lock.
-    # A transaction-scoped advisory lock keyed on the printer_id closes that
-    # window; the lock is released automatically at commit/rollback. Different
-    # printers don't contend. SQLite serializes writes implicitly so this is a
-    # no-op there.
-    #
-    # Dialect is checked against the actual session binding, NOT the
-    # `is_sqlite()` helper, because the test fixture overrides `get_db` with a
-    # SQLite engine while `settings.database_url` still points at Postgres
-    # (the helper reads settings). Inspecting the connection directly is the
-    # right shape for any code that mutates SQL based on the live dialect.
-    from sqlalchemy import text
-
-    bind = db.get_bind()
-    if bind.dialect.name == "postgresql":
-        scope_key = data.printer_id if data.printer_id is not None else 0
-        # 1625 namespaces the lock so it can't collide with other advisory
-        # locks elsewhere in the codebase.
-        await db.execute(text("SELECT pg_advisory_xact_lock(1625, :k)"), {"k": scope_key})
+    # Positions are one sequence across every pending item (#3200), so a new
+    # item lands relative to the whole list, not to its printer's share of it.
+    queue_scope = (PrintQueueItem.status == "pending",)
+    await lock_queue_positions(db)
 
     insert_position = max(1, data.insert_position or 1)
     if data.insert_at_top or data.insert_position is not None:
-        result = await db.execute(select(func.max(PrintQueueItem.position)).where(*queue_scope))
-        max_pos = result.scalar() or 0
+        max_pos = await max_queue_position(db)
         insert_position = min(insert_position, max_pos + 1)
         await db.execute(
             update(PrintQueueItem)
@@ -1049,9 +1024,7 @@ async def add_to_queue(
         )
         start_position = insert_position
     else:
-        result = await db.execute(select(func.max(PrintQueueItem.position)).where(*queue_scope))
-        max_pos = result.scalar() or 0
-        start_position = max_pos + 1
+        start_position = await max_queue_position(db) + 1
 
     # Resolve print_time_seconds for SJF scheduling (cache on item at creation)
     cached_print_time = None
@@ -1192,6 +1165,7 @@ async def add_to_queue(
             timelapse=data.timelapse,
             use_ams=data.use_ams,
             nozzle_offset_cali=data.nozzle_offset_cali,
+            confirm_outcome=data.confirm_outcome,
             preheat_override=data.preheat_override,
             preheat_chamber_target_override=data.preheat_chamber_target_override,
             gcode_injection=data.gcode_injection,
@@ -1424,6 +1398,12 @@ async def _load_batch_for_write(
     return batch
 
 
+# Deliberately without the existing batch's id: the caller may not be allowed
+# to read it. They can look it up by the pair, which applies the usual
+# ownership rules.
+_EXTERNAL_REF_TAKEN = "A batch for this external_source and external_ref already exists"
+
+
 @router.post("/batches", response_model=PrintBatchResponse)
 async def create_batch(
     data: PrintBatchCreate,
@@ -1449,6 +1429,15 @@ async def create_batch(
 
     plate_targets = _validate_plate_targets(data.plates)
     await _validate_batch_project(db, data.project_id, current_user)
+    if data.external_source is not None:
+        existing = await db.execute(
+            select(PrintBatch.id).where(
+                PrintBatch.external_source == data.external_source,
+                PrintBatch.external_ref == data.external_ref,
+            )
+        )
+        if existing.scalar_one_or_none() is not None:
+            raise HTTPException(409, _EXTERNAL_REF_TAKEN)
 
     batch = PrintBatch(
         name=data.name.strip()[:255],
@@ -1460,9 +1449,17 @@ async def create_batch(
         project_id=data.project_id,
         due_date=data.due_date,
         notes=data.notes,
+        external_source=data.external_source,
+        external_ref=data.external_ref,
     )
     db.add(batch)
-    await db.flush()  # Need batch.id before assigning to items
+    try:
+        await db.flush()  # Need batch.id before assigning to items
+    except IntegrityError:
+        # Lost a race with a concurrent create for the same external record:
+        # the unique index caught what the lookup above could not.
+        await db.rollback()
+        raise HTTPException(409, _EXTERNAL_REF_TAKEN) from None
 
     if plate_targets is not None:
         for target in plate_targets:
@@ -1663,6 +1660,8 @@ async def ungroup_batch(
 @router.get("/batches", response_model=list[PrintBatchResponse])
 async def list_batches(
     status: str | None = Query(None, description="Filter by status (active, completed, cancelled)"),
+    external_source: str | None = Query(None, description="Filter by the integration that created the batch"),
+    external_ref: str | None = Query(None, description="Filter by the external record the batch fulfils"),
     db: AsyncSession = Depends(get_db),
     auth_result: tuple[User | None, bool] = Depends(
         require_ownership_permission(
@@ -1690,6 +1689,10 @@ async def list_batches(
     )
     if status:
         query = query.where(PrintBatch.status == status)
+    if external_source is not None:
+        query = query.where(PrintBatch.external_source == external_source)
+    if external_ref is not None:
+        query = query.where(PrintBatch.external_ref == external_ref)
     if current_user is not None and not can_read_all:
         query = query.where(PrintBatch.created_by_id == current_user.id)
     result = await db.execute(query)
@@ -1808,6 +1811,8 @@ async def _build_batch_response(
         project_id=batch.project_id,
         due_date=batch.due_date,
         notes=batch.notes,
+        external_source=batch.external_source,
+        external_ref=batch.external_ref,
         pending_count=progress.pending,
         printing_count=progress.printing,
         completed_count=progress.completed,

+ 136 - 22
backend/app/api/routes/printers.py

@@ -36,7 +36,6 @@ from backend.app.schemas.printer import (
     ExtruderSlotResponse,
     FilaSwitchResponse,
     HmsActionBody,
-    HMSErrorResponse,
     NozzleInfoResponse,
     NozzleRackSlot,
     PrinterCreate,
@@ -48,8 +47,9 @@ from backend.app.schemas.printer import (
     PrinterStatus,
     PrinterUpdate,
     PrintOptionsResponse,
+    hms_error_responses,
 )
-from backend.app.services import drying_preflight
+from backend.app.services import drying_preflight, kprofile_drift
 from backend.app.services.bambu_ftp import (
     cache_3mf_download,
     delete_file_async,
@@ -88,7 +88,7 @@ from backend.app.services.printer_media import (
     remove_printer_files_zip,
     start_printer_files_job,
 )
-from backend.app.services.slicer_filament_resolver import _ORCA_PROFILE_ID
+from backend.app.services.slicer_filament_resolver import _ORCA_PROFILE_ID, lookup_orca_filament_id
 from backend.app.services.slot_nozzle import resolve_slot_nozzle
 from backend.app.utils.ams_humidity import ams_humidity_percent
 from backend.app.utils.filament_ids import filament_id_to_setting_id
@@ -508,20 +508,7 @@ async def get_printer_status(
     if state.state in ("RUNNING", "PAUSE") and state.gcode_file:
         cover_url = f"/api/v1/printers/{printer_id}/cover"
 
-    # Convert HMS errors to response format
-    hms_errors = [
-        HMSErrorResponse(
-            code=e.code,
-            attr=e.attr,
-            module=e.module,
-            severity=e.severity,
-            actions=e.actions,
-            job_id=e.job_id,
-            full_code=e.full_code,
-            description=e.description,
-        )
-        for e in (state.hms_errors or [])
-    ]
+    hms_errors = hms_error_responses(state.hms_errors)
 
     # Parse AMS data from raw_data
     ams_units = []
@@ -628,6 +615,7 @@ async def get_printer_status(
                     sw_ver=str(ams_data.get("sw_ver") or ""),
                     # Drying: dry_time > 0 means drying is active (minutes remaining)
                     dry_time=int(ams_data.get("dry_time") or 0),
+                    dry_countdown_stalled=bool(ams_data.get("dry_countdown_stalled") or False),
                     dry_target_temp=dry_target_temp,
                     dry_filament=dry_filament,
                     module_type=str(ams_data.get("module_type") or ""),
@@ -883,7 +871,7 @@ async def get_overlay_status(
 
     A token-authenticated sibling of ``get_printer_status`` for embeds with no
     login session — OBS loads ``/overlay/{id}?token=...`` and this feeds it.
-    Deliberately flat and minimal (name, camera rotation, live print state, and
+    Deliberately flat and minimal (name, model, camera rotation, live print state, and
     the one setting the overlay reads) rather than the full ``PrinterStatus``:
     a token holder gets exactly the fields the overlay renders, nothing more.
 
@@ -907,6 +895,7 @@ async def get_overlay_status(
         return {
             "id": printer_id,
             "name": printer.name,
+            "model": printer.model,
             "camera_rotation": printer.camera_rotation or 0,
             "connected": False,
             "state": None,
@@ -924,6 +913,7 @@ async def get_overlay_status(
     return {
         "id": printer_id,
         "name": printer.name,
+        "model": printer.model,
         "camera_rotation": printer.camera_rotation or 0,
         "connected": state.connected,
         "state": state.state,
@@ -2566,6 +2556,7 @@ async def get_slot_presets(
             "tray_id": mapping.tray_id,
             "preset_id": mapping.preset_id,
             "preset_name": mapping.preset_name,
+            "tray_info_idx": mapping.tray_info_idx,
         }
         for mapping in mappings
     }
@@ -2597,6 +2588,7 @@ async def get_slot_preset(
         "tray_id": mapping.tray_id,
         "preset_id": mapping.preset_id,
         "preset_name": mapping.preset_name,
+        "tray_info_idx": mapping.tray_info_idx,
     }
 
 
@@ -2608,10 +2600,20 @@ async def save_slot_preset(
     preset_id: str,
     preset_name: str,
     preset_source: str = "cloud",
+    tray_info_idx: str | None = None,
     _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_UPDATE),
     db: AsyncSession = Depends(get_db),
 ):
-    """Save a preset mapping for a specific slot."""
+    """Save a preset mapping for a specific slot.
+
+    ``tray_info_idx`` is the filament id the slot was configured with (what
+    ``configure`` returns). Recording it lets the slot card notice a later
+    re-configuration from elsewhere and stop showing this preset (#3216).
+    Omitted, it is cleared, so a stale id never outlives the preset it came with.
+    """
+    # Longer than the column is not an id the printer holds: record nothing,
+    # which keeps the row shown, rather than refuse the whole save.
+    tray_info_idx = tray_info_idx if tray_info_idx and len(tray_info_idx) <= 32 else None
     # Check printer exists
     result = await db.execute(select(Printer).where(Printer.id == printer_id))
     if not result.scalar_one_or_none():
@@ -2632,6 +2634,7 @@ async def save_slot_preset(
         mapping.preset_id = preset_id
         mapping.preset_name = preset_name
         mapping.preset_source = preset_source
+        mapping.tray_info_idx = tray_info_idx
     else:
         # Create new
         mapping = SlotPresetMapping(
@@ -2641,6 +2644,7 @@ async def save_slot_preset(
             preset_id=preset_id,
             preset_name=preset_name,
             preset_source=preset_source,
+            tray_info_idx=tray_info_idx,
         )
         db.add(mapping)
 
@@ -2653,6 +2657,7 @@ async def save_slot_preset(
         "preset_id": mapping.preset_id,
         "preset_name": mapping.preset_name,
         "preset_source": mapping.preset_source,
+        "tray_info_idx": mapping.tray_info_idx,
     }
 
 
@@ -2781,6 +2786,46 @@ async def get_slot_spool_defaults(
     }
 
 
+# Generic Bambu filament ids by material, as the Configure dialog has always
+# picked them for a preset without an id of its own (#3216 moved the Orca case
+# here). Wider than configure_ams_slot's own table: Silk, High Speed, PCTG, PE
+# and PP have generics of their own.
+_ORCA_GENERIC_IDS = {
+    "PLA": "GFL99",
+    "PLA-CF": "GFL98",
+    "PLA SILK": "GFL96",
+    "PLA HIGH SPEED": "GFL95",
+    "PETG": "GFG99",
+    "PETG HF": "GFG96",
+    "PETG-CF": "GFG98",
+    "PCTG": "GFG97",
+    "ABS": "GFB99",
+    "ASA": "GFB98",
+    "PC": "GFC99",
+    "PA": "GFN99",
+    "PA-CF": "GFN98",
+    "NYLON": "GFN99",
+    "TPU": "GFU99",
+    "PVA": "GFS99",
+    "HIPS": "GFS98",
+    "PE": "GFP99",
+    "PP": "GFP97",
+}
+
+
+def _orca_generic_filament_id(material: str) -> str:
+    """The generic for a material, tried as given, without a CF suffix, without
+    a trailing "+", then by its first word -- the dialog's order."""
+    material = (material or "").upper().strip()
+    return (
+        _ORCA_GENERIC_IDS.get(material)
+        or _ORCA_GENERIC_IDS.get(re.sub(r"[-\s]?CF$", "", material))
+        or _ORCA_GENERIC_IDS.get(re.sub(r"\+$", "", material))
+        or _ORCA_GENERIC_IDS.get(re.split(r"[-\s]", material)[0])
+        or ""
+    )
+
+
 @router.post("/{printer_id}/slots/{ams_id}/{tray_id}/configure")
 async def configure_ams_slot(
     printer_id: int,
@@ -2798,8 +2843,9 @@ async def configure_ams_slot(
     kprofile_filament_id: str = Query(""),
     kprofile_setting_id: str = Query(""),
     k_value: float = Query(0.0),
+    orca_profile_id: str = Query(""),
     db: AsyncSession = Depends(get_db),
-    _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
+    current_user: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
 ):
     """Configure an AMS slot with a specific filament setting and K profile.
 
@@ -2822,6 +2868,9 @@ async def configure_ams_slot(
         setting_id: Full setting ID with version (e.g., "GFSL05_07") - optional
         kprofile_filament_id: K profile's filament_id for proper K profile linking
         k_value: Direct K value to set (0.0 to skip direct K value setting)
+        orca_profile_id: Orca Cloud profile the slot is being set to. With no
+            tray_info_idx, the profile's own filament id is looked up here
+            (#3216); the response says when none was found.
     """
     logger = logging.getLogger(__name__)
     logger.info("[configure_ams_slot] printer_id=%s, ams_id=%s, tray_id=%s", printer_id, ams_id, tray_id)
@@ -2887,6 +2936,40 @@ async def configure_ams_slot(
             setting_id = tray_info_idx
         tray_info_idx = ""
 
+    # An Orca Cloud profile: look up the filament id it puts in the slot. It is
+    # the one value OrcaSlicer's "Sync filaments" matches a slot by, so a
+    # generic here is what turned every Orca custom filament into "Generic
+    # <material>" in the slicer (#3216). Done here rather than in the browser so
+    # the outcome is in the log, and so a failure can be reported back.
+    orca_fallback_reason = ""
+    if orca_profile_id and not tray_info_idx:
+        orca_lookup = await lookup_orca_filament_id(db, current_user, orca_profile_id)
+        found = orca_lookup.filament_id
+        reason = orca_lookup.reason
+        if found and (
+            found.startswith("PFUS")
+            or found.startswith("PFCN")
+            or _ORCA_PROFILE_ID.fullmatch(found)
+            or is_material_name(found)
+        ):
+            # Same refusal as the guard above: never put an id the printer
+            # cannot store in the slot.
+            found, reason = "", "no_filament_id"
+        if found:
+            tray_info_idx = found
+        else:
+            orca_fallback_reason = reason or "no_filament_id"
+            # The generic the Configure dialog picked for an Orca profile before
+            # the lookup moved here -- its table, not the shorter one below, so
+            # PLA Silk, PCTG, PP and PE keep their own generics.
+            tray_info_idx = _orca_generic_filament_id(requested_tray_type)
+        logger.info(
+            "[configure_ams_slot] Orca profile %r → tray_info_idx=%r (%s)",
+            orca_profile_id,
+            tray_info_idx,
+            orca_lookup.source or orca_fallback_reason,
+        )
+
     # Resolve tray_info_idx for the MQTT command.
     # Priority:
     #   1. Use the provided tray_info_idx if set, once the guard above has had
@@ -2944,7 +3027,11 @@ async def configure_ams_slot(
                     current_tray_type = cur_tray.get("tray_type", "")
 
         if (
-            current_tray_info_idx
+            # An Orca profile without an id must not inherit whatever specific
+            # filament the slot held before: that would put the previous spool's
+            # preset in front of the slicer. It gets the generic for its material.
+            not orca_profile_id
+            and current_tray_info_idx
             and current_tray_info_idx not in _GENERIC_ID_VALUES
             and current_tray_type
             and current_tray_type.upper() == tray_type.upper()
@@ -2991,11 +3078,13 @@ async def configure_ams_slot(
     # valid tray_info_idx (GF* official, P* local — not PFUS* cloud-user
     # which the slicer rejects in tray_info_idx).
     effective_setting_id = setting_id
+    realigned_to_kprofile = False
     if (
         kprofile_filament_id
         and kprofile_filament_id != effective_tray_info_idx
         and not kprofile_filament_id.startswith("PFUS")
     ):
+        realigned_to_kprofile = True
         logger.info(
             "[configure_ams_slot] realigning slot filament context to kp: tray_info_idx %r → %r, setting_id %r → %r",
             effective_tray_info_idx,
@@ -3049,6 +3138,9 @@ async def configure_ams_slot(
         filament_id=filament_id_for_kprofile,
         nozzle_diameter=nozzle_diameter,
     )
+    # A deliberate Default pick must not be "restored" to the spool's stored
+    # profile by the lost-selection check (#3219).
+    kprofile_drift.note_slot_configured(printer_id, ams_id, tray_id, cali_idx)
 
     # Method 2: Only send extrusion_cali_set when NO existing profile was selected
     # (cali_idx == -1). When cali_idx >= 0, extrusion_cali_sel already selected the
@@ -3253,6 +3345,13 @@ async def configure_ams_slot(
     return {
         "success": True,
         "message": f"Configured AMS {ams_id} tray {tray_id} with {tray_sub_brands}",
+        # What the slot was actually given, for the slot preset row (#3216).
+        "tray_info_idx": effective_tray_info_idx,
+        # Set when an Orca profile's own filament id could not be used, so the
+        # slicer will see the generic for the material: "no_filament_id",
+        # "lookup_failed" or "no_permission". Cleared when a K-profile realigned
+        # the slot to its own specific filament after all.
+        "orca_fallback_reason": "" if realigned_to_kprofile else orca_fallback_reason,
     }
 
 
@@ -3537,6 +3636,21 @@ async def clear_plate(
 
     printer_manager.set_awaiting_plate_clear(printer_id, False)
 
+    # #1898: releasing the plate without answering the outcome prompt can
+    # count as "good" (opt-in setting) — this is the moment the operator
+    # moves on, so an unanswered prompt would otherwise linger unconfirmed.
+    from backend.app.api.routes.settings import get_setting, setting_is_true
+
+    # setting_is_true rather than a comparison of our own: one reader deciding
+    # for itself what "on" spells is how two parts of the app end up
+    # disagreeing about the same row.
+    if setting_is_true(await get_setting(db, "confirm_default_good_on_plate_clear")):
+        from backend.app.services.print_confirmation import resolve_pending_confirmation_as_good
+
+        resolved = await resolve_pending_confirmation_as_good(db, printer_id)
+        if resolved is not None:
+            await db.commit()
+
     return {"success": True, "message": "Plate cleared, next print will start shortly"}
 
 
@@ -4291,7 +4405,7 @@ async def refresh_ams_slot(
     if not client:
         raise HTTPException(400, "Printer not connected")
 
-    success, message = client.ams_refresh_tray(ams_id, slot_id)
+    success, message = await client.ams_refresh_tray(ams_id, slot_id)
     if not success:
         raise HTTPException(400, message)
 

+ 21 - 4
backend/app/api/routes/projects.py

@@ -11,7 +11,7 @@ from pathlib import Path
 
 from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
 from fastapi.responses import FileResponse, StreamingResponse
-from sqlalchemy import case, func, select, update
+from sqlalchemy import and_, case, func, or_, select, update
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
@@ -54,6 +54,12 @@ router = APIRouter(prefix="/projects", tags=["projects"])
 
 _FAILURE_STATUSES = ("failed", "aborted", "cancelled", "stopped")
 
+# A completed run whose user verdict is 'reject' (#1898) finished on the
+# machine but produced scrap — everywhere a project counts good parts, that
+# run must not contribute. NULL verdict (never asked / not answered) counts
+# as good, matching behaviour before the feature existed.
+_NOT_REJECTED = or_(PrintLogEntry.user_verdict.is_(None), PrintLogEntry.user_verdict != "reject")
+
 # Soft-deleted archives (#1343) keep their row — and therefore their
 # ``project_id`` — after their files have been removed from disk, so that global
 # Quick Stats can still count their filament / time / cost. Nothing in this
@@ -140,8 +146,12 @@ async def _load_totals(db: AsyncSession, project_ids: Sequence[int]) -> dict[int
             func.coalesce(func.sum(PrintLogEntry.energy_kwh), 0).label("total_energy"),
             func.coalesce(func.sum(PrintLogEntry.energy_cost), 0).label("total_energy_cost"),
             func.coalesce(func.sum(PrintArchive.quantity), 0).label("total_items"),
+            # A completed run the user marked as reject (#1898) produced no
+            # usable parts — keep it out of the good-parts count.
             func.coalesce(
-                func.sum(case((PrintLogEntry.status == "completed", PrintArchive.quantity), else_=0)),
+                func.sum(
+                    case((and_(PrintLogEntry.status == "completed", _NOT_REJECTED), PrintArchive.quantity), else_=0)
+                ),
                 0,
             ).label("completed_items"),
             func.coalesce(
@@ -423,7 +433,9 @@ async def list_projects(
                 func.count(PrintLogEntry.id).label("archive_count"),
                 func.coalesce(func.sum(PrintArchive.quantity), 0).label("total_items"),
                 func.coalesce(
-                    func.sum(case((PrintLogEntry.status == "completed", PrintArchive.quantity), else_=0)),
+                    func.sum(
+                        case((and_(PrintLogEntry.status == "completed", _NOT_REJECTED), PrintArchive.quantity), else_=0)
+                    ),
                     0,
                 ).label("completed_count"),
                 func.coalesce(
@@ -1009,7 +1021,12 @@ async def get_project_file_progress(
             func.count(PrintLogEntry.id),
         )
         .join(PrintArchive, PrintArchive.id == PrintLogEntry.archive_id)
-        .where(PrintArchive.project_id == project_id, PrintLogEntry.status == "completed", _LIVE_ARCHIVE)
+        .where(
+            PrintArchive.project_id == project_id,
+            PrintLogEntry.status == "completed",
+            _NOT_REJECTED,
+            _LIVE_ARCHIVE,
+        )
         .group_by(PrintArchive.library_file_id, PrintArchive.content_hash, PrintArchive.filename)
     )
 

+ 133 - 85
backend/app/api/routes/settings.py

@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
 
 from backend.app.core.auth import (
     RequirePermissionIfAuthEnabled,
+    ScopedCaller,
     caller_is_api_key,
     require_auth_if_enabled,
     require_energy_cost_update,
@@ -129,6 +130,22 @@ def normalize_str_setting(key: str, value: object) -> str:
     raise HTTPException(400, f"{key} must be a string; got {type(value).__name__}")
 
 
+async def get_external_base_url(db: AsyncSession) -> str:
+    """Base URL for links Bambuddy hands to the outside world (no trailing slash).
+
+    ``external_url`` is optional and has no default, so anything that must be
+    absolute — a login link in an e-mail, the one-tap outcome verdict links
+    (#1898), whose Telegram/ntfy buttons are dropped for a relative URL — falls
+    back to APP_URL and finally to the dev origin.
+    """
+    import os
+
+    external_url = await get_setting(db, "external_url")
+    if external_url:
+        return external_url.rstrip("/")
+    return os.environ.get("APP_URL", "http://localhost:5173").rstrip("/")
+
+
 async def get_external_login_url(db: AsyncSession) -> str:
     """Get the external URL for the login page.
 
@@ -140,14 +157,7 @@ async def get_external_login_url(db: AsyncSession) -> str:
     Returns:
         Full URL to the login page
     """
-    import os
-
-    external_url = await get_setting(db, "external_url")
-    if external_url:
-        external_url = external_url.rstrip("/")
-    else:
-        external_url = os.environ.get("APP_URL", "http://localhost:5173")
-    return external_url + "/login"
+    return await get_external_base_url(db) + "/login"
 
 
 async def set_setting(db: AsyncSession, key: str, value: str) -> None:
@@ -157,6 +167,98 @@ async def set_setting(db: AsyncSession, key: str, value: str) -> None:
     await upsert_setting(db, Settings, key, value)
 
 
+# Settings stored as booleans / numbers. Storage is a VARCHAR column, so
+# _build_settings_response() parses these back, and update_settings() refuses
+# an explicit null for them: a null is stored as the literal "None", which
+# reads back as False for a boolean and is not a number at all.
+_BOOL_SETTING_KEYS = frozenset(
+    {
+        "auto_archive",
+        "save_thumbnails",
+        "capture_finish_photo",
+        "finish_photo_restore_plate",
+        "spoolman_enabled",
+        "spoolman_disable_weight_sync",
+        "spoolman_report_partial_usage",
+        "auto_add_unknown_rfid",
+        "disable_filament_warnings",
+        "prefer_lowest_filament",
+        "check_updates",
+        "check_printer_firmware",
+        "include_beta_updates",
+        "announcements_enabled",
+        "announcements_all_users",
+        "virtual_printer_enabled",
+        "ftp_retry_enabled",
+        "mqtt_enabled",
+        "mqtt_use_tls",
+        "ha_enabled",
+        "per_printer_mapping_expanded",
+        "prometheus_enabled",
+        "user_notifications_enabled",
+        "queue_drying_enabled",
+        "queue_drying_block",
+        "ambient_drying_enabled",
+        "print_drying_enabled",
+        "require_plate_clear",
+        "queue_shortest_first",
+        # default_bed_levelling / default_flow_cali / default_nozzle_offset_cali
+        # are tri-state strings (off/on/auto) — parsed via the raw-string else
+        # branch; the TriState validator coerces legacy "true"/"false" rows.
+        "default_vibration_cali",
+        "default_layer_inspect",
+        "default_timelapse",
+        "default_confirm_outcome",
+        "confirm_outcome_external_prints",
+        "confirm_default_good_on_plate_clear",
+        "billing_enabled",
+        "printer_kill_switch_enabled",
+        "ldap_enabled",
+        "ldap_auto_provision",
+        "local_login_enabled",
+        "preheat_enabled",
+        "queue_keep_bed_warm",
+    }
+)
+
+_FLOAT_SETTING_KEYS = frozenset(
+    {
+        "default_filament_cost",
+        "energy_cost_per_kwh",
+        "ams_temp_good",
+        "ams_temp_fair",
+        "library_disk_warning_gb",
+        "low_stock_threshold",
+    }
+)
+
+_INT_SETTING_KEYS = frozenset(
+    {
+        "ams_humidity_good",
+        "ams_humidity_fair",
+        "ams_history_retention_days",
+        "printer_sensor_history_retention_days",
+        "ftp_retry_count",
+        "ftp_retry_delay",
+        "ftp_timeout",
+        "mqtt_port",
+        "stagger_group_size",
+        "stagger_interval_minutes",
+        "forecast_global_lead_time_days",
+        "location_sensor_poll_interval",
+        "finance_budget_reset_day",
+        "session_max_hours",
+        "pipeline_max_copies",
+        "preheat_max_wait_seconds",
+        "preheat_soak_seconds",
+        "queue_keep_warm_bed_temp",
+        "queue_keep_warm_max_minutes",
+        "queue_max_concurrent_uploads",
+        "ambient_drying_sustained_minutes",
+    }
+)
+
+
 async def _build_settings_response(db: AsyncSession, is_api_key: bool = False) -> AppSettings:
     """Build the full settings response, scrubbing secrets for API-key callers."""
     settings_dict = DEFAULT_SETTINGS.model_dump()
@@ -165,91 +267,27 @@ async def _build_settings_response(db: AsyncSession, is_api_key: bool = False) -
     for setting in result.scalars().all():
         if setting.key not in settings_dict:
             continue
-        if setting.key in [
-            "auto_archive",
-            "save_thumbnails",
-            "capture_finish_photo",
-            "finish_photo_restore_plate",
-            "spoolman_enabled",
-            "spoolman_disable_weight_sync",
-            "spoolman_report_partial_usage",
-            "auto_add_unknown_rfid",
-            "disable_filament_warnings",
-            "prefer_lowest_filament",
-            "check_updates",
-            "check_printer_firmware",
-            "include_beta_updates",
-            "virtual_printer_enabled",
-            "ftp_retry_enabled",
-            "mqtt_enabled",
-            "mqtt_use_tls",
-            "ha_enabled",
-            "per_printer_mapping_expanded",
-            "prometheus_enabled",
-            "user_notifications_enabled",
-            "queue_drying_enabled",
-            "queue_drying_block",
-            "ambient_drying_enabled",
-            "print_drying_enabled",
-            "require_plate_clear",
-            "queue_shortest_first",
-            # default_bed_levelling / default_flow_cali / default_nozzle_offset_cali
-            # are tri-state strings (off/on/auto) — parsed via the raw-string else
-            # branch; the TriState validator coerces legacy "true"/"false" rows.
-            "default_vibration_cali",
-            "default_layer_inspect",
-            "default_timelapse",
-            "billing_enabled",
-            "printer_kill_switch_enabled",
-            "ldap_enabled",
-            "ldap_auto_provision",
-            "local_login_enabled",
-            "preheat_enabled",
-            "queue_keep_bed_warm",
-        ]:
+        if setting.key in _BOOL_SETTING_KEYS:
             settings_dict[setting.key] = setting.value.lower() == "true"
-        elif setting.key in [
-            "default_filament_cost",
-            "energy_cost_per_kwh",
-            "ams_temp_good",
-            "ams_temp_fair",
-            "library_disk_warning_gb",
-            "low_stock_threshold",
-        ]:
-            settings_dict[setting.key] = float(setting.value)
+        elif setting.key in _FLOAT_SETTING_KEYS or setting.key in _INT_SETTING_KEYS:
+            # A value that does not parse (the literal "None" from an old
+            # null save, or a hand-edited row) keeps the default instead of
+            # taking the whole settings response down with it.
+            parse = int if setting.key in _INT_SETTING_KEYS else float
+            try:
+                settings_dict[setting.key] = parse(setting.value)
+            except (TypeError, ValueError):
+                logger.warning("Setting %s has an unparseable value; using the default", setting.key)
         elif setting.key in [
             # Nullable floats. Settings storage stringifies None to the literal
-            # "None", so these cannot go in the list above -- float("None")
-            # raises and would take the whole settings response with it (#2905).
+            # "None", which must read back as null here -- not as the default
+            # the _FLOAT_SETTING_KEYS branch above falls back to (#2905).
             "ams_temp_alarm",
         ]:
             try:
                 settings_dict[setting.key] = float(setting.value)
             except (TypeError, ValueError):
                 settings_dict[setting.key] = None
-        elif setting.key in [
-            "ams_humidity_good",
-            "ams_humidity_fair",
-            "ams_history_retention_days",
-            "printer_sensor_history_retention_days",
-            "ftp_retry_count",
-            "ftp_retry_delay",
-            "ftp_timeout",
-            "mqtt_port",
-            "stagger_group_size",
-            "stagger_interval_minutes",
-            "forecast_global_lead_time_days",
-            "location_sensor_poll_interval",
-            "finance_budget_reset_day",
-            "session_max_hours",
-            "pipeline_max_copies",
-            "preheat_max_wait_seconds",
-            "preheat_soak_seconds",
-            "queue_keep_warm_bed_temp",
-            "queue_keep_warm_max_minutes",
-            "queue_max_concurrent_uploads",
-        ]:
-            settings_dict[setting.key] = int(setting.value)
         elif setting.key == "default_printer_id":
             settings_dict[setting.key] = int(setting.value) if setting.value and setting.value != "None" else None
         elif setting.key == "open_in_slicer":
@@ -294,6 +332,16 @@ async def update_settings(
     """Update application settings."""
     update_data = settings_update.model_dump(exclude_unset=True)
 
+    # An explicit null for a boolean or numeric setting has no meaning -- these
+    # are not clearable -- and would be stored as the literal "None".
+    null_keys = sorted(
+        key
+        for key, value in update_data.items()
+        if value is None and key in (_BOOL_SETTING_KEYS | _FLOAT_SETTING_KEYS | _INT_SETTING_KEYS)
+    )
+    if null_keys:
+        raise HTTPException(status_code=422, detail=f"These settings cannot be null: {', '.join(null_keys)}")
+
     # Safety refusals on disabling local login (#1589). Two failure modes
     # would otherwise lock everyone out of the install:
     #   1. No enabled OIDC provider exists — nobody could authenticate.
@@ -397,7 +445,7 @@ class ElectricityPriceUpdate(BaseModel):
 async def update_electricity_price(
     payload: ElectricityPriceUpdate,
     db: AsyncSession = Depends(get_db),
-    _: User | None = Depends(require_energy_cost_update()),
+    _: ScopedCaller = Depends(require_energy_cost_update()),
     _is_api_key: bool = Depends(caller_is_api_key),
 ):
     """Update the per-kWh electricity cost used by the energy-tracking pipeline.

+ 13 - 9
backend/app/api/routes/spoolbuddy.py

@@ -427,6 +427,10 @@ async def nfc_tag_scanned(
                             # the colour catalog over "Silk+" (#3090).
                             "color_name_is_synthesized": mapped["color_name_is_synthesized"],
                             "rgba": mapped["rgba"],
+                            # The kiosk paints the disc from these, as the
+                            # Filament page does (#3033).
+                            "extra_colors": mapped["extra_colors"],
+                            "effect_type": mapped["effect_type"],
                             "brand": mapped["brand"],
                             "label_weight": mapped["label_weight"],
                             "core_weight": mapped["core_weight"],
@@ -494,6 +498,8 @@ async def nfc_tag_scanned(
                         # set, and nothing else — never a stand-in (#3090).
                         "color_name_is_synthesized": False,
                         "rgba": spool.rgba,
+                        "extra_colors": spool.extra_colors,
+                        "effect_type": spool.effect_type,
                         "brand": spool.brand,
                         "label_weight": spool.label_weight,
                         "core_weight": spool.core_weight,
@@ -885,7 +891,7 @@ async def update_spool_weight(
     update while the Spoolman row the user is actually looking at stayed
     unchanged (#1530). Mirrors the routing already used by ``nfc/tag-scanned``.
     """
-    from backend.app.api.routes._spoolman_helpers import _safe_float
+    from backend.app.api.routes._spoolman_helpers import _safe_float, spoolman_net_weight, spoolman_tare
     from backend.app.models.spool import Spool
 
     sm_client = await _get_spoolman_client_or_none(db)
@@ -914,20 +920,18 @@ async def update_spool_weight(
     async with _translate_spoolbuddy_errors():
         sm_spool = await sm_client.get_spool(req.spool_id)
 
-    filament = sm_spool.get("filament") or {}
-    spool_tare = sm_spool.get("spool_weight")
-    raw_tare = spool_tare if spool_tare is not None else filament.get("spool_weight")
+    core_weight, tare_source = spoolman_tare(sm_spool)
     spool_weight_warning: str | None = None
-    if raw_tare is None:
+    if tare_source == "fallback":
         logger.warning(
-            "Spoolman spool %d has no spool_weight set; using 250g fallback for tare",
+            "Spoolman spool %d has no spool_weight or vendor empty_spool_weight set; using 250g fallback for tare",
             req.spool_id,
         )
         spool_weight_warning = (
-            "spool_weight_not_set: Spoolman filament has no spool_weight configured; weight estimate uses 250g fallback"
+            "spool_weight_not_set: Spoolman spool, filament and vendor have no empty-spool weight configured; "
+            "weight estimate uses 250g fallback"
         )
-    core_weight = _safe_float(raw_tare, 250.0)
-    label_weight = _safe_float(filament.get("weight"), 1000.0)
+    label_weight = _safe_float(spoolman_net_weight(sm_spool), 1000.0)
     remaining_weight = max(0.0, req.weight_grams - core_weight)
 
     async with _translate_spoolbuddy_errors():

+ 10 - 3
backend/app/api/routes/spoolman.py

@@ -10,7 +10,7 @@ from sqlalchemy import delete, select, text
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
-from backend.app.api.routes._spoolman_helpers import _map_spoolman_spool
+from backend.app.api.routes._spoolman_helpers import _map_spoolman_spool, spoolman_net_weight
 from backend.app.api.routes.spoolman_inventory import _clear_stale_tag_links
 from backend.app.core.auth import RequirePermissionIfAuthEnabled
 from backend.app.core.database import get_db
@@ -21,6 +21,7 @@ from backend.app.models.spool_assignment import SpoolAssignment
 from backend.app.models.spoolman_k_profile import SpoolmanKProfile
 from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
 from backend.app.models.user import User
+from backend.app.services import slot_unlink_grace
 from backend.app.services.printer_manager import printer_manager
 from backend.app.services.slicer_filament_resolver import resolve_slicer_filament
 from backend.app.services.slot_nozzle import resolve_slot_nozzle
@@ -362,6 +363,7 @@ async def sync_printer_ams(
                 sync_result = await client.sync_ams_tray(
                     tray,
                     printer.name,
+                    db,
                     # Per-print tracking owns weight updates (#1119); manual sync
                     # only refreshes spool metadata + slot assignments here.
                     disable_weight_sync=True,
@@ -579,6 +581,7 @@ async def sync_all_printers(
                     sync_result = await client.sync_ams_tray(
                         tray,
                         printer.name,
+                        db,
                         # Per-print tracking owns weight updates (#1119); manual
                         # sync-all only refreshes spool metadata + slot assignments.
                         disable_weight_sync=True,
@@ -803,11 +806,12 @@ async def get_linked_spools(
             # Remove quotes if present (JSON encoded string)
             clean_tag = tag.strip('"').upper()
             if clean_tag:
-                filament = spool.get("filament") or {}
                 linked[clean_tag] = {
                     "id": spool["id"],
                     "remaining_weight": spool.get("remaining_weight"),
-                    "filament_weight": filament.get("weight"),
+                    # The spool's own net weight, falling back to the
+                    # filament's; the key predates initial_weight (#3194).
+                    "filament_weight": spoolman_net_weight(spool),
                 }
 
     return {"linked": linked}
@@ -896,6 +900,7 @@ async def link_spool(
                 {"printer_id": p_id, "ams_id": a_id, "tray_id": t_id, "spool_id": spool_id},
             )
             await db.commit()
+            slot_unlink_grace.forget_slot(p_id, a_id, t_id)
         except Exception as e:
             await db.rollback()
             logger.error(
@@ -1290,6 +1295,7 @@ async def create_spool_from_slot(
     sync_result = await client.sync_ams_tray(
         tray,
         printer.name,
+        db,
         disable_weight_sync=True,
         auto_add_unknown_rfid=True,
     )
@@ -1318,6 +1324,7 @@ async def create_spool_from_slot(
                 },
             )
             await db.commit()
+            slot_unlink_grace.forget_slot(req.printer_id, req.ams_id, req.tray_id)
         except Exception as exc:
             await db.rollback()
             logger.exception("Failed to persist Spoolman slot assignment")

+ 219 - 34
backend/app/api/routes/spoolman_inventory.py

@@ -26,10 +26,14 @@ from backend.app.api.routes._spoolman_helpers import (
     NormalizedFilament,
     NormalizedVendorRef,
     _map_spoolman_spool,
-    _safe_float,
     _safe_int,
     _safe_optional_float,
     assert_safe_spoolman_url,
+    cost_per_kg_to_spoolman_price,
+    spoolman_cost_per_kg,
+    spoolman_net_weight,
+    spoolman_price_weight,
+    spoolman_tare,
 )
 from backend.app.core.auth import RequirePermissionIfAuthEnabled
 from backend.app.core.database import get_db
@@ -41,9 +45,12 @@ from backend.app.models.settings import Settings
 from backend.app.models.spool_filament_preset import SpoolmanFilamentPreset
 from backend.app.models.spoolman_k_profile import SpoolmanKProfile
 from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
+from backend.app.models.supplier import SpoolmanSpoolSupplier, Supplier
 from backend.app.models.user import User
 from backend.app.schemas.spool import SpoolFilamentPresetBase, SpoolKProfileBase
 from backend.app.schemas.spoolman import SpoolmanFilamentPatch, SpoolmanSlotAssignmentEnriched
+from backend.app.schemas.supplier import SpoolSupplierLinkInput
+from backend.app.services import slot_unlink_grace
 from backend.app.services.location_service import (
     enrich_spool_dicts_with_location_id,
     maybe_sync_spoolman_locations,
@@ -255,6 +262,9 @@ def _raise_if_partial_failure(spools: list[dict], results: list, operation: str)
 async def _apply_price_if_set(client: SpoolmanClient, spool: dict, cost_per_kg: float | None) -> tuple[dict, list[str]]:
     """Patch the spool price; return (updated_spool, warnings).
 
+    Spoolman's spool price is what the whole spool cost, so the per-kg rate is
+    converted at the net weight of the spool Spoolman just created (#3194).
+
     Returns the original spool and a non-empty warnings list when the price
     update fails, so the caller can return HTTP 207 instead of silently
     discarding the price.
@@ -263,7 +273,9 @@ async def _apply_price_if_set(client: SpoolmanClient, spool: dict, cost_per_kg:
         return spool, []
     try:
         async with _translate_spoolman_errors():
-            updated = await client.update_spool_full(spool["id"], price=cost_per_kg)
+            updated = await client.update_spool_full(
+                spool["id"], price=cost_per_kg_to_spoolman_price(cost_per_kg, spoolman_price_weight(spool))
+            )
         return updated, []
     except HTTPException as exc:
         if exc.status_code >= 500:
@@ -311,9 +323,12 @@ class SpoolmanInventoryCreate(BaseModel):
     color_name: str | None = Field(None, max_length=64)
     rgba: str | None = Field(None, max_length=8, description="6-digit hex (RRGGBB) or 8-digit (RRGGBBAA)")
     label_weight: int = Field(1000, ge=1, le=100_000)
-    core_weight: int = Field(
-        250, ge=0, le=10_000
-    )  # Accepted for schema parity but not persisted to Spoolman (stored on filament type, not spool)
+    # Persisted to the Spoolman spool's own `spool_weight` (tare), which takes
+    # priority over the filament-level value both in _map_spoolman_spool and in
+    # the weigh endpoint. Only written when the request actually sets it: the
+    # 250 default is the display fallback, and writing it on every create would
+    # stamp an explicit tare on spools that should keep inheriting one (#2908).
+    core_weight: int = Field(250, ge=0, le=10_000)
     weight_used: float = Field(0.0, ge=0.0, le=100_000.0)
     note: str | None = Field(None, max_length=1000)
     cost_per_kg: float | None = Field(None, ge=0.0, le=1_000_000.0)
@@ -352,9 +367,11 @@ class SpoolmanInventoryUpdate(BaseModel):
     color_name: str | None = Field(None, max_length=64)
     rgba: str | None = Field(None, max_length=8, description="6-digit hex (RRGGBB) or 8-digit (RRGGBBAA)")
     label_weight: int | None = Field(None, ge=1, le=100_000)
-    core_weight: int | None = Field(
-        None, ge=0, le=10_000
-    )  # Accepted for schema parity but not persisted to Spoolman (stored on filament type, not spool)
+    # Persisted to the spool's own `spool_weight` (see the Create schema).
+    # Omitted / null leaves the current value alone, as with every other field
+    # here. There is no per-spool "go back to inheriting" through this route;
+    # the filament-level route already owns that decision (#2908).
+    core_weight: int | None = Field(None, ge=0, le=10_000)
     weight_used: float | None = Field(None, ge=0.0, le=100_000.0)
     note: str | None = Field(None, max_length=1000)
     cost_per_kg: float | None = Field(None, ge=0.0, le=1_000_000.0)
@@ -471,6 +488,19 @@ async def list_spools(
         for m in mapped:
             m["k_profiles"] = kp_by_spool.get(m["id"], [])
 
+        # Supplier assignments (#2988) live Bambuddy-side even for Spoolman
+        # spools, so the list carries them in both modes identically.
+        link_result = await db.execute(
+            select(SpoolmanSpoolSupplier)
+            .options(selectinload(SpoolmanSpoolSupplier.supplier))
+            .where(SpoolmanSpoolSupplier.spoolman_spool_id.in_(spool_ids))
+        )
+        links_by_spool: dict[int, list[dict]] = {}
+        for link in link_result.scalars().all():
+            links_by_spool.setdefault(link.spoolman_spool_id, []).append(_supplier_link_to_dict(link))
+        for m in mapped:
+            m["suppliers"] = links_by_spool.get(m["id"], [])
+
     await enrich_spool_dicts_with_location_id(db, mapped)
     return mapped
 
@@ -493,6 +523,12 @@ async def get_spool(
 
     kp_result = await db.execute(select(SpoolmanKProfile).where(SpoolmanKProfile.spoolman_spool_id == spool_id))
     mapped["k_profiles"] = [_k_profile_to_dict(kp) for kp in kp_result.scalars().all()]
+    link_result = await db.execute(
+        select(SpoolmanSpoolSupplier)
+        .options(selectinload(SpoolmanSpoolSupplier.supplier))
+        .where(SpoolmanSpoolSupplier.spoolman_spool_id == spool_id)
+    )
+    mapped["suppliers"] = [_supplier_link_to_dict(link) for link in link_result.scalars().all()]
     await enrich_spool_dicts_with_location_id(db, [mapped])
     return mapped
 
@@ -552,6 +588,11 @@ async def create_spool(
                 remaining_weight=remaining,
                 comment=data.note or None,
                 location=storage_location or None,
+                spool_weight=(data.core_weight if "core_weight" in data.model_fields_set else None),
+                # Only a label weight the caller gave: left off, Spoolman uses
+                # the filament's weight, which is the right size for a caller
+                # that didn't say (#3194).
+                initial_weight=(float(data.label_weight) if "label_weight" in data.model_fields_set else None),
             )
     except HTTPException as exc:
         if exc.status_code == 404 and data.spoolman_filament_id is not None:
@@ -641,6 +682,11 @@ async def bulk_create_spools(
                 remaining_weight=remaining,
                 comment=data.note or None,
                 location=storage_location or None,
+                spool_weight=(data.core_weight if "core_weight" in data.model_fields_set else None),
+                # Only a label weight the caller gave: left off, Spoolman uses
+                # the filament's weight, which is the right size for a caller
+                # that didn't say (#3194).
+                initial_weight=(float(data.label_weight) if "label_weight" in data.model_fields_set else None),
             )
         except (SpoolmanUnavailableError, SpoolmanClientError, SpoolmanNotFoundError) as exc:
             logger.warning("Bulk spool creation: one spool failed: %s", exc)
@@ -720,7 +766,17 @@ async def update_spool(
     # straight back off by `spoolman_color_hex` below, so the two paths landed on
     # the same string and the append only obscured which shape was in hand (#2912).
     rgba = data.rgba if data.rgba is not None else cur_color
-    label_weight = data.label_weight if data.label_weight is not None else int(cur_filament.get("weight") or 1000)
+    # The label weight is the spool's own net weight (initial_weight), not the
+    # filament's: one filament can have spools of different sizes (#3194).
+    # The form shows it in whole grams, so it only counts as changed when it
+    # differs from that; the exact stored value is otherwise left alone. A
+    # spool with no size at either level takes the one it is given: Spoolman
+    # refuses a remaining_weight until the spool has one.
+    cur_net_weight = spoolman_net_weight(current)
+    cur_has_size = cur_net_weight is not None and cur_net_weight > 0
+    cur_label_weight = _safe_int(cur_net_weight, 1000) or 1000
+    label_weight = data.label_weight if data.label_weight is not None else cur_label_weight
+    label_weight_changed = data.label_weight is not None and (not cur_has_size or data.label_weight != cur_label_weight)
     # Default weight_used from the synthetic mapping (label - remaining) so an
     # edit that doesn't touch the weight field preserves Spoolman's real
     # remaining_weight after a "Reset usage to 0" — the previous code read
@@ -766,14 +822,16 @@ async def update_spool(
     # alpha-only edit invisible so the change never lands (#2912).
     cur_color_norm = spoolman_color_hex(cur_filament.get("color_hex")) or ""
     cur_vendor_name = (cur_vendor.get("name") or "").strip()
-    cur_weight_int = int(cur_filament.get("weight") or 0)
+    # The label weight is not part of this: it is written to the spool's
+    # initial_weight below. Comparing it against the filament's weight PATCHed
+    # the filament for every spool of another size, or on a shared filament
+    # relinked the spool to a new duplicate one (#3194).
     metadata_unchanged = (
         cur_filament_id
         and (cur_filament.get("name") or "").strip() == desired_name
         and (cur_filament.get("material") or "").upper() == material.upper()
         and cur_color_norm == (color_hex or "").upper()
         and cur_vendor_name.lower() == ((brand or "").strip().lower())
-        and cur_weight_int == int(label_weight)
     )
 
     if metadata_unchanged:
@@ -791,7 +849,6 @@ async def update_spool(
                 "name": desired_name,
                 "material": material,
                 "color_hex": color_hex,
-                "weight": float(label_weight),
             }
             if brand:
                 vendor_id = await client.find_or_create_vendor(brand)
@@ -817,6 +874,25 @@ async def update_spool(
 
     remaining = max(0.0, label_weight - weight_used)
 
+    # The spool's size lives on the spool (initial_weight), never the filament
+    # (#3194). A relink also sends the current size: Spoolman stamps the new
+    # filament's weight on a spool without an initial_weight of its own.
+    new_initial_weight: float | None = None
+    if label_weight_changed:
+        new_initial_weight = float(label_weight)
+    elif filament_id != cur_filament_id and cur_has_size:
+        new_initial_weight = cur_net_weight
+
+    # Spoolman's price is per spool, the form's per kg (#3194). An unchanged
+    # rate on an unchanged size is not written, so a no-op edit cannot move the
+    # price by rounding. On a resize the rate is kept, as in internal mode.
+    cur_cost_per_kg = spoolman_cost_per_kg(current)
+    cost_per_kg = data.cost_per_kg if data.cost_per_kg is not None else cur_cost_per_kg
+    new_price: float | None = None
+    if cost_per_kg is not None and (label_weight_changed or cost_per_kg != cur_cost_per_kg):
+        price_weight = float(label_weight) if label_weight_changed else spoolman_price_weight(current)
+        new_price = cost_per_kg_to_spoolman_price(cost_per_kg, price_weight)
+
     # Tag removal: clear only the "tag" key so other custom Spoolman extra fields
     # set outside Bambuddy are preserved.
     tag_nulled = (
@@ -848,12 +924,20 @@ async def update_spool(
             updated = await client.update_spool_full(
                 spool_id=spool_id,
                 filament_id=filament_id,
-                remaining_weight=remaining,
+                # Without a size Spoolman rejects the whole PATCH (HTTP 400),
+                # so a spool that still has none gets no remaining_weight.
+                remaining_weight=(remaining if new_initial_weight is not None or cur_has_size else None),
                 comment=note or "",
-                price=data.cost_per_kg,
+                price=new_price,
+                initial_weight=new_initial_weight,
                 extra=extra,
                 location=storage_location or None,
                 clear_location=storage_location_changed and not storage_location,
+                # No model_fields_set guard here, unlike create: this schema
+                # already defaults core_weight to None, and None is what
+                # update_spool_full reads as "leave the tare alone". A guard
+                # would be a second spelling of the same condition.
+                spool_weight=data.core_weight,
             )
 
     # Persist BambuStudio slicer preset AND color_name under spool.extra.
@@ -890,6 +974,22 @@ async def update_spool(
     return _map_spoolman_spool(updated)
 
 
+async def _purge_local_rows_for_spool(db: AsyncSession, spool_id: int) -> None:
+    """Drop the Bambuddy-side rows a deleted Spoolman spool leaves behind.
+
+    Spoolman owns the spool; Bambuddy owns the K profiles, the filament preset
+    overrides and the supplier assignments, each keyed by the remote id with no
+    foreign key that could cascade. The K-profile and preset leaks are inert,
+    but a leaked ``spoolman_spool_suppliers`` row keeps the supplier's
+    reference count non-zero, so deleting that supplier answers 409 forever
+    with no way for the user to find the phantom reference (#2988).
+
+    The caller commits.
+    """
+    for model in (SpoolmanKProfile, SpoolmanFilamentPreset, SpoolmanSpoolSupplier):
+        await db.execute(delete(model).where(model.spoolman_spool_id == spool_id))
+
+
 @router.delete("/spools/{spool_id}")
 async def delete_spool(
     spool_id: int = Path(..., gt=0),
@@ -900,6 +1000,8 @@ async def delete_spool(
     client = await _get_client(db)
     async with _translate_spoolman_errors():
         await client.delete_spool(spool_id)
+    await _purge_local_rows_for_spool(db, spool_id)
+    await db.commit()
     await ws_manager.broadcast({"type": "inventory_changed"})
     return {"status": "deleted"}
 
@@ -950,18 +1052,21 @@ async def reset_spool_consumed_counter(
 ) -> dict:
     """Zero the displayed "Total Consumed" counter for a Spoolman spool.
 
-    Spoolman doesn't have a native "baseline" field, so the implementation
-    reaches for the closest equivalent: PATCH `used_weight=0` upstream.
-    The read mapping in ``_map_spoolman_spool`` then derives Bambuddy's
-    `weight_used = label - remaining_weight` and `baseline = weight_used -
-    real_used_weight`, so the Inventory page's `weight_used - baseline`
-    display lands at 0 while remaining (= label - weight_used) is preserved
-    — parity with the internal-mode endpoint (#1390, see also
+    Spoolman has no native baseline field, so the baseline lives in
+    ``spool.extra`` — the same mechanism internal mode uses for its
+    ``weight_used_baseline`` column. ``_map_spoolman_spool`` folds it back in,
+    so the Inventory page's ``weight_used - baseline`` reads 0 while every
+    native Spoolman field is left exactly as it was.
+
+    It used to PATCH ``used_weight = 0`` upstream, which is not the same thing:
+    Spoolman recomputes ``remaining_weight`` from initial minus used, so the
+    spool jumped back to full and the measured remaining filament was gone
+    (#2906). Parity with the internal-mode endpoint (#1644, #1390, see also
     ``backend/app/api/routes/inventory.py::reset_spool_consumed_counter``).
     """
     client = await _get_client(db)
     async with _translate_spoolman_errors():
-        spool = await client.reset_spool_usage(spool_id)
+        spool = await client.reset_spool_consumed_counter(spool_id)
     try:
         mapped = _map_spoolman_spool(spool)
     except ValueError as exc:
@@ -1027,6 +1132,7 @@ async def bulk_delete_spools(
         try:
             async with _translate_spoolman_errors():
                 await client.delete_spool(sid)
+            await _purge_local_rows_for_spool(db, sid)
             deleted += 1
         except HTTPException as exc:
             errors.append({"id": sid, "status": exc.status_code, "detail": exc.detail})
@@ -1034,6 +1140,7 @@ async def bulk_delete_spools(
             logger.exception("Spoolman bulk-delete failed for spool %s", sid)
             errors.append({"id": sid, "status": 500, "detail": str(exc)})
     if deleted:
+        await db.commit()
         await ws_manager.broadcast({"type": "inventory_changed"})
     return {"deleted": deleted, "errors": errors}
 
@@ -1112,7 +1219,7 @@ async def bulk_reset_spool_consumed_counter(
     for spool_id in spool_ids:
         try:
             async with _translate_spoolman_errors():
-                await client.reset_spool_usage(spool_id)
+                await client.reset_spool_consumed_counter(spool_id)
             reset_count += 1
         except HTTPException as exc:
             logger.warning("Spoolman reset-consumed-counter failed for spool %s: %s", spool_id, exc.detail)
@@ -1131,25 +1238,21 @@ async def sync_spool_weight(
 ) -> dict:
     """Update a spool's remaining weight from a measured gross weight.
 
-    Computes remaining = gross_weight - tare, where tare = spool.spool_weight
-    if set, else filament.spool_weight; falls back to 250 g when both unset.
+    Computes remaining = gross_weight - tare, with the tare resolved the way
+    Spoolman does it (spool, filament, vendor, then 250 g; see spoolman_tare).
     """
     client = await _get_client(db)
 
     async with _translate_spoolman_errors():
         current = await client.get_spool(spool_id)
 
-    cur_filament = current.get("filament") or {}
-    spool_tare = current.get("spool_weight")
-    raw_tare = spool_tare if spool_tare is not None else cur_filament.get("spool_weight")
-    core_weight = _safe_float(raw_tare, 250.0)
+    core_weight, _source = spoolman_tare(current)
     remaining = max(0.0, data.weight_grams - core_weight)
 
     async with _translate_spoolman_errors():
         updated = await client.update_spool_full(spool_id=spool_id, remaining_weight=remaining)
 
-    upd_filament = updated.get("filament") or {}
-    label_weight = _safe_int(upd_filament.get("weight"), 1000)
+    label_weight = _safe_int(spoolman_net_weight(updated), 1000)
     weight_used = max(0.0, label_weight - remaining)
     await ws_manager.broadcast({"type": "inventory_changed"})
     return {"status": "ok", "weight_used": weight_used}
@@ -1355,7 +1458,7 @@ async def sync_spoolman_ams_weights(
             skipped += 1
             continue
 
-        label_weight = _safe_int((spool_dict.get("filament") or {}).get("weight"), 1000)
+        label_weight = _safe_int(spoolman_net_weight(spool_dict), 1000)
         if label_weight <= 0:
             logger.debug("Spoolman AMS sync: spool %d has no label_weight, skipping", assignment.spoolman_spool_id)
             skipped += 1
@@ -1483,6 +1586,7 @@ async def assign_spoolman_slot(
             },
         )
         await db.commit()
+        slot_unlink_grace.forget_slot(body.printer_id, body.ams_id, body.tray_id)
     except Exception as exc:
         await db.rollback()
         logger.error("Failed to persist slot assignment: %s", exc)
@@ -2003,7 +2107,8 @@ async def patch_spoolman_filament(
     """Update a Spoolman filament's name and/or spool_weight.
 
     When spool_weight changes, Option A (keep_existing_spools=True) stamps the old
-    weight onto spools currently inheriting it (spool.spool_weight is None) so their
+    weight (the filament's, or its vendor's empty_spool_weight when the filament had
+    none) onto spools currently inheriting it (spool.spool_weight is None) so their
     tare calculations are unaffected by the filament change.
     Option B (keep_existing_spools=False, the default): when spool_weight is a
     concrete value, stamps it onto every affected spool explicitly; when spool_weight
@@ -2031,7 +2136,13 @@ async def patch_spoolman_filament(
 
         if affected_spools:
             if body.keep_existing_spools:
+                # What the inheriting spools weigh against today: the filament's
+                # own value, or when it has none, its vendor's (#3195). With
+                # neither they sit on the 250 g fallback, which is Bambuddy's
+                # alone, so there is nothing real to keep.
                 old_weight = _safe_optional_float(current.get("spool_weight"))
+                if old_weight is None:
+                    old_weight = _safe_optional_float((current.get("vendor") or {}).get("empty_spool_weight"))
                 if old_weight is not None:
                     spools_to_fix = [s for s in affected_spools if s.get("spool_weight") is None]
                     if spools_to_fix:
@@ -2060,7 +2171,7 @@ async def patch_spoolman_filament(
                     _raise_if_partial_failure(affected_spools, results, "spool_weight stamp (option B)")
                 else:
                     # Filament weight is being cleared — remove any per-spool override
-                    # so spools fall back to whatever the filament now provides.
+                    # so spools fall back to the vendor's empty_spool_weight, or 250 g.
                     spools_to_clear = [s for s in affected_spools if s.get("spool_weight") is not None]
                     if spools_to_clear:
                         async with _translate_spoolman_errors():
@@ -2134,3 +2245,77 @@ async def save_spoolman_k_profiles(
         await db.refresh(obj)
 
     return [_k_profile_to_dict(p) for p in saved]
+
+
+def _supplier_link_to_dict(link: SpoolmanSpoolSupplier) -> dict:
+    """Same shape as ``SpoolSupplierResponse`` so the frontend renders both
+    inventories with one component."""
+    return {
+        "id": link.id,
+        "supplier_id": link.supplier_id,
+        "supplier_name": link.supplier_name,
+        "supplier_article_number": link.supplier_article_number,
+        "quoted_price_per_kg": link.quoted_price_per_kg,
+        "is_purchase_source": link.is_purchase_source,
+    }
+
+
+@router.get("/spools/{spool_id}/suppliers")
+async def get_spoolman_spool_suppliers(
+    spool_id: int = Path(..., gt=0),
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_READ),
+) -> list[dict]:
+    """Supplier assignments for a Spoolman spool (#2988, Bambuddy-side rows)."""
+    await _get_client(db)
+    result = await db.execute(
+        select(SpoolmanSpoolSupplier)
+        .options(selectinload(SpoolmanSpoolSupplier.supplier))
+        .where(SpoolmanSpoolSupplier.spoolman_spool_id == spool_id)
+    )
+    return [_supplier_link_to_dict(link) for link in result.scalars().all()]
+
+
+@router.put("/spools/{spool_id}/suppliers")
+async def save_spoolman_spool_suppliers(
+    spool_id: int = Path(..., gt=0),
+    links: list[SpoolSupplierLinkInput] = Body(...),
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
+) -> list[dict]:
+    """Replace a Spoolman spool's supplier assignments (#2988).
+
+    Mirror of the built-in inventory's replace-all endpoint — Spoolman owns
+    the spool, Bambuddy owns the assignment (``SpoolmanKProfile`` precedent),
+    so the rows are local and the spool is only verified to exist remotely.
+    """
+    client = await _get_client(db)
+    async with _translate_spoolman_errors():
+        await client.get_spool(spool_id)
+
+    supplier_ids = [link.supplier_id for link in links]
+    if len(set(supplier_ids)) != len(supplier_ids):
+        raise HTTPException(400, "Duplicate supplier in assignment list")
+    if sum(1 for link in links if link.is_purchase_source) > 1:
+        raise HTTPException(400, "Only one assignment can be the purchase source")
+    if supplier_ids:
+        found = await db.execute(select(Supplier.id).where(Supplier.id.in_(supplier_ids)))
+        missing = set(supplier_ids) - {row[0] for row in found.all()}
+        if missing:
+            raise HTTPException(404, f"Supplier(s) not found: {sorted(missing)}")
+
+    await db.execute(delete(SpoolmanSpoolSupplier).where(SpoolmanSpoolSupplier.spoolman_spool_id == spool_id))
+    saved: list[SpoolmanSpoolSupplier] = []
+    for link in links:
+        row = SpoolmanSpoolSupplier(spoolman_spool_id=spool_id, **link.model_dump())
+        db.add(row)
+        saved.append(row)
+    await db.commit()
+    refreshed = await db.execute(
+        select(SpoolmanSpoolSupplier)
+        .options(selectinload(SpoolmanSpoolSupplier.supplier))
+        .where(SpoolmanSpoolSupplier.id.in_([row.id for row in saved]))
+        .order_by(SpoolmanSpoolSupplier.id)
+    )
+    await ws_manager.broadcast({"type": "inventory_changed"})
+    return [_supplier_link_to_dict(link) for link in refreshed.scalars().all()]

+ 27 - 0
backend/app/api/routes/users.py

@@ -25,6 +25,8 @@ from backend.app.core.database import get_db
 from backend.app.core.permissions import Permission
 from backend.app.models.api_key import APIKey
 from backend.app.models.archive import PrintArchive
+from backend.app.models.auth_ephemeral import AuthEphemeralToken, TokenType
+from backend.app.models.connected_app import ConnectedApp, ConnectedAppGrant
 from backend.app.models.group import Group
 from backend.app.models.library import LibraryFile
 from backend.app.models.long_lived_token import LongLivedToken
@@ -50,6 +52,7 @@ from backend.app.services.email_service import (
     send_email,
 )
 from backend.app.services.finance_defaults import ensure_user_finance_defaults
+from backend.app.utils.library_paths import remove_library_photos_dir
 
 router = APIRouter(prefix="/users", tags=["users"])
 
@@ -444,7 +447,14 @@ async def delete_user(
             detail="Cannot delete your own account",
         )
 
+    # Photo directories of the library rows about to go, resolved while the
+    # rows still exist to say which ids they belong to. Removed after the
+    # commit, so a failed delete leaves the pictures alone (#3077).
+    doomed_library_file_ids: list[int] = []
     if delete_items:
+        doomed_library_file_ids = list(
+            (await db.execute(select(LibraryFile.id).where(LibraryFile.created_by_id == user_id))).scalars().all()
+        )
         # Delete all items created by this user
         await db.execute(delete(PrintArchive).where(PrintArchive.created_by_id == user_id))
         await db.execute(delete(PrintQueueItem).where(PrintQueueItem.created_by_id == user_id))
@@ -491,9 +501,26 @@ async def delete_user(
     await db.execute(delete(UserOTPCode).where(UserOTPCode.user_id == user_id))
     await db.execute(delete(LongLivedToken).where(LongLivedToken.user_id == user_id))
 
+    # Connected apps, same SQLite/FK pattern. A leftover consent row would
+    # skip the consent screen for whoever is next given this user id, and a
+    # code issued in the last minute is keyed by username, not id.
+    from sqlalchemy import update as _update
+
+    await db.execute(delete(ConnectedAppGrant).where(ConnectedAppGrant.user_id == user_id))
+    await db.execute(
+        delete(AuthEphemeralToken).where(
+            AuthEphemeralToken.token_type == TokenType.CONNECT_CODE,
+            func.lower(AuthEphemeralToken.username) == user.username.lower(),
+        )
+    )
+    await db.execute(_update(ConnectedApp).where(ConnectedApp.created_by_id == user_id).values(created_by_id=None))
+
     await db.delete(user)
     await db.commit()
 
+    for file_id in doomed_library_file_ids:
+        remove_library_photos_dir(file_id)
+
 
 @router.post("/me/change-password", response_model=dict)
 async def change_own_password(

+ 60 - 17
backend/app/api/routes/webhook.py

@@ -11,7 +11,10 @@ from backend.app.models.api_key import APIKey
 from backend.app.models.archive import PrintArchive
 from backend.app.models.print_queue import PrintQueueItem
 from backend.app.models.printer import Printer
+from backend.app.schemas.printer import HMSErrorResponse, hms_error_responses
+from backend.app.services.print_confirmation import confirm_outcome_for_new_queue_item
 from backend.app.services.printer_manager import printer_manager
+from backend.app.services.queue_position import next_queue_position
 
 logger = logging.getLogger(__name__)
 
@@ -40,11 +43,28 @@ class QueueAddResponse(BaseModel):
 class PrinterStatusResponse(BaseModel):
     id: int
     name: str
+    # The printer's own serial, so a client can tell printers apart by what
+    # they report rather than by Bambuddy's row id (#2919).
+    serial_number: str
     connected: bool
     state: str | None
     current_print: str | None
     progress: float | None
+    # Minutes, as the printer reports it. Kept for existing clients;
+    # remaining_seconds is the same estimate in seconds, the unit the
+    # notification pipeline uses (#2919).
     remaining_time: int | None
+    remaining_seconds: int | None = None
+    layer_num: int | None = None
+    total_layers: int | None = None
+    # Bambu's id for the running job. A new value marks a new print, even when
+    # two prints of the same file run back to back between two polls. None
+    # when the job has no id: Bambu reports "0" or "" for local prints (for
+    # example one started on the printer), the same reading main.py uses.
+    subtask_id: str | None = None
+    # Live HMS faults, in the same shape as GET /printers/{id}/status. They
+    # tell a filament runout apart from someone pressing pause.
+    hms_errors: list[HMSErrorResponse] = []
 
 
 class QueueStatusResponse(BaseModel):
@@ -55,6 +75,18 @@ class QueueStatusResponse(BaseModel):
     items: list[dict]
 
 
+def _job_id(subtask_id) -> str | None:
+    """The printer's job id as text, or None when the job has none.
+
+    Stored as the printer sent it, so coerce: a numeric id would fail
+    validation and turn a status poll into a 500.
+    """
+    if subtask_id is None:
+        return None
+    value = str(subtask_id).strip()
+    return None if value in ("", "0") else value
+
+
 # Webhook endpoints
 
 
@@ -83,18 +115,9 @@ async def webhook_add_to_queue(
     if not printer:
         raise HTTPException(status_code=404, detail="Printer not found")
 
-    # Get next position
-    result = await db.execute(
-        select(PrintQueueItem.position)
-        .where(
-            PrintQueueItem.printer_id == data.printer_id,
-            PrintQueueItem.status == "pending",
-        )
-        .order_by(PrintQueueItem.position.desc())
-        .limit(1)
-    )
-    max_position = result.scalar()
-    next_position = (max_position or 0) + 1
+    # Append to the end of the queue: positions are one sequence across all
+    # pending items, not one per printer (#3200).
+    next_position = await next_queue_position(db)
 
     # Parse scheduled time if provided
     scheduled_time = None
@@ -115,6 +138,9 @@ async def webhook_add_to_queue(
         scheduled_time=scheduled_time,
         require_previous_success=data.require_previous_success,
         auto_off_after=data.auto_off_after,
+        # No dialog to pick this per job, so the install-wide default decides
+        # whether the finished print asks for a verdict (#1898).
+        confirm_outcome=await confirm_outcome_for_new_queue_item(db),
         # Attribute to the key's owner so the item shows up under `queue:read_own`
         # for the person whose key it is. Legacy keys predating per-user ownership
         # have no `user_id`, and those rows stay ownerless.
@@ -274,14 +300,31 @@ async def webhook_get_printer_status(
     # attribute access, not dict lookup. The previous `.get(...)` calls raised
     # AttributeError and surfaced as a generic 500 for any printer that
     # actually had a status row (#1584).
+    if status is None:
+        return PrinterStatusResponse(
+            id=printer.id,
+            name=printer.name,
+            serial_number=printer.serial_number,
+            connected=False,
+            state=None,
+            current_print=None,
+            progress=None,
+            remaining_time=None,
+        )
     return PrinterStatusResponse(
         id=printer.id,
         name=printer.name,
-        connected=status.connected if status else False,
-        state=status.state if status else None,
-        current_print=status.current_print if status else None,
-        progress=status.progress if status else None,
-        remaining_time=status.remaining_time if status else None,
+        serial_number=printer.serial_number,
+        connected=status.connected,
+        state=status.state,
+        current_print=status.current_print,
+        progress=status.progress,
+        remaining_time=status.remaining_time,
+        remaining_seconds=status.remaining_time * 60 if status.remaining_time is not None else None,
+        layer_num=status.layer_num,
+        total_layers=status.total_layers,
+        subtask_id=_job_id(status.subtask_id),
+        hms_errors=hms_error_responses(status.hms_errors),
     )
 
 

+ 66 - 24
backend/app/core/auth.py

@@ -5,6 +5,7 @@ import os
 import secrets
 import time
 from contextvars import ContextVar
+from dataclasses import dataclass
 from datetime import datetime, timedelta, timezone
 from typing import Annotated
 
@@ -480,28 +481,38 @@ def _check_apikey_permissions(
         raise last_failure
 
 
-def require_energy_cost_update():
-    """Dependency for ``POST /settings/electricity-price`` (#1356).
+@dataclass(frozen=True)
+class ScopedCaller:
+    """Who passed a scoped door: an API key, a user, or nobody (auth disabled)."""
 
-    Bypasses the ``_APIKEY_DENIED_PERMISSIONS`` ``SETTINGS_UPDATE`` block for
-    API keys that explicitly opt into ``can_update_energy_cost``. Full
-    ``SETTINGS_UPDATE`` for API keys stays denied — this is a narrowly-scoped
-    door for the Home Assistant dynamic-tariff use case documented in
-    ``wiki/features/energy.md``, not a general settings-write capability.
+    api_key: APIKey | None = None
+    user: User | None = None
+
+
+def require_api_key_scope(
+    scope_attr: str, scope_name: str, user_permission: Permission, *, owner_needs_permission: bool = False
+):
+    """A narrow door for API keys that carry one explicit scope flag.
+
+    For routes an API key may call only when its ``scope_attr`` flag is set,
+    where the matching user permission stays out of the general API-key
+    mapping (``_APIKEY_DENIED_PERMISSIONS`` / the allowlist).
 
     Accepts:
-      * Auth disabled  → always allowed (matches other settings routes)
-      * JWT user with ``SETTINGS_UPDATE`` permission
-      * API key with ``can_update_energy_cost = True``
+      * Auth disabled  → always allowed (matches the other routes)
+      * JWT user with ``user_permission``
+      * API key with ``scope_attr`` set; fails closed when its owner was deactivated,
+        and with ``owner_needs_permission`` also when its owner lacks
+        ``user_permission`` (so a key can't do what its owner may not)
     """
 
     async def permission_checker(
         credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)] = None,
         x_api_key: Annotated[str | None, Header(alias="X-API-Key")] = None,
-    ) -> User | None:
+    ) -> ScopedCaller:
         async with async_session() as db:
             if not await is_auth_enabled(db):
-                return None
+                return ScopedCaller()
 
             credentials_exception = HTTPException(
                 status_code=status.HTTP_401_UNAUTHORIZED,
@@ -524,18 +535,28 @@ def require_energy_cost_update():
                         detail="Invalid API key",
                         headers={"WWW-Authenticate": "Bearer"},
                     )
-                # Fails closed if the owner has been deactivated. The scope
-                # flag itself is not narrowed against the owner's permissions
-                # the way the general gate is: this door exists precisely
-                # because no user permission maps to it (SETTINGS_UPDATE stays
-                # denied for keys even when the owner is an administrator).
-                await resolve_apikey_owner(db, api_key)
-                if not api_key.can_update_energy_cost:
+                # Fails closed if the owner has been deactivated. For the
+                # energy-cost door the scope flag is deliberately not narrowed
+                # against the owner's permissions: no user permission maps to it
+                # (SETTINGS_UPDATE stays denied for keys even when the owner is
+                # an administrator). Doors that do have a matching user
+                # permission pass ``owner_needs_permission``.
+                owner = await resolve_apikey_owner(db, api_key)
+                if (
+                    owner_needs_permission
+                    and owner is not None
+                    and not owner.has_all_permissions(user_permission.value)
+                ):
                     raise HTTPException(
                         status_code=status.HTTP_403_FORBIDDEN,
-                        detail="API key does not have 'update_energy_cost' permission",
+                        detail=f"The API key's owner lacks the permission: {user_permission.value}",
                     )
-                return None
+                if not getattr(api_key, scope_attr):
+                    raise HTTPException(
+                        status_code=status.HTTP_403_FORBIDDEN,
+                        detail=f"API key does not have '{scope_name}' permission",
+                    )
+                return ScopedCaller(api_key=api_key)
 
             # JWT path
             if credentials is None:
@@ -558,16 +579,37 @@ def require_energy_cost_update():
                 raise credentials_exception
             if not _is_token_fresh(iat, user):
                 raise credentials_exception
-            if not user.has_all_permissions(Permission.SETTINGS_UPDATE.value):
+            if not user.has_all_permissions(user_permission.value):
                 raise HTTPException(
                     status_code=status.HTTP_403_FORBIDDEN,
-                    detail=f"Missing required permissions: {Permission.SETTINGS_UPDATE.value}",
+                    detail=f"Missing required permissions: {user_permission.value}",
                 )
-            return user
+            return ScopedCaller(user=user)
 
     return permission_checker
 
 
+def require_energy_cost_update():
+    """Dependency for ``POST /settings/electricity-price`` (#1356).
+
+    Bypasses the ``_APIKEY_DENIED_PERMISSIONS`` ``SETTINGS_UPDATE`` block for
+    API keys that explicitly opt into ``can_update_energy_cost``. Full
+    ``SETTINGS_UPDATE`` for API keys stays denied — this is a narrowly-scoped
+    door for the Home Assistant dynamic-tariff use case documented in
+    ``wiki/features/energy.md``, not a general settings-write capability.
+    """
+    return require_api_key_scope("can_update_energy_cost", "update_energy_cost", Permission.SETTINGS_UPDATE)
+
+
+def require_notification_send():
+    """Dependency for ``POST /notifications/app-message``: another application
+    sending a message through the channels that accept app messages. API keys
+    need ``can_send_notifications``; users need ``NOTIFICATIONS_UPDATE``."""
+    return require_api_key_scope(
+        "can_send_notifications", "send_notifications", Permission.NOTIFICATIONS_UPDATE, owner_needs_permission=True
+    )
+
+
 # Password hashing
 # Use pbkdf2_sha256 instead of bcrypt to avoid 72-byte limit and passlib initialization issues
 # pbkdf2_sha256 is a secure password hashing algorithm without bcrypt's limitations

+ 4 - 2
backend/app/core/config.py

@@ -7,7 +7,7 @@ from pydantic import Field
 from pydantic_settings import BaseSettings
 
 # Application version - single source of truth
-APP_VERSION = "1.2.5.6"
+APP_VERSION = "1.2.5.7"
 GITHUB_REPO = "maziggy/bambuddy"
 BUG_REPORT_RELAY_URL = os.environ.get("BUG_REPORT_RELAY_URL", "https://bambuddy.cool/api/bug-report")
 
@@ -75,7 +75,7 @@ class Settings(BaseSettings):
     database_url: str = _external_db_url or f"sqlite+aiosqlite:///{_db_path}"
 
     # Database connection pool sizing. ``None`` = use the built-in, dialect-aware
-    # default (PostgreSQL: pool_size 20 + max_overflow 80; SQLite: 20 + 200).
+    # default (PostgreSQL: pool_size 20 + max_overflow 60; SQLite: 20 + 200).
     # Large PostgreSQL printer farms can raise these via the DB_POOL_SIZE /
     # DB_MAX_OVERFLOW / DB_POOL_TIMEOUT / DB_POOL_RECYCLE env vars (issue #2572).
     # Make sure PostgreSQL ``max_connections`` comfortably exceeds
@@ -151,6 +151,8 @@ _INTENTIONAL_UNSETTINGS = {
     "BAMBUDDY_OIDC_AUTO_LINK_EXISTING",
     "BAMBUDDY_OIDC_EMAIL_CLAIM",
     "BAMBUDDY_OIDC_REQUIRE_EMAIL_VERIFIED",
+    "BAMBUDDY_OIDC_GROUP_CLAIM",
+    "BAMBUDDY_OIDC_GROUP_MAPPING",
     "BAMBUDDY_OIDC_ICON_URL",
     "BAMBUDDY_OIDC_AUTOLOGIN",
     "BAMBUDDY_OIDC_DEFAULT_GROUP",

+ 475 - 3
backend/app/core/database.py

@@ -37,10 +37,12 @@ def _resolve_pool_kwargs() -> dict:
     """Build the pool kwargs for ``create_async_engine`` (issue #2572).
 
     Dialect-aware defaults, each overridable via env (``DB_POOL_SIZE`` etc.):
-      - PostgreSQL: pool_size 20 + max_overflow 80, ``pool_pre_ping`` (recover
+      - PostgreSQL: pool_size 20 + max_overflow 60, ``pool_pre_ping`` (recover
         server-dropped connections instead of erroring the request) and
         ``pool_recycle`` 1800s. The old hard-coded 10 + 20 exhausted on large
-        farms while printer callbacks held connections.
+        farms while printer callbacks held connections. The 80-connection
+        ceiling fits a stock server (max_connections 100, 3 reserved for
+        superusers); 20 + 80 did not, and tripped the startup pool check.
       - SQLite: pool_size 20 + max_overflow 200 (unchanged); no pre-ping /
         recycle — the connection is a local file, not a server socket.
     """
@@ -50,7 +52,7 @@ def _resolve_pool_kwargs() -> dict:
         kwargs = {"pool_size": pool_size, "max_overflow": max_overflow}
     else:
         pool_size = settings.db_pool_size if settings.db_pool_size is not None else 20
-        max_overflow = settings.db_max_overflow if settings.db_max_overflow is not None else 80
+        max_overflow = settings.db_max_overflow if settings.db_max_overflow is not None else 60
         kwargs = {
             "pool_size": pool_size,
             "max_overflow": max_overflow,
@@ -288,11 +290,13 @@ async def init_db():
         active_print_spoolman,
         ams_history,
         ams_label,
+        announcement,
         api_key,
         archive,
         auth_ephemeral,
         bug_report,
         color_catalog,
+        connected_app,
         external_link,
         filament,
         filament_sku_settings,
@@ -1702,6 +1706,31 @@ _LEGACY_FAILURE_REASON_LABELS: dict[str, str] = {
 }
 
 
+async def _table_has_column(conn, table: str, column: str) -> bool:
+    """Whether ``table`` has ``column``, on either dialect.
+
+    ``table`` is interpolated into the SQLite PRAGMA (it cannot be bound), so
+    callers pass literals only.
+    """
+    from sqlalchemy import text
+
+    if is_sqlite():
+        result = await conn.execute(text(f"PRAGMA table_info({table})"))
+        return any(row[1] == column for row in result)
+    result = await conn.execute(
+        text("SELECT 1 FROM information_schema.columns WHERE table_name = :table AND column_name = :col"),
+        {"table": table, "col": column},
+    )
+    return result.first() is not None
+
+
+async def _sqlite_table_exists(conn, name: str) -> bool:
+    from sqlalchemy import text
+
+    result = await conn.execute(text("SELECT 1 FROM sqlite_master WHERE name = :name"), {"name": name})
+    return result.first() is not None
+
+
 async def _migrate_failure_reason_vocabulary(conn):
     """Fold historical failure-reason labels onto the canonical keys (#2974).
 
@@ -1734,6 +1763,8 @@ async def _migrate_failure_reason_vocabulary(conn):
         if label != key:
             by_key[key].append(label)
 
+    all_labels = [label for labels in by_key.values() for label in labels]
+
     total = 0
     async with conn.begin_nested():
         # nosec B608 — the only interpolated fragment is `table`, which the loop
@@ -1741,6 +1772,30 @@ async def _migrate_failure_reason_vocabulary(conn):
         # Both the key and the label list are bound parameters. A table name
         # cannot be expressed as one, which is why it is interpolated at all.
         for table in ("print_archives", "print_log_entries"):
+            # A database older than #1378 has no print_log_entries.failure_reason
+            # yet (a later ALTER in run_migrations adds it). Such a table cannot
+            # hold a legacy label, and querying it crashed startup with "no such
+            # column: failure_reason".
+            if not await _table_has_column(conn, table, "failure_reason"):
+                continue
+            has_work = (
+                await conn.execute(
+                    text(
+                        f"SELECT 1 FROM {table} WHERE failure_reason IN :labels LIMIT 1"  # noqa: S608  # nosec B608
+                    ).bindparams(bindparam("labels", expanding=True)),
+                    {"labels": all_labels},
+                )
+            ).first() is not None
+            if not has_work:
+                continue
+            if table == "print_archives" and is_sqlite() and await _sqlite_table_exists(conn, "archive_fts"):
+                # Same trap as the plate_id backfill further down: archives
+                # created before the external-content FTS index existed were
+                # never indexed, and the AFTER UPDATE trigger's FTS 'delete' on
+                # such a row fails with "database disk image is malformed".
+                # Rebuild first so every row is present. Only when there is
+                # work, since a rebuild re-reads every archive.
+                await conn.execute(text("INSERT INTO archive_fts(archive_fts) VALUES('rebuild')"))
             for key, labels in by_key.items():
                 result = await conn.execute(
                     text(
@@ -1799,6 +1854,17 @@ async def run_migrations(conn):
     # Migration: Add is_favorite column to print_archives
     await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN is_favorite BOOLEAN DEFAULT 0")
 
+    # Migration: Add OIDC group sync columns (#3107). group_claim defaults to
+    # 'groups'; group_mapping defaults to '{}' (empty JSON object = sync off,
+    # the pre-#3107 behaviour). NOT NULL DEFAULT explicitly so an upgraded
+    # database matches what create_all builds on a fresh install (the model
+    # columns are non-nullable with server defaults) — a bare DEFAULT would
+    # leave the column nullable on the ALTER path and the two installs would
+    # disagree on the schema. Existing rows backfill the default on both
+    # SQLite and PostgreSQL.
+    await _safe_execute(conn, "ALTER TABLE oidc_providers ADD COLUMN group_claim VARCHAR(64) NOT NULL DEFAULT 'groups'")
+    await _safe_execute(conn, "ALTER TABLE oidc_providers ADD COLUMN group_mapping JSON NOT NULL DEFAULT '{}'")
+
     # Migration: Add wallet_charge_skipped column to print_archives so deleted print charges stay deleted
     if is_sqlite():
         await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN wallet_charge_skipped BOOLEAN DEFAULT 0")
@@ -4955,6 +5021,12 @@ async def run_migrations(conn):
         conn, "ALTER TABLE notification_providers ADD COLUMN on_ams_drying_suspended BOOLEAN DEFAULT TRUE"
     )
 
+    # Migration: user link + photos on library files (#3077), the same trio
+    # print_archives carries (external_url / photos). Photos are stored under
+    # <archive_dir>/library/photos/<file_id>/ — the column only holds the names.
+    await _safe_execute(conn, "ALTER TABLE library_files ADD COLUMN external_url VARCHAR(500)")
+    await _safe_execute(conn, "ALTER TABLE library_files ADD COLUMN photos JSON")
+
     # Migration: storage location sensor alerts (#2824), own column rather than
     # reusing on_ha_sensor_alert. That column can be scoped to one printer
     # (printer_id), and a location alert has no printer to scope by — sharing
@@ -4964,6 +5036,86 @@ async def run_migrations(conn):
         conn, "ALTER TABLE notification_providers ADD COLUMN on_location_ha_sensor_alert BOOLEAN DEFAULT FALSE"
     )
 
+    # Migration: post-print outcome confirmation (#1898). VARCHAR and the
+    # BOOLEAN DEFAULT FALSE/TRUE spellings are identical on SQLite and
+    # Postgres (see the on_ha_sensor_alert note above for why not DEFAULT 0).
+    await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN user_verdict VARCHAR(10)")
+    await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN confirm_requested BOOLEAN DEFAULT FALSE")
+    await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN confirm_token VARCHAR(64)")
+    await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN user_verdict_source VARCHAR(16)")
+    # ``DATETIME`` is a SQLite-only alias; PostgreSQL rejects it and
+    # _safe_execute would swallow the error, leaving the column missing.
+    if is_sqlite():
+        await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN confirm_token_used_at DATETIME")
+    else:
+        await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN confirm_token_used_at TIMESTAMP")
+    # When the verdict on file was recorded (#1898): the "already answered"
+    # page dates the verdict by this, not by the moment the one-tap token was
+    # spent, so a verdict changed later in the app reads correctly.
+    if is_sqlite():
+        await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN user_verdict_at DATETIME")
+    else:
+        await _safe_execute(conn, "ALTER TABLE print_archives ADD COLUMN user_verdict_at TIMESTAMP")
+    await _safe_execute(conn, "ALTER TABLE print_log_entries ADD COLUMN user_verdict VARCHAR(10)")
+    await _safe_execute(conn, "ALTER TABLE print_queue ADD COLUMN confirm_outcome BOOLEAN DEFAULT FALSE")
+    await _safe_execute(
+        conn, "ALTER TABLE notification_providers ADD COLUMN on_print_confirm_request BOOLEAN DEFAULT TRUE"
+    )
+    # The one-tap verdict route looks archives up by this token and runs with no
+    # authentication, so an upgraded install needs the index too — without it
+    # every tap, and every unauthenticated request carrying a bogus token, is a
+    # sequential scan of print_archives.
+    await _safe_execute(
+        conn,
+        "CREATE UNIQUE INDEX IF NOT EXISTS ix_print_archives_confirm_token ON print_archives (confirm_token)",
+    )
+    # Migration: take the capability URLs out of the outcome prompt's body
+    # (#1898). Seeding only ever inserts a template that is missing, so an
+    # install that already ran an earlier build of this feature would keep
+    # sending the verdict links as body text for every channel.
+    await _migrate_confirm_prompt_body_template(conn)
+
+    # Migration: Telegram verdict-by-reaction (#3046). Per-provider mode plus
+    # the table of delivered prompts the reaction poller matches updates
+    # against. create_all covers fresh installs; this covers upgrades.
+    await _safe_execute(
+        conn, "ALTER TABLE notification_providers ADD COLUMN telegram_verdict_mode VARCHAR(16) DEFAULT 'buttons'"
+    )
+    await _safe_execute(
+        conn,
+        """
+        CREATE TABLE IF NOT EXISTS telegram_pending_verdicts (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            provider_id INTEGER NOT NULL REFERENCES notification_providers(id) ON DELETE CASCADE,
+            chat_id VARCHAR(64) NOT NULL,
+            message_id INTEGER NOT NULL,
+            archive_id INTEGER NOT NULL REFERENCES print_archives(id) ON DELETE CASCADE,
+            confirm_token VARCHAR(64),
+            has_caption BOOLEAN DEFAULT FALSE,
+            message_text TEXT,
+            created_at DATETIME
+        )
+        """
+        if is_sqlite()
+        else """
+        CREATE TABLE IF NOT EXISTS telegram_pending_verdicts (
+            id SERIAL PRIMARY KEY,
+            provider_id INTEGER NOT NULL REFERENCES notification_providers(id) ON DELETE CASCADE,
+            chat_id VARCHAR(64) NOT NULL,
+            message_id INTEGER NOT NULL,
+            archive_id INTEGER NOT NULL REFERENCES print_archives(id) ON DELETE CASCADE,
+            confirm_token VARCHAR(64),
+            has_caption BOOLEAN DEFAULT FALSE,
+            message_text TEXT,
+            created_at TIMESTAMP
+        )
+        """,
+    )
+    await _safe_execute(
+        conn,
+        "CREATE INDEX IF NOT EXISTS ix_telegram_pending_verdicts_created_at ON telegram_pending_verdicts (created_at)",
+    )
+
     # Migration: rename the ha_sensor_alert template (#2824). "Home Assistant
     # Sensor Alert" was fine as a name while it was the only such template;
     # next to the new "Storage Location Sensor Alert" it no longer says which
@@ -4979,6 +5131,22 @@ async def run_migrations(conn):
     # on fresh installs only — this covers databases whose table predates it.
     await _migrate_location_ha_sensor_unique_binding(conn)
 
+    # Migration: the stock alert templates name the colour and subtype (#2955).
+    # The forecast groups by colour, so two colours of one product would
+    # otherwise send the same message. A plain UPDATE guarded on the old text, so
+    # a template an admin has edited is left alone.
+    await _migrate_stock_alert_template_sku_variables(conn)
+
+    # Migration: supplier master list + spool assignments (#2988).
+    # create_all() covers fresh installs; this covers upgrades.
+    await _migrate_create_supplier_tables(conn)
+
+    # Migration: Add material_number to spool (#2870). Nullable free text —
+    # the internal purchasing/article number a business costs by, shared by
+    # all spools of the same product. VARCHAR(64) is spelled identically on
+    # SQLite and Postgres.
+    await _safe_execute(conn, "ALTER TABLE spool ADD COLUMN material_number VARCHAR(64)")
+
     # Migration: repair the tare of spools the RFID auto-add gave the wrong
     # Bambu spool row (#2909). Runs last so the spool catalogue it reads is
     # whatever this database actually holds.
@@ -4988,6 +5156,281 @@ async def run_migrations(conn):
     # Spoolman and the location sync then imported as storage locations.
     await _migrate_drop_ams_slot_locations(conn)
 
+    # Migration: link a batch to the external record that asked for it (a shop
+    # order an integration turned into prints). The unique index is what makes
+    # a retried create safe; both columns are new, so no row can violate it.
+    await _safe_execute(conn, "ALTER TABLE print_batches ADD COLUMN external_source VARCHAR(32)")
+    await _safe_execute(conn, "ALTER TABLE print_batches ADD COLUMN external_ref VARCHAR(255)")
+    await _safe_execute(
+        conn,
+        "CREATE UNIQUE INDEX IF NOT EXISTS uq_print_batches_external ON print_batches (external_source, external_ref)",
+    )
+
+    # Migration: messages from other applications through the notification
+    # channels. Both flags default off, so no channel starts delivering them and
+    # no existing API key gains the right to send them on upgrade. BOOLEAN
+    # DEFAULT FALSE is accepted by SQLite and PostgreSQL alike. The backfill
+    # covers a table create_all() already gave the column (the ALTER is then
+    # swallowed as a duplicate and existing rows keep NULL; see the stock alert
+    # flags above).
+    await _safe_execute(conn, "ALTER TABLE notification_providers ADD COLUMN on_app_message BOOLEAN DEFAULT FALSE")
+    await _safe_execute(conn, "ALTER TABLE api_keys ADD COLUMN can_send_notifications BOOLEAN DEFAULT FALSE")
+    async with conn.begin_nested():
+        await conn.execute(
+            text("UPDATE notification_providers SET on_app_message = :off WHERE on_app_message IS NULL"), {"off": False}
+        )
+        await conn.execute(
+            text("UPDATE api_keys SET can_send_notifications = :off WHERE can_send_notifications IS NULL"),
+            {"off": False},
+        )
+
+    # Migration: a code for why a scheduled drying failed, so the card can show
+    # the reason in the user's language. Nullable: rows that failed before this
+    # keep showing their English error_message.
+    await _safe_execute(conn, "ALTER TABLE scheduled_dryings ADD COLUMN error_code VARCHAR(32)")
+
+    # Migration: per-provider photo attachment opt-out. Defaults TRUE so
+    # existing providers keep attaching snapshots exactly as before. The
+    # backfill covers a table create_all() already gave the column (the ALTER
+    # is then swallowed and existing rows keep NULL, which reads as off).
+    await _safe_execute(conn, "ALTER TABLE notification_providers ADD COLUMN attach_photo BOOLEAN DEFAULT TRUE")
+    async with conn.begin_nested():
+        await conn.execute(
+            text("UPDATE notification_providers SET attach_photo = :on WHERE attach_photo IS NULL"), {"on": True}
+        )
+
+    # Migration: the filament id a slot preset was written with, so the slot
+    # card can tell when something else re-configured the slot (#3216).
+    # Nullable: existing rows have none and keep being shown as before.
+    await _safe_execute(conn, "ALTER TABLE slot_preset_mappings ADD COLUMN tray_info_idx VARCHAR(32)")
+
+
+async def _migrate_confirm_prompt_body_template(conn) -> None:
+    """Replace the one-tap verdict URLs in the outcome prompt's body (#1898).
+
+    The first shape of this template put ``{good_url}`` and ``{reject_url}``
+    into the message body, where a link unfurler, a mail gateway or a proxy
+    reaches them and spends the single-use token before the operator has read
+    the question. The body now carries ``{confirm_url}``, which only opens the
+    archive in Bambuddy; the capability links travel in the ntfy action buttons
+    and the Telegram inline keyboard instead.
+
+    Rewrites only a body that is still the old default verbatim — an admin who
+    edited the template keeps their own text. Same shape as the two template
+    renames below.
+    """
+    from sqlalchemy import text
+
+    await conn.execute(
+        text("UPDATE notification_templates SET body_template = :new WHERE event_type = :et AND body_template = :old"),
+        {
+            "new": "{printer}: {filename}\nConfirm: {confirm_url}",
+            "et": "print_confirm_request",
+            "old": "{printer}: {filename}\nGood: {good_url}\nReject: {reject_url}",
+        },
+    )
+
+
+async def _migrate_create_supplier_tables(conn) -> None:
+    """Create the supplier tables on databases that predate #2988.
+
+    ``Base.metadata.create_all()`` covers fresh installs; upgrades get the
+    tables here, following the ``_migrate_create_finance_tables`` shape.
+    ``spool_suppliers`` deliberately has NO ON DELETE CASCADE on the supplier
+    side — the API refuses to delete a referenced supplier (409) so
+    assignments can never silently orphan.
+    """
+    if is_sqlite():
+        statements = [
+            """
+            CREATE TABLE IF NOT EXISTS suppliers (
+                id INTEGER PRIMARY KEY,
+                name VARCHAR(200) NOT NULL,
+                name_key VARCHAR(200) NOT NULL,
+                website VARCHAR(500),
+                customer_number VARCHAR(100),
+                note VARCHAR(500),
+                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+            )
+            """,
+            """
+            CREATE TABLE IF NOT EXISTS spool_suppliers (
+                id INTEGER PRIMARY KEY,
+                spool_id INTEGER NOT NULL REFERENCES spool(id) ON DELETE CASCADE,
+                supplier_id INTEGER NOT NULL REFERENCES suppliers(id),
+                supplier_article_number VARCHAR(100),
+                quoted_price_per_kg FLOAT,
+                is_purchase_source BOOLEAN NOT NULL DEFAULT 0,
+                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                CONSTRAINT uq_spool_suppliers_spool_supplier UNIQUE (spool_id, supplier_id)
+            )
+            """,
+            """
+            CREATE TABLE IF NOT EXISTS spoolman_spool_suppliers (
+                id INTEGER PRIMARY KEY,
+                spoolman_spool_id INTEGER NOT NULL,
+                supplier_id INTEGER NOT NULL REFERENCES suppliers(id),
+                supplier_article_number VARCHAR(100),
+                quoted_price_per_kg FLOAT,
+                is_purchase_source BOOLEAN NOT NULL DEFAULT 0,
+                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                CONSTRAINT uq_spoolman_spool_suppliers_pair UNIQUE (spoolman_spool_id, supplier_id)
+            )
+            """,
+        ]
+    else:
+        statements = [
+            """
+            CREATE TABLE IF NOT EXISTS suppliers (
+                id SERIAL PRIMARY KEY,
+                name VARCHAR(200) NOT NULL,
+                name_key VARCHAR(200) NOT NULL,
+                website VARCHAR(500),
+                customer_number VARCHAR(100),
+                note VARCHAR(500),
+                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+            )
+            """,
+            """
+            CREATE TABLE IF NOT EXISTS spool_suppliers (
+                id SERIAL PRIMARY KEY,
+                spool_id INTEGER NOT NULL REFERENCES spool(id) ON DELETE CASCADE,
+                supplier_id INTEGER NOT NULL REFERENCES suppliers(id),
+                supplier_article_number VARCHAR(100),
+                quoted_price_per_kg FLOAT,
+                is_purchase_source BOOLEAN NOT NULL DEFAULT FALSE,
+                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                CONSTRAINT uq_spool_suppliers_spool_supplier UNIQUE (spool_id, supplier_id)
+            )
+            """,
+            """
+            CREATE TABLE IF NOT EXISTS spoolman_spool_suppliers (
+                id SERIAL PRIMARY KEY,
+                spoolman_spool_id INTEGER NOT NULL,
+                supplier_id INTEGER NOT NULL REFERENCES suppliers(id),
+                supplier_article_number VARCHAR(100),
+                quoted_price_per_kg FLOAT,
+                is_purchase_source BOOLEAN NOT NULL DEFAULT FALSE,
+                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                CONSTRAINT uq_spoolman_spool_suppliers_pair UNIQUE (spoolman_spool_id, supplier_id)
+            )
+            """,
+        ]
+    for statement in statements:
+        await _safe_execute(conn, statement)
+    # The model declares index=True on these; fresh installs get them from
+    # create_all(), migrated databases need them spelled out.
+    await _safe_execute(conn, "CREATE INDEX IF NOT EXISTS ix_suppliers_name ON suppliers (name)")
+    await _migrate_supplier_name_key(conn)
+    await _safe_execute(conn, "CREATE INDEX IF NOT EXISTS ix_spool_suppliers_spool_id ON spool_suppliers (spool_id)")
+    await _safe_execute(
+        conn, "CREATE INDEX IF NOT EXISTS ix_spool_suppliers_supplier_id ON spool_suppliers (supplier_id)"
+    )
+    await _safe_execute(
+        conn,
+        "CREATE INDEX IF NOT EXISTS ix_spoolman_spool_suppliers_spoolman_spool_id"
+        " ON spoolman_spool_suppliers (spoolman_spool_id)",
+    )
+    await _safe_execute(
+        conn,
+        "CREATE INDEX IF NOT EXISTS ix_spoolman_spool_suppliers_supplier_id ON spoolman_spool_suppliers (supplier_id)",
+    )
+
+
+async def _migrate_supplier_name_key(conn) -> None:
+    """Backfill ``suppliers.name_key`` and collapse case-insensitive duplicates (#2988).
+
+    The unique index cannot simply be created: a database written by an
+    earlier build of this branch is allowed to hold two suppliers whose names
+    differ only in case, and ``CREATE UNIQUE INDEX`` refuses to build over
+    them. ``_safe_execute`` re-raises that IntegrityError, which would abort
+    startup with no route to recovery from the UI — the same trap
+    ``_migrate_location_ha_sensor_unique_binding`` clears first.
+
+    Colliding rows are merged rather than deleted, because a supplier is
+    referenced: the oldest row wins (it is the one assignments and the CSV
+    import already resolved to), its empty fields are filled from the
+    duplicate, every assignment is re-pointed to it, and only then is the
+    duplicate dropped. An assignment the surviving row already holds for the
+    same spool is dropped instead of re-pointed — the (spool, supplier) pair
+    is unique.
+    """
+    from sqlalchemy import text
+
+    from backend.app.models.supplier import supplier_name_key
+
+    # Only a table written by an earlier build of this branch lacks the column
+    # (the CREATE TABLE above declares it NOT NULL). ADD COLUMN cannot carry
+    # NOT NULL without a default, so it is added nullable, backfilled below and
+    # tightened afterwards where the database can do that in place.
+    await _safe_execute(conn, "ALTER TABLE suppliers ADD COLUMN name_key VARCHAR(200)")
+
+    async with conn.begin_nested():
+        rows = (
+            await conn.execute(
+                text("SELECT id, name, name_key, website, customer_number, note FROM suppliers ORDER BY id")
+            )
+        ).fetchall()
+        kept: dict[str, int] = {}
+        for row in rows:
+            key = supplier_name_key(row.name or "")
+            winner_id = kept.get(key)
+            if winner_id is None:
+                kept[key] = row.id
+                if row.name_key != key:
+                    await conn.execute(
+                        text("UPDATE suppliers SET name_key = :key WHERE id = :id"),
+                        {"key": key, "id": row.id},
+                    )
+                continue
+            params = {"keep": winner_id, "drop": row.id}
+            await conn.execute(
+                text(
+                    "DELETE FROM spool_suppliers WHERE supplier_id = :drop AND spool_id IN "
+                    "(SELECT spool_id FROM spool_suppliers WHERE supplier_id = :keep)"
+                ),
+                params,
+            )
+            await conn.execute(text("UPDATE spool_suppliers SET supplier_id = :keep WHERE supplier_id = :drop"), params)
+            await conn.execute(
+                text(
+                    "DELETE FROM spoolman_spool_suppliers WHERE supplier_id = :drop AND spoolman_spool_id IN "
+                    "(SELECT spoolman_spool_id FROM spoolman_spool_suppliers WHERE supplier_id = :keep)"
+                ),
+                params,
+            )
+            await conn.execute(
+                text("UPDATE spoolman_spool_suppliers SET supplier_id = :keep WHERE supplier_id = :drop"), params
+            )
+            await conn.execute(
+                text(
+                    "UPDATE suppliers SET website = COALESCE(website, :website), "
+                    "customer_number = COALESCE(customer_number, :customer_number), "
+                    "note = COALESCE(note, :note) WHERE id = :keep"
+                ),
+                {
+                    "website": row.website,
+                    "customer_number": row.customer_number,
+                    "note": row.note,
+                    "keep": winner_id,
+                },
+            )
+            await conn.execute(text("DELETE FROM suppliers WHERE id = :drop"), {"drop": row.id})
+            logger.info("Merged duplicate supplier %r (id=%s) into id=%s", row.name, row.id, winner_id)
+
+    # Superseded by ix_suppliers_name_key: lower(name) folds ASCII only, so it
+    # never enforced the rule for non-ASCII names in the first place.
+    await _safe_execute(conn, "DROP INDEX IF EXISTS uq_suppliers_name_lower")
+    await _safe_execute(conn, "CREATE UNIQUE INDEX IF NOT EXISTS ix_suppliers_name_key ON suppliers (name_key)")
+    # NULLs never collide in a unique index, so the guarantee belongs in the
+    # schema, as create_all() declares it on fresh installs. Every row has a
+    # key by now. SQLite cannot alter a column in place; there the ORM hook on
+    # Supplier.name is what writes it.
+    if not is_sqlite():
+        await _safe_execute(conn, "ALTER TABLE suppliers ALTER COLUMN name_key SET NOT NULL")
+
 
 async def _migrate_rename_ha_sensor_alert_template(conn) -> None:
     """Rename the ha_sensor_alert template to "Printer Sensor Alert" (#2824).
@@ -5003,6 +5446,35 @@ async def _migrate_rename_ha_sensor_alert_template(conn) -> None:
     )
 
 
+_STOCK_ALERT_TEMPLATE_BODIES = {
+    "stock_reorder_alert": (
+        "{material} ({brand}) has reached the reorder point.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Days left: {days_left}d\nReorder now to avoid a stock break.",
+        "{material} {subtype} {color} ({brand}) has reached the reorder point.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Days left: {days_left}d\nReorder now to avoid a stock break.",
+    ),
+    "stock_break_alert": (
+        "{material} ({brand}) will run out before replenishment arrives.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Lead time: {lead_time_days}d\nOnly {days_left}d of stock remaining — order immediately.",
+        "{material} {subtype} {color} ({brand}) will run out before replenishment arrives.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Lead time: {lead_time_days}d\nOnly {days_left}d of stock remaining — order immediately.",
+    ),
+}
+
+
+async def _migrate_stock_alert_template_sku_variables(conn) -> None:
+    """Give the stock alert templates {subtype} and {color} (#2955).
+
+    Updates a body only while it is still the shipped default, so an admin's own
+    wording is kept.
+    """
+    from sqlalchemy import text
+
+    for event_type, (old, new) in _STOCK_ALERT_TEMPLATE_BODIES.items():
+        await conn.execute(
+            text(
+                "UPDATE notification_templates SET body_template = :new WHERE event_type = :et AND body_template = :old"
+            ),
+            {"new": new, "et": event_type, "old": old},
+        )
+
+
 async def _migrate_location_ha_sensor_unique_binding(conn) -> None:
     """Unique index on location_ha_sensors (location_id, entity_id) (#2824).
 

+ 69 - 3
backend/app/core/oidc_env.py

@@ -10,6 +10,7 @@ check the UI enforces.
 from __future__ import annotations
 
 import contextlib
+import json
 import logging
 import os
 
@@ -33,10 +34,42 @@ _TRUTHY = {"true", "1", "yes"}
 _FALSY = {"false", "0", "no"}
 
 
+def _env_group_mapping() -> dict[str, str]:
+    """#3107 — parse BAMBUDDY_OIDC_GROUP_MAPPING as a JSON object.
+
+    Unset or blank -> {} (sync off). Invalid JSON or a non-object raises
+    EnvOIDCConfigError so the config is refused loudly at boot instead of
+    silently running without the mapping the operator thought they set —
+    same disposition as a bad boolean. Key/value contents are validated by
+    the OIDCProviderCreate schema like every other field.
+    """
+    raw = (os.environ.get("BAMBUDDY_OIDC_GROUP_MAPPING") or "").strip()
+    if not raw:
+        return {}
+    try:
+        parsed = json.loads(raw)
+    except json.JSONDecodeError as exc:
+        raise EnvOIDCConfigError(f"BAMBUDDY_OIDC_GROUP_MAPPING is not valid JSON ({exc.lineno}:{exc.colno})") from exc
+    if not isinstance(parsed, dict):
+        raise EnvOIDCConfigError("BAMBUDDY_OIDC_GROUP_MAPPING must be a JSON object")
+    # Shape-checked here, before apply looks the values up as group names: a
+    # null or a number would otherwise reach that query and surface only as
+    # "could not be applied: TypeError", naming neither the variable nor why.
+    # Imported here for the same cycle reason as in _apply_env_oidc_provider.
+    from backend.app.schemas.auth import _validate_group_mapping
+
+    try:
+        return _validate_group_mapping(parsed)
+    except ValueError as exc:
+        raise EnvOIDCConfigError(f"BAMBUDDY_OIDC_GROUP_MAPPING is invalid: {exc}") from exc
+
+
 class EnvOIDCConfigError(Exception):
     """A BAMBUDDY_OIDC_* value the reader cannot interpret. Only ever carries a
-    boolean variable's name and value -- booleans are not secret, so the message
-    is safe to log in full (unlike client_secret, which never reaches here)."""
+    boolean variable's name and value, or a GROUP_MAPPING problem (a JSON parse
+    position, or the IdP group names involved) -- none of it is secret, so the
+    message is safe to log in full (unlike client_secret, which never reaches
+    here)."""
 
 
 def env_bool(key: str, default: bool, *, strict: bool = True) -> bool:
@@ -89,6 +122,12 @@ def read_env_oidc_config() -> dict | None:
         "auto_link_existing_accounts": env_bool("BAMBUDDY_OIDC_AUTO_LINK_EXISTING", False),
         "email_claim": (os.environ.get("BAMBUDDY_OIDC_EMAIL_CLAIM") or "").strip() or "email",
         "require_email_verified": env_bool("BAMBUDDY_OIDC_REQUIRE_EMAIL_VERIFIED", True),
+        # #3107 — group sync. The mapping is JSON: {"IdP group": "Bambuddy group"}.
+        # Blank/unset means "no mapping", which leaves sync off — the same
+        # default the UI path has. Values are group *names* (not ids), resolved
+        # against the database below alongside DEFAULT_GROUP.
+        "group_claim": (os.environ.get("BAMBUDDY_OIDC_GROUP_CLAIM") or "").strip() or "groups",
+        "group_mapping": _env_group_mapping(),
         "icon_url": (os.environ.get("BAMBUDDY_OIDC_ICON_URL") or "").strip() or None,
         "is_autologin": env_bool("BAMBUDDY_OIDC_AUTOLOGIN", False),
         # A name, not an id: ids are assigned per install, so the same compose
@@ -111,6 +150,11 @@ _APPLIED_FIELDS = (
     "auto_link_existing_accounts",
     "email_claim",
     "require_email_verified",
+    # #3107 — written on every boot like the rest, so removing the env vars
+    # disables group sync rather than leaving a stale mapping behind (the
+    # environment is the whole truth for this row).
+    "group_claim",
+    "group_mapping",
     "icon_url",
     "is_autologin",
     # Written on every boot, so a group that is no longer declared is cleared:
@@ -153,7 +197,7 @@ async def _apply_env_oidc_provider(db: AsyncSession) -> None:
     except EnvOIDCConfigError as exc:
         # Same disposition as a ValidationError or an unmatched DEFAULT_GROUP:
         # log clearly and leave any running provider as it was. Safe to log the
-        # full message -- EnvOIDCConfigError only ever carries a boolean var.
+        # full message -- EnvOIDCConfigError never carries a secret (see its docstring).
         logger.error("BAMBUDDY_OIDC_* config rejected, provider not applied: %s", exc)
         return
 
@@ -215,6 +259,28 @@ async def _apply_env_oidc_provider(db: AsyncSession) -> None:
             return
         config["default_group_id"] = group.id
 
+    # #3107 — mapping values are Bambuddy group *names* (the sync resolves them
+    # per login, same as the LDAP mapping stores names). Names, not ids, for the
+    # same reason as DEFAULT_GROUP: ids differ per install, so a shared compose
+    # file would point somewhere else on every deployment. Every value must
+    # resolve here, or the provider is not applied: a half-resolved mapping
+    # would grant exactly the groups whose names happened to match and silently
+    # drop the rest, which is the least diagnosable failure mode available.
+    env_mapping = config.get("group_mapping") or {}
+    if env_mapping:
+        missing = []
+        for target in env_mapping.values():
+            found = (await db.execute(select(Group.id).where(Group.name == target))).scalar_one_or_none()
+            if found is None:
+                missing.append(target)
+        if missing:
+            logger.error(
+                "BAMBUDDY_OIDC_GROUP_MAPPING values match no group (%s), provider not applied (%s).",
+                ", ".join(sorted(set(missing))),
+                "previous config left running" if existing is not None else "no provider created",
+            )
+            return
+
     try:
         # The same schema the API uses, so env config cannot reach a state the
         # UI would have refused (notably the SEC-1 auto-link check).

+ 10 - 0
backend/app/core/websocket.py

@@ -109,6 +109,16 @@ class ConnectionManager:
             }
         )
 
+    async def send_print_confirm_request(self, printer_id: int, data: dict):
+        """Ask connected clients for a post-print outcome verdict (#1898)."""
+        await self.broadcast(
+            {
+                "type": "print_confirm_request",
+                "printer_id": printer_id,
+                "data": data,
+            }
+        )
+
     async def send_archive_created(self, archive: dict):
         """Notify clients that a new archive was created."""
         await self.broadcast(

+ 7799 - 0
backend/app/data/hms_catalog.json

@@ -0,0 +1,7799 @@
+{
+ "_source": {
+  "generator": "scripts/generate_hms_catalog.py",
+  "bambu_studio_commit": "b94b6da251f8",
+  "model_file_versions": {
+   "093": 202508192303,
+   "094": 202503231214,
+   "20P": 202511162200,
+   "22E": 202510142200,
+   "239": 202506171403,
+   "26A": 202511162200,
+   "31B": 202511162200
+  },
+  "extra": "backend/app/data/hms_catalog_extra.json"
+ },
+ "hms": {
+  "0300010000010001": "The heatbed temperature is abnormal; the heater may have a short circuit.",
+  "0300010000010002": "The heatbed temperature is abnormal; the heater may have an open circuit, or the thermal switch may be open.",
+  "0300010000010003": "The heatbed temperature is abnormal; the heater is over temperature.",
+  "0300010000010005": "A heatbed temperature control issue has been detected and the heating module may be damaged. Please power off the device immediately and follow the Wiki to replace the AC board.",
+  "0300010000010006": "The heatbed temperature is abnormal; the sensor may have a short circuit.",
+  "0300010000010007": "The heatbed temperature is abnormal; the sensor may have an open circuit.",
+  "0300010000010008": "An abnormality occurs during the heating process of the heatbed; the heating modules may be broken.",
+  "030001000001000A": "The heatbed temperature control is abnormal; the AC board may be broken.",
+  "030001000001000C": "The heatbed has worked at full load for a long time. The temperature control system may be abnormal.",
+  "030001000001000D": "An abnormality occured in heating modules of heatbed previously. To continue using your printer, please refer to the wiki to troubleshoot.",
+  "030001000001000E": "The power supply voltage does not match the machine; the heatbed has been disabled.",
+  "030001000002000F": "The chamber target temperature is set too high, while the heatbed target temperature is set too low. Heatbed cooling has been skipped. It is recommended to set matching chamber and heatbed temperatures.",
+  "0300010000030008": "The temperature of the heated bed exceeds the limit and automatically adjusts to the limit temperature.",
+  "030001000003000F": "The chamber temperature is currently high, and the heatbed is cooling down slowly. It is recommended to open the top cover or front door to accelerate the cooling process.",
+  "0300020000010001": "The nozzle temperature is abnormal; the heater may have a short circuit.",
+  "0300020000010002": "The nozzle temperature is abnormal; the heater may have an open circuit.",
+  "0300020000010003": "The nozzle temperature is abnormal; the heater is over temperature.",
+  "0300020000010004": "The right nozzle temperature control is abnormal.  The hotend might have structural damage.",
+  "0300020000010005": "Abnormal temperature control detected on the right extruder nozzle; the heating module may be damaged. Please disconnect the power immediately and stop using the device, and contact customer support for troubleshooting guidance.",
+  "0300020000010006": "The right nozzle temperature is abnormal; the sensor may have a short circuit, please check whether the connector is properly plugged in.",
+  "0300020000010007": "The right nozzle temperature is abnormal; the sensor may have an open circuit.",
+  "0300020000010008": "The extruder nozzle temperature is abnormal and cannot reach the set value. This may be caused by an improperly installed nozzle silicone sock.",
+  "0300020000010009": "Nozzle temperature control is abnormal. The hot end may not be installed. To heat the heating assembly without the hotend, enable Maintenance Mode in the settings.",
+  "030002000001000B": "The right extruder nozzle temperature control is abnormal, possibly due to the hotend not being properly installed or a malfunction of the wireless temperature measurement board.",
+  "0300030000010001": "The hotend cooling fan speed is too slow or stopped. It may be stuck or the connector may not be plugged in properly.",
+  "0300030000020002": "The hotend cooling fan is running slowly. It may be obstructed. Please check for debris and clean if necessary.",
+  "0300040000020001": "The speed of the part cooling fan is too slow or stopped. It may be stuck, or the connector may not be plugged in properly.",
+  "0300050000010001": "The motor driver is overheating. Its radiator may be loose, or its cooling fan may be damaged.",
+  "0300060000010001": "Motor-A has an open-circuit. There may be a loose connection, or the motor may have failed.",
+  "0300060000010002": "Motor-A has a short-circuit. It may have failed.",
+  "0300060000010003": "The resistance of Motor-A is abnormal; the motor may have failed.",
+  "0300070000010001": "Motor-B has an open-circuit. The connection may be loose, or the motor may have failed.",
+  "0300070000010002": "Motor-B has a short-circuit. It may have failed.",
+  "0300070000010003": "The resistance of Motor-B is abnormal; the motor may have failed.",
+  "0300080000010001": "Motor-Z has an open-circuit. The connection may be loose, or the motor may have failed.",
+  "0300080000010002": "Motor-Z has a short-circuit. It may have failed.",
+  "0300080000010003": "The resistance of Motor-Z is abnormal; the motor may have failed.",
+  "0300090000010001": "The extruder servo motor has an open circuit. The connection may be loose, or the motor may have failed.",
+  "0300090000010002": "The extruder servo motor has a short-circuit. It may have failed.",
+  "0300090000010003": "The resistance of the extruder servo motor is abnormal; the motor may have failed.",
+  "0300090000020001": "The extrusion motor is overloaded. The extruder may be clogged or the filament may be stuck in the tool head.",
+  "0300090000020002": "The extrusion resistance is abnormal. The extruder may be clogged or there may be filament stuck in the toolhead.",
+  "0300090000020003": "The extruder is extruding abnormally. It may be clogged, or the filament may be too thin, causing the extruder to slip.",
+  "0300090000020004": "The extrusion resistance is abnormal. The extruder may be clogged or there may be filament stuck in the toolhead, or the externally mounted filament may be tangled.",
+  "0300090000020005": "The extruder is extruding abnormally. It may be clogged, or the filament may be too thin, causing the extruder to slip, or the externally mounted filament may be tangled.",
+  "0300090000030007": "Filament extrusion anomaly detected. Automatically retrying recovery, please wait.",
+  "0300090000030008": "Filament extrusion anomaly detected. Automatically retrying recovery, please wait.",
+  "03000A0000010001": "Heatbed force sensor 1 is too sensitive. It may be stuck between the strain arm and heatbed support, or the adjusting screw may be too tight.",
+  "03000A0000010002": "The signal of heatbed force sensor 1 is weak. The force sensor may be broken or have poor electric connection.",
+  "03000A0000010003": "The signal of heatbed force sensor 1 is too weak. The electronic connection to the sensor may be broken.",
+  "03000A0000010004": "An external disturbance was detected on force sensor 1. The heatbed plate may have touched something outside the heatbed.",
+  "03000A0000010005": "Force sensor 1 detected unexpected continuous force. The heatbed may be stuck, or the analog front end may be broken.",
+  "03000B0000010001": "Heatbed force sensor 2 is too sensitive. It may be stuck between the strain arm and heatbed support, or the adjusting screw may be too tight.",
+  "03000B0000010002": "The signal of heatbed force sensor 2 is weak. The force sensor may be broken or have poor electric connection.",
+  "03000B0000010003": "The signal of heatbed force sensor 2 is too weak. The electronic connection to the sensor may be broken.",
+  "03000B0000010004": "An external disturbance was detected on force sensor 2. The heatbed plate may have touched something outside the heatbed.",
+  "03000B0000010005": "Force sensor 2 detected unexpected continuous force. The heatbed may be stuck, or the analog front end may be broken.",
+  "03000C0000010001": "Heatbed force sensor 3 is too sensitive. It may be stuck between the strain arm and heatbed support, or the adjusting screw may be too tight.",
+  "03000C0000010002": "The signal of heatbed force sensor 3 is weak. The force sensor may be broken or have poor electric connection.",
+  "03000C0000010003": "The signal of heatbed force sensor 3 is too weak. The electronic connection to the sensor may be broken.",
+  "03000C0000010004": "An external disturbance was detected on force sensor 3. The heatbed plate may have touched something outside the heatbed.",
+  "03000C0000010005": "Force sensor 3 detected unexpected continuous force. The heatbed may be stuck, or the analog front end may be broken.",
+  "03000D000001000B": "The Z axis motor seems to be stuck when moving. Please check if there is any foreign matter on the Z sliders or Z timing belt wheels.",
+  "03000D0000020003": "The build plate may not be properly placed. If this message appears repeatedly, please check the Wiki for more explanations.",
+  "03000F0000010001": "Abnormal accelerometer data detected. Please try restarting the printer.",
+  "0300100000020001": "The resonance frequency of the X axis is low. The timing belt may be loose.",
+  "0300100000020002": "The resonance frequency of the X-axis differs significantly from the last calibration.  Please clean the X-axis linear rail and conduct a calibration after printing.",
+  "0300110000020001": "The resonance frequency of the Y axis is low. The timing belt may be loose.",
+  "0300110000020002": "The resonance frequency of the Y-axis differs greatly from the last calibration. Please clean the Y-axis liner rod and conduct a calibration after printing.",
+  "0300120000020001": "The front cover of the toolhead fell off.",
+  "0300130000010001": "The current sensor of Motor-A is abnormal. This may be caused by a failure of the hardware sampling circuit.",
+  "0300140000010001": "The current sensor of Motor-B is abnormal. This may be caused by a failure of the hardware sampling circuit.",
+  "0300150000010001": "The current sensor of Motor-Z is abnormal. This may be caused by a failure of the hardware sampling circuit.",
+  "0300160000010001": "The extruder servo motor's current sensor is abnormal. A failure of the hardware sampling circuit may cause this.",
+  "0300170000010001": "The hotend cooling fan speed is too slow or stopped. It may be stuck or the connector may not be plugged in properly.",
+  "0300170000020002": "The hotend cooling fan speed is slow. It may be stuck and need cleaning.",
+  "0300180000000000": "",
+  "0300180000010001": "The frequency of the extrusion force sensor is too low. The nozzle may not be installed, or the nozzle heat sink may be too far away from the sensor.",
+  "0300180000010002": "The sensitivity of the extrusion force sensor is low; the nozzle may not be installed correctly.",
+  "0300180000010003": "The extrusion force sensor is not available; the link between the MC and TH may be broken, or the sensor may be broken.",
+  "0300180000010004": "The signal of the extrusion force sensor is abnormal. The sensor may be damaged, or the MC-TH communication may be abnormal.",
+  "0300180000010005": "Z-axis motor rotation is obstructed; please check if foreign objects are stuck in the Z slider or Z timing pulley. Also, ensure the build plate is placed correctly to avoid collisions with surrounding structures.",
+  "0300180000010006": "The heatbed leveling data is abnormal. Please check whether there are any foreign objects on the heatbed and Z slider. If so, please remove them and try again.",
+  "0300180000010007": "The frequency of the extrusion force sensor is too high. The sensor may be damaged, or the nozzle heat sink may be too close to the sensor.",
+  "0300180000010008": "The nozzle touches the heatbed abnormally. Please check whether there is filament residue on the nozzle or foreign matter where the nozzle touches the bed.",
+  "030018000001000B": "Nozzle presence detection failed: nozzle not installed or improperly installed.",
+  "030018000001000C": "An anomalous jump in the extruder extrusion force sensor data has been detected, potentially caused by poor sensor contact or a faulty sensor.",
+  "030018000001000D": "Nozzle clumping detection calibration failed. Excessive force detected on the nozzle. Please ensure the nozzle is installed correctly.",
+  "030018000001000E": "Nozzle clumping detection calibration failed. The nozzle did not touch the inner wall of the hole. Please clean the nozzle and the steel plate hole at the rear-right side of the heatbed.",
+  "0300180000030009": "Performing initial High-temperature Bed Leveling.",
+  "03001A0000020001": "The nozzle is covered with filament, or the build plate is crooked.",
+  "03001A0000020002": "The nozzle is clogged with filament.",
+  "03001A0000020003": "The nozzle clumping sensor is malfunctioning; Please refer to the Wiki for troubleshooting.",
+  "03001A0000030004": "Nozzle clumping detection timed out. The check has been skipped.",
+  "03001B0000010001": "The signal of the heatbed acceleration sensor is weak. The sensor may have fallen off or been damaged.",
+  "03001B0000010002": "External disturbance was detected on the heatbed acceleeration sensor. The sensor signal wire may not be affixed.",
+  "03001B0000010003": "The heatbed acceleration sensor detected unexpected continuous force. The sensor may be stuck, or the analog front end may be broken.",
+  "03001C0000010001": "The extrusion motor driver is abnormal. The MOSFET may have a short circuit.",
+  "03001D0000010001": "The position sensor of extrusion motor is abnormal. The connection to the sensor may be loose.",
+  "03001E0000010001": "The left nozzle temperature is abnormal; the heater may have a short circuit.",
+  "03001E0000010002": "The left nozzle temperature is abnormal; the heater may have an open circuit.",
+  "03001E0000010003": "The left nozzle temperature is abnormal; the heater is overheated.",
+  "03001E0000010005": "Abnormal temperature control detected on the left extruder nozzle; the heating module may be damaged. Please disconnect the power immediately and stop using the device, and contact customer support for troubleshooting guidance.",
+  "03001E0000010006": "The left nozzle temperature is abnormal; the sensor may have a short circuit, please check whether the connector is properly plugged in.",
+  "03001E0000010007": "The left nozzle temperature is abnormal; the sensor may have an open circuit.",
+  "03001E0000010008": "The left extruder nozzle temperature is abnormal and cannot reach the set value. This may be caused by an improperly installed nozzle silicone sock.",
+  "03001E0000010009": "The left nozzle temperature control is abnormal; the hot end may not be installed. If you want to heat the hot end without it being installed, please turn on maintenance mode in the settings page.",
+  "0300200000010001": "X-axis homing abnormal: please check if the toolhead is stuck or if the resistance on the X-axis linear rail is too high.",
+  "0300200000010002": "Y-axis homing abnormal: please check if the toolhead is stuck or the Y carriage has too much resistance.",
+  "0300200000010003": "X axis homing abnormal: the timing belt may be loose.",
+  "0300200000010004": "Y axis homing abnormal: the timing belt may be loose.",
+  "0300210000010001": "Auxiliary extruder motor open circuit, possibly due to a loose connection or motor failure.",
+  "0300210000010002": "Auxiliary extruder motor short circuit, possibly caused by motor failure.",
+  "0300210000010003": "The resistance of the Auxiliary extruder motor is abnormal; the motor may have failed.",
+  "0300220000010001": "Auxiliary extruder motor current sensor error, possibly caused by a sampling circuit hardware failure.",
+  "0300250000010001": "The frequency of the extrusion force sensor of the right extruder is too low. The nozzle may not be installed, or the nozzle heat sink may be too far away from the sensor.",
+  "0300250000010002": "The sensitivity of the right extruder extrusion force sensor is low; the nozzle may not installed correctly.",
+  "0300250000010003": "Unable to read data from the right extruder force sensor; there may be a communication break or sensor damage.",
+  "0300250000010004": "The signal of the extrusion force sensor of the right extruder is abnormal. The sensor may be damaged, or the MC-TH communication may be abnormal.",
+  "0300250000010005": "Z-axis motor rotation is obstructed; please check if foreign objects are stuck in the Z slider or Z timing pulley. Also, ensure the build plate is placed correctly to avoid collisions with surrounding structures.",
+  "0300250000010007": "The frequency of the extrusion force sensor of the right extruder is too high. The sensor may be damaged, or the nozzle heat sink may be too close to the sensor.",
+  "0300250000010008": "The right nozzle touches the heating bed abnormally. Please check whether there is filament residue on the nozzle or foreign matter where the nozzle touches the bed.",
+  "030025000001000A": "Nozzle offset calibration failed. Filament sticks to the nozzle, which may affect print quality. Please clean the nozzle and try again.",
+  "030025000001000B": "Nozzle presence detection failed: Right extruder nozzle not installed or improperly installed.",
+  "030025000001000C": "An anomalous jump in the right extruder eddy current sensor data has been detected, potentially caused by poor sensor contact or a faulty sensor.",
+  "0300260000000000": "",
+  "0300260000010001": "The frequency of the extrusion force sensor of the left extruder is too low. The sensor may be installed too far, or the sensor may be loose.",
+  "0300260000010002": "The sensitivity of the left extruder extrusion force sensor is low; the force sensor may not be installed correctly.",
+  "0300260000010003": "Unable to read data from the left extruder force sensor; there may be a communication break or sensor damage.",
+  "0300260000010004": "The signal of the extrusion force sensor of the left extruder is abnormal. The sensor may be damaged, or the MC-TH communication may be abnormal.",
+  "0300260000010005": "Z-axis motor rotation is obstructed; please check if foreign objects are stuck in the Z slider or Z timing pulley. Also, ensure the build plate is placed correctly to avoid collisions with surrounding structures.",
+  "0300260000010007": "The frequency of the extrusion force sensor of the left extruder is too high. The sensor may be installed too close, or the sensor may be loose.",
+  "0300260000010008": "The left nozzle touches the heating bed abnormally. Please check whether there is filament residue on the nozzle or foreign matter where the nozzle touches the bed.",
+  "030026000001000B": "Nozzle presence detection failed: Left extruder nozzle not installed or improperly installed.",
+  "030026000001000C": "An anomalous jump in the left extruder eddy current sensor data has been detected, potentially caused by poor sensor contact or a faulty sensor.",
+  "0300270000010001": "The nozzle offset calibration sensor frequency is too low. The sensor may be damaged.",
+  "0300270000010002": "Too many attempts at nozzle offset calibration, possibly due to purged filament between the nozzle and the heated base, or incorrect installation of the nozzle. Please inspect and retry.",
+  "0300270000010003": "The nozzle offset calibration result shows significant deviation, possibly due to the heat insulation sheet on the right nozzle heating base not being installed. Please install it correctly and try again.",
+  "0300270000010004": "The signal of the nozzle offset calibration sensor is abnormal. The sensor may be damaged, or the wiring may not be connected properly.",
+  "0300270000010005": "The nozzle offset calibration indicates significant deviation, possibly caused by an additional heat insulation sheet installed on the right nozzle heating base. Please verify this and attempt the calibration again.",
+  "0300270000010006": "The nozzle offset calibration shows significant deviation, possibly due to incorrect installation of the nozzle or heating base. Please inspect and retry.",
+  "0300270000010007": "The nozzle offset calibration sensor frequency is too high. The sensor may be damaged.",
+  "0300270000010008": "The nozzle offset calibration sensor signal is too weak. It may be that the nozzle is sticky or the nozzle does not move above the sensor during calibration, causing the distance between the nozzle and the sensor to be too far.",
+  "0300280000010001": "The data of the force sensor of the Cutting Module is abnormal. The magnet on the Tool Holder may fall off or the force sensor may be damaged.",
+  "0300280000010003": "Communication failure between cutting module and toolhead during Z-axis homing. Please check if the cutting module signal cable is loose or broken or verify if the force sensor coil is intact.",
+  "0300280000010004": "The force sensor of the Cutting Module is abnormal. The force sensor cable may be disconnected or the force sensor is damaged.",
+  "0300280000010005": "Z-axis homing failed in cutting mode. Please check if there are any foreign objects in the Z-axis slider and Z-axis synchronous pulley.",
+  "0300280000010007": "Communication failure between the Cutting Module and toolhead module. Please check if the Cutting module signal cable is loose or broken. It could also be due to a broken force sensor coil.",
+  "0300280000010008": "Z-axis homing failed. Please check if the blade holder movement is smooth and ensure no foreign objects are at the contact position on the heatbed.",
+  "0300290000010001": "Vision encoder patterns can not be recognized; possible reasons include vision encoder pattern distortion, light overexposure, and plate misplacement.",
+  "03002A0000010001": "The left hotend cooling fan speed is too slow or stopped. The fan may be stuck or the connector may not be plugged in properly.",
+  "03002A0000020002": "The left hotend cooling fan speed is slow. It may be stuck and need cleaning.",
+  "03002B0000020001": "Air-door calibration failed. Please check if there is any foreign object blocking the damper.",
+  "03002C0000010001": "Obstacle detected under the heatbed. Please remove it to ensure proper printing.",
+  "03002D0000010006": "Heated bed leveling failed, possibly due to foreign objects on the bed or bed tilt. Continuing to print may damage the build plate. Please remove any debris or manually level the bed before retrying.",
+  "03002D0000030009": "Performing initial High-temperature Bed Leveling.",
+  "03002E0000030001": "The motor noise cancellation data needs to be updated; please recalibrate.",
+  "0300310000010001": "The Part Cooling Fan speed is too slow or stopped. It may be stuck, or the connector may not be plugged in properly.",
+  "0300310000020002": "The Part Cooling Fan speed is slow. It may be stuck and need cleaning.",
+  "0300320000010001": "The Auxiliary Part Cooling Fan speed is too slow or stopped. It may be stuck, or the connector may not be plugged in properly.",
+  "0300320000010002": "The Right Side (Auxiliary Component Cooling-Filtration) Fan speed is slow. It may be stuck and need cleaning.",
+  "0300320000020002": "The Auxiliary Part Cooling Fan speed is slow. It may be stuck and need cleaning.",
+  "0300330000010001": "The Chamber Exhaust Fan speed is too slow or stopped. It may be stuck, or the connector may not be plugged in properly.",
+  "0300330000020002": "The Chamber Exhaust Fan speed is slow. It may be stuck and need cleaning.",
+  "0300350000010001": "The MC module cooling fan speed is too slow or stopped. It may be stuck, or the connector may not be plugged in properly.",
+  "0300350000020002": "The MC module cooling fan speed is slow. It may be stuck and need cleaning.",
+  "0300360000010001": "The Chamber Heat Circulation Fan speed is too slow or stopped. It may be stuck, or the connector may not be plugged in properly.",
+  "0300360000020002": "The Chamber Heat Circulation Fan speed is slow. It may be stuck and need cleaning.",
+  "0300390000010001": "The External Fan on the Toolhead is running too slowly or has stopped, possibly due to debris blockage or a loose connector.",
+  "0300390000020002": "The External Fan on the Toolhead is running at a low speed, possibly due to debris buildup.",
+  "03003A0000010001": "The Right Side (Auxiliary Component Cooling-Filtration) Fan speed is too slow or stopped. It may be stuck, or the connector may not be plugged in properly.",
+  "03003A0000020002": "The Right Side (Auxiliary Component Cooling-Filtration) Fan speed is slow. It may be stuck and need cleaning.",
+  "0300400000020001": "Data transmission over the serial port is abnormal; the software system may be faulty.",
+  "0300400000020002": "G-code data error detected. The system has automatically reloaded the G-code and resumed printing.",
+  "0300410000010001": "The system voltage is unstable. Triggering the power failure protection function.",
+  "0300420000010001": "Toolhead power abnormality detected; it may be caused by a damaged TH board or a short circuit in the USB-C cable.",
+  "0300900000010001": "Chamber heating failed. The heater may not be blowing hot air.",
+  "0300900000010002": "Chamber heating failed. Possible causes: the chamber is not fully enclosed, ambient temperature is too low, or the power supply heat dissipation vent is blocked.",
+  "0300900000010003": "Chamber heating failed. The power supply temperature may be too high.",
+  "0300900000010004": "Chamber heating failed. The speed of the heating fan is too low.",
+  "0300900000010005": "Chamber heating failed. The thermal resistance is too high.",
+  "0300900000010006": "Chamber heating failed. The air duct could not switch to chamber temperature heating mode.",
+  "0300900000010007": "High chamber temperature detected. If the hotend is overheating or emitting smoke, immediately power off the printer. Please refer to the Wiki for detailed troubleshooting instructions.",
+  "0300900000010010": "The communication of chamber temperature controller is abnormal.",
+  "0300910000010001": "The temperature of chamber heater 1 is abnormal. The heater may have a short circuit.",
+  "0300910000010002": "The temperature of chamber heater 1 is abnormal. The heater may have an open circuit or the thermal fuse may have burned out.",
+  "0300910000010003": "The temperature of chamber heater 1 is abnormal. The heater is over temperature.",
+  "0300910000010005": "A chamber heater temperature control issue has been detected and the heating module may be damaged. Please power off the device immediately and follow the Wiki to replace the AC board.",
+  "0300910000010006": "The temperature of chamber heater 1 is abnormal. The sensor may have a short circuit.",
+  "0300910000010007": "The temperature of chamber heater 1 is abnormal. The sensor may have an open circuit.",
+  "0300910000010008": "The chamber heater failed to reach the target temperature.",
+  "030091000001000A": "The temperature of chamber heater 1 is abnormal. The AC board may be broken.",
+  "030091000001000C": "The chamber heater 1 has worked at full load for a long time. The temperature control system may be abnormal.",
+  "030091000001000E": "The power supply voltage does not match the machine; chamber heater 1 has been disabled.",
+  "0300920000010001": "The temperature of chamber heater 2 is abnormal. The heater may have a short circuit.",
+  "0300920000010002": "The temperature of chamber heater 2 is abnormal. The heater may have an open circuit or the thermal fuse may be in effect.",
+  "0300920000010003": "The temperature of chamber heater 2 is abnormal. The heater is over temperature.",
+  "0300920000010006": "The temperature of chamber heater 2 is abnormal. The sensor may have a short circuit.",
+  "0300920000010007": "The temperature of chamber heater 2 is abnormal. The sensor may have an open circuit.",
+  "0300920000010008": "Chamber heater 2 failed to rise to target temperature.",
+  "030092000001000A": "The temperature of chamber heater 2 is abnormal. The AC board may be broken.",
+  "0300930000010001": "Chamber temperature is abnormal. The chamber heater's temperature sensor may have a short circuit.",
+  "0300930000010002": "Chamber temperature is abnormal. The chamber heater's temperature sensor may have an open circuit.",
+  "0300930000010003": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air outlet may have a short circuit.",
+  "0300930000010004": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air outlet may have an open circuit.",
+  "0300930000010005": "Chamber temperature is abnormal. The chamber temperature sensor may have a short circuit.",
+  "0300930000010006": "Chamber temperature is abnormal. The chamber temperature sensor at the air inlet may have an open circuit.",
+  "0300930000010007": "Chamber temperature is abnormal. The temperature sensor at the power supply may have a short circuit.",
+  "0300930000010008": "Chamber temperature is abnormal. The temperature sensor at power supply may have an open circuit.",
+  "0300940000020003": "Chamber failed to reach the desired temperature. The machine will stop waiting for the chamber temperature.",
+  "0300940000030001": "Chamber cooling may be too slow. You can open the front door or top cover to help cooling if the air in the chamber is non-toxic.",
+  "0300940000030002": "Chamber temperature setting value exceed the limit, the boundary value will be set.",
+  "0300950000010001": "The temperature sensor of the Laser Module may have a short circuit.",
+  "0300950000010002": "The temperature sensor of the Laser Module may have an open circuit.",
+  "0300950000010003": "Laser Module overheating",
+  "0300950000010004": "The speed of the Laser Module cooling fan is too low or stopped. It may be stuck or the connector may not be plugged in properly.",
+  "0300950000010005": "The Laser Module communication is abnormal; please check the connector.",
+  "0300950000010006": "Laser Module not detected: the module may have fallen off, or the quick-release lever may not be locked.",
+  "0300950000010007": "The engraving laser module is abnormal; the laser may have an open circuit or may be damaged.",
+  "0300950000010008": "The Laser Module communication is abnormal; please check the connector.",
+  "0300950000010009": "The ambient temperature sensor at the bottom of the laser module is abnormal; the sensor is short-circuited.",
+  "030095000001000A": "The ambient temperature sensor at the bottom of the laser module is abnormal; the sensor has an open circuit.",
+  "0300960000010001": "The front door seems to be open; the task has been paused.",
+  "0300960000010002": "The front door Hall sensor (Upper) is abnormal; please check whether the connection wire is loose.",
+  "0300960000010003": "The front door Hall sensor (Lower) is abnormal; please check whether the connection wire is loose.",
+  "0300960000010004": "The Front Laser Safety Window is not detected. Please install it according to the Wiki and re-initiate the task.",
+  "0300960000030001": "The front door is open.",
+  "0300970000010001": "The top cover seems to be open; the task has been paused",
+  "0300970000010002": "The top cover Hall sensor (Front Right) is abnormal; please check whether the connection wire is loose.",
+  "0300970000010003": "The top cover Hall sensor (Rear Left) is abnormal; please check whether the connection wire is loose.",
+  "0300970000010004": "The Top Laser Protection Plate is not detected. Please install it according to the Wiki and re-initiate the task.",
+  "0300970000030001": "The top cover is open.",
+  "0300980000010001": "The left side window seems to be open, the task has been paused.",
+  "0300980000010002": "The left side window Hall sensor (Upper) is abnormal; please check whether the connection wire is loose.",
+  "0300980000010003": "The left side window Hall sensor (Lower) is abnormal; please check whether the connection wire is loose.",
+  "0300980000010004": "The Left Laser Safety Window is not detected. Please install it according to the Wiki and re-initiate the task.",
+  "0300980000030001": "The left side window seems to be open.",
+  "0300990000010001": "The right side window seems to be open; the task has been paused.",
+  "0300990000010002": "The right side window Hall sensor (Lower) is abnormal; please check whether the connection wire is loose.",
+  "0300990000010003": "The right side window Hall sensor (Upper) is abnormal; please check whether the connection wire is loose.",
+  "0300990000010004": "The Right Laser Safety Window is not detected. Please install it according to the Wiki and re-initiate the task.",
+  "0300990000030001": "The right side window is detected to be open.",
+  "03009B0000010001": "Emergency stop button is not installed. Please follow the Wiki to install it.",
+  "03009B0000010002": "Safety key is not inserted. Please follow the Wiki to install it.",
+  "03009B0000010003": "Emergency Stop Button is not in the right position. Please follow the Wiki to install it.",
+  "03009C0000010001": "The Air Pump is not detected. Please check whether the connector is plugged in properly.",
+  "03009C0000010002": "The Air Pump is abnormal and may be damaged.",
+  "03009D0000020001": "The engrave laser focal point XY calibration has failed. Please clean up the Laser Homing Area on the Laser Platform, and re-run the Laser Module Mount Calibration.",
+  "03009D0000020002": "The engrave laser focal point XY calibration result differs significantly from the design values. Please re-install the Laser Module and re-run the Laser Module Setup.",
+  "03009D0000020003": "The engrave laser focal point Z calibration result differs significantly from the design values. Please re-install the Laser Module and re-run the Laser Module Setup. If it fails repeatedly, please get in touch with customer support.",
+  "03009E0000030001": "Open Door Detection level for this print job will be set as \"Notification\".",
+  "0300A10000010001": "Chamber temperature is too high. Please open the top cover and front door to cool down, or lower the ambient temperature.",
+  "0300A10000020002": "The ambient temperature is too high. Please lower the temperature, as excessive heat may cause the print to pause.",
+  "0300A20000010001": "MC module temperature is too high, possibly because of high chamber temperature of the printer. You can try lowering the environmental temperature before use.",
+  "0300A40000010001": "Heatbed temperature is too high. Please wait for it to cool down to room temperature before restarting the task.",
+  "0300A40000010002": "The right hotend temperature is too high. Please wait for it to cool down to room temperature before restarting the task.",
+  "0300A40000010003": "The left hotend temperature is too high. Please wait for it to cool down to room temperature before restarting the task.",
+  "0300A40000010004": "Chamber temperature is too high. Please wait for it to cool down to room temperature before restarting the task.",
+  "0300A40000010005": "The heatbed temperature sensor is abnormal. Please troubleshoot the issue before restarting the task.",
+  "0300A40000010008": "The chamber temperature sensor is abnormal. Please troubleshoot the issue before restarting the task.",
+  "0300A50000010001": "Flame Sensor 1 is abnormal. The sensor may be short-circuited.",
+  "0300A50000010002": "Flame Sensor 2 is abnormal. The sensor may be short-circuited.",
+  "0300A50000010003": "Flame Sensor 3 is abnormal. The sensor may be short-circuited.",
+  "0300A50000010004": "Flame Sensor 4 is abnormal. The sensor may be short-circuited.",
+  "0300A50000010005": "Flame Sensor 5 is abnormal. The sensor may be short-circuited.",
+  "0300A60000010001": "The Toolhead Enhanced Cooling Fan is not properly installed; it may not be securely fastened or may have fallen off.",
+  "0300A60000010002": "The Toolhead Enhanced Cooling Fan has lost communication; please check the connector.",
+  "0300A70000030001": "The chamber temperature is high, and the system has increased the fan speed, which may cause more noise. It’s recommended to lower the room temperature or open the front door/top cover to help cool it down.",
+  "0300A80000010001": "AMS power supply abnormality, possibly due to AMS damage or a short circuit in the AMS interface, or too many AMS connections. Please check if it is correctly connected.",
+  "0300AB0000010001": "The right hotend heatbreak temperature is too high, which may cause clogging. Please open the printer’s top cover and front door to reduce the chamber temperature, or lower the ambient temperature.",
+  "0300AB0000020002": "The right hotend heatbreak temperature is relatively high, which may cause nozzle clogging. Please open the printer’s top cover and front door to reduce the chamber temperature, or lower the ambient temperature.",
+  "0300AB0000020003": "The temperature sensor on the right hotend heatbreak is abnormal. To prevent nozzle clogs, the cooling fan will continue running at high speed. You can troubleshoot the sensor issue when the print is idle.",
+  "0300C00000010001": "Active Chamber Exhaust air door malfunction: it may be stuck.",
+  "0300C00000010002": "Filter Switch Flap air door malfunction: it may be stuck.",
+  "0300C00000010003": "Automatic Top Vent air door malfunction: it may be stuck.",
+  "0300C10000010001": "Airflow System failed to activate cooling mode; please check the air door status.",
+  "0300C10000010002": "Airflow System failed to activate heating mode; please check the air door status.",
+  "0300C10000010003": "Airflow System failed to activate laser mode; please check the air door status.",
+  "0300C10000010004": "Airflow System failed to activate strong cooling mode; please check the air door status.",
+  "0300C20000010001": "Hall sensor of Active Chamber Exhaust malfunction: please check if the wiring is loose.",
+  "0300C20000010002": "Hall sensor of Filter Switch Flap malfunction: please check if the wiring is loose.",
+  "0300C30000010001": "Current sensor of Active Chamber Exhaust malfunction: this may be due to an open circuit or a hardware sampling circuit fault.",
+  "0300C30000010002": "Current sensor of Filter Switch Flap malfunction: this may be due to an open circuit or a hardware sampling circuit fault.",
+  "0300C30000010003": "Current sensor of Automatic Top Vent malfunction: this may be due to open circuit or a hardware sampling circuit fault.",
+  "0300D00000010001": "The cutting module base has fallen off; please reinstall it.",
+  "0300D00000010002": "The blade holder has fallen off; please reinstall it.",
+  "0300D00000010003": "The cutting module cable has come loose; please check the cable connection.",
+  "0300D10000010001": "Fire extinguisher motor has an open-circuit. There may be a loose connection, or the motor may have failed.",
+  "0300D10000010002": "Fire extinguisher motor error. Please power off and then power on to restart the extinguisher.",
+  "0300D10000010003": "The resistance of the fire extinguisher motor is abnormal; the motor may have failed.",
+  "0300D30000020001": "Fire extinguisher not detected. Please ensure the signal cable is securely connected and the plug is fully inserted.",
+  "0300D40000010002": "The fire extinguisher presence detection sensor is malfunctioning. Please check if the sensor cable is loose or damaged.",
+  "0300D40000010003": "Fire Extinguisher Cylinder presence detection sensor is short-circuited. Please check the sensor connection.",
+  "0300D50000020004": "Fire extinguisher cylinder not installed. Please confirm on the extinguisher page.",
+  "0300D50000020005": "The Fire Extinguisher Gas Cylinder is empty. Please replace the gas cylinder and then confirm the replacement on the Auto Fire Extinguishing System page.",
+  "0300D50000030004": "Gas cylinder of the fire extinguisher is not installed.",
+  "0300D60000010006": "Fire extinguisher motor reset failed. Please power off and then power on to restart the extinguisher.",
+  "0300D60000010007": "The fire extinguisher motor reset is jammed. Please gently turn the emergency handwheel left and right until the resistance decreases. Then, power off the fire extinguisher and turn it back on.",
+  "0300D70000020001": "Laser fourth axis is not connected",
+  "0300D80000010001": "Laser fourth axis motor has an open-circuit. There may be a loose connection, or the motor may have failed.",
+  "0300D80000010002": "Laser fourth axis motor error. Please power off and then power on to restart the laser fourth axis.",
+  "0300D90000020001": "Communication error between the laser fourth axis motor and the position sensor. If restarting fails, please contact customer support.",
+  "0300DA0000020001": "Laser module detected. Please remove the hotend from the hotend rack to prevent laser processing debris from affecting printing performance.",
+  "0300DB0000020001": "Laser module detected. Please install the right nozzle correctly to ensure proper Laser Module Mounting Calibration.",
+  "0300E00000010001": "Hotend holder motor is disconnected or has an open-circuit fault. Please check the motor’s flat cable connection.",
+  "0300E00000010002": "Inter-phase short circuit on hotend holder motor. If restarting doesn’t resolve the issue, please contact support.",
+  "0300E00000010003": "Hotend holder motor has abnormal or unbalanced three-phase resistance. If restarting doesn’t help, please contact support.",
+  "0300E00000010004": "Hotend holder motor is stalled. Please check for foreign objects or collisions with the toolhead.",
+  "0300E10000010001": "Current sensor fault on hotend holder motor. This may be caused by a sampling circuit failure. If restarting the printer doesn’t resolve the issue, please contact customer support.",
+  "0300E20000020001": "Communication error between the hotend holder motor and the position sensor. If restarting fails, please contact customer support.",
+  "0300E30000010001": "The MOS tubes of the Hotend Holder motor drive board are short-circuited. If restarting the printer does not resolve the problem, please contact customer support.",
+  "0300E30000010002": "MC communication with hotend holder motor driver failed. Please check if the connection cable is properly plugged in.",
+  "050001000001000A": "The BirdsEye Camera is malfunctioning. Please try restarting the device. If the issue persists after multiple restarts, check the camera connection status or contact customer support.",
+  "0500010000020001": "The media pipeline is malfunctioning. Please restart the printer. If multiple attempts fail, please contact customer support.",
+  "0500010000020002": "Live View camera is not connected. Please check the hardware and cable connections.",
+  "0500010000020003": "",
+  "0500010000020008": "",
+  "0500010000020009": "",
+  "050001000002000B": "",
+  "050001000002000F": "",
+  "0500010000020010": "",
+  "0500010000020011": "",
+  "0500010000020012": "The Liveview camera firmware update encountered an error. Please restart the printer and try to upgrade again.",
+  "0500010000020013": "The Liveview camera firmware update encountered an error. Please restart the printer and try to upgrade again.",
+  "0500010000020014": "The Liveview camera firmware update encountered an error. Please restart the printer and try to upgrade again.",
+  "0500010000030004": "Not enough space on the USB flash drive; please clear some space.",
+  "0500010000030005": "The USB drive is in Read-Only mode. Video recording and Timelapse recording cannot be performed. Please refer to the Wiki for assistance.",
+  "0500010000030006": "The USB drive is not formatted or cannot be written; please format the USB drive.",
+  "0500010000030007": "Unable to record time-lapse photography without USB flash drive inserted.",
+  "0500010000030009": "",
+  "0500010000030015": "The printer’s internal storage is low and timelapse recording may fail. Please delete files from the internal storage to free up space, then try recording again.",
+  "0500020000020001": "Failed to connect to the internet. Please check the network connection.",
+  "0500020000020002": "Device login failed; please check your account information.",
+  "0500020000020003": "Failed to connect to the internet; please check the network connection.",
+  "0500020000020004": "Unauthorized user: please check your account information.",
+  "0500020000020005": "Failed to connect to the internet; please check the network connection.",
+  "0500020000020006": "Streaming function error. Please check the network and try again. You can restart or update the printer if the issue persists.",
+  "0500020000020007": "Liveview service login failed; please check your network connection.",
+  "0500020000020008": "Time synchronization failed",
+  "0500020000020009": "Gateway unreachable detected, WiFi reconnection has been triggered.",
+  "050002000002000A": "Network anomaly detected (failed to obtain IP address). WiFi reconnection has been triggered.",
+  "050002000002000B": "Abnormal WiFi driver response detected. WiFi recovery process initiated.",
+  "0500020000030009": "",
+  "050002000003000A": "",
+  "050002000003000B": "",
+  "050002000003000C": "",
+  "0500030000010001": "The MC module is malfunctioning; please restart the device or check device cable connection.",
+  "0500030000010002": "The toolhead is malfunctioning. Please restart the device.",
+  "0500030000010003": "The AMS module is malfunctioning. Please restart the device.",
+  "0500030000010004": "The Filament Buffer module is malfunctioning. Please restart the device.",
+  "0500030000010005": "Internal service is malfunctioning. Please restart the device.",
+  "0500030000010006": "A system panic occurred. Please restart the device.",
+  "0500030000010007": "The Toolhead expansion module is malfunctioning. Please power off, check the connection, and restart the device.",
+  "0500030000010008": "A system hang occurred. Please restart the device.",
+  "0500030000010009": "A system hang occurred. It has been recovered by automatic restart.",
+  "050003000001000A": "System state is abnormal; please restore to factory settings.",
+  "050003000001000B": "The screen is malfunctioning; please restart the device.",
+  "050003000001000C": "The MC motor controller module is malfunctioning. Please power off, check the connection, and restart the device.",
+  "050003000001000D": "The induction hotend rack is malfunctioning. Please power off, check the connection, and restart the device.",
+  "050003000001000E": "Some external modules are incompatible with the printer's firmware version, which may affect normal operation. Please go to the \"Firmware\" page while connected to the internet to upgrade.",
+  "0500030000010021": "Hardware incompatible; please check the Toolhead Camera.",
+  "0500030000010023": "The Chamber Temperature Control module is malfunctioning. Please restart the device.",
+  "0500030000010024": "The current temperature is too low. In order to protect you and your printer, printing tasks, moving an axis and other operations are disabled. Please move the printer to an environment above 10 degrees Celsius.",
+  "0500030000010025": "The current firmware is abnormal. Please update again.",
+  "0500030000010026": "The Toolhead expansion module is malfunctioning. Please power off, check the connection, and restart the device.",
+  "0500030000010053": "",
+  "050003000002000C": "Wireless hardware error: please turn off/on WiFi or restart the device.",
+  "050003000002000D": "The SD Card controller is malfunctioning.",
+  "050003000002000E": "Some modules are incompatible with the printer's firmware version, which may affect use. Please go to the \"Firmware\" page to update after connected to the internet, or you may update offline according to wiki.",
+  "0500030000020010": "",
+  "0500030000020011": "",
+  "0500030000020012": "",
+  "0500030000020013": "",
+  "0500030000020014": "",
+  "0500030000020015": "",
+  "0500030000020016": "",
+  "0500030000020017": "",
+  "0500030000020018": "",
+  "0500030000020019": "",
+  "0500030000020020": "USB flash drive capacity is insufficient to cache print files.",
+  "0500030000020050": "",
+  "0500030000020051": "",
+  "0500030000020052": "",
+  "0500030000020053": "",
+  "0500030000020054": "",
+  "0500030000020055": "User information has expired, please log in again.",
+  "0500030000030007": "",
+  "0500030000030022": "MicroSD Card performance degradation has been detected. It may affect print jobs, logs, and video records. Please format or change the MicroSD card.",
+  "0500040000010001": "Failed to download print job; please check your network connection.",
+  "0500040000010002": "Failed to report print state; please check your network connection.",
+  "0500040000010003": "The content of print file is unreadable; please resend the print job.",
+  "0500040000010004": "The print file is unauthorized.",
+  "0500040000010006": "Failed to resume previous print",
+  "0500040000010044": "The firmware of AMS A does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0500040000010046": "The firmware of Laser Module does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0500040000010047": "The firmware of Air Pump does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0500040000010048": "The firmware of Cutting Module does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "050004000001004F": "Unknown module detected, please try updating the firmware to the latest version.",
+  "0500040000010051": "Emergency Stop Button is not in the right position. Please follow the Wiki to install it.",
+  "0500040000010052": "Safety Key is not inserted. Please follow the Wiki to install it.",
+  "0500040000010056": "",
+  "0500040000010060": "",
+  "0500040000010061": "",
+  "0500040000020007": "The bed temperature exceeds the filament's vitrification temperature, which may cause a nozzle clog. Please keep the front door of the printer open or lower the bed temperature.",
+  "0500040000020010": "The RFID-tag on AMS A Slot1 cannot be identified.",
+  "0500040000020011": "The RFID-tag on AMS A Slot2 cannot be identified.",
+  "0500040000020012": "The RFID-tag on AMS A Slot3 cannot be identified.",
+  "0500040000020013": "The RFID-tag on AMS A Slot4 cannot be identified.",
+  "0500040000020014": "The RFID-tag on AMS B Slot1 cannot be identified.",
+  "0500040000020015": "The RFID-tag on AMS B Slot2 cannot be identified.",
+  "0500040000020016": "The RFID-tag on AMS B Slot3 cannot be identified.",
+  "0500040000020017": "The RFID-tag on AMS B Slot4 cannot be identified.",
+  "0500040000020018": "The RFID-tag on AMS C Slot1 cannot be identified.",
+  "0500040000020019": "The RFID-tag on AMS C Slot2 cannot be identified.",
+  "050004000002001A": "The RFID-tag on AMS C Slot3 cannot be identified.",
+  "050004000002001B": "The RFID-tag on AMS C Slot4 cannot be identified.",
+  "050004000002001C": "The RFID-tag on AMS D Slot1 cannot be identified.",
+  "050004000002001D": "The RFID-tag on AMS D Slot2 cannot be identified.",
+  "050004000002001E": "The RFID-tag on AMS D Slot3 cannot be identified.",
+  "050004000002001F": "The RFID-tag on AMS D Slot4 cannot be identified.",
+  "0500040000020020": "",
+  "0500040000020030": "The BirdsEye Camera is not installed. Please power off printer and then install the camera.",
+  "0500040000020031": "Before using a Laser/Cutting Module, the pose of the BirdsEye Camera needs to be determined. Please complete the setup to calibrate the camera.",
+  "0500040000020032": "Please slide in Laser Module and lock the quick-release lever.",
+  "0500040000020033": "Please plug in the module connector.",
+  "0500040000020034": "Laser module detected for the first time. Complete the setup (~4 min) before use for precise cutting and engraving. Also, ensure the H2.0 screw at the back securing the air pump is removed.",
+  "0500040000020035": "The Laser Module needs calibration to get the focus position. Please perform mounting calibration before use. (about 2 minutes)",
+  "0500040000020036": "New Cutting Module connected. To ensure more precise cutting, please set it up before use. (about 3 minutes)",
+  "0500040000020037": "The Cutting Module needs calibration to get the tool position. Please perform the mounting calibration before use. (about 2-4 minutes)",
+  "0500040000020038": "Please slide in cutting module and fasten the quick release lever. If already installed, the module might not be properly aligned. Please try reinstalling it.",
+  "0500040000020039": "Please plug in the module connector",
+  "0500040000020041": "The laser module has been used for a long time. Please clean it promptly to avoid affecting laser processing.",
+  "0500040000020042": "The Live View Camera is dirty or obstructed; please clean it and continue.",
+  "0500040000020043": "The Toolhead Camera is dirty or obstructed; please clean it and continue.",
+  "0500040000020050": "Laser Safety Window is not installed.",
+  "0500040000030008": "The door seems to be open.",
+  "0500040000030009": "The bed temperature exceeds filament's vitrification temperature, which may cause nozzle clog. Please keep the front door of the printer open. Door open detection has been temporarily turned off.",
+  "0500040000030054": "Auto Fire Extinguishing System detected. It is recommended to conduct a Fire Drill to become familiar with the operation process.",
+  "0500040000030057": "The print job has been completed. Automatic air purification is in progress.",
+  "0500050000010001": "The factory data of the AP board is abnormal; please replace the AP board with a new one.",
+  "0500050000010006": "The factory data of AP board is abnormal; please replace the AP board with a new one.",
+  "0500050000010007": "MQTT Command verification failed. Please update Studio (including the network plugin) or Handy to the latest version, then restart the software and try again.",
+  "0500050000010008": "",
+  "0500050000010009": "",
+  "050005000001000A": "",
+  "050005000001000B": "",
+  "050005000001000C": "",
+  "050005000001000D": "The BirdsEye Camera is malfunctioning. Please try restarting the device. If the issue persists after multiple restarts, check the camera connection status or contact customer support.",
+  "050005000001000E": "Laser Module firmware does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "050005000001000F": "The accessory firmware does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0500050000010010": "The firmware of AMS A does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0500050000010011": "The Air Pump firmware does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0500050000010012": "The Cutting Module firmware does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0500050000010013": "Laser module certification failed. Please reconnect the cable or restart the printer.",
+  "0500050000010014": "AMS A certification failed. Please reconnect the cable or restart the printer.",
+  "0500050000010015": "Air Pump certification failed. Please reconnect the cable or restart the printer.",
+  "0500050000010016": "Cutting module certification failed. Please reconnect the cable or restart the printer.",
+  "0500050000010018": "Laser Fourth Axis module certification failed. Please reconnect the module cable or restart the printer.",
+  "0500050000010019": "Fire Extinguisher module certification failed. Please reconnect the module cable or restart the printer.",
+  "050005000001001A": "Filament Track Switch certification failed. Please reconnect the cable or restart the printer.",
+  "0500050000010020": "Hotend 1 authentication failed. Please switch the hotend or restart the printer.",
+  "0500050000010021": "Time-lapse kit authentication failed. Please reconnect the cable or restart the printer.",
+  "0500050000010022": "The External Exhaust Fan certification failed. Please restart the printer or reconnect the fan cable.",
+  "0500050000020023": "A new version of the Timelapse Kit is available. Please connect to CyberBrick App to update this accessory separately.",
+  "0500050000020030": "",
+  "0500050000030002": "The device is in the engineering state; please pay attention to information security related matters.",
+  "0500060000020001": "The Toolhead Camera is not in place; please check the hardware connection.",
+  "0500060000020002": "The Nozzle Camera is not in place; please check the hardware connection.",
+  "0500060000020003": "BirdsEye Camera is not in place; please check the hardware connection",
+  "0500060000020004": "The Live View camera is not in place; please check the hardware connection.",
+  "0500060000020005": "",
+  "0500060000020006": "",
+  "0500060000020007": "",
+  "0500060000020008": "",
+  "0500060000020009": "",
+  "0500060000020011": "",
+  "0500060000020012": "",
+  "0500060000020013": "",
+  "0500060000020014": "",
+  "0500060000020015": "",
+  "0500060000020021": "",
+  "0500060000020022": "",
+  "0500060000020023": "",
+  "0500060000020024": "",
+  "0500060000020025": "",
+  "0500060000020031": "ToolHead Camera is not connected. Please check the hardware and cable connections.",
+  "0500060000020032": "Nozzle Camera is not connected. Please check the hardware and cable connections.",
+  "0500060000020033": "BirdsEye Camera is not connected. Please check the hardware and cable connections.",
+  "0500060000020034": "Live View camera is not connected. Please check the hardware and cable connections.",
+  "0500060000020035": "",
+  "0500060000020041": "",
+  "0500060000020042": "",
+  "0500060000020043": "",
+  "0500060000020044": "",
+  "0500060000020045": "",
+  "0500060000020051": "",
+  "0500060000020052": "",
+  "0500060000020053": "",
+  "0500060000020054": "",
+  "0500060000020061": "",
+  "0500060000020062": "",
+  "0500060000020070": "",
+  "0500060000020071": "",
+  "05000600000200A0": "",
+  "05000600000200A1": "",
+  "0500070000020001": "",
+  "0500070000020002": "",
+  "0500070000020003": "",
+  "0500070000020101": "",
+  "0500070000020201": "",
+  "05001A0000010001": "The feeder module is offline. Please check if the feeder module connection cable is loose.",
+  "05001A0000010002": "A feeder module replacement is detected. Please ensure that the corresponding extrusion gear and nozzle have been replaced, and manually update the nozzle type in the printer.",
+  "0500400000010039": "Laser module Serial Number error",
+  "0500400000010040": "Cutting module Serial Number error",
+  "0501040000010044": "The firmware of AMS B does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0501040000030002": "Threaded rods need lubrication now.",
+  "0501040000030003": "Linear rods need cleaning and lubrication now.",
+  "0501040000030004": "Please clean and lubricate the X-axis linear rail and YZ-axis linear rods.",
+  "0501040000030005": "Please clean and lubricate the Induction Hotend Latch and the linear rods of the Hotend Change System.",
+  "0501040000030006": "The Induction Hotend Latch may be worn. To ensure proper induction hotend switching, please replace the  Induction Hotend Latch and clean the rods of Induction Hotend Rack.",
+  "0501040000030010": "Laser module is dusty and needs cleaning now.",
+  "0501050000010010": "The firmware of AMS B does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0501050000010014": "AMS B certification failed. Please reconnect the cable or restart the printer.",
+  "0501050000010020": "Hotend 2 authentication failed. Please switch the hotend or restart the printer.",
+  "0502040000010044": "The firmware of AMS C does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0502050000010010": "The firmware of AMS C does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0502050000010014": "AMS C certification failed. Please reconnect the cable or restart the printer.",
+  "0502050000010020": "Hotend 3 authentication failed. Please switch the hotend or restart the printer.",
+  "0503000000030027": "",
+  "0503000000030028": "",
+  "0503000000030029": "",
+  "0503040000010044": "The firmware of AMS D does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0503050000010010": "The firmware of AMS D does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0503050000010014": "AMS D certification failed. Please reconnect the cable or restart the printer.",
+  "0503050000010020": "Hotend 4 authentication failed. Please switch the hotend or restart the printer.",
+  "0504050000010010": "The firmware of AMS E does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0504050000010020": "Hotend 5 authentication failed. Please switch the hotend or restart the printer.",
+  "0505050000010010": "The firmware of AMS F does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0505050000010020": "Hotend 6 authentication failed. Please switch the hotend or restart the printer.",
+  "0506050000010010": "The firmware of AMS G does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0507050000010010": "The firmware of AMS H does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0580040000010045": "The firmware of AMS-HT A does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0580050000010010": "The firmware of AMS-HT A does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0580050000010017": "AMS-HT A certification failed. Please reconnect the cable or restart the printer.",
+  "0581040000010045": "The firmware of AMS-HT B does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0581050000010010": "The firmware of AMS-HT B does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0581050000010017": "AMS-HT B certification failed. Please reconnect the cable or restart the printer.",
+  "0582040000010045": "The firmware of AMS-HT C does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0582050000010010": "The firmware of AMS-HT C does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0582050000010017": "AMS-HT C certification failed. Please reconnect the cable or restart the printer.",
+  "0583040000010045": "The firmware of AMS-HT D does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0583050000010010": "The firmware of AMS-HT D does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0583050000010017": "AMS-HT D certification failed. Please reconnect the cable or restart the printer.",
+  "0584040000010045": "The firmware of AMS-HT E does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0584050000010010": "The firmware of AMS-HT E does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0584050000010017": "AMS-HT E certification failed. Please reconnect the cable or restart the printer.",
+  "0585040000010045": "The firmware of AMS-HT F does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0585050000010010": "The firmware of AMS-HT F does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0585050000010017": "AMS-HT F certification failed. Please reconnect the cable or restart the printer.",
+  "0586040000010045": "The firmware of AMS-HT G does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0586050000010010": "The firmware of AMS-HT G does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0586050000010017": "AMS-HT G certification failed. Please reconnect the cable or restart the printer.",
+  "0587040000010045": "The firmware of AMS-HT H does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0587050000010010": "The firmware of AMS-HT H does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "0587050000010017": "AMS-HT H certification failed. Please reconnect the cable or restart the printer.",
+  "0700010000010001": "The AMS A assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "0700010000010003": "The AMS A assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0700010000010004": "The AMS A assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "0700010000010005": "AMS A The current sensor of assist motor may be faulty.",
+  "0700010000010011": "AMS A The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0700010000020002": "The AMS A assist motor is overloaded. The filament may be tangled or stuck.",
+  "0700010000020006": "AMS A The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "0700010000020007": "AMS A The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "0700010000020008": "AMS A The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "0700010000020009": "AMS A The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "0700010000020010": "AMS A The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "0700010000020011": "AMS A The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0700020000010001": "AMS A Filament speed and length error: The filament odometry may be faulty.",
+  "0700020000020002": "AMS A The odometer has no signal. The odometer connector may have poor contact.",
+  "0700020000020003": "AMS A filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "0700100000010001": "The AMS A slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0700100000010003": "The AMS A slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0700100000020002": "The AMS A slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "0700100000020004": "AMS A The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0700110000010001": "The AMS A slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0700110000010003": "The AMS A slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0700110000020002": "The AMS A slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "0700110000020004": "AMS A The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0700120000010001": "The AMS A slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0700120000010003": "The AMS A slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0700120000020002": "The AMS A slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "0700120000020004": "AMS A The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0700130000010001": "The AMS A slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0700130000010003": "The AMS A slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0700130000020002": "The AMS A slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "0700130000020004": "AMS A The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0700200000010081": "Failed to read the filament information from AMS A slot 1. The AMS main board may be malfunctioning.",
+  "0700200000010082": "Failed to read the filament information from AMS A slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0700200000010083": "Failed to read the filament information from AMS A slot 1. The RFID tag may be damaged.",
+  "0700200000010084": "Failed to read the filament information from AMS A slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0700200000010085": "Failed to read the filament information from AMS A slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0700200000010086": "Failed to read the filament information from AMS A slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0700200000020001": "AMS A Slot 1 filament has run out. Please insert a new filament.",
+  "0700200000020002": "AMS A Slot 1 is empty; please insert a new filament.",
+  "0700200000020003": "AMS A Slot 1's filament may be broken in AMS.",
+  "0700200000020004": "AMS A Slot 1 filament may be broken in the tool head.",
+  "0700200000020005": "AMS A Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0700200000020006": "AMS A has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0700200000020007": "AMS A Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0700200000020008": "AMS A Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0700200000020009": "Failed to extrude AMS A Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070020000002000A": "Failed to adjust the filament position of AMS A Slot 1. The filament or buffer may be stuck.",
+  "0700200000020010": "AMS A slot 1 feeds filament out of AMS timeout.",
+  "0700200000020011": "AMS A slot 1 pulls filament back to AMS timeout.",
+  "0700200000020012": "AMS A slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "0700200000020013": "AMS A slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0700200000020014": "AMS A slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0700200000020015": "AMS A slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0700200000020016": "AMS A slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0700200000020017": "AMS A slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0700200000020018": "AMS A slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0700200000020019": "AMS A slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0700200000020020": "AMS A slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0700200000020021": "AMS A slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0700200000020022": "AMS A slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0700200000020023": "AMS A slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0700200000020024": "AMS A slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0700200000020025": "AMS A slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0700200000020026": "AMS A slot 1 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0700200000020027": "AMS A slot 1 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0700200000020057": "The RFID-tag on AMS A Slot 1 cannot be identified.",
+  "0700200000030001": "AMS A Slot 1 filament has run out. Please wait while old filament is purged.",
+  "0700200000030002": "AMS A Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "0700210000010081": "Failed to read the filament information from AMS A slot 2. The AMS main board may be malfunctioning.",
+  "0700210000010082": "Failed to read the filament information from AMS A slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0700210000010083": "Failed to read the filament information from AMS A slot 2. The RFID tag may be damaged.",
+  "0700210000010084": "Failed to read the filament information from AMS A slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0700210000010085": "Failed to read the filament information from AMS A slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0700210000010086": "Failed to read the filament information from AMS A slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0700210000020001": "AMS A Slot 2 filament has run out. Please insert a new filament.",
+  "0700210000020002": "AMS A Slot 2 is empty; please insert a new filament.",
+  "0700210000020003": "AMS A Slot 2's filament may be broken in AMS.",
+  "0700210000020004": "AMS A Slot 2 filament may be broken in the tool head.",
+  "0700210000020005": "AMS A Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0700210000020006": "AMS A has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0700210000020007": "AMS A Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0700210000020008": "AMS A Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0700210000020009": "Failed to extrude AMS A Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070021000002000A": "Failed to adjust the filament position of AMS A Slot 2. The filament or buffer may be stuck.",
+  "0700210000020010": "AMS A slot 2 feeds filament out of AMS timeout.",
+  "0700210000020011": "AMS A slot 2 pulls filament back to AMS timeout.",
+  "0700210000020012": "AMS A slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "0700210000020013": "AMS A slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0700210000020014": "AMS A slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0700210000020015": "AMS A slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0700210000020016": "AMS A slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0700210000020017": "AMS A slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0700210000020018": "AMS A slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0700210000020019": "AMS A slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0700210000020020": "AMS A slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0700210000020021": "AMS A slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0700210000020022": "AMS A slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0700210000020023": "AMS A slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0700210000020024": "AMS A slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0700210000020025": "AMS A slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0700210000020026": "AMS A slot 2 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0700210000020027": "AMS A slot 2 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0700210000020057": "The RFID-tag on AMS A Slot 2 cannot be identified.",
+  "0700210000030001": "AMS A Slot 2 filament has run out. Please wait while old filament is purged.",
+  "0700210000030002": "AMS A Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "0700220000010081": "Failed to read the filament information from AMS A slot 3. The AMS main board may be malfunctioning.",
+  "0700220000010082": "Failed to read the filament information from AMS A slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0700220000010083": "Failed to read the filament information from AMS A slot 3. The RFID tag may be damaged.",
+  "0700220000010084": "Failed to read the filament information from AMS A slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0700220000010085": "Failed to read the filament information from AMS A slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0700220000010086": "Failed to read the filament information from AMS A slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0700220000020001": "AMS A Slot 3 filament has run out. Please insert a new filament.",
+  "0700220000020002": "AMS A Slot 3 is empty; please insert a new filament.",
+  "0700220000020003": "AMS A Slot 3's filament may be broken in AMS.",
+  "0700220000020004": "AMS A Slot 3 filament may be broken in the tool head.",
+  "0700220000020005": "AMS A Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0700220000020006": "AMS A has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0700220000020007": "AMS A Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0700220000020008": "AMS A Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0700220000020009": "Failed to extrude AMS A Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070022000002000A": "Failed to adjust the filament position of AMS A Slot 3. The filament or buffer may be stuck.",
+  "0700220000020010": "AMS A slot 3 feeds filament out of AMS timeout.",
+  "0700220000020011": "AMS A slot 3 pulls filament back to AMS timeout.",
+  "0700220000020012": "AMS A slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "0700220000020013": "AMS A slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0700220000020014": "AMS A slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0700220000020015": "AMS A slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0700220000020016": "AMS A slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0700220000020017": "AMS A slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0700220000020018": "AMS A slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0700220000020019": "AMS A slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0700220000020020": "AMS A slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0700220000020021": "AMS A slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0700220000020022": "AMS A slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0700220000020023": "AMS A slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0700220000020024": "AMS A slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0700220000020025": "AMS A slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0700220000020026": "AMS A slot 3 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0700220000020027": "AMS A slot 3 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0700220000020057": "The RFID-tag on AMS A Slot 3 cannot be identified.",
+  "0700220000030001": "AMS A Slot 3 filament has run out. Please wait while old filament is purged.",
+  "0700220000030002": "AMS A Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "0700230000010081": "Failed to read the filament information from AMS A slot 4. The AMS main board may be malfunctioning.",
+  "0700230000010082": "Failed to read the filament information from AMS A slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0700230000010083": "Failed to read the filament information from AMS A slot 4. The RFID tag may be damaged.",
+  "0700230000010084": "Failed to read the filament information from AMS A slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0700230000010085": "Failed to read the filament information from AMS A slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0700230000010086": "Failed to read the filament information from AMS A slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0700230000020001": "AMS A Slot 4 filament has run out. Please insert a new filament.",
+  "0700230000020002": "AMS A Slot 4 is empty; please insert a new filament.",
+  "0700230000020003": "AMS A Slot 4's filament may be broken in AMS.",
+  "0700230000020004": "AMS A Slot 4 filament may be broken in the tool head.",
+  "0700230000020005": "AMS A Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0700230000020006": "AMS A has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0700230000020007": "AMS A Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0700230000020008": "AMS A Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0700230000020009": "Failed to extrude AMS A Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070023000002000A": "Failed to adjust the filament position of AMS A Slot 4. The filament or buffer may be stuck.",
+  "0700230000020010": "AMS A slot 4 feeds filament out of AMS timeout.",
+  "0700230000020011": "AMS A slot 4 pulls filament back to AMS timeout.",
+  "0700230000020012": "AMS A slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "0700230000020013": "AMS A slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0700230000020014": "AMS A slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0700230000020015": "AMS A slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0700230000020016": "AMS A slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0700230000020017": "AMS A slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0700230000020018": "AMS A slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0700230000020019": "AMS A slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0700230000020020": "AMS A slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0700230000020021": "AMS A slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0700230000020022": "AMS A slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0700230000020023": "AMS A slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0700230000020024": "AMS A slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0700230000020025": "AMS A slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0700230000020026": "AMS A slot 4 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0700230000020027": "AMS A slot 4 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0700230000020057": "The RFID-tag on AMS A Slot 4 cannot be identified.",
+  "0700230000030001": "AMS A Slot 4 filament has run out. Please wait while old filament is purged.",
+  "0700230000030002": "AMS A Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "0700250000020001": "AMS A uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "0700260000020001": "The induction hotend is heating up, and the drying power of AMS A will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "0700300000010001": "The AMS A RFID 1 board has an error.",
+  "0700300000010004": "Encryption chip failure",
+  "0700300000020002": "The RFID-tag on AMS A Slot1 is damaged, or its content cannot be identified.",
+  "0700300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0700310000010001": "The AMS A RFID 2 board has an error.",
+  "0700310000010004": "Encryption chip failure",
+  "0700310000020002": "The RFID-tag on AMS A Slot2 is damaged, or its content cannot be identified.",
+  "0700310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0700320000020002": "The RFID-tag on AMS A Slot3 is damaged, or its content cannot be identified.",
+  "0700330000020002": "The RFID-tag on AMS A Slot4 is damaged, or its content cannot be identified.",
+  "0700350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "0700350000010002": "AMS A The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "0700400000020001": "AMS A Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "0700400000020002": "Filament buffer position signal error: the position sensor may be malfunctioning.",
+  "0700400000020003": "The AMS Hub communication is abnormal; the cable may be not well connected.",
+  "0700400000020004": "The filament buffer signal is abnormal; the spring may be stuck, or the filament may be tangled.",
+  "0700450000020001": "The filament cutter sensor is malfunctioning; please check whether the connector is properly plugged in.",
+  "0700450000020002": "The filament cutter's cutting distance is too large. The XY motor may lose steps.",
+  "0700450000020003": "The filament cutter handle has not been released. The handle or blade may be jammed, or there could be an issue with the filament sensor connection.",
+  "0700500000020001": "AMS A communication is abnormal; please check the connection cable.",
+  "0700500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0700510000030001": "The AMS is disabled; please load filament from the spool holder.",
+  "0700520000030001": "Abnormal number or type of connected AMS units detected. Please refer to the Wiki for supported AMS combinations and adjust the current connection.",
+  "0700550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "0700550000010003": "AMS A was detected offline during the AMS initialization process.",
+  "0700550000010004": "The binding between AMS A and the extruder is incorrect. Please run the AMS Setup.",
+  "0700550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "0700550000020001": "A new AMS detected. Please set it up to check which extruder the AMS is connected to.",
+  "0700560000030001": "AMS A is undergoing dry cooling; please wait for it to cool down before operating.",
+  "0700600000020001": "The AMS A Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0700610000020001": "The AMS A Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0700620000020001": "The AMS A Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0700630000020001": "The AMS A Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0700700000020001": "Failed to pull out the AMS A Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0700700000020002": "Failed to feed the AMS A Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0700700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "0700700000020004": "Failed to pull back the AMS A Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0700700000020005": "Failed to feed the AMS A Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0700700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "0700700000020007": "AMS filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "0700700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "070070000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "0700710000020001": "Failed to pull out the AMS A Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0700710000020002": "Failed to feed the AMS A Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0700710000020004": "Failed to pull back the AMS A Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0700710000020005": "Failed to feed the AMS A Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0700720000020001": "Failed to pull out the AMS A Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0700720000020002": "Failed to feed the AMS A Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0700720000020004": "Failed to pull back the AMS A Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0700720000020005": "Failed to feed the AMS A Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0700730000020001": "Failed to pull out the AMS A Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0700730000020002": "Failed to feed the AMS A Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0700730000020004": "Failed to pull back the AMS A Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0700730000020005": "Failed to feed the AMS A Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0700740000010001": "The Filament Track Switch is offline. Please check for disconnected cables or loose connectors.",
+  "0700740000020006": "The Filament Track Switch was connected during AMS initialization. Please unplug the Filament Track Switch and reconnect it after AMS initialization is complete.",
+  "0700740000020007": "Filament Track Switch detected during loading/unloading. Please remove it and reconnect after the process is complete.",
+  "0700750000010005": "The Filament Track Switch was connected while filament is still in the extruder or buffer. Please unplug the switch, manually unload the filament, then reconnect and try again.",
+  "0700750000010008": "Filament was detected in the extruder when the Filament Track Switch was unplugged. Please reconnect the Filament Track Switch, unload the filament, then unplug the Filament Track Switch.",
+  "0700800000010001": "AMS A The current sensor of heater 1 is abnormal.",
+  "0700800000010002": "AMS A The heater 1 is disconnected, which may be due to poor connector contact.",
+  "0700800000010003": "AMS A The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0700800000010004": "AMS A The heater 1 is heating abnormally.",
+  "0700810000010001": "AMS A The current sensor of heater 2 is abnormal.",
+  "0700810000010002": "AMS A The heater 2 is disconnected, which may be due to poor connector contact.",
+  "0700810000010003": "AMS A The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0700810000010004": "AMS A The heater 2 is heating abnormally.",
+  "0700860000010001": "The Filament Track Switch is offline. Please check for disconnected cables or loose connectors.",
+  "0700860000010004": "AMS A is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "0700860000010005": "The Filament Track Switch was connected while filament is still loaded in the extruder or buffer. Please unplug the Filament Track Switch, unload the filament, then reconnect the Filament Track Switch and try again.",
+  "0700860000010008": "Filament was detected in the extruder when the Filament Track Switch was unplugged. Please reconnect the Filament Track Switch, unload the filament, then unplug the Filament Track Switch.",
+  "0700860000020002": "The AMS has been detected. Please set it up to confirm which side of the Filament Track Switch inlet the AMS is connected to.",
+  "0700860000020003": "Filament is blocked at the inlet of the filament track switch.",
+  "0700860000020006": "The Filament Track Switch was connected during AMS initialization. Please unplug the Filament Track Switch and reconnect it after AMS initialization is complete.",
+  "0700860000020007": "The Filament Track Switch was connected during printing. Please unplug the Filament Track Switch and reconnect it after the print is finished.",
+  "0700860000020009": "AMS setup error: Filament track switch detected online, but filament did not pass through. Please check the PTFE tube connection between the AMS and the filament track switch; disconnect the switch if not in use.",
+  "0700870000020001": "A filament jam or component damage may have occurred at the Filament Track Switch.",
+  "0700870000020003": "Filament is blocked at the inlet of the filament track switch.",
+  "0700870000020009": "AMS setup error: Filament track switch detected online, but filament did not pass through. Please check the PTFE tube connection between the AMS and the filament track switch; disconnect the switch if not in use.",
+  "0700880000020003": "Abnormal movement detected on the IN-A side of the Filament Track Switch. This may be caused by debris obstruction, a loose cable, or a damaged coil. Please refer to the Wiki for troubleshooting.",
+  "0700890000020004": "Abnormal switching detection sensor detected on the IN-A side of the Filament Track Switch. The cable on this side may be loose. Please reconnect or replace the sensor and try again.",
+  "07008A0000020005": "Abnormal switching detection sensor detected on the IN-B side of the Filament Track Switch. The cable on this side may be loose. Please reconnect or replace the sensor and try again.",
+  "07008B0000020006": "Abnormal feed detection sensor detected on the IN-A side of the Filament Track Switch. The cable on this side may be loose. Please reconnect or replace the sensor and try again.",
+  "07008C0000020007": "Abnormal feed detection sensor detected on the IN-B side of the Filament Track Switch. The cable on this side may be loose. Please reconnect or replace the sensor and try again.",
+  "07008D0000020008": "Abnormal coil resistance detected on the IN-A side of the Filament Track Switch. Please refer to the Wiki to replace the coil assembly.",
+  "07008E0000020009": "Abnormal coil resistance detected on the IN-B side of the Filament Track Switch. Please refer to the Wiki to replace the coil assembly.",
+  "07008F0000020002": "Abnormal movement detected on the IN-B side of the Filament Track Switch. This may be caused by debris obstruction, a loose cable, or a damaged coil. Please refer to the Wiki for troubleshooting.",
+  "0700900000010002": "AMS A The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "0700900000010003": "AMS A The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "0700900000010004": "The current sensor of AMS A exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0700900000020001": "AMS A The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "0700910000010002": "AMS A The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "0700910000010003": "AMS A The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "0700910000010004": "The current sensor of AMS A exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0700910000020001": "AMS A The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "0700920000010001": "AMS A The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "0700920000020002": "AMS A The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "0700920000020003": "The AMS A heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "0700930000010001": "AMS A The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "0700930000020002": "AMS A The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "0700930000020003": "The AMS A heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "0700940000010001": "AMS A The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "0700940000010002": "Temperature sensor 1 on the AMS A heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0700950000010001": "AMS A The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "0700950000010002": "Temperature sensor 2 on the AMS A heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0700960000010001": "AMS A The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "0700960000010003": "AMS A Unable to start drying; please pull out the filament from filament hub and try again.",
+  "0700960000020002": "AMS A Environmental temperature is too low, which will affect the drying capability.",
+  "0700960000020004": "AMS A The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "0700970000030001": "AMS A chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "0700980000020001": "AMS A The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "0700980000020002": "AMS A The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "0700F00000020001": "Filament and hotend matching failed. Please verify that the hotend in the hotend rack slot 1 is correctly installed.",
+  "0700F10000020001": "Filament and hotend matching failed. Please verify that the hotend in the hotend rack slot 2 is correctly installed.",
+  "0700F20000020001": "Filament and hotend matching failed. Please verify that the hotend in the hotend rack slot 3 is correctly installed.",
+  "0700F30000020001": "Filament and hotend matching failed. Please verify that the hotend in the hotend rack slot 4 is correctly installed.",
+  "0700F40000020001": "Filament and hotend matching failed. Please verify that the hotend in the hotend rack slot 5 is correctly installed.",
+  "0700F50000020001": "Filament and hotend matching failed. Please verify that the hotend in the hotend rack slot 6 is correctly installed.",
+  "0701010000010001": "The AMS B assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "0701010000010003": "The AMS B assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0701010000010004": "The AMS B assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "0701010000010005": "AMS B The current sensor of assist motor may be faulty.",
+  "0701010000010011": "AMS B The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0701010000020002": "The AMS B assist motor is overloaded. The filament may be tangled or stuck.",
+  "0701010000020006": "AMS B The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "0701010000020007": "AMS B The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "0701010000020008": "AMS B The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "0701010000020009": "AMS B The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "0701010000020010": "AMS B The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "0701010000020011": "AMS B The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0701020000010001": "AMS B Filament speed and length error: The filament odometry may be faulty.",
+  "0701020000020002": "AMS B The odometer has no signal. The odometer connector may have poor contact.",
+  "0701020000020003": "AMS B filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "0701100000010001": "The AMS B slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0701100000010003": "The AMS B slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0701100000020002": "The AMS B slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "0701100000020004": "AMS B The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0701110000010001": "The AMS B slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0701110000010003": "The AMS B slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0701110000020002": "The AMS B slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "0701110000020004": "AMS B The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0701120000010001": "The AMS B slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0701120000010003": "The AMS B slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0701120000020002": "The AMS B slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "0701120000020004": "AMS B The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0701130000010001": "The AMS B slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0701130000010003": "The AMS B slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0701130000020002": "The AMS B slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "0701130000020004": "AMS B The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0701200000010081": "Failed to read the filament information from AMS B slot 1. The AMS main board may be malfunctioning.",
+  "0701200000010082": "Failed to read the filament information from AMS B slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0701200000010083": "Failed to read the filament information from AMS B slot 1. The RFID tag may be damaged.",
+  "0701200000010084": "Failed to read the filament information from AMS B slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0701200000010085": "Failed to read the filament information from AMS B slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0701200000010086": "Failed to read the filament information from AMS B slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0701200000020001": "AMS B Slot 1 filament has run out. Please insert a new filament.",
+  "0701200000020002": "AMS B Slot 1 is empty; please insert a new filament.",
+  "0701200000020003": "AMS B Slot 1's filament may be broken in AMS.",
+  "0701200000020004": "AMS B Slot 1 filament may be broken in the tool head.",
+  "0701200000020005": "AMS B Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0701200000020006": "AMS B has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0701200000020007": "AMS B Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0701200000020008": "AMS B Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0701200000020009": "Failed to extrude AMS B Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070120000002000A": "Failed to adjust the filament position of AMS B Slot 1. The filament or buffer may be stuck.",
+  "0701200000020010": "AMS B slot 1 feeds filament out of AMS timeout.",
+  "0701200000020011": "AMS B slot 1 pulls filament back to AMS timeout.",
+  "0701200000020012": "AMS B slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "0701200000020013": "AMS B slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0701200000020014": "AMS B slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0701200000020015": "AMS B slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0701200000020016": "AMS B slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0701200000020017": "AMS B slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0701200000020018": "AMS B slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0701200000020019": "AMS B slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0701200000020020": "AMS B slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0701200000020021": "AMS B slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0701200000020022": "AMS B slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0701200000020023": "AMS B slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0701200000020024": "AMS B slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0701200000020025": "AMS B slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0701200000020026": "AMS B slot 1 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0701200000020027": "AMS B slot 1 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0701200000020057": "The RFID-tag on AMS B Slot 1 cannot be identified.",
+  "0701200000030001": "AMS B Slot 1 filament has run out. Please wait while old filament is purged.",
+  "0701200000030002": "AMS B Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "0701210000010081": "Failed to read the filament information from AMS B slot 2. The AMS main board may be malfunctioning.",
+  "0701210000010082": "Failed to read the filament information from AMS B slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0701210000010083": "Failed to read the filament information from AMS B slot 2. The RFID tag may be damaged.",
+  "0701210000010084": "Failed to read the filament information from AMS B slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0701210000010085": "Failed to read the filament information from AMS B slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0701210000010086": "Failed to read the filament information from AMS B slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0701210000020001": "AMS B Slot 2 filament has run out. Please insert a new filament.",
+  "0701210000020002": "AMS B Slot 2 is empty; please insert a new filament.",
+  "0701210000020003": "AMS B Slot 2's filament may be broken in AMS.",
+  "0701210000020004": "AMS B Slot 2 filament may be broken in the tool head.",
+  "0701210000020005": "AMS B Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0701210000020006": "AMS B has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0701210000020007": "AMS B Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0701210000020008": "AMS B Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0701210000020009": "Failed to extrude AMS B Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070121000002000A": "Failed to adjust the filament position of AMS B Slot 2. The filament or buffer may be stuck.",
+  "0701210000020010": "AMS B slot 2 feeds filament out of AMS timeout.",
+  "0701210000020011": "AMS B slot 2 pulls filament back to AMS timeout.",
+  "0701210000020012": "AMS B slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "0701210000020013": "AMS B slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0701210000020014": "AMS B slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0701210000020015": "AMS B slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0701210000020016": "AMS B slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0701210000020017": "AMS B slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0701210000020018": "AMS B slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0701210000020019": "AMS B slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0701210000020020": "AMS B slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0701210000020021": "AMS B slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0701210000020022": "AMS B slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0701210000020023": "AMS B slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0701210000020024": "AMS B slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0701210000020025": "AMS B slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0701210000020026": "AMS B slot 2 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0701210000020027": "AMS B slot 2 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0701210000020057": "The RFID-tag on AMS B Slot 2 cannot be identified.",
+  "0701210000030001": "AMS B Slot 2 filament has run out. Please wait while old filament is purged.",
+  "0701210000030002": "AMS B Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "0701220000010081": "Failed to read the filament information from AMS B slot 3. The AMS main board may be malfunctioning.",
+  "0701220000010082": "Failed to read the filament information from AMS B slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0701220000010083": "Failed to read the filament information from AMS B slot 3. The RFID tag may be damaged.",
+  "0701220000010084": "Failed to read the filament information from AMS B slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0701220000010085": "Failed to read the filament information from AMS B slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0701220000010086": "Failed to read the filament information from AMS B slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0701220000020001": "AMS B Slot 3 filament has run out. Please insert a new filament.",
+  "0701220000020002": "AMS B Slot 3 is empty; please insert a new filament.",
+  "0701220000020003": "AMS B Slot 3's filament may be broken in AMS.",
+  "0701220000020004": "AMS B Slot 3 filament may be broken in the tool head.",
+  "0701220000020005": "AMS B Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0701220000020006": "AMS B has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0701220000020007": "AMS B Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0701220000020008": "AMS B Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0701220000020009": "Failed to extrude AMS B Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070122000002000A": "Failed to adjust the filament position of AMS B Slot 3. The filament or buffer may be stuck.",
+  "0701220000020010": "AMS B slot 3 feeds filament out of AMS timeout.",
+  "0701220000020011": "AMS B slot 3 pulls filament back to AMS timeout.",
+  "0701220000020012": "AMS B slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "0701220000020013": "AMS B slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0701220000020014": "AMS B slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0701220000020015": "AMS B slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0701220000020016": "AMS B slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0701220000020017": "AMS B slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0701220000020018": "AMS B slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0701220000020019": "AMS B slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0701220000020020": "AMS B slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0701220000020021": "AMS B slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0701220000020022": "AMS B slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0701220000020023": "AMS B slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0701220000020024": "AMS B slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0701220000020025": "AMS B slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0701220000020026": "AMS B slot 3 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0701220000020027": "AMS B slot 3 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0701220000020057": "The RFID-tag on AMS B Slot 3 cannot be identified.",
+  "0701220000030001": "AMS B Slot 3 filament has run out. Please wait while old filament is purged.",
+  "0701220000030002": "AMS B Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "0701230000010081": "Failed to read the filament information from AMS B slot 4. The AMS main board may be malfunctioning.",
+  "0701230000010082": "Failed to read the filament information from AMS B slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0701230000010083": "Failed to read the filament information from AMS B slot 4. The RFID tag may be damaged.",
+  "0701230000010084": "Failed to read the filament information from AMS B slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0701230000010085": "Failed to read the filament information from AMS B slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0701230000010086": "Failed to read the filament information from AMS B slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0701230000020001": "AMS B Slot 4 filament has run out. Please insert a new filament.",
+  "0701230000020002": "AMS B Slot 4 is empty; please insert a new filament.",
+  "0701230000020003": "AMS B Slot 4's filament may be broken in AMS.",
+  "0701230000020004": "AMS B Slot 4 filament may be broken in the tool head.",
+  "0701230000020005": "AMS B Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0701230000020006": "AMS B has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0701230000020007": "AMS B Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0701230000020008": "AMS B Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0701230000020009": "Failed to extrude AMS B Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070123000002000A": "Failed to adjust the filament position of AMS B Slot 4. The filament or buffer may be stuck.",
+  "0701230000020010": "AMS B slot 4 feeds filament out of AMS timeout.",
+  "0701230000020011": "AMS B slot 4 pulls filament back to AMS timeout.",
+  "0701230000020012": "AMS B slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "0701230000020013": "AMS B slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0701230000020014": "AMS B slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0701230000020015": "AMS B slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0701230000020016": "AMS B slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0701230000020017": "AMS B slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0701230000020018": "AMS B slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0701230000020019": "AMS B slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0701230000020020": "AMS B slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0701230000020021": "AMS B slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0701230000020022": "AMS B slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0701230000020023": "AMS B slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0701230000020024": "AMS B slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0701230000020025": "AMS B slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0701230000020026": "AMS B slot 4 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0701230000020027": "AMS B slot 4 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0701230000020057": "The RFID-tag on AMS B Slot 4 cannot be identified.",
+  "0701230000030001": "AMS B Slot 4 filament has run out. Please wait while old filament is purged.",
+  "0701230000030002": "AMS B Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "0701250000020001": "AMS B uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "0701260000020001": "The induction hotend is heating up, and the drying power of AMS B will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "0701300000010001": "The AMS B RFID 1 board has an error.",
+  "0701300000010004": "Encryption chip failure",
+  "0701300000020002": "The RFID-tag on AMS B Slot1 is damaged, or its content cannot be identified.",
+  "0701300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0701310000010001": "The AMS B RFID 2 board has an error.",
+  "0701310000010004": "Encryption chip failure",
+  "0701310000020002": "The RFID-tag on AMS B Slot2 is damaged, or its content cannot be identified.",
+  "0701310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0701320000020002": "The RFID-tag on AMS B Slot3 is damaged, or its content cannot be identified.",
+  "0701330000020002": "The RFID-tag on AMS B Slot4 is damaged, or its content cannot be identified.",
+  "0701350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "0701350000010002": "AMS B The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "0701400000020001": "AMS B Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "0701500000020001": "AMS B communication is abnormal; please check the connection cable.",
+  "0701500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0701550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "0701550000010003": "AMS B was detected offline during the AMS initialization process.",
+  "0701550000010004": "The binding between AMS B and the extruder is incorrect. Please run the AMS Setup.",
+  "0701550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "0701560000030001": "AMS B is undergoing dry cooling; please wait for it to cool down before operating.",
+  "0701600000020001": "The AMS B Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0701610000020001": "The AMS B Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0701620000020001": "The AMS B Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0701630000020001": "The AMS B Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0701700000020001": "Failed to pull out the AMS B Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0701700000020002": "Failed to feed the AMS B Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0701700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "0701700000020004": "Failed to pull back the AMS B Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0701700000020005": "Failed to feed the AMS B Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0701700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "0701700000020007": "AMS filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "0701700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "070170000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "0701710000020001": "Failed to pull out the AMS B Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0701710000020002": "Failed to feed the AMS B Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0701710000020004": "Failed to pull back the AMS B Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0701710000020005": "Failed to feed the AMS B Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0701720000020001": "Failed to pull out the AMS B Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0701720000020002": "Failed to feed the AMS B Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0701720000020004": "Failed to pull back the AMS B Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0701720000020005": "Failed to feed the AMS B Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0701730000020001": "Failed to pull out the AMS B Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0701730000020002": "Failed to feed the AMS B Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0701730000020004": "Failed to pull back the AMS B Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0701730000020005": "Failed to feed the AMS B Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0701800000010001": "AMS B The current sensor of heater 1 is abnormal.",
+  "0701800000010002": "AMS B The heater 1 is disconnected, which may be due to poor connector contact.",
+  "0701800000010003": "AMS B The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0701800000010004": "AMS B The heater 1 is heating abnormally.",
+  "0701810000010001": "AMS B The current sensor of heater 2 is abnormal.",
+  "0701810000010002": "AMS B The heater 2 is disconnected, which may be due to poor connector contact.",
+  "0701810000010003": "AMS B The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0701810000010004": "AMS B The heater 2 is heating abnormally.",
+  "0701860000010004": "AMS B is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "0701900000010002": "AMS B The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "0701900000010003": "AMS B The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "0701900000010004": "The current sensor of AMS B exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0701900000020001": "AMS B The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "0701910000010002": "AMS B The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "0701910000010003": "AMS B The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "0701910000010004": "The current sensor of AMS B exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0701910000020001": "AMS B The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "0701920000010001": "AMS B The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "0701920000020002": "AMS B The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "0701920000020003": "The AMS B heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "0701930000010001": "AMS B The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "0701930000020002": "AMS B The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "0701930000020003": "The AMS B heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "0701940000010001": "AMS B The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "0701940000010002": "Temperature sensor 1 on the AMS B heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0701950000010001": "AMS B The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "0701950000010002": "Temperature sensor 2 on the AMS B heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0701960000010001": "AMS B The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "0701960000010003": "AMS B Unable to start drying; please pull out the filament from filament hub and try again.",
+  "0701960000020002": "AMS B Environmental temperature is too low, which will affect the drying capability.",
+  "0701960000020004": "AMS B The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "0701970000030001": "AMS B chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "0701980000020001": "AMS B The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "0701980000020002": "AMS B The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "0702010000010001": "The AMS C assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "0702010000010003": "The AMS C assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0702010000010004": "The AMS C assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "0702010000010005": "AMS C The current sensor of assist motor may be faulty.",
+  "0702010000010011": "AMS C The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0702010000020002": "The AMS C assist motor is overloaded. The filament may be tangled or stuck.",
+  "0702010000020006": "AMS C The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "0702010000020007": "AMS C The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "0702010000020008": "AMS C The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "0702010000020009": "AMS C The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "0702010000020010": "AMS C The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "0702010000020011": "AMS C The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0702020000010001": "AMS C Filament speed and length error: The filament odometry may be faulty.",
+  "0702020000020002": "AMS C The odometer has no signal. The odometer connector may have poor contact.",
+  "0702020000020003": "AMS C filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "0702100000010001": "The AMS C slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0702100000010003": "The AMS C slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0702100000020002": "The AMS C slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "0702100000020004": "AMS C The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0702110000010001": "The AMS C slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0702110000010003": "The AMS C slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0702110000020002": "The AMS C slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "0702110000020004": "AMS C The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0702120000010001": "The AMS C slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0702120000010003": "The AMS C slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0702120000020002": "The AMS C slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "0702120000020004": "AMS C The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0702130000010001": "The AMS C slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0702130000010003": "The AMS C slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0702130000020002": "The AMS C slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "0702130000020004": "AMS C The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0702200000010081": "Failed to read the filament information from AMS C slot 1. The AMS main board may be malfunctioning.",
+  "0702200000010082": "Failed to read the filament information from AMS C slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0702200000010083": "Failed to read the filament information from AMS C slot 1. The RFID tag may be damaged.",
+  "0702200000010084": "Failed to read the filament information from AMS C slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0702200000010085": "Failed to read the filament information from AMS C slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0702200000010086": "Failed to read the filament information from AMS C slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0702200000020001": "AMS C Slot 1 filament has run out. Please insert a new filament.",
+  "0702200000020002": "AMS C Slot 1 is empty; please insert a new filament.",
+  "0702200000020003": "AMS C Slot 1's filament may be broken in AMS.",
+  "0702200000020004": "AMS C Slot 1 filament may be broken in the tool head.",
+  "0702200000020005": "AMS C Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0702200000020006": "AMS C has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0702200000020007": "AMS C Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0702200000020008": "AMS C Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0702200000020009": "Failed to extrude AMS C Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070220000002000A": "Failed to adjust the filament position of AMS C Slot 1. The filament or buffer may be stuck.",
+  "0702200000020010": "AMS C slot 1 feeds filament out of AMS timeout.",
+  "0702200000020011": "AMS C slot 1 pulls filament back to AMS timeout.",
+  "0702200000020012": "AMS C slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "0702200000020013": "AMS C slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0702200000020014": "AMS C slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0702200000020015": "AMS C slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0702200000020016": "AMS C slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0702200000020017": "AMS C slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0702200000020018": "AMS C slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0702200000020019": "AMS C slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0702200000020020": "AMS C slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0702200000020021": "AMS C slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0702200000020022": "AMS C slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0702200000020023": "AMS C slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0702200000020024": "AMS C slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0702200000020025": "AMS C slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0702200000020026": "AMS C slot 1 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0702200000020027": "AMS C slot 1 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0702200000020057": "The RFID-tag on AMS C Slot 1 cannot be identified.",
+  "0702200000030001": "AMS C Slot 1 filament has run out. Please wait while old filament is purged.",
+  "0702200000030002": "AMS C Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "0702210000010081": "Failed to read the filament information from AMS C slot 2. The AMS main board may be malfunctioning.",
+  "0702210000010082": "Failed to read the filament information from AMS C slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0702210000010083": "Failed to read the filament information from AMS C slot 2. The RFID tag may be damaged.",
+  "0702210000010084": "Failed to read the filament information from AMS C slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0702210000010085": "Failed to read the filament information from AMS C slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0702210000010086": "Failed to read the filament information from AMS C slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0702210000020001": "AMS C Slot 2 filament has run out. Please insert a new filament.",
+  "0702210000020002": "AMS C Slot 2 is empty; please insert a new filament.",
+  "0702210000020003": "AMS C Slot 2's filament may be broken in AMS.",
+  "0702210000020004": "AMS C Slot 2 filament may be broken in the tool head.",
+  "0702210000020005": "AMS C Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0702210000020006": "AMS C has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0702210000020007": "AMS C Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0702210000020008": "AMS C Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0702210000020009": "Failed to extrude AMS C Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070221000002000A": "Failed to adjust the filament position of AMS C Slot 2. The filament or buffer may be stuck.",
+  "0702210000020010": "AMS C slot 2 feeds filament out of AMS timeout.",
+  "0702210000020011": "AMS C slot 2 pulls filament back to AMS timeout.",
+  "0702210000020012": "AMS C slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "0702210000020013": "AMS C slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0702210000020014": "AMS C slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0702210000020015": "AMS C slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0702210000020016": "AMS C slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0702210000020017": "AMS C slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0702210000020018": "AMS C slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0702210000020019": "AMS C slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0702210000020020": "AMS C slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0702210000020021": "AMS C slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0702210000020022": "AMS C slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0702210000020023": "AMS C slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0702210000020024": "AMS C slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0702210000020025": "AMS C slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0702210000020026": "AMS C slot 2 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0702210000020027": "AMS C slot 2 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0702210000020057": "The RFID-tag on AMS C Slot 2 cannot be identified.",
+  "0702210000030001": "AMS C Slot 2 filament has run out. Please wait while old filament is purged.",
+  "0702210000030002": "AMS C Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "0702220000010081": "Failed to read the filament information from AMS C slot 3. The AMS main board may be malfunctioning.",
+  "0702220000010082": "Failed to read the filament information from AMS C slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0702220000010083": "Failed to read the filament information from AMS C slot 3. The RFID tag may be damaged.",
+  "0702220000010084": "Failed to read the filament information from AMS C slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0702220000010085": "Failed to read the filament information from AMS C slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0702220000010086": "Failed to read the filament information from AMS C slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0702220000020001": "AMS C Slot 3 filament has run out. Please insert a new filament.",
+  "0702220000020002": "AMS C Slot 3 is empty; please insert a new filament.",
+  "0702220000020003": "AMS C Slot 3's filament may be broken in AMS.",
+  "0702220000020004": "AMS C Slot 3 filament may be broken in the tool head.",
+  "0702220000020005": "AMS C Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0702220000020006": "AMS C has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0702220000020007": "AMS C Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0702220000020008": "AMS C Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0702220000020009": "Failed to extrude AMS C Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070222000002000A": "Failed to adjust the filament position of AMS C Slot 3. The filament or buffer may be stuck.",
+  "0702220000020010": "AMS C slot 3 feeds filament out of AMS timeout.",
+  "0702220000020011": "AMS C slot 3 pulls filament back to AMS timeout.",
+  "0702220000020012": "AMS C slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "0702220000020013": "AMS C slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0702220000020014": "AMS C slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0702220000020015": "AMS C slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0702220000020016": "AMS C slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0702220000020017": "AMS C slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0702220000020018": "AMS C slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0702220000020019": "AMS C slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0702220000020020": "AMS C slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0702220000020021": "AMS C slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0702220000020022": "AMS C slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0702220000020023": "AMS C slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0702220000020024": "AMS C slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0702220000020025": "AMS C slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0702220000020026": "AMS C slot 3 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0702220000020027": "AMS C slot 3 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0702220000020057": "The RFID-tag on AMS C Slot 3 cannot be identified.",
+  "0702220000030001": "AMS C Slot 3 filament has run out. Please wait while old filament is purged.",
+  "0702220000030002": "AMS C Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "0702230000010081": "Failed to read the filament information from AMS C slot 4. The AMS main board may be malfunctioning.",
+  "0702230000010082": "Failed to read the filament information from AMS C slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0702230000010083": "Failed to read the filament information from AMS C slot 4. The RFID tag may be damaged.",
+  "0702230000010084": "Failed to read the filament information from AMS C slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0702230000010085": "Failed to read the filament information from AMS C slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0702230000010086": "Failed to read the filament information from AMS C slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0702230000020001": "AMS C Slot 4 filament has run out. Please insert a new filament.",
+  "0702230000020002": "AMS C Slot 4 is empty; please insert a new filament.",
+  "0702230000020003": "AMS C Slot 4's filament may be broken in AMS.",
+  "0702230000020004": "AMS C Slot 4 filament may be broken in the tool head.",
+  "0702230000020005": "AMS C Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0702230000020006": "AMS C has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0702230000020007": "AMS C Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0702230000020008": "AMS C Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0702230000020009": "Failed to extrude AMS C Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070223000002000A": "Failed to adjust the filament position of AMS C Slot 4. The filament or buffer may be stuck.",
+  "0702230000020010": "AMS C slot 4 feeds filament out of AMS timeout.",
+  "0702230000020011": "AMS C slot 4 pulls filament back to AMS timeout.",
+  "0702230000020012": "AMS C slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "0702230000020013": "AMS C slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0702230000020014": "AMS C slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0702230000020015": "AMS C slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0702230000020016": "AMS C slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0702230000020017": "AMS C slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0702230000020018": "AMS C slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0702230000020019": "AMS C slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0702230000020020": "AMS C slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0702230000020021": "AMS C slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0702230000020022": "AMS C slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0702230000020023": "AMS C slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0702230000020024": "AMS C slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0702230000020025": "AMS C slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0702230000020026": "AMS C slot 4 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0702230000020027": "AMS C slot 4 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0702230000020057": "The RFID-tag on AMS C Slot 4 cannot be identified.",
+  "0702230000030001": "AMS C Slot 4 filament has run out. Please wait while old filament is purged.",
+  "0702230000030002": "AMS C Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "0702250000020001": "AMS C uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "0702260000020001": "The induction hotend is heating up, and the drying power of AMS C will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "0702300000010001": "The AMS C RFID 1 board has an error.",
+  "0702300000010004": "Encryption chip failure",
+  "0702300000020002": "The RFID-tag on AMS C Slot1 is damaged, or its content cannot be identified.",
+  "0702300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0702310000010001": "The AMS C RFID 2 board has an error.",
+  "0702310000010004": "Encryption chip failure",
+  "0702310000020002": "The RFID-tag on AMS C Slot2 is damaged, or its content cannot be identified.",
+  "0702310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0702320000020002": "The RFID-tag on AMS C Slot3 is damaged, or its content cannot be identified.",
+  "0702330000020002": "The RFID-tag on AMS C Slot4 is damaged, or its content cannot be identified.",
+  "0702350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "0702350000010002": "AMS C The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "0702400000020001": "AMS C Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "0702500000020001": "AMS C communication is abnormal; please check the connection cable.",
+  "0702500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0702550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "0702550000010003": "AMS C was detected offline during the AMS initialization process.",
+  "0702550000010004": "The binding between AMS C and the extruder is incorrect. Please run the AMS Setup.",
+  "0702550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "0702560000030001": "AMS C is undergoing dry cooling; please wait for it to cool down before operating.",
+  "0702600000020001": "The AMS C Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0702610000020001": "The AMS C Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0702620000020001": "The AMS C Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0702630000020001": "The AMS C Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0702700000020001": "Failed to pull out the AMS C Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0702700000020002": "Failed to feed the AMS C Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0702700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "0702700000020004": "Failed to pull back the AMS C Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0702700000020005": "Failed to feed the AMS C Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0702700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "0702700000020007": "AMS filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "0702700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "070270000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "0702710000020001": "Failed to pull out the AMS C Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0702710000020002": "Failed to feed the AMS C Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0702710000020004": "Failed to pull back the AMS C Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0702710000020005": "Failed to feed the AMS C Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0702720000020001": "Failed to pull out the AMS C Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0702720000020002": "Failed to feed the AMS C Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0702720000020004": "Failed to pull back the AMS C Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0702720000020005": "Failed to feed the AMS C Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0702730000020001": "Failed to pull out the AMS C Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0702730000020002": "Failed to feed the AMS C Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0702730000020004": "Failed to pull back the AMS C Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0702730000020005": "Failed to feed the AMS C Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0702800000010001": "AMS C The current sensor of heater 1 is abnormal.",
+  "0702800000010002": "AMS C The heater 1 is disconnected, which may be due to poor connector contact.",
+  "0702800000010003": "AMS C The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0702800000010004": "AMS C The heater 1 is heating abnormally.",
+  "0702810000010001": "AMS C The current sensor of heater 2 is abnormal.",
+  "0702810000010002": "AMS C The heater 2 is disconnected, which may be due to poor connector contact.",
+  "0702810000010003": "AMS C The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0702810000010004": "AMS C The heater 2 is heating abnormally.",
+  "0702860000010004": "AMS C is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "0702900000010002": "AMS C The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "0702900000010003": "AMS C The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "0702900000010004": "The current sensor of AMS C exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0702900000020001": "AMS C The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "0702910000010002": "AMS C The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "0702910000010003": "AMS C The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "0702910000010004": "The current sensor of AMS C exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0702910000020001": "AMS C The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "0702920000010001": "AMS C The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "0702920000020002": "AMS C The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "0702920000020003": "The AMS C heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "0702930000010001": "AMS C The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "0702930000020002": "AMS C The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "0702930000020003": "The AMS C heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "0702940000010001": "AMS C The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "0702940000010002": "Temperature sensor 1 on the AMS C heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0702950000010001": "AMS C The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "0702950000010002": "Temperature sensor 2 on the AMS C heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0702960000010001": "AMS C The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "0702960000010003": "AMS C Unable to start drying; please pull out the filament from filament hub and try again.",
+  "0702960000020002": "AMS C Environmental temperature is too low, which will affect the drying capability.",
+  "0702960000020004": "AMS C The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "0702970000030001": "AMS C chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "0702980000020001": "AMS C The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "0702980000020002": "AMS C The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "0703010000010001": "The AMS D assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "0703010000010003": "The AMS D assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0703010000010004": "The AMS D assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "0703010000010005": "AMS D The current sensor of assist motor may be faulty.",
+  "0703010000010011": "AMS D The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0703010000020002": "The AMS D assist motor is overloaded. The filament may be tangled or stuck.",
+  "0703010000020006": "AMS D The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "0703010000020007": "AMS D The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "0703010000020008": "AMS D The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "0703010000020009": "AMS D The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "0703010000020010": "AMS D The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "0703010000020011": "AMS D The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0703020000010001": "AMS D Filament speed and length error: The filament odometry may be faulty.",
+  "0703020000020002": "AMS D The odometer has no signal. The odometer connector may have poor contact.",
+  "0703020000020003": "AMS D filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "0703100000010001": "The AMS D slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0703100000010003": "The AMS D slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0703100000020002": "The AMS D slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "0703100000020004": "AMS D The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0703110000010001": "The AMS D slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0703110000010003": "The AMS D slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0703110000020002": "The AMS D slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "0703110000020004": "AMS D The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0703120000010001": "The AMS D slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0703120000010003": "The AMS D slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0703120000020002": "The AMS D slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "0703120000020004": "AMS D The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0703130000010001": "The AMS D slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0703130000010003": "The AMS D slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0703130000020002": "The AMS D slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "0703130000020004": "AMS D The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0703200000010081": "Failed to read the filament information from AMS D slot 1. The AMS main board may be malfunctioning.",
+  "0703200000010082": "Failed to read the filament information from AMS D slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0703200000010083": "Failed to read the filament information from AMS D slot 1. The RFID tag may be damaged.",
+  "0703200000010084": "Failed to read the filament information from AMS D slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0703200000010085": "Failed to read the filament information from AMS D slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0703200000010086": "Failed to read the filament information from AMS D slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0703200000020001": "AMS D Slot 1 filament has run out. Please insert a new filament.",
+  "0703200000020002": "AMS D Slot 1 is empty; please insert a new filament.",
+  "0703200000020003": "AMS D Slot 1's filament may be broken in AMS.",
+  "0703200000020004": "AMS D Slot 1 filament may be broken in the tool head.",
+  "0703200000020005": "AMS D Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0703200000020006": "AMS D has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0703200000020007": "AMS D Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0703200000020008": "AMS D Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0703200000020009": "Failed to extrude AMS D Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070320000002000A": "Failed to adjust the filament position of AMS D Slot 1. The filament or buffer may be stuck.",
+  "0703200000020010": "AMS D slot 1 feeds filament out of AMS timeout.",
+  "0703200000020011": "AMS D slot 1 pulls filament back to AMS timeout.",
+  "0703200000020012": "AMS D slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "0703200000020013": "AMS D slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0703200000020014": "AMS D slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0703200000020015": "AMS D slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0703200000020016": "AMS D slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0703200000020017": "AMS D slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0703200000020018": "AMS D slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0703200000020019": "AMS D slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0703200000020020": "AMS D slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0703200000020021": "AMS D slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0703200000020022": "AMS D slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0703200000020023": "AMS D slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0703200000020024": "AMS D slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0703200000020025": "AMS D slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0703200000020026": "AMS D slot 1 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0703200000020027": "AMS D slot 1 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0703200000020057": "The RFID-tag on AMS D Slot 1 cannot be identified.",
+  "0703200000030001": "AMS D Slot 1 filament has run out. Please wait while old filament is purged.",
+  "0703200000030002": "AMS D Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "0703210000010081": "Failed to read the filament information from AMS D slot 2. The AMS main board may be malfunctioning.",
+  "0703210000010082": "Failed to read the filament information from AMS D slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0703210000010083": "Failed to read the filament information from AMS D slot 2. The RFID tag may be damaged.",
+  "0703210000010084": "Failed to read the filament information from AMS D slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0703210000010085": "Failed to read the filament information from AMS D slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0703210000010086": "Failed to read the filament information from AMS D slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0703210000020001": "AMS D Slot 2 filament has run out. Please insert a new filament.",
+  "0703210000020002": "AMS D Slot 2 is empty; please insert a new filament.",
+  "0703210000020003": "AMS D Slot 2's filament may be broken in AMS.",
+  "0703210000020004": "AMS D Slot 2 filament may be broken in the tool head.",
+  "0703210000020005": "AMS D Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0703210000020006": "AMS D has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0703210000020007": "AMS D Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0703210000020008": "AMS D Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0703210000020009": "Failed to extrude AMS D Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070321000002000A": "Failed to adjust the filament position of AMS D Slot 2. The filament or buffer may be stuck.",
+  "0703210000020010": "AMS D slot 2 feeds filament out of AMS timeout.",
+  "0703210000020011": "AMS D slot 2 pulls filament back to AMS timeout.",
+  "0703210000020012": "AMS D slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "0703210000020013": "AMS D slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0703210000020014": "AMS D slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0703210000020015": "AMS D slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0703210000020016": "AMS D slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0703210000020017": "AMS D slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0703210000020018": "AMS D slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0703210000020019": "AMS D slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0703210000020020": "AMS D slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0703210000020021": "AMS D slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0703210000020022": "AMS D slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0703210000020023": "AMS D slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0703210000020024": "AMS D slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0703210000020025": "AMS D slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0703210000020026": "AMS D slot 2 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0703210000020027": "AMS D slot 2 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0703210000020057": "The RFID-tag on AMS D Slot 2 cannot be identified.",
+  "0703210000030001": "AMS D Slot 2 filament has run out. Please wait while old filament is purged.",
+  "0703210000030002": "AMS D Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "0703220000010081": "Failed to read the filament information from AMS D slot 3. The AMS main board may be malfunctioning.",
+  "0703220000010082": "Failed to read the filament information from AMS D slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0703220000010083": "Failed to read the filament information from AMS D slot 3. The RFID tag may be damaged.",
+  "0703220000010084": "Failed to read the filament information from AMS D slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0703220000010085": "Failed to read the filament information from AMS D slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0703220000010086": "Failed to read the filament information from AMS D slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0703220000020001": "AMS D Slot 3 filament has run out. Please insert a new filament.",
+  "0703220000020002": "AMS D Slot 3 is empty; please insert a new filament.",
+  "0703220000020003": "AMS D Slot 3's filament may be broken in AMS.",
+  "0703220000020004": "AMS D Slot 3 filament may be broken in the tool head.",
+  "0703220000020005": "AMS D Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0703220000020006": "AMS D has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0703220000020007": "AMS D Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0703220000020008": "AMS D Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0703220000020009": "Failed to extrude AMS D Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070322000002000A": "Failed to adjust the filament position of AMS D Slot 3. The filament or buffer may be stuck.",
+  "0703220000020010": "AMS D slot 3 feeds filament out of AMS timeout.",
+  "0703220000020011": "AMS D slot 3 pulls filament back to AMS timeout.",
+  "0703220000020012": "AMS D slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "0703220000020013": "AMS D slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0703220000020014": "AMS D slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0703220000020015": "AMS D slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0703220000020016": "AMS D slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0703220000020017": "AMS D slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0703220000020018": "AMS D slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0703220000020019": "AMS D slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0703220000020020": "AMS D slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0703220000020021": "AMS D slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0703220000020022": "AMS D slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0703220000020023": "AMS D slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0703220000020024": "AMS D slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0703220000020025": "AMS D slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0703220000020026": "AMS D slot 3 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0703220000020027": "AMS D slot 3 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0703220000020057": "The RFID-tag on AMS D Slot 3 cannot be identified.",
+  "0703220000030001": "AMS D Slot 3 filament has run out. Please wait while old filament is purged.",
+  "0703220000030002": "AMS D Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "0703230000010081": "Failed to read the filament information from AMS D slot 4. The AMS main board may be malfunctioning.",
+  "0703230000010082": "Failed to read the filament information from AMS D slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0703230000010083": "Failed to read the filament information from AMS D slot 4. The RFID tag may be damaged.",
+  "0703230000010084": "Failed to read the filament information from AMS D slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0703230000010085": "Failed to read the filament information from AMS D slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0703230000010086": "Failed to read the filament information from AMS D slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0703230000020001": "AMS D Slot 4 filament has run out. Please insert a new filament.",
+  "0703230000020002": "AMS D Slot 4 is empty; please insert a new filament.",
+  "0703230000020003": "AMS D Slot 4's filament may be broken in AMS.",
+  "0703230000020004": "AMS D Slot 4 filament may be broken in the tool head.",
+  "0703230000020005": "AMS D Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0703230000020006": "AMS D has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0703230000020007": "AMS D Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0703230000020008": "AMS D Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0703230000020009": "Failed to extrude AMS D Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070323000002000A": "Failed to adjust the filament position of AMS D Slot 4. The filament or buffer may be stuck.",
+  "0703230000020010": "AMS D slot 4 feeds filament out of AMS timeout.",
+  "0703230000020011": "AMS D slot 4 pulls filament back to AMS timeout.",
+  "0703230000020012": "AMS D slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "0703230000020013": "AMS D slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0703230000020014": "AMS D slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0703230000020015": "AMS D slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0703230000020016": "AMS D slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0703230000020017": "AMS D slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0703230000020018": "AMS D slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0703230000020019": "AMS D slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0703230000020020": "AMS D slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0703230000020021": "AMS D slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0703230000020022": "AMS D slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0703230000020023": "AMS D slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0703230000020024": "AMS D slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0703230000020025": "AMS D slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0703230000020026": "AMS D slot 4 assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "0703230000020027": "AMS D slot 4 assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "0703230000020057": "The RFID-tag on AMS D Slot 4 cannot be identified.",
+  "0703230000030001": "AMS D Slot 4 filament has run out. Please wait while old filament is purged.",
+  "0703230000030002": "AMS D Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "0703250000020001": "AMS D uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "0703260000020001": "The induction hotend is heating up, and the drying power of AMS D will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "0703300000010001": "The AMS D RFID 1 board has an error.",
+  "0703300000010004": "Encryption chip failure",
+  "0703300000020002": "The RFID-tag on AMS D Slot1 is damaged, or its content cannot be identified.",
+  "0703300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0703310000010001": "The AMS D RFID 2 board has an error.",
+  "0703310000010004": "Encryption chip failure",
+  "0703310000020002": "The RFID-tag on AMS D Slot2 is damaged, or its content cannot be identified.",
+  "0703310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0703320000020002": "The RFID-tag on AMS D Slot3 is damaged, or its content cannot be identified.",
+  "0703330000020002": "The RFID-tag on AMS D Slot4 is damaged, or its content cannot be identified.",
+  "0703350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "0703350000010002": "AMS D The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "0703400000020001": "AMS D Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "0703500000020001": "AMS D communication is abnormal; please check the connection cable.",
+  "0703500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0703550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "0703550000010003": "AMS D was detected offline during the AMS initialization process.",
+  "0703550000010004": "The binding between AMS D and the extruder is incorrect. Please run the AMS Setup.",
+  "0703550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "0703560000030001": "AMS D is undergoing dry cooling; please wait for it to cool down before operating.",
+  "0703600000020001": "The AMS D Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0703610000020001": "The AMS D Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0703620000020001": "The AMS D Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0703630000020001": "The AMS D Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0703700000020001": "Failed to pull out the AMS D Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0703700000020002": "Failed to feed the AMS D Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0703700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "0703700000020004": "Failed to pull back the AMS D Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0703700000020005": "Failed to feed the AMS D Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0703700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "0703700000020007": "AMS filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "0703700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "070370000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "0703710000020001": "Failed to pull out the AMS D Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0703710000020002": "Failed to feed the AMS D Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0703710000020004": "Failed to pull back the AMS D Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0703710000020005": "Failed to feed the AMS D Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0703720000020001": "Failed to pull out the AMS D Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0703720000020002": "Failed to feed the AMS D Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0703720000020004": "Failed to pull back the AMS D Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0703720000020005": "Failed to feed the AMS D Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0703730000020001": "Failed to pull out the AMS D Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0703730000020002": "Failed to feed the AMS D Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0703730000020004": "Failed to pull back the AMS D Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0703730000020005": "Failed to feed the AMS D Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0703800000010001": "AMS D The current sensor of heater 1 is abnormal.",
+  "0703800000010002": "AMS D The heater 1 is disconnected, which may be due to poor connector contact.",
+  "0703800000010003": "AMS D The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0703800000010004": "AMS D The heater 1 is heating abnormally.",
+  "0703810000010001": "AMS D The current sensor of heater 2 is abnormal.",
+  "0703810000010002": "AMS D The heater 2 is disconnected, which may be due to poor connector contact.",
+  "0703810000010003": "AMS D The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0703810000010004": "AMS D The heater 2 is heating abnormally.",
+  "0703860000010004": "AMS D is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "0703900000010002": "AMS D The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "0703900000010003": "AMS D The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "0703900000010004": "The current sensor of AMS D exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0703900000020001": "AMS D The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "0703910000010002": "AMS D The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "0703910000010003": "AMS D The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "0703910000010004": "The current sensor of AMS D exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0703910000020001": "AMS D The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "0703920000010001": "AMS D The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "0703920000020002": "AMS D The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "0703920000020003": "The AMS D heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "0703930000010001": "AMS D The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "0703930000020002": "AMS D The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "0703930000020003": "The AMS D heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "0703940000010001": "AMS D The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "0703940000010002": "Temperature sensor 1 on the AMS D heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0703950000010001": "AMS D The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "0703950000010002": "Temperature sensor 2 on the AMS D heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0703960000010001": "AMS D The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "0703960000010003": "AMS D Unable to start drying; please pull out the filament from filament hub and try again.",
+  "0703960000020002": "AMS D Environmental temperature is too low, which will affect the drying capability.",
+  "0703960000020004": "AMS D The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "0703970000030001": "AMS D chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "0703980000020001": "AMS D The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "0703980000020002": "AMS D The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "0704010000010001": "The AMS E assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "0704010000010003": "The AMS E assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0704010000010004": "The AMS E assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "0704010000010005": "AMS E The current sensor of assist motor may be faulty.",
+  "0704010000010011": "AMS E The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0704010000020002": "The AMS E assist motor is overloaded. The filament may be tangled or stuck.",
+  "0704010000020006": "AMS E The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "0704010000020007": "AMS E The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "0704010000020008": "AMS E The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "0704010000020009": "AMS E The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "0704010000020010": "AMS E The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "0704010000020011": "AMS E The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0704020000010001": "AMS E Filament speed and length error: The filament odometry may be faulty.",
+  "0704020000020002": "AMS E The odometer has no signal. The odometer connector may have poor contact.",
+  "0704020000020003": "AMS E filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "0704100000010001": "The AMS E slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0704100000010003": "The AMS E slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0704100000020002": "The AMS E slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "0704100000020004": "AMS E The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0704110000010001": "The AMS E slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0704110000010003": "The AMS E slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0704110000020002": "The AMS E slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "0704110000020004": "AMS E The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0704120000010001": "The AMS E slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0704120000010003": "The AMS E slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0704120000020002": "The AMS E slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "0704120000020004": "AMS E The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0704130000010001": "The AMS E slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0704130000010003": "The AMS E slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0704130000020002": "The AMS E slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "0704130000020004": "AMS E The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0704200000010081": "Failed to read the filament information from AMS E slot 1. The AMS main board may be malfunctioning.",
+  "0704200000010082": "Failed to read the filament information from AMS E slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0704200000010083": "Failed to read the filament information from AMS E slot 1. The RFID tag may be damaged.",
+  "0704200000010084": "Failed to read the filament information from AMS E slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0704200000010085": "Failed to read the filament information from AMS E slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0704200000010086": "Failed to read the filament information from AMS E slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0704200000020001": "AMS E Slot 1 filament has run out. Please insert a new filament.",
+  "0704200000020002": "AMS E Slot 1 is empty; please insert a new filament.",
+  "0704200000020003": "AMS E Slot 1's filament may be broken in AMS.",
+  "0704200000020004": "AMS E Slot 1 filament may be broken in the tool head.",
+  "0704200000020005": "AMS E Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0704200000020006": "AMS E has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0704200000020007": "AMS E Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0704200000020008": "AMS E Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0704200000020009": "Failed to extrude AMS E Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070420000002000A": "Failed to adjust the filament position of AMS E Slot 1. The filament or buffer may be stuck.",
+  "0704200000020010": "AMS E slot 1 feeds filament out of AMS timeout.",
+  "0704200000020011": "AMS E slot 1 pulls filament back to AMS timeout.",
+  "0704200000020012": "AMS E slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "0704200000020013": "AMS E slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0704200000020014": "AMS E slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0704200000020015": "AMS E slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0704200000020016": "AMS E slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0704200000020017": "AMS E slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0704200000020018": "AMS E slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0704200000020019": "AMS E slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0704200000020020": "AMS E slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0704200000020021": "AMS E slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0704200000020022": "AMS E slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0704200000020023": "AMS E slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0704200000020024": "AMS E slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0704200000020025": "AMS E slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0704200000030001": "AMS E Slot 1 filament has run out. Please wait while old filament is purged.",
+  "0704200000030002": "AMS E Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "0704210000010081": "Failed to read the filament information from AMS E slot 2. The AMS main board may be malfunctioning.",
+  "0704210000010082": "Failed to read the filament information from AMS E slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0704210000010083": "Failed to read the filament information from AMS E slot 2. The RFID tag may be damaged.",
+  "0704210000010084": "Failed to read the filament information from AMS E slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0704210000010085": "Failed to read the filament information from AMS E slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0704210000010086": "Failed to read the filament information from AMS E slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0704210000020001": "AMS E Slot 2 filament has run out. Please insert a new filament.",
+  "0704210000020002": "AMS E Slot 2 is empty; please insert a new filament.",
+  "0704210000020003": "AMS E Slot 2's filament may be broken in AMS.",
+  "0704210000020004": "AMS E Slot 2 filament may be broken in the tool head.",
+  "0704210000020005": "AMS E Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0704210000020006": "AMS E has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0704210000020007": "AMS E Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0704210000020008": "AMS E Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0704210000020009": "Failed to extrude AMS E Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070421000002000A": "Failed to adjust the filament position of AMS E Slot 2. The filament or buffer may be stuck.",
+  "0704210000020010": "AMS E slot 2 feeds filament out of AMS timeout.",
+  "0704210000020011": "AMS E slot 2 pulls filament back to AMS timeout.",
+  "0704210000020012": "AMS E slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "0704210000020013": "AMS E slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0704210000020014": "AMS E slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0704210000020015": "AMS E slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0704210000020016": "AMS E slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0704210000020017": "AMS E slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0704210000020018": "AMS E slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0704210000020019": "AMS E slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0704210000020020": "AMS E slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0704210000020021": "AMS E slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0704210000020022": "AMS E slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0704210000020023": "AMS E slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0704210000020024": "AMS E slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0704210000020025": "AMS E slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0704210000030001": "AMS E Slot 2 filament has run out. Please wait while old filament is purged.",
+  "0704210000030002": "AMS E Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "0704220000010081": "Failed to read the filament information from AMS E slot 3. The AMS main board may be malfunctioning.",
+  "0704220000010082": "Failed to read the filament information from AMS E slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0704220000010083": "Failed to read the filament information from AMS E slot 3. The RFID tag may be damaged.",
+  "0704220000010084": "Failed to read the filament information from AMS E slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0704220000010085": "Failed to read the filament information from AMS E slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0704220000010086": "Failed to read the filament information from AMS E slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0704220000020001": "AMS E Slot 3 filament has run out. Please insert a new filament.",
+  "0704220000020002": "AMS E Slot 3 is empty; please insert a new filament.",
+  "0704220000020003": "AMS E Slot 3's filament may be broken in AMS.",
+  "0704220000020004": "AMS E Slot 3 filament may be broken in the tool head.",
+  "0704220000020005": "AMS E Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0704220000020006": "AMS E has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0704220000020007": "AMS E Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0704220000020008": "AMS E Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0704220000020009": "Failed to extrude AMS E Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070422000002000A": "Failed to adjust the filament position of AMS E Slot 3. The filament or buffer may be stuck.",
+  "0704220000020010": "AMS E slot 3 feeds filament out of AMS timeout.",
+  "0704220000020011": "AMS E slot 3 pulls filament back to AMS timeout.",
+  "0704220000020012": "AMS E slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "0704220000020013": "AMS E slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0704220000020014": "AMS E slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0704220000020015": "AMS E slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0704220000020016": "AMS E slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0704220000020017": "AMS E slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0704220000020018": "AMS E slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0704220000020019": "AMS E slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0704220000020020": "AMS E slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0704220000020021": "AMS E slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0704220000020022": "AMS E slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0704220000020023": "AMS E slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0704220000020024": "AMS E slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0704220000020025": "AMS E slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0704220000030001": "AMS E Slot 3 filament has run out. Please wait while old filament is purged.",
+  "0704220000030002": "AMS E Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "0704230000010081": "Failed to read the filament information from AMS E slot 4. The AMS main board may be malfunctioning.",
+  "0704230000010082": "Failed to read the filament information from AMS E slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0704230000010083": "Failed to read the filament information from AMS E slot 4. The RFID tag may be damaged.",
+  "0704230000010084": "Failed to read the filament information from AMS E slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0704230000010085": "Failed to read the filament information from AMS E slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0704230000010086": "Failed to read the filament information from AMS E slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0704230000020001": "AMS E Slot 4 filament has run out. Please insert a new filament.",
+  "0704230000020002": "AMS E Slot 4 is empty; please insert a new filament.",
+  "0704230000020003": "AMS E Slot 4's filament may be broken in AMS.",
+  "0704230000020004": "AMS E Slot 4 filament may be broken in the tool head.",
+  "0704230000020005": "AMS E Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0704230000020006": "AMS E has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0704230000020007": "AMS E Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0704230000020008": "AMS E Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0704230000020009": "Failed to extrude AMS E Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070423000002000A": "Failed to adjust the filament position of AMS E Slot 4. The filament or buffer may be stuck.",
+  "0704230000020010": "AMS E slot 4 feeds filament out of AMS timeout.",
+  "0704230000020011": "AMS E slot 4 pulls filament back to AMS timeout.",
+  "0704230000020012": "AMS E slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "0704230000020013": "AMS E slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0704230000020014": "AMS E slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0704230000020015": "AMS E slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0704230000020016": "AMS E slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0704230000020017": "AMS E slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0704230000020018": "AMS E slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0704230000020019": "AMS E slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0704230000020020": "AMS E slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0704230000020021": "AMS E slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0704230000020022": "AMS E slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0704230000020023": "AMS E slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0704230000020024": "AMS E slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0704230000020025": "AMS E slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0704230000030001": "AMS E Slot 4 filament has run out. Please wait while old filament is purged.",
+  "0704230000030002": "AMS E Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "0704250000020001": "AMS E uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "0704260000020001": "The induction hotend is heating up, and the drying power of AMS E will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "0704300000010001": "The AMS E RFID 1 board has an error.",
+  "0704300000010004": "Encryption chip failure",
+  "0704300000020002": "The RFID-tag on AMS E Slot1 is damaged, or its content cannot be identified.",
+  "0704300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0704310000010001": "The AMS E RFID 2 board has an error.",
+  "0704310000010004": "Encryption chip failure",
+  "0704310000020002": "The RFID-tag on AMS E Slot2 is damaged, or its content cannot be identified.",
+  "0704310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0704320000020002": "The RFID-tag on AMS E Slot3 is damaged, or its content cannot be identified.",
+  "0704330000020002": "The RFID-tag on AMS E Slot4 is damaged, or its content cannot be identified.",
+  "0704350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "0704350000010002": "AMS E The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "0704400000020001": "AMS E Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "0704500000020001": "AMS E communication is abnormal; please check the connection cable.",
+  "0704500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0704550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "0704550000010003": "AMS E was detected offline during the AMS initialization process.",
+  "0704550000010004": "The binding between AMS E and the extruder is incorrect. Please run the AMS Setup.",
+  "0704550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "0704560000030001": "AMS E is undergoing dry cooling; please wait for it to cool down before operating.",
+  "0704600000020001": "The AMS E Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0704610000020001": "The AMS E Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0704620000020001": "The AMS E Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0704630000020001": "The AMS E Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0704700000020001": "Failed to pull out the AMS E Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0704700000020002": "Failed to feed the AMS E Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0704700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "0704700000020004": "Failed to pull back the AMS E Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0704700000020005": "Failed to feed the AMS E Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0704700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "0704700000020007": "AMS filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "0704700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "070470000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "0704710000020001": "Failed to pull out the AMS E Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0704710000020002": "Failed to feed the AMS E Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0704710000020004": "Failed to pull back the AMS E Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0704710000020005": "Failed to feed the AMS E Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0704720000020001": "Failed to pull out the AMS E Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0704720000020002": "Failed to feed the AMS E Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0704720000020004": "Failed to pull back the AMS E Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0704720000020005": "Failed to feed the AMS E Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0704730000020001": "Failed to pull out the AMS E Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0704730000020002": "Failed to feed the AMS E Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0704730000020004": "Failed to pull back the AMS E Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0704730000020005": "Failed to feed the AMS E Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0704800000010001": "AMS E The current sensor of heater 1 is abnormal.",
+  "0704800000010002": "AMS E The heater 1 is disconnected, which may be due to poor connector contact.",
+  "0704800000010003": "AMS E The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0704800000010004": "AMS E The heater 1 is heating abnormally.",
+  "0704810000010001": "AMS E The current sensor of heater 2 is abnormal.",
+  "0704810000010002": "AMS E The heater 2 is disconnected, which may be due to poor connector contact.",
+  "0704810000010003": "AMS E The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0704810000010004": "AMS E The heater 2 is heating abnormally.",
+  "0704900000010002": "AMS E The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "0704900000010003": "AMS E The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "0704900000010004": "The current sensor of AMS E exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0704900000020001": "AMS E The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "0704910000010002": "AMS E The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "0704910000010003": "AMS E The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "0704910000010004": "The current sensor of AMS E exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0704910000020001": "AMS E The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "0704920000010001": "AMS E The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "0704920000020002": "AMS E The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "0704920000020003": "The AMS E heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "0704930000010001": "AMS E The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "0704930000020002": "AMS E The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "0704930000020003": "The AMS E heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "0704940000010001": "AMS E The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "0704940000010002": "Temperature sensor 1 on the AMS E heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0704950000010001": "AMS E The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "0704950000010002": "Temperature sensor 2 on the AMS E heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0704960000010001": "AMS E The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "0704960000010003": "AMS E Unable to start drying; please pull out the filament from filament hub and try again.",
+  "0704960000020002": "AMS E Environmental temperature is too low, which will affect the drying capability.",
+  "0704960000020004": "AMS E The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "0704970000030001": "AMS E chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "0704980000020001": "AMS E The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "0704980000020002": "AMS E The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "0705010000010001": "The AMS F assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "0705010000010003": "The AMS F assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0705010000010004": "The AMS F assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "0705010000010005": "AMS F The current sensor of assist motor may be faulty.",
+  "0705010000010011": "AMS F The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0705010000020002": "The AMS F assist motor is overloaded. The filament may be tangled or stuck.",
+  "0705010000020006": "AMS F The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "0705010000020007": "AMS F The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "0705010000020008": "AMS F The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "0705010000020009": "AMS F The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "0705010000020010": "AMS F The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "0705010000020011": "AMS F The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0705020000010001": "AMS F Filament speed and length error: The filament odometry may be faulty.",
+  "0705020000020002": "AMS F The odometer has no signal. The odometer connector may have poor contact.",
+  "0705020000020003": "AMS F filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "0705100000010001": "The AMS F slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0705100000010003": "The AMS F slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0705100000020002": "The AMS F slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "0705100000020004": "AMS F The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0705110000010001": "The AMS F slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0705110000010003": "The AMS F slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0705110000020002": "The AMS F slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "0705110000020004": "AMS F The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0705120000010001": "The AMS F slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0705120000010003": "The AMS F slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0705120000020002": "The AMS F slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "0705120000020004": "AMS F The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0705130000010001": "The AMS F slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0705130000010003": "The AMS F slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0705130000020002": "The AMS F slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "0705130000020004": "AMS F The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0705200000010081": "Failed to read the filament information from AMS F slot 1. The AMS main board may be malfunctioning.",
+  "0705200000010082": "Failed to read the filament information from AMS F slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0705200000010083": "Failed to read the filament information from AMS F slot 1. The RFID tag may be damaged.",
+  "0705200000010084": "Failed to read the filament information from AMS F slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0705200000010085": "Failed to read the filament information from AMS F slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0705200000010086": "Failed to read the filament information from AMS F slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0705200000020001": "AMS F Slot 1 filament has run out. Please insert a new filament.",
+  "0705200000020002": "AMS F Slot 1 is empty; please insert a new filament.",
+  "0705200000020003": "AMS F Slot 1's filament may be broken in AMS.",
+  "0705200000020004": "AMS F Slot 1 filament may be broken in the tool head.",
+  "0705200000020005": "AMS F Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0705200000020006": "AMS F has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0705200000020007": "AMS F Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0705200000020008": "AMS F Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0705200000020009": "Failed to extrude AMS F Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070520000002000A": "Failed to adjust the filament position of AMS F Slot 1. The filament or buffer may be stuck.",
+  "0705200000020010": "AMS F slot 1 feeds filament out of AMS timeout.",
+  "0705200000020011": "AMS F slot 1 pulls filament back to AMS timeout.",
+  "0705200000020012": "AMS F slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "0705200000020013": "AMS F slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0705200000020014": "AMS F slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0705200000020015": "AMS F slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0705200000020016": "AMS F slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0705200000020017": "AMS F slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0705200000020018": "AMS F slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0705200000020019": "AMS F slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0705200000020020": "AMS F slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0705200000020021": "AMS F slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0705200000020022": "AMS F slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0705200000020023": "AMS F slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0705200000020024": "AMS F slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0705200000020025": "AMS F slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0705200000030001": "AMS F Slot 1 filament has run out. Please wait while old filament is purged.",
+  "0705200000030002": "AMS F Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "0705210000010081": "Failed to read the filament information from AMS F slot 2. The AMS main board may be malfunctioning.",
+  "0705210000010082": "Failed to read the filament information from AMS F slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0705210000010083": "Failed to read the filament information from AMS F slot 2. The RFID tag may be damaged.",
+  "0705210000010084": "Failed to read the filament information from AMS F slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0705210000010085": "Failed to read the filament information from AMS F slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0705210000010086": "Failed to read the filament information from AMS F slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0705210000020001": "AMS F Slot 2 filament has run out. Please insert a new filament.",
+  "0705210000020002": "AMS F Slot 2 is empty; please insert a new filament.",
+  "0705210000020003": "AMS F Slot 2's filament may be broken in AMS.",
+  "0705210000020004": "AMS F Slot 2 filament may be broken in the tool head.",
+  "0705210000020005": "AMS F Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0705210000020006": "AMS F has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0705210000020007": "AMS F Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0705210000020008": "AMS F Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0705210000020009": "Failed to extrude AMS F Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070521000002000A": "Failed to adjust the filament position of AMS F Slot 2. The filament or buffer may be stuck.",
+  "0705210000020010": "AMS F slot 2 feeds filament out of AMS timeout.",
+  "0705210000020011": "AMS F slot 2 pulls filament back to AMS timeout.",
+  "0705210000020012": "AMS F slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "0705210000020013": "AMS F slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0705210000020014": "AMS F slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0705210000020015": "AMS F slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0705210000020016": "AMS F slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0705210000020017": "AMS F slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0705210000020018": "AMS F slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0705210000020019": "AMS F slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0705210000020020": "AMS F slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0705210000020021": "AMS F slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0705210000020022": "AMS F slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0705210000020023": "AMS F slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0705210000020024": "AMS F slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0705210000020025": "AMS F slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0705210000030001": "AMS F Slot 2 filament has run out. Please wait while old filament is purged.",
+  "0705210000030002": "AMS F Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "0705220000010081": "Failed to read the filament information from AMS F slot 3. The AMS main board may be malfunctioning.",
+  "0705220000010082": "Failed to read the filament information from AMS F slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0705220000010083": "Failed to read the filament information from AMS F slot 3. The RFID tag may be damaged.",
+  "0705220000010084": "Failed to read the filament information from AMS F slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0705220000010085": "Failed to read the filament information from AMS F slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0705220000010086": "Failed to read the filament information from AMS F slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0705220000020001": "AMS F Slot 3 filament has run out. Please insert a new filament.",
+  "0705220000020002": "AMS F Slot 3 is empty; please insert a new filament.",
+  "0705220000020003": "AMS F Slot 3's filament may be broken in AMS.",
+  "0705220000020004": "AMS F Slot 3 filament may be broken in the tool head.",
+  "0705220000020005": "AMS F Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0705220000020006": "AMS F has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0705220000020007": "AMS F Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0705220000020008": "AMS F Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0705220000020009": "Failed to extrude AMS F Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070522000002000A": "Failed to adjust the filament position of AMS F Slot 3. The filament or buffer may be stuck.",
+  "0705220000020010": "AMS F slot 3 feeds filament out of AMS timeout.",
+  "0705220000020011": "AMS F slot 3 pulls filament back to AMS timeout.",
+  "0705220000020012": "AMS F slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "0705220000020013": "AMS F slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0705220000020014": "AMS F slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0705220000020015": "AMS F slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0705220000020016": "AMS F slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0705220000020017": "AMS F slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0705220000020018": "AMS F slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0705220000020019": "AMS F slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0705220000020020": "AMS F slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0705220000020021": "AMS F slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0705220000020022": "AMS F slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0705220000020023": "AMS F slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0705220000020024": "AMS F slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0705220000020025": "AMS F slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0705220000030001": "AMS F Slot 3 filament has run out. Please wait while old filament is purged.",
+  "0705220000030002": "AMS F Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "0705230000010081": "Failed to read the filament information from AMS F slot 4. The AMS main board may be malfunctioning.",
+  "0705230000010082": "Failed to read the filament information from AMS F slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0705230000010083": "Failed to read the filament information from AMS F slot 4. The RFID tag may be damaged.",
+  "0705230000010084": "Failed to read the filament information from AMS F slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0705230000010085": "Failed to read the filament information from AMS F slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0705230000010086": "Failed to read the filament information from AMS F slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0705230000020001": "AMS F Slot 4 filament has run out. Please insert a new filament.",
+  "0705230000020002": "AMS F Slot 4 is empty; please insert a new filament.",
+  "0705230000020003": "AMS F Slot 4's filament may be broken in AMS.",
+  "0705230000020004": "AMS F Slot 4 filament may be broken in the tool head.",
+  "0705230000020005": "AMS F Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0705230000020006": "AMS F has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0705230000020007": "AMS F Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0705230000020008": "AMS F Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0705230000020009": "Failed to extrude AMS F Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070523000002000A": "Failed to adjust the filament position of AMS F Slot 4. The filament or buffer may be stuck.",
+  "0705230000020010": "AMS F slot 4 feeds filament out of AMS timeout.",
+  "0705230000020011": "AMS F slot 4 pulls filament back to AMS timeout.",
+  "0705230000020012": "AMS F slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "0705230000020013": "AMS F slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0705230000020014": "AMS F slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0705230000020015": "AMS F slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0705230000020016": "AMS F slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0705230000020017": "AMS F slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0705230000020018": "AMS F slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0705230000020019": "AMS F slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0705230000020020": "AMS F slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0705230000020021": "AMS F slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0705230000020022": "AMS F slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0705230000020023": "AMS F slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0705230000020024": "AMS F slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0705230000020025": "AMS F slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0705230000030001": "AMS F Slot 4 filament has run out. Please wait while old filament is purged.",
+  "0705230000030002": "AMS F Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "0705250000020001": "AMS F uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "0705260000020001": "The induction hotend is heating up, and the drying power of AMS F will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "0705300000010001": "The AMS F RFID 1 board has an error.",
+  "0705300000010004": "Encryption chip failure",
+  "0705300000020002": "The RFID-tag on AMS F Slot1 is damaged, or its content cannot be identified.",
+  "0705300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0705310000010001": "The AMS F RFID 2 board has an error.",
+  "0705310000010004": "Encryption chip failure",
+  "0705310000020002": "The RFID-tag on AMS F Slot2 is damaged, or its content cannot be identified.",
+  "0705310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0705320000020002": "The RFID-tag on AMS F Slot3 is damaged, or its content cannot be identified.",
+  "0705330000020002": "The RFID-tag on AMS F Slot4 is damaged, or its content cannot be identified.",
+  "0705350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "0705350000010002": "AMS F The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "0705400000020001": "AMS F Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "0705500000020001": "AMS F communication is abnormal; please check the connection cable.",
+  "0705500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0705550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "0705550000010003": "AMS F was detected offline during the AMS initialization process.",
+  "0705550000010004": "The binding between AMS F and the extruder is incorrect. Please run the AMS Setup.",
+  "0705550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "0705560000030001": "AMS F is undergoing dry cooling; please wait for it to cool down before operating.",
+  "0705600000020001": "The AMS F Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0705610000020001": "The AMS F Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0705620000020001": "The AMS F Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0705630000020001": "The AMS F Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0705700000020001": "Failed to pull out the AMS F Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0705700000020002": "Failed to feed the AMS F Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0705700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "0705700000020004": "Failed to pull back the AMS F Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0705700000020005": "Failed to feed the AMS F Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0705700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "0705700000020007": "AMS filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "0705700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "070570000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "0705710000020001": "Failed to pull out the AMS F Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0705710000020002": "Failed to feed the AMS F Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0705710000020004": "Failed to pull back the AMS F Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0705710000020005": "Failed to feed the AMS F Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0705720000020001": "Failed to pull out the AMS F Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0705720000020002": "Failed to feed the AMS F Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0705720000020004": "Failed to pull back the AMS F Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0705720000020005": "Failed to feed the AMS F Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0705730000020001": "Failed to pull out the AMS F Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0705730000020002": "Failed to feed the AMS F Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0705730000020004": "Failed to pull back the AMS F Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0705730000020005": "Failed to feed the AMS F Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0705800000010001": "AMS F The current sensor of heater 1 is abnormal.",
+  "0705800000010002": "AMS F The heater 1 is disconnected, which may be due to poor connector contact.",
+  "0705800000010003": "AMS F The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0705800000010004": "AMS F The heater 1 is heating abnormally.",
+  "0705810000010001": "AMS F The current sensor of heater 2 is abnormal.",
+  "0705810000010002": "AMS F The heater 2 is disconnected, which may be due to poor connector contact.",
+  "0705810000010003": "AMS F The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0705810000010004": "AMS F The heater 2 is heating abnormally.",
+  "0705900000010002": "AMS F The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "0705900000010003": "AMS F The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "0705900000010004": "The current sensor of AMS F exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0705900000020001": "AMS F The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "0705910000010002": "AMS F The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "0705910000010003": "AMS F The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "0705910000010004": "The current sensor of AMS F exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0705910000020001": "AMS F The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "0705920000010001": "AMS F The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "0705920000020002": "AMS F The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "0705920000020003": "The AMS F heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "0705930000010001": "AMS F The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "0705930000020002": "AMS F The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "0705930000020003": "The AMS F heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "0705940000010001": "AMS F The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "0705940000010002": "Temperature sensor 1 on the AMS F heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0705950000010001": "AMS F The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "0705950000010002": "Temperature sensor 2 on the AMS F heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0705960000010001": "AMS F The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "0705960000010003": "AMS F Unable to start drying; please pull out the filament from filament hub and try again.",
+  "0705960000020002": "AMS F Environmental temperature is too low, which will affect the drying capability.",
+  "0705960000020004": "AMS F The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "0705970000030001": "AMS F chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "0705980000020001": "AMS F The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "0705980000020002": "AMS F The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "0706010000010001": "The AMS G assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "0706010000010003": "The AMS G assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0706010000010004": "The AMS G assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "0706010000010005": "AMS G The current sensor of assist motor may be faulty.",
+  "0706010000010011": "AMS G The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0706010000020002": "The AMS G assist motor is overloaded. The filament may be tangled or stuck.",
+  "0706010000020006": "AMS G The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "0706010000020007": "AMS G The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "0706010000020008": "AMS G The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "0706010000020009": "AMS G The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "0706010000020010": "AMS G The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "0706010000020011": "AMS G The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0706020000010001": "AMS G Filament speed and length error: The filament odometry may be faulty.",
+  "0706020000020002": "AMS G The odometer has no signal. The odometer connector may have poor contact.",
+  "0706020000020003": "AMS G filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "0706100000010001": "The AMS G slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0706100000010003": "The AMS G slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0706100000020002": "The AMS G slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "0706100000020004": "AMS G The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0706110000010001": "The AMS G slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0706110000010003": "The AMS G slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0706110000020002": "The AMS G slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "0706110000020004": "AMS G The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0706120000010001": "The AMS G slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0706120000010003": "The AMS G slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0706120000020002": "The AMS G slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "0706120000020004": "AMS G The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0706130000010001": "The AMS G slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0706130000010003": "The AMS G slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0706130000020002": "The AMS G slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "0706130000020004": "AMS G The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0706200000010081": "Failed to read the filament information from AMS G slot 1. The AMS main board may be malfunctioning.",
+  "0706200000010082": "Failed to read the filament information from AMS G slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0706200000010083": "Failed to read the filament information from AMS G slot 1. The RFID tag may be damaged.",
+  "0706200000010084": "Failed to read the filament information from AMS G slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0706200000010085": "Failed to read the filament information from AMS G slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0706200000010086": "Failed to read the filament information from AMS G slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0706200000020001": "AMS G Slot 1 filament has run out. Please insert a new filament.",
+  "0706200000020002": "AMS G Slot 1 is empty; please insert a new filament.",
+  "0706200000020003": "AMS G Slot 1's filament may be broken in AMS.",
+  "0706200000020004": "AMS G Slot 1 filament may be broken in the tool head.",
+  "0706200000020005": "AMS G Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0706200000020006": "AMS G has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0706200000020007": "AMS G Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0706200000020008": "AMS G Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0706200000020009": "Failed to extrude AMS G Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070620000002000A": "Failed to adjust the filament position of AMS G Slot 1. The filament or buffer may be stuck.",
+  "0706200000020010": "AMS G slot 1 feeds filament out of AMS timeout.",
+  "0706200000020011": "AMS G slot 1 pulls filament back to AMS timeout.",
+  "0706200000020012": "AMS G slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "0706200000020013": "AMS G slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0706200000020014": "AMS G slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0706200000020015": "AMS G slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0706200000020016": "AMS G slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0706200000020017": "AMS G slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0706200000020018": "AMS G slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0706200000020019": "AMS G slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0706200000020020": "AMS G slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0706200000020021": "AMS G slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0706200000020022": "AMS G slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0706200000020023": "AMS G slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0706200000020024": "AMS G slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0706200000020025": "AMS G slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0706200000030001": "AMS G Slot 1 filament has run out. Please wait while old filament is purged.",
+  "0706200000030002": "AMS G Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "0706210000010081": "Failed to read the filament information from AMS G slot 2. The AMS main board may be malfunctioning.",
+  "0706210000010082": "Failed to read the filament information from AMS G slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0706210000010083": "Failed to read the filament information from AMS G slot 2. The RFID tag may be damaged.",
+  "0706210000010084": "Failed to read the filament information from AMS G slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0706210000010085": "Failed to read the filament information from AMS G slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0706210000010086": "Failed to read the filament information from AMS G slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0706210000020001": "AMS G Slot 2 filament has run out. Please insert a new filament.",
+  "0706210000020002": "AMS G Slot 2 is empty; please insert a new filament.",
+  "0706210000020003": "AMS G Slot 2's filament may be broken in AMS.",
+  "0706210000020004": "AMS G Slot 2 filament may be broken in the tool head.",
+  "0706210000020005": "AMS G Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0706210000020006": "AMS G has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0706210000020007": "AMS G Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0706210000020008": "AMS G Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0706210000020009": "Failed to extrude AMS G Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070621000002000A": "Failed to adjust the filament position of AMS G Slot 2. The filament or buffer may be stuck.",
+  "0706210000020010": "AMS G slot 2 feeds filament out of AMS timeout.",
+  "0706210000020011": "AMS G slot 2 pulls filament back to AMS timeout.",
+  "0706210000020012": "AMS G slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "0706210000020013": "AMS G slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0706210000020014": "AMS G slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0706210000020015": "AMS G slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0706210000020016": "AMS G slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0706210000020017": "AMS G slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0706210000020018": "AMS G slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0706210000020019": "AMS G slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0706210000020020": "AMS G slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0706210000020021": "AMS G slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0706210000020022": "AMS G slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0706210000020023": "AMS G slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0706210000020024": "AMS G slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0706210000020025": "AMS G slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0706210000030001": "AMS G Slot 2 filament has run out. Please wait while old filament is purged.",
+  "0706210000030002": "AMS G Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "0706220000010081": "Failed to read the filament information from AMS G slot 3. The AMS main board may be malfunctioning.",
+  "0706220000010082": "Failed to read the filament information from AMS G slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0706220000010083": "Failed to read the filament information from AMS G slot 3. The RFID tag may be damaged.",
+  "0706220000010084": "Failed to read the filament information from AMS G slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0706220000010085": "Failed to read the filament information from AMS G slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0706220000010086": "Failed to read the filament information from AMS G slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0706220000020001": "AMS G Slot 3 filament has run out. Please insert a new filament.",
+  "0706220000020002": "AMS G Slot 3 is empty; please insert a new filament.",
+  "0706220000020003": "AMS G Slot 3's filament may be broken in AMS.",
+  "0706220000020004": "AMS G Slot 3 filament may be broken in the tool head.",
+  "0706220000020005": "AMS G Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0706220000020006": "AMS G has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0706220000020007": "AMS G Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0706220000020008": "AMS G Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0706220000020009": "Failed to extrude AMS G Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070622000002000A": "Failed to adjust the filament position of AMS G Slot 3. The filament or buffer may be stuck.",
+  "0706220000020010": "AMS G slot 3 feeds filament out of AMS timeout.",
+  "0706220000020011": "AMS G slot 3 pulls filament back to AMS timeout.",
+  "0706220000020012": "AMS G slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "0706220000020013": "AMS G slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0706220000020014": "AMS G slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0706220000020015": "AMS G slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0706220000020016": "AMS G slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0706220000020017": "AMS G slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0706220000020018": "AMS G slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0706220000020019": "AMS G slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0706220000020020": "AMS G slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0706220000020021": "AMS G slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0706220000020022": "AMS G slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0706220000020023": "AMS G slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0706220000020024": "AMS G slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0706220000020025": "AMS G slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0706220000030001": "AMS G Slot 3 filament has run out. Please wait while old filament is purged.",
+  "0706220000030002": "AMS G Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "0706230000010081": "Failed to read the filament information from AMS G slot 4. The AMS main board may be malfunctioning.",
+  "0706230000010082": "Failed to read the filament information from AMS G slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0706230000010083": "Failed to read the filament information from AMS G slot 4. The RFID tag may be damaged.",
+  "0706230000010084": "Failed to read the filament information from AMS G slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0706230000010085": "Failed to read the filament information from AMS G slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0706230000010086": "Failed to read the filament information from AMS G slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0706230000020001": "AMS G Slot 4 filament has run out. Please insert a new filament.",
+  "0706230000020002": "AMS G Slot 4 is empty; please insert a new filament.",
+  "0706230000020003": "AMS G Slot 4's filament may be broken in AMS.",
+  "0706230000020004": "AMS G Slot 4 filament may be broken in the tool head.",
+  "0706230000020005": "AMS G Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0706230000020006": "AMS G has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0706230000020007": "AMS G Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0706230000020008": "AMS G Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0706230000020009": "Failed to extrude AMS G Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070623000002000A": "Failed to adjust the filament position of AMS G Slot 4. The filament or buffer may be stuck.",
+  "0706230000020010": "AMS G slot 4 feeds filament out of AMS timeout.",
+  "0706230000020011": "AMS G slot 4 pulls filament back to AMS timeout.",
+  "0706230000020012": "AMS G slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "0706230000020013": "AMS G slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0706230000020014": "AMS G slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0706230000020015": "AMS G slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0706230000020016": "AMS G slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0706230000020017": "AMS G slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0706230000020018": "AMS G slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0706230000020019": "AMS G slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0706230000020020": "AMS G slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0706230000020021": "AMS G slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0706230000020022": "AMS G slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0706230000020023": "AMS G slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0706230000020024": "AMS G slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0706230000020025": "AMS G slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0706230000030001": "AMS G Slot 4 filament has run out. Please wait while old filament is purged.",
+  "0706230000030002": "AMS G Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "0706250000020001": "AMS G uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "0706260000020001": "The induction hotend is heating up, and the drying power of AMS G will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "0706300000010001": "The AMS G RFID 1 board has an error.",
+  "0706300000010004": "Encryption chip failure",
+  "0706300000020002": "The RFID-tag on AMS G Slot1 is damaged, or its content cannot be identified.",
+  "0706300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0706310000010001": "The AMS G RFID 2 board has an error.",
+  "0706310000010004": "Encryption chip failure",
+  "0706310000020002": "The RFID-tag on AMS G Slot2 is damaged, or its content cannot be identified.",
+  "0706310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0706320000020002": "The RFID-tag on AMS G Slot3 is damaged, or its content cannot be identified.",
+  "0706330000020002": "The RFID-tag on AMS G Slot4 is damaged, or its content cannot be identified.",
+  "0706350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "0706350000010002": "AMS G The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "0706400000020001": "AMS G Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "0706500000020001": "AMS G communication is abnormal; please check the connection cable.",
+  "0706500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0706550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "0706550000010003": "AMS G was detected offline during the AMS initialization process.",
+  "0706550000010004": "The binding between AMS G and the extruder is incorrect. Please run the AMS Setup.",
+  "0706550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "0706560000030001": "AMS G is undergoing dry cooling; please wait for it to cool down before operating.",
+  "0706600000020001": "The AMS G Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0706610000020001": "The AMS G Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0706620000020001": "The AMS G Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0706630000020001": "The AMS G Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0706700000020001": "Failed to pull out the AMS G Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0706700000020002": "Failed to feed the AMS G Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0706700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "0706700000020004": "Failed to pull back the AMS G Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0706700000020005": "Failed to feed the AMS G Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0706700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "0706700000020007": "AMS filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "0706700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "070670000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "0706710000020001": "Failed to pull out the AMS G Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0706710000020002": "Failed to feed the AMS G Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0706710000020004": "Failed to pull back the AMS G Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0706710000020005": "Failed to feed the AMS G Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0706720000020001": "Failed to pull out the AMS G Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0706720000020002": "Failed to feed the AMS G Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0706720000020004": "Failed to pull back the AMS G Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0706720000020005": "Failed to feed the AMS G Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0706730000020001": "Failed to pull out the AMS G Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0706730000020002": "Failed to feed the AMS G Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0706730000020004": "Failed to pull back the AMS G Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0706730000020005": "Failed to feed the AMS G Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0706800000010001": "AMS G The current sensor of heater 1 is abnormal.",
+  "0706800000010002": "AMS G The heater 1 is disconnected, which may be due to poor connector contact.",
+  "0706800000010003": "AMS G The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0706800000010004": "AMS G The heater 1 is heating abnormally.",
+  "0706810000010001": "AMS G The current sensor of heater 2 is abnormal.",
+  "0706810000010002": "AMS G The heater 2 is disconnected, which may be due to poor connector contact.",
+  "0706810000010003": "AMS G The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0706810000010004": "AMS G The heater 2 is heating abnormally.",
+  "0706900000010002": "AMS G The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "0706900000010003": "AMS G The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "0706900000010004": "The current sensor of AMS G exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0706900000020001": "AMS G The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "0706910000010002": "AMS G The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "0706910000010003": "AMS G The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "0706910000010004": "The current sensor of AMS G exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0706910000020001": "AMS G The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "0706920000010001": "AMS G The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "0706920000020002": "AMS G The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "0706920000020003": "The AMS G heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "0706930000010001": "AMS G The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "0706930000020002": "AMS G The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "0706930000020003": "The AMS G heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "0706940000010001": "AMS G The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "0706940000010002": "Temperature sensor 1 on the AMS G heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0706950000010001": "AMS G The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "0706950000010002": "Temperature sensor 2 on the AMS G heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0706960000010001": "AMS G The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "0706960000010003": "AMS G Unable to start drying; please pull out the filament from filament hub and try again.",
+  "0706960000020002": "AMS G Environmental temperature is too low, which will affect the drying capability.",
+  "0706960000020004": "AMS G The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "0706970000030001": "AMS G chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "0706980000020001": "AMS G The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "0706980000020002": "AMS G The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "0707010000010001": "The AMS H assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "0707010000010003": "The AMS H assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0707010000010004": "The AMS H assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "0707010000010005": "AMS H The current sensor of assist motor may be faulty.",
+  "0707010000010011": "AMS H The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0707010000020002": "The AMS H assist motor is overloaded. The filament may be tangled or stuck.",
+  "0707010000020006": "AMS H The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "0707010000020007": "AMS H The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "0707010000020008": "AMS H The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "0707010000020009": "AMS H The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "0707010000020010": "AMS H The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "0707010000020011": "AMS H The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "0707020000010001": "AMS H Filament speed and length error: The filament odometry may be faulty.",
+  "0707020000020002": "AMS H The odometer has no signal. The odometer connector may have poor contact.",
+  "0707020000020003": "AMS H filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "0707100000010001": "The AMS H slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0707100000010003": "The AMS H slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0707100000020002": "The AMS H slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "0707100000020004": "AMS H The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0707110000010001": "The AMS H slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0707110000010003": "The AMS H slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0707110000020002": "The AMS H slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "0707110000020004": "AMS H The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0707120000010001": "The AMS H slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0707120000010003": "The AMS H slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0707120000020002": "The AMS H slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "0707120000020004": "AMS H The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0707130000010001": "The AMS H slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "0707130000010003": "The AMS H slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "0707130000020002": "The AMS H slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "0707130000020004": "AMS H The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0707200000010081": "Failed to read the filament information from AMS H slot 1. The AMS main board may be malfunctioning.",
+  "0707200000010082": "Failed to read the filament information from AMS H slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0707200000010083": "Failed to read the filament information from AMS H slot 1. The RFID tag may be damaged.",
+  "0707200000010084": "Failed to read the filament information from AMS H slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0707200000010085": "Failed to read the filament information from AMS H slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0707200000010086": "Failed to read the filament information from AMS H slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0707200000020001": "AMS H Slot 1 filament has run out. Please insert a new filament.",
+  "0707200000020002": "AMS H Slot 1 is empty; please insert a new filament.",
+  "0707200000020003": "AMS H Slot 1's filament may be broken in AMS.",
+  "0707200000020004": "AMS H Slot 1 filament may be broken in the tool head.",
+  "0707200000020005": "AMS H Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0707200000020006": "AMS H has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0707200000020007": "AMS H Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0707200000020008": "AMS H Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0707200000020009": "Failed to extrude AMS H Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070720000002000A": "Failed to adjust the filament position of AMS H Slot 1. The filament or buffer may be stuck.",
+  "0707200000020010": "AMS H slot 1 feeds filament out of AMS timeout.",
+  "0707200000020011": "AMS H slot 1 pulls filament back to AMS timeout.",
+  "0707200000020012": "AMS H slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "0707200000020013": "AMS H slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0707200000020014": "AMS H slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0707200000020015": "AMS H slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0707200000020016": "AMS H slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0707200000020017": "AMS H slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0707200000020018": "AMS H slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0707200000020019": "AMS H slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0707200000020020": "AMS H slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0707200000020021": "AMS H slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0707200000020022": "AMS H slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0707200000020023": "AMS H slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0707200000020024": "AMS H slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0707200000020025": "AMS H slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0707200000030001": "AMS H Slot 1 filament has run out. Please wait while old filament is purged.",
+  "0707200000030002": "AMS H Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "0707210000010081": "Failed to read the filament information from AMS H slot 2. The AMS main board may be malfunctioning.",
+  "0707210000010082": "Failed to read the filament information from AMS H slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0707210000010083": "Failed to read the filament information from AMS H slot 2. The RFID tag may be damaged.",
+  "0707210000010084": "Failed to read the filament information from AMS H slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0707210000010085": "Failed to read the filament information from AMS H slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0707210000010086": "Failed to read the filament information from AMS H slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0707210000020001": "AMS H Slot 2 filament has run out. Please insert a new filament.",
+  "0707210000020002": "AMS H Slot 2 is empty; please insert a new filament.",
+  "0707210000020003": "AMS H Slot 2's filament may be broken in AMS.",
+  "0707210000020004": "AMS H Slot 2 filament may be broken in the tool head.",
+  "0707210000020005": "AMS H Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0707210000020006": "AMS H has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0707210000020007": "AMS H Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0707210000020008": "AMS H Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0707210000020009": "Failed to extrude AMS H Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070721000002000A": "Failed to adjust the filament position of AMS H Slot 2. The filament or buffer may be stuck.",
+  "0707210000020010": "AMS H slot 2 feeds filament out of AMS timeout.",
+  "0707210000020011": "AMS H slot 2 pulls filament back to AMS timeout.",
+  "0707210000020012": "AMS H slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "0707210000020013": "AMS H slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0707210000020014": "AMS H slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0707210000020015": "AMS H slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0707210000020016": "AMS H slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0707210000020017": "AMS H slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0707210000020018": "AMS H slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0707210000020019": "AMS H slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0707210000020020": "AMS H slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0707210000020021": "AMS H slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0707210000020022": "AMS H slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0707210000020023": "AMS H slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0707210000020024": "AMS H slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0707210000020025": "AMS H slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0707210000030001": "AMS H Slot 2 filament has run out. Please wait while old filament is purged.",
+  "0707210000030002": "AMS H Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "0707220000010081": "Failed to read the filament information from AMS H slot 3. The AMS main board may be malfunctioning.",
+  "0707220000010082": "Failed to read the filament information from AMS H slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0707220000010083": "Failed to read the filament information from AMS H slot 3. The RFID tag may be damaged.",
+  "0707220000010084": "Failed to read the filament information from AMS H slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0707220000010085": "Failed to read the filament information from AMS H slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0707220000010086": "Failed to read the filament information from AMS H slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0707220000020001": "AMS H Slot 3 filament has run out. Please insert a new filament.",
+  "0707220000020002": "AMS H Slot 3 is empty; please insert a new filament.",
+  "0707220000020003": "AMS H Slot 3's filament may be broken in AMS.",
+  "0707220000020004": "AMS H Slot 3 filament may be broken in the tool head.",
+  "0707220000020005": "AMS H Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0707220000020006": "AMS H has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0707220000020007": "AMS H Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0707220000020008": "AMS H Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0707220000020009": "Failed to extrude AMS H Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070722000002000A": "Failed to adjust the filament position of AMS H Slot 3. The filament or buffer may be stuck.",
+  "0707220000020010": "AMS H slot 3 feeds filament out of AMS timeout.",
+  "0707220000020011": "AMS H slot 3 pulls filament back to AMS timeout.",
+  "0707220000020012": "AMS H slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "0707220000020013": "AMS H slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0707220000020014": "AMS H slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0707220000020015": "AMS H slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0707220000020016": "AMS H slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0707220000020017": "AMS H slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0707220000020018": "AMS H slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0707220000020019": "AMS H slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0707220000020020": "AMS H slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0707220000020021": "AMS H slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0707220000020022": "AMS H slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0707220000020023": "AMS H slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0707220000020024": "AMS H slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0707220000020025": "AMS H slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0707220000030001": "AMS H Slot 3 filament has run out. Please wait while old filament is purged.",
+  "0707220000030002": "AMS H Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "0707230000010081": "Failed to read the filament information from AMS H slot 4. The AMS main board may be malfunctioning.",
+  "0707230000010082": "Failed to read the filament information from AMS H slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "0707230000010083": "Failed to read the filament information from AMS H slot 4. The RFID tag may be damaged.",
+  "0707230000010084": "Failed to read the filament information from AMS H slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "0707230000010085": "Failed to read the filament information from AMS H slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "0707230000010086": "Failed to read the filament information from AMS H slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "0707230000020001": "AMS H Slot 4 filament has run out. Please insert a new filament.",
+  "0707230000020002": "AMS H Slot 4 is empty; please insert a new filament.",
+  "0707230000020003": "AMS H Slot 4's filament may be broken in AMS.",
+  "0707230000020004": "AMS H Slot 4 filament may be broken in the tool head.",
+  "0707230000020005": "AMS H Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "0707230000020006": "AMS H has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "0707230000020007": "AMS H Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "0707230000020008": "AMS H Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "0707230000020009": "Failed to extrude AMS H Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "070723000002000A": "Failed to adjust the filament position of AMS H Slot 4. The filament or buffer may be stuck.",
+  "0707230000020010": "AMS H slot 4 feeds filament out of AMS timeout.",
+  "0707230000020011": "AMS H slot 4 pulls filament back to AMS timeout.",
+  "0707230000020012": "AMS H slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "0707230000020013": "AMS H slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "0707230000020014": "AMS H slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "0707230000020015": "AMS H slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "0707230000020016": "AMS H slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "0707230000020017": "AMS H slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "0707230000020018": "AMS H slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "0707230000020019": "AMS H slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "0707230000020020": "AMS H slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "0707230000020021": "AMS H slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "0707230000020022": "AMS H slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "0707230000020023": "AMS H slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "0707230000020024": "AMS H slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "0707230000020025": "AMS H slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "0707230000030001": "AMS H Slot 4 filament has run out. Please wait while old filament is purged.",
+  "0707230000030002": "AMS H Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "0707250000020001": "AMS H uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "0707260000020001": "The induction hotend is heating up, and the drying power of AMS H will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "0707300000010001": "The AMS H RFID 1 board has an error.",
+  "0707300000010004": "Encryption chip failure",
+  "0707300000020002": "The RFID-tag on AMS H Slot1 is damaged, or its content cannot be identified.",
+  "0707300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0707310000010001": "The AMS H RFID 2 board has an error.",
+  "0707310000010004": "Encryption chip failure",
+  "0707310000020002": "The RFID-tag on AMS H Slot2 is damaged, or its content cannot be identified.",
+  "0707310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "0707320000020002": "The RFID-tag on AMS H Slot3 is damaged, or its content cannot be identified.",
+  "0707330000020002": "The RFID-tag on AMS H Slot4 is damaged, or its content cannot be identified.",
+  "0707350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "0707350000010002": "AMS H The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "0707400000020001": "AMS H Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "0707500000020001": "AMS H communication is abnormal; please check the connection cable.",
+  "0707500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0707550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "0707550000010003": "AMS H was detected offline during the AMS initialization process.",
+  "0707550000010004": "The binding between AMS H and the extruder is incorrect. Please run the AMS Setup.",
+  "0707550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "0707560000030001": "AMS H is undergoing dry cooling; please wait for it to cool down before operating.",
+  "0707600000020001": "The AMS H Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0707610000020001": "The AMS H Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0707620000020001": "The AMS H Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0707630000020001": "The AMS H Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "0707700000020001": "Failed to pull out the AMS H Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0707700000020002": "Failed to feed the AMS H Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0707700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "0707700000020004": "Failed to pull back the AMS H Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0707700000020005": "Failed to feed the AMS H Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0707700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "0707700000020007": "AMS filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "0707700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "070770000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "0707710000020001": "Failed to pull out the AMS H Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0707710000020002": "Failed to feed the AMS H Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0707710000020004": "Failed to pull back the AMS H Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0707710000020005": "Failed to feed the AMS H Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0707720000020001": "Failed to pull out the AMS H Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0707720000020002": "Failed to feed the AMS H Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0707720000020004": "Failed to pull back the AMS H Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0707720000020005": "Failed to feed the AMS H Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0707730000020001": "Failed to pull out the AMS H Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "0707730000020002": "Failed to feed the AMS H Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "0707730000020004": "Failed to pull back the AMS H Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "0707730000020005": "Failed to feed the AMS H Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "0707800000010001": "AMS H The current sensor of heater 1 is abnormal.",
+  "0707800000010002": "AMS H The heater 1 is disconnected, which may be due to poor connector contact.",
+  "0707800000010003": "AMS H The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0707800000010004": "AMS H The heater 1 is heating abnormally.",
+  "0707810000010001": "AMS H The current sensor of heater 2 is abnormal.",
+  "0707810000010002": "AMS H The heater 2 is disconnected, which may be due to poor connector contact.",
+  "0707810000010003": "AMS H The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "0707810000010004": "AMS H The heater 2 is heating abnormally.",
+  "0707900000010002": "AMS H The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "0707900000010003": "AMS H The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "0707900000010004": "The current sensor of AMS H exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0707900000020001": "AMS H The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "0707910000010002": "AMS H The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "0707910000010003": "AMS H The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "0707910000010004": "The current sensor of AMS H exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS mainboard.",
+  "0707910000020001": "AMS H The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "0707920000010001": "AMS H The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "0707920000020002": "AMS H The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "0707920000020003": "The AMS H heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "0707930000010001": "AMS H The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "0707930000020002": "AMS H The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "0707930000020003": "The AMS H heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "0707940000010001": "AMS H The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "0707940000010002": "Temperature sensor 1 on the AMS H heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0707950000010001": "AMS H The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "0707950000010002": "Temperature sensor 2 on the AMS H heater has malfunctioned, resulting in abnormal temperature readings.",
+  "0707960000010001": "AMS H The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "0707960000010003": "AMS H Unable to start drying; please pull out the filament from filament hub and try again.",
+  "0707960000020002": "AMS H Environmental temperature is too low, which will affect the drying capability.",
+  "0707960000020004": "AMS H The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "0707970000030001": "AMS H chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "0707980000020001": "AMS H The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "0707980000020002": "AMS H The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "07FE200000020001": "External filament of left extruder has run out; please load a new filament.",
+  "07FE200000020002": "No filament was detected in the left extruder from the external spool; please load the new filament.",
+  "07FE200000020004": "Please pull the external filament from the left extruder.",
+  "07FE450000020001": "The left filament cutter sensor is malfunctioning; please check whether the connector is properly plugged in.",
+  "07FE450000020002": "The filament cutter's cutting distance is too large. Possible causes include the filament cutter stopper skipping teeth, motor losing steps, or the XY axis not being homed.",
+  "07FE450000020003": "The filament cutter handle has not been released. The handle or blade may be jammed, or there could be an issue with the filament sensor connection.",
+  "07FE600000020001": "External spool connected to left extruder may be tangled or jammed.",
+  "07FE700000020003": "Please check if the material is coming out of the left nozzle. If not, gently push the material and try to extrude again.",
+  "07FE700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07FE70000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "07FE800000010001": "The toolhead lift motor works abnormally. Please check whether the connecting cable is loose.",
+  "07FE800000010002": "The Toolhead Lifting Motor position hall sensor has an open circuit; please check whether the connecting cable is loose.",
+  "07FE800000010003": "The hall signal of the Toolhead Lifting Motor is abnormal, possibly due to internal communication failure in the Toolhead module.",
+  "07FE800000010004": "The Toolhead Lifting Motor position hall sensor is short-circuited; please check if the Hall sensor is malfunctioning.",
+  "07FE800000010005": "The extruder's switching limit angle has drifted significantly. Please check if the flow blocker is jammed or if filament is stuck in the toolhead.",
+  "07FE800000010006": "The TH board disconnected during the extruder switching process. Please check if the connecting cable is loose.",
+  "07FE800000020001": "The lifting action is abnormal during the extruder switch. Please check whether the flow blocker is stuck or there is filament stuck in the toolhead.",
+  "07FE800000020002": "The position of left hotend is abnormal during printing. Please check whether the flow blocker scratches the printed model.",
+  "07FE800000020003": "The deviation in the positioning calibration value of the extruder is too large; please recalibrate.",
+  "07FE810000010001": "The extruder switching motor is working abnormally. Please check whether the connecting cable is loose.",
+  "07FE810000010002": "The position hall sensor of the Extruder Switching Motor has an open circuit. Please check whether the connecting cable is loose.",
+  "07FE810000010003": "The hall signal of the Extruder Switching Motor is abnormal, possibly due to internal communication failure in the Toolhead module.",
+  "07FE810000010004": "The position hall sensor of the Extruder Switching Motor has a short circuit; please check if the Hall sensor is malfunctioning.",
+  "07FE810000010006": "The TH board disconnected during the extruder switching process. Please check if the connecting cable is loose.",
+  "07FE810000020001": "The extruder switching action is abnormal. Please check whether there is something stuck in the toolhead.",
+  "07FEA00000020001": "The left nozzle cold pull process has timed out. Please click \"Retry\" and then manually pull out the filament.",
+  "07FF200000020001": "External filament has run out; please load a new filament.",
+  "07FF200000020002": "External filament is missing; please load a new filament.",
+  "07FF200000020003": "Filament remains were detected in the PTFE tube between the Auxiliary Extruder and the Toolhead. Please refer to the Wiki for removal instructions.",
+  "07FF200000020004": "Please pull the external filament from the extruder.",
+  "07FF200000020008": "The Auxiliary Extruder failed to unload the filament, possibly due to slippage caused by worn filament. Please pull out the filament manually, cut off the worn section, and try again.",
+  "07FF200000020009": "Auxiliary extruder feeding failed, possibly due to a clogged filament tube or worn filament, causing the extruder to slip. Please remove the filament, clear the tube, trim the worn section, and try again.",
+  "07FF200000030007": "Flushing the remaining filament between the Auxiliary Extruder and the Toolhead. Please wait.",
+  "07FF450000020001": "The right filament cutter sensor is malfunctioning; please check whether the connector is properly plugged in.",
+  "07FF450000020002": "The filament cutter's cutting distance is too large. Possible causes include the filament cutter stopper skipping teeth, motor losing steps, or the XY axis not being homed.",
+  "07FF450000020003": "The filament cutter handle has not been released. The handle or blade may be jammed, or there could be an issue with the filament sensor connection.",
+  "07FF600000020001": "External spool may be tangled or jammed.",
+  "07FF700000020003": "Please check if the filament is coming out of the nozzle. If not, gently push the material and try to extrude again.",
+  "07FF700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07FF700000020009": "Failure to feed material from the right extruder to the toolhead may be due to the filament not being gripped by the extruder or a loose PTFE tube between the extruder and the toolhead.",
+  "07FF70000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "07FF800000010001": "The toolhead lift motor works abnormally. Please check whether the connecting cable is loose.",
+  "07FF800000010002": "The Toolhead Lifting Motor position hall sensor has an open circuit; please check whether the connecting cable is loose.",
+  "07FF800000010003": "The hall signal of the Toolhead Lifting Motor is abnormal, possibly due to internal communication failure in the Toolhead module.",
+  "07FF800000010004": "The Toolhead Lifting Motor position hall sensor is short-circuited; please check if the Hall sensor is malfunctioning.",
+  "07FF800000010005": "The extruder's switching limit angle has drifted significantly. Please check if the flow blocker is jammed or if filament is stuck in the toolhead.",
+  "07FF800000010006": "The TH board disconnected during the extruder switching process. Please check if the connecting cable is loose.",
+  "07FF800000020001": "The lifting action is abnormal during the extruder switch. Please check whether the flow blocker is stuck or there is filament stuck in the toolhead.",
+  "07FF800000020002": "The position of left hotend is abnormal during printing. Please check whether the flow blocker scratches the printed model.",
+  "07FF800000020003": "The deviation in the positioning calibration value of the extruder is too large; please recalibrate.",
+  "07FF810000010001": "The extruder switching motor is working abnormally. Please check whether the connecting cable is loose.",
+  "07FF810000010002": "The position hall sensor of the Extruder Switching Motor has an open circuit. Please check whether the connecting cable is loose.",
+  "07FF810000010003": "The hall signal of the Extruder Switching Motor is abnormal, possibly due to internal communication failure in the Toolhead module.",
+  "07FF810000010004": "The position hall sensor of the Extruder Switching Motor has a short circuit; please check if the Hall sensor is malfunctioning.",
+  "07FF810000010006": "The TH board disconnected during the extruder switching process. Please check if the connecting cable is loose.",
+  "07FF810000020001": "The extruder switching action is abnormal. Please check whether there is something stuck in the toolhead.",
+  "07FFA00000020001": "The nozzle cold pull process has timed out. Please click \"Retry\" and then manually pull out the filament.",
+  "0C00010000010001": "Toolhead Camera is offline. Please check the hardware connection.",
+  "0C00010000010003": "Synchronization between the Toolhead Camera and MC is abnormal. Please restart your printer.",
+  "0C00010000010004": "Toolhead Camera lens seems to be dirty. Please clean the lens.",
+  "0C00010000010005": "Toolhead Camera parameter is abnormal. Please contact customer support.",
+  "0C0001000001000A": "The Micro Lidar LED may be broken.",
+  "0C0001000001000B": "Failed to calibrate Micro Lidar. Please make sure the calibration chart is clean and not obscured. Then, run machine calibration again.",
+  "0C0001000001000F": "The Live View Camera calibration failed. Please restart the devices or update the firmware.",
+  "0C00010000010010": "The Live View Camera calibration failed. Please check if the heatbed is clear and ensure the camera image is clear and dirt-free. After these steps, please recalibrate.",
+  "0C00010000010011": "The Live View Camera calibration failed, please recalibrate. Ensure the build plate is empty, and the camera view is clear and properly oriented. Please contact customer support if repeated failures occur.",
+  "0C00010000010012": "The Live View Camera calibration failed and the calibration result could not be saved. Please try recalibrating. If calibration fails repeatedly, please contact the customer support team.",
+  "0C00010000010013": "The Live View Camera calibration failed and the Liveview camera serial number cannot be read. Please contact the customer support team.",
+  "0C00010000010018": "BirdsEye Camera is not certified, and camera-related functions are invalid.",
+  "0C00010000020002": "Toolhead camera is malfunctioning. If this issue occurs multiple times during printing, please contact customer support.",
+  "0C00010000020006": "Toolhead Camera parameter is abnormal. Please enable flowrate calibration in the next print.",
+  "0C00010000020007": "Micro Lidar laser parameters have drifted. Please re-calibrate your printer.",
+  "0C00010000020008": "Failed to get image from Live View Camera. Spaghetti and waste chute pileup detection is not available at this time.",
+  "0C0001000002000E": "",
+  "0C00010000020014": "Nozzle Camera is malfunctioning. If this issue occurs multiple times during printing, please contact customer support.",
+  "0C00010000020015": "",
+  "0C00010000020016": "",
+  "0C00010000020017": "Nozzle camera lens is dirty, which may affect the AI monitoring functionality. Please clean the surface of the nozzle camera lens as soon as possible.",
+  "0C00010000020019": "",
+  "0C0001000002001A": "",
+  "0C0001000002001B": "",
+  "0C0001000003000C": "Nozzle Camera's temperature is too high, causing the AI detection feature to pause. The feature will automatically resume once the temperature returns to normal.",
+  "0C00020000010001": "The horizontal laser is not lit. Please check if it's covered or hardware connection has a problem.",
+  "0C00020000010005": "A new Micro Lidar was detected. Please calibrate it on the Calibration page before use.",
+  "0C00020000020002": "The horizontal laser line is too wide. Please check if the heatbed is dirty.",
+  "0C00020000020003": "The horizontal laser is not bright enough at homing position. Please clean or replace the heatbed if this message appears repeatedly.",
+  "0C00020000020004": "Nozzle height seems to be too low. Please check if the nozzle is worn or tilted. Re-calibrate Lidar if the nozzle has been replaced.",
+  "0C00020000020006": "Nozzle height seems to be too high. Please check if there is residual filament attached to the nozzle.",
+  "0C00020000020007": "The vertical laser is not lit. Please check if it's covered or hardware connection has a problem.",
+  "0C00020000020008": "The vertical laser line is too wide. Please check if the heatbed is dirty.",
+  "0C00020000020009": "The vertical laser is not bright enough at homing position. Please clean or replace the heatbed if this message appears repeatedly.",
+  "0C00030000010009": "The first layer inspection module rebooted abnormally. The inspection result may be inaccurate.",
+  "0C0003000001000A": "Your printer is in factory mode. Please contact Technical Support.",
+  "0C00030000020001": "Filament exposure metering failed because laser reflection is too weak on this material. First layer inspection may be inaccurate.",
+  "0C00030000020002": "First layer inspection terminated due to abnormal Lidar data.",
+  "0C00030000020004": "First layer inspection is not supported for the current print job.",
+  "0C00030000020005": "First layer inspection timed out abnormally, and the current results may be inaccurate.",
+  "0C0003000002000C": "The build plate localization marker was not detected. Please check if the build plate is aligned correctly.",
+  "0C0003000002000E": "Your nozzle seems to be covered with jammed or clogged material.",
+  "0C0003000002000F": "Parts skipped before first layer inspection; the inspection is not supported for the current print.",
+  "0C00030000020010": "Foreign objects detected on heatbed; Please check and clean the heatbed.",
+  "0C00030000020011": "The high-precision nozzle offset calibration failed; please recalibrate.",
+  "0C00030000020012": "Foreign object detection is not working. The Live View Camera needs calibration. Please tap \"Settings > Calibration\" on the printer screen. If a laser or cutting module is installed, please remove it before calibration.",
+  "0C00030000020013": "Foreign object detection is not working. Please restart the devices or update the firmware.",
+  "0C00030000020014": "Foreign object detection accuracy has decreased. If this occurs frequently, perform a Live View Camera calibration (“Settings” > “Calibration” on the printer screen). If a laser or cutting module is installed, remove it before proceeding.",
+  "0C00030000020015": "Foreign object detection is not working. Detected the Live View Camera has been replaced. If a laser or cutting module is installed, please uninstall the module, tap \"Settings>Calibration\" on printer screen and re-calibrate the Live View Camera.",
+  "0C00030000020016": "Foreign object detection is not working and the Live View camera serial number cannot be read. Please contact the customer support team.",
+  "0C00030000020017": "Laser engraving Z-axis focus calibration failed. Please check if the Laser Test Material (350g paperboard) is properly placed and its surface is clean and intact.",
+  "0C00030000020018": "Insufficient system memory was detected, and the foreign object detection function was not working. Please restart the devices or update the firmware after the task is completed",
+  "0C00030000020019": "The Vision Encoder Plate is either not placed or incorrectly placed. Please ensure it is correctly positioned on the heatbed.",
+  "0C0003000002001C": "Your nozzle seems to be covered with jammed or clogged material.",
+  "0C0003000002001D": "Foreign object detection is not working, and images cannot be obtained. Please check the live camera connection.",
+  "0C00030000030006": "Purged filament may have piled up in the waste chute. Please check and clean the chute.",
+  "0C00030000030007": "Possible first layer defects have been detected. Please check the first layer quality and decide if the job should be stopped.",
+  "0C00030000030008": "Possible spaghetti defects were detected. Please check the print quality and decide if the job should be stopped.",
+  "0C0003000003000B": "Inspecting the first layer: please wait a moment.",
+  "0C0003000003000D": "Detected that the extruder may not be extruding normally. Please check and decide if printing should be stopped.",
+  "0C00030000030010": "Your printer seems to be printing without extruding.",
+  "0C0003000003001A": "Foreign object detection did not work, possibly due to manual interruption (e.g., tapping stop or opening the door in laser mode). You can ignore this if that's the case. Otherwise, try restarting the printer or updating the firmware to restore the function.",
+  "0C0003000003001B": "Possible spaghetti defects were detected. Please check the print quality and decide if the job should be stopped.",
+  "0C00040000010005": "BirdsEye Camera malfunction: please contact customer support.",
+  "0C00040000010010": "Failed to measure material thickness due to laser module malfunction.",
+  "0C00040000010011": "Failed to measure material thickness: device parameters abnormal; please setup BirdsEye Camera again.",
+  "0C00040000010012": "Live View Camera data link is abnormal.",
+  "0C00040000010013": "The exposure parameters of the BirdsEye Camera are not effective; please try again.",
+  "0C00040000010014": "Cutting Protection Base not detected, which\nmay lead to heatbed damage. Please place it and continue.",
+  "0C00040000010015": "Laser Protective Insert not detected. Please place it and continue.",
+  "0C00040000010016": "Quick-release Lever is not locked. Please push it down to secure.",
+  "0C00040000010020": "The visual marker on the heatbed is damaged, please contact after-sales.",
+  "0C00040000010025": "The device malfunctioned; please restart.",
+  "0C00040000020002": "Cutting Platform not detected. Please place the required mat for the task and continue.",
+  "0C00040000020003": "The Laser Platform is not properly aligned. Please ensure all four corners are aligned with the heatbed.",
+  "0C00040000020004": "The type of platform is not supported for this task. Please use the correct platform to continue.",
+  "0C00040000020006": "",
+  "0C00040000020007": "BirdsEye Camera is setting up. Please clear all objects and remove the mat. Make sure the marker is not obstructed. Meanwhile, clean both the BirdsEye Camera and Toolhead Camera, and remove any foreign objects blocking their view.",
+  "0C00040000020008": "Material not detected. Please confirm placement and continue.",
+  "0C00040000020017": "The visual marker was not detected during PrintThenCut; please re-paste the material to the correct position. Meanwhile, please clean the Toolhead Camera to prevent contamination and remove any objects that may obstruct its view.",
+  "0C00040000020018": "The Cutting Blade offset calibration failed. This may affect the cutting accuracy. Please refer to the Assistant to check if the blade tip is worn.",
+  "0C00040000020019": "The Birdseye Camera is installed offset. Please refer to the Wiki to reinstall it.",
+  "0C00040000020023": "Thickness measurement failed, the Toolhead Camera was unable to detect the material surface.",
+  "0C00040000020026": "Liveview Camera initialization failed, and some AI functions such as Spaghetti Detection will be disabled. Please restart the printer. If the problem persists, please contact customer support.",
+  "0C00040000020029": "The Laser or Cutting module has not been calibrated. Please calibrate it on the printer first.",
+  "0C00040000020030": "The rotary engraving accessory is placed incorrectly. Please adjust and reposition it.",
+  "0C00040000030018": "The Cutting blade offset calibration failed. This may affect the cutting accuracy. Please refer to the Wiki to check if the blade tip is worn.",
+  "0C00040000030024": "The BirdsEye Camera position is detected to be offset. To ensure engraving accuracy, it is recommended to rerun the BirdsEye Camera Setup.",
+  "0C00040000030026": "To improve flame detection accuracy, the right chamber light has been automatically turned off.",
+  "0C00040000030028": "The Cutting Module offset calibration failed, which may result in inaccurate cuts. Please ensure the 80g white printer paper(letter paper thickness) is properly positioned and check whether the cutting blade tip is worn.",
+  "1200100000010001": "The AMS A Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1200100000010003": "The AMS A Slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1200100000020002": "The AMS A Slot1 motor is overloaded. The filament may be tangled or stuck.",
+  "1200110000010001": "The AMS A Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1200110000010003": "The AMS A Slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1200110000020002": "The AMS A Slot2 motor is overloaded. The filament may be tangled or stuck.",
+  "1200120000010001": "The AMS A Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1200120000010003": "The AMS A Slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1200120000020002": "The AMS A Slot3 motor is overloaded. The filament may be tangled or stuck.",
+  "1200130000010001": "The AMS A Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1200130000010003": "The AMS A Slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1200130000020002": "The AMS A Slot4 motor is overloaded. The filament may be tangled or stuck.",
+  "1200200000020001": "AMS A Slot1 filament has run out; please insert a new filament.",
+  "1200200000020002": "AMS A Slot 1 is empty; please insert a new filament.",
+  "1200200000020003": "AMS A Slot1 filament may be broken in the PTFE tube.",
+  "1200200000020004": "AMS A Slot1 filament may be broken in the tool head.",
+  "1200200000020005": "AMS A Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1200200000020006": "Failed to extrude AMS A Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1200200000030001": "AMS A Slot1 filament has run out. Purging the old filament; please wait.",
+  "1200200000030002": "AMS A Slot1 filament has run out and automatically switched to the slot with the same filament.",
+  "1200210000020001": "AMS A Slot2 filament has run out; please insert a new filament.",
+  "1200210000020002": "AMS A Slot 2 is empty; please insert a new filament.",
+  "1200210000020003": "AMS A Slot2 filament may be broken in the PTFE tube.",
+  "1200210000020004": "AMS A Slot2 filament may be broken in the tool head.",
+  "1200210000020005": "AMS A Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1200210000020006": "Failed to extrude AMS A Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1200210000030001": "AMS A Slot2 filament has run out. Purging the old filament; please wait.",
+  "1200210000030002": "AMS A Slot2 filament has run out and automatically switched to the slot with the same filament.",
+  "1200220000020001": "AMS A Slot3 filament has run out; please insert a new filament.",
+  "1200220000020002": "AMS A Slot 3 is empty; please insert a new filament.",
+  "1200220000020003": "AMS A Slot3 filament may be broken in the PTFE tube.",
+  "1200220000020004": "AMS A Slot3 filament may be broken in the tool head.",
+  "1200220000020005": "AMS A Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1200220000020006": "Failed to extrude AMS A Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1200220000030001": "AMS A Slot3 filament has run out. Purging the old filament; please wait.",
+  "1200220000030002": "AMS A Slot3 filament has run out and automatically switched to the slot with the same filament.",
+  "1200230000020001": "AMS A Slot4 filament has run out; please insert a new filament.",
+  "1200230000020002": "AMS A Slot 4 is empty; please insert a new filament.",
+  "1200230000020003": "AMS A Slot4 filament may be broken in the PTFE tube.",
+  "1200230000020004": "AMS A Slot4 filament may be broken in the tool head.",
+  "1200230000020005": "AMS A Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1200230000020006": "Failed to extrude AMS A Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1200230000030001": "AMS A Slot4 filament has run out. Purging the old filament; please wait.",
+  "1200230000030002": "AMS A Slot4 filament has run out and automatically switched to the slot with the same filament.",
+  "1200300000010001": "AMS A Slot 1 RFID coil is broken or the RF hardware circuit has an error.",
+  "1200300000010004": "RFID cannot be read because of an encryption chip failure in AMS A.",
+  "1200300000020002": "The RFID-tag on AMS A Slot 1 is damaged.",
+  "1200300000030003": "AMS A Slot1 RFID cannot be read because of a structural error.",
+  "1200310000010001": "AMS A Slot 2 RFID coil is broken or the RF hardware circuit has an error.",
+  "1200310000020002": "The RFID-tag on AMS A Slot 2 is damaged.",
+  "1200310000030003": "AMS A Slot2 RFID cannot be read because of a structural error.",
+  "1200320000010001": "AMS A Slot 3 RFID coil is broken or the RF hardware circuit has an error.",
+  "1200320000020002": "The RFID-tag on AMS A Slot 3 is damaged.",
+  "1200320000030003": "AMS A Slot3 RFID cannot be read because of a structural error.",
+  "1200330000010001": "AMS A Slot 4 RFID coil is broken or the RF hardware circuit has an error.",
+  "1200330000020002": "The RFID-tag on AMS A Slot 4 is damaged.",
+  "1200330000030003": "AMS A Slot4 RFID cannot be read because of a structural error.",
+  "1200450000020001": "The filament cutter sensor is malfunctioning. Please check whether the connector is properly plugged in.",
+  "1200450000020002": "The filament cutter's cutting distance is too large. The X motor may lose steps.",
+  "1200450000020003": "The filament cutter handle has not been released. The handle or blade may be jammed, or there could be an issue with the filament sensor connection.",
+  "1200500000020001": "AMS A communication is abnormal; please check the connection cable.",
+  "1200510000030001": "AMS is disabled; please load filament from spool holder.",
+  "1200700000010001": "AMS A Filament speed and length error: The slot 1 filament odometry may be faulty.",
+  "1200710000010001": "AMS A Filament speed and length error: The slot 2 filament odometry may be faulty.",
+  "1200720000010001": "AMS A Filament speed and length error: The slot 3 filament odometry may be faulty.",
+  "1200730000010001": "AMS A Filament speed and length error: The slot 4 filament odometry may be faulty.",
+  "1200800000020001": "AMS A Slot1 filament may be tangled or stuck.",
+  "1200810000020001": "AMS A Slot2 filament may be tangled or stuck.",
+  "1200820000020001": "AMS A Slot3 filament may be tangled or stuck.",
+  "1200830000020001": "AMS A Slot4 filament may be tangled or stuck.",
+  "1201100000010001": "The AMS B Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1201100000010003": "The AMS B Slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1201100000020002": "The AMS B Slot1 motor is overloaded. The filament may be tangled or stuck.",
+  "1201110000010001": "The AMS B Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1201110000010003": "The AMS B Slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1201110000020002": "The AMS B Slot2 motor is overloaded. The filament may be tangled or stuck.",
+  "1201120000010001": "The AMS B Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1201120000010003": "The AMS B Slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1201120000020002": "The AMS B Slot3 motor is overloaded. The filament may be tangled or stuck.",
+  "1201130000010001": "The AMS B Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1201130000010003": "The AMS B Slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1201130000020002": "The AMS B Slot4 motor is overloaded. The filament may be tangled or stuck.",
+  "1201200000020001": "AMS B Slot1 filament has run out; please insert a new filament.",
+  "1201200000020002": "AMS B Slot 1 is empty; please insert a new filament.",
+  "1201200000020003": "AMS B Slot1 filament may be broken in the PTFE tube.",
+  "1201200000020004": "AMS B Slot1 filament may be broken in the tool head.",
+  "1201200000020005": "AMS B Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1201200000020006": "Failed to extrude AMS B Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1201200000030001": "AMS B Slot1 filament has run out. Purging the old filament; please wait.",
+  "1201200000030002": "AMS B Slot1 filament has run out and automatically switched to the slot with the same filament.",
+  "1201210000020001": "AMS B Slot2 filament has run out; please insert a new filament.",
+  "1201210000020002": "AMS B Slot 2 is empty; please insert a new filament.",
+  "1201210000020003": "AMS B Slot2 filament may be broken in the PTFE tube.",
+  "1201210000020004": "AMS B Slot2 filament may be broken in the tool head.",
+  "1201210000020005": "AMS B Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1201210000020006": "Failed to extrude AMS B Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1201210000030001": "AMS B Slot2 filament has run out. Purging the old filament; please wait.",
+  "1201210000030002": "AMS B Slot2 filament has run out and automatically switched to the slot with the same filament.",
+  "1201220000020001": "AMS B Slot3 filament has run out; please insert a new filament.",
+  "1201220000020002": "AMS B Slot 3 is empty; please insert a new filament.",
+  "1201220000020003": "AMS B Slot3 filament may be broken in the PTFE tube.",
+  "1201220000020004": "AMS B Slot3 filament may be broken in the tool head.",
+  "1201220000020005": "AMS B Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1201220000020006": "Failed to extrude AMS B Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1201220000030001": "AMS B Slot3 filament has run out. Purging the old filament; please wait.",
+  "1201220000030002": "AMS B Slot3 filament has run out and automatically switched to the slot with the same filament.",
+  "1201230000020001": "AMS B Slot4 filament has run out; please insert a new filament.",
+  "1201230000020002": "AMS B Slot 4 is empty; please insert a new filament.",
+  "1201230000020003": "AMS B Slot4 filament may be broken in the PTFE tube.",
+  "1201230000020004": "AMS B Slot4 filament may be broken in the tool head.",
+  "1201230000020005": "AMS B Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1201230000020006": "Failed to extrude AMS B Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1201230000030001": "AMS B Slot4 filament has run out. Purging the old filament; please wait.",
+  "1201230000030002": "AMS B Slot4 filament has run out and automatically switched to the slot with the same filament.",
+  "1201300000010001": "AMS B Slot 1 RFID coil is broken or the RF hardware circuit has an error.",
+  "1201300000010004": "RFID cannot be read because of an encryption chip failure in AMS B.",
+  "1201300000020002": "The RFID-tag on AMS B Slot 1 is damaged.",
+  "1201300000030003": "AMS B Slot1 RFID cannot be read because of a structural error.",
+  "1201310000010001": "AMS B Slot 2 RFID coil is broken or the RF hardware circuit has an error.",
+  "1201310000020002": "The RFID-tag on AMS B Slot 2 is damaged.",
+  "1201310000030003": "AMS B Slot2 RFID cannot be read because of a structural error.",
+  "1201320000010001": "AMS B Slot 3 RFID coil is broken or the RF hardware circuit has an error.",
+  "1201320000020002": "The RFID-tag on AMS B Slot 3 is damaged.",
+  "1201320000030003": "AMS B Slot3 RFID cannot be read because of a structural error.",
+  "1201330000010001": "AMS B Slot 4 RFID coil is broken or the RF hardware circuit has an error.",
+  "1201330000020002": "The RFID-tag on AMS B Slot 4 is damaged.",
+  "1201330000030003": "AMS B Slot4 RFID cannot be read because of a structural error.",
+  "1201500000020001": "AMS B communication is abnormal; please check the connection cable.",
+  "1201700000010001": "AMS B Filament speed and length error: The slot 1 filament odometry may be faulty.",
+  "1201710000010001": "AMS B Filament speed and length error: The slot 2 filament odometry may be faulty.",
+  "1201720000010001": "AMS B Filament speed and length error: The slot 3 filament odometry may be faulty.",
+  "1201730000010001": "AMS B Filament speed and length error: The slot 4 filament odometry may be faulty.",
+  "1201800000020001": "AMS B Slot1 filament may be tangled or stuck.",
+  "1201810000020001": "AMS B Slot2 filament may be tangled or stuck.",
+  "1201820000020001": "AMS B Slot3 filament may be tangled or stuck.",
+  "1201830000020001": "AMS B Slot4 filament may be tangled or stuck.",
+  "1202100000010001": "The AMS C Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1202100000010003": "The AMS C Slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1202100000020002": "The AMS C Slot1 motor is overloaded. The filament may be tangled or stuck.",
+  "1202110000010001": "The AMS C Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1202110000010003": "The AMS C Slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1202110000020002": "The AMS C Slot2 motor is overloaded. The filament may be tangled or stuck.",
+  "1202120000010001": "The AMS C Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1202120000010003": "The AMS C Slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1202120000020002": "The AMS C Slot3 motor is overloaded. The filament may be tangled or stuck.",
+  "1202130000010001": "The AMS C Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1202130000010003": "The AMS C Slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1202130000020002": "The AMS C Slot4 motor is overloaded. The filament may be tangled or stuck.",
+  "1202200000020001": "AMS C Slot1 filament has run out; please insert a new filament.",
+  "1202200000020002": "AMS C Slot 1 is empty; please insert a new filament.",
+  "1202200000020003": "AMS C Slot1 filament may be broken in the PTFE tube.",
+  "1202200000020004": "AMS C Slot1 filament may be broken in the tool head.",
+  "1202200000020005": "AMS C Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1202200000020006": "Failed to extrude AMS C Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1202200000030001": "AMS C Slot1 filament has run out. Purging the old filament; please wait.",
+  "1202200000030002": "AMS C Slot1 filament has run out and automatically switched to the slot with the same filament.",
+  "1202210000020001": "AMS C Slot2 filament has run out; please insert a new filament.",
+  "1202210000020002": "AMS C Slot 2 is empty; please insert a new filament.",
+  "1202210000020003": "AMS C Slot2 filament may be broken in the PTFE tube.",
+  "1202210000020004": "AMS C Slot2 filament may be broken in the tool head.",
+  "1202210000020005": "AMS C Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1202210000020006": "Failed to extrude AMS C Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1202210000030001": "AMS C Slot2 filament has run out. Purging the old filament; please wait.",
+  "1202210000030002": "AMS C Slot2 filament has run out and automatically switched to the slot with the same filament.",
+  "1202220000020001": "AMS C Slot3 filament has run out; please insert a new filament.",
+  "1202220000020002": "AMS C Slot 3 is empty; please insert a new filament.",
+  "1202220000020003": "AMS C Slot3 filament may be broken in the PTFE tube.",
+  "1202220000020004": "AMS C Slot3 filament may be broken in the tool head.",
+  "1202220000020005": "AMS C Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1202220000020006": "Failed to extrude AMS C Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1202220000030001": "AMS C Slot3 filament has run out. Purging the old filament; please wait.",
+  "1202220000030002": "AMS C Slot3 filament has run out and automatically switched to the slot with the same filament.",
+  "1202230000020001": "AMS C Slot4 filament has run out; please insert a new filament.",
+  "1202230000020002": "AMS C Slot 4 is empty; please insert a new filament.",
+  "1202230000020003": "AMS C Slot4 filament may be broken in the PTFE tube.",
+  "1202230000020004": "AMS C Slot4 filament may be broken in the tool head.",
+  "1202230000020005": "AMS C Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1202230000020006": "Failed to extrude AMS C Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1202230000030001": "AMS C Slot4 filament has run out. Purging the old filament; please wait.",
+  "1202230000030002": "AMS C Slot4 filament has run out and automatically switched to the slot with the same filament.",
+  "1202300000010001": "AMS C Slot 1 RFID coil is broken or the RF hardware circuit has an error.",
+  "1202300000010004": "RFID cannot be read because of an encryption chip failure in AMS C.",
+  "1202300000020002": "The RFID-tag on AMS C Slot 1 is damaged.",
+  "1202300000030003": "AMS C Slot1 RFID cannot be read because of a structural error.",
+  "1202310000010001": "AMS C Slot 2 RFID coil is broken or the RF hardware circuit has an error.",
+  "1202310000020002": "The RFID-tag on AMS C Slot 2 is damaged.",
+  "1202310000030003": "AMS C Slot2 RFID cannot be read because of a structural error.",
+  "1202320000010001": "AMS C Slot 3 RFID coil is broken or the RF hardware circuit has an error.",
+  "1202320000020002": "The RFID-tag on AMS C Slot 3 is damaged.",
+  "1202320000030003": "AMS C Slot3 RFID cannot be read because of a structural error.",
+  "1202330000010001": "AMS C Slot 4 RFID coil is broken or the RF hardware circuit has an error.",
+  "1202330000020002": "The RFID-tag on AMS C Slot 4 is damaged.",
+  "1202330000030003": "AMS C Slot4 RFID cannot be read because of a structural error.",
+  "1202500000020001": "AMS C communication is abnormal; please check the connection cable.",
+  "1202700000010001": "AMS C Filament speed and length error: The slot 1 filament odometry may be faulty.",
+  "1202710000010001": "AMS C Filament speed and length error: The slot 2 filament odometry may be faulty.",
+  "1202720000010001": "AMS C Filament speed and length error: The slot 3 filament odometry may be faulty.",
+  "1202730000010001": "AMS C Filament speed and length error: The slot 4 filament odometry may be faulty.",
+  "1202800000020001": "AMS C Slot1 filament may be tangled or stuck.",
+  "1202810000020001": "AMS C Slot2 filament may be tangled or stuck.",
+  "1202820000020001": "AMS C Slot3 filament may be tangled or stuck.",
+  "1202830000020001": "AMS C Slot4 filament may be tangled or stuck.",
+  "1203100000010001": "The AMS D Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1203100000010003": "The AMS D Slot1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1203100000020002": "The AMS D Slot1 motor is overloaded. The filament may be tangled or stuck.",
+  "1203110000010001": "The AMS D Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1203110000010003": "The AMS D Slot2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1203110000020002": "The AMS D Slot2 motor is overloaded. The filament may be tangled or stuck.",
+  "1203120000010001": "The AMS D Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1203120000010003": "The AMS D Slot3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1203120000020002": "The AMS D Slot3 motor is overloaded. The filament may be tangled or stuck.",
+  "1203130000010001": "The AMS D Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1203130000010003": "The AMS D Slot4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1203130000020002": "The AMS D Slot4 motor is overloaded. The filament may be tangled or stuck.",
+  "1203200000020001": "AMS D Slot1 filament has run out; please insert a new filament.",
+  "1203200000020002": "AMS D Slot 1 is empty; please insert a new filament.",
+  "1203200000020003": "AMS D Slot1 filament may be broken in the PTFE tube.",
+  "1203200000020004": "AMS D Slot1 filament may be broken in the tool head.",
+  "1203200000020005": "AMS D Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1203200000020006": "Failed to extrude AMS D Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1203200000030001": "AMS D Slot1 filament has run out. Purging the old filament; please wait.",
+  "1203200000030002": "AMS D Slot1 filament has run out and automatically switched to the slot with the same filament.",
+  "1203210000020001": "AMS D Slot2 filament has run out; please insert a new filament.",
+  "1203210000020002": "AMS D Slot 2 is empty; please insert a new filament.",
+  "1203210000020003": "AMS D Slot2 filament may be broken in the PTFE tube.",
+  "1203210000020004": "AMS D Slot2 filament may be broken in the tool head.",
+  "1203210000020005": "AMS D Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1203210000020006": "Failed to extrude AMS D Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1203210000030001": "AMS D Slot2 filament has run out. Purging the old filament; please wait.",
+  "1203210000030002": "AMS D Slot2 filament has run out and automatically switched to the slot with the same filament.",
+  "1203220000020001": "AMS D Slot3 filament has run out; please insert a new filament.",
+  "1203220000020002": "AMS D Slot 3 is empty; please insert a new filament.",
+  "1203220000020003": "AMS D Slot3 filament may be broken in the PTFE tube.",
+  "1203220000020004": "AMS D Slot3 filament may be broken in the tool head.",
+  "1203220000020005": "AMS D Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1203220000020006": "Failed to extrude AMS D Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1203220000030001": "AMS D Slot3 filament has run out. Purging the old filament; please wait.",
+  "1203220000030002": "AMS D Slot3 filament has run out and automatically switched to the slot with the same filament.",
+  "1203230000020001": "AMS D Slot4 filament has run out; please insert a new filament.",
+  "1203230000020002": "AMS D Slot 4 is empty; please insert a new filament.",
+  "1203230000020003": "AMS D Slot4 filament may be broken in the PTFE tube.",
+  "1203230000020004": "AMS D Slot4 filament may be broken in the tool head.",
+  "1203230000020005": "AMS D Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.",
+  "1203230000020006": "Failed to extrude AMS D Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "1203230000030001": "AMS D Slot4 filament has run out. Purging the old filament; please wait.",
+  "1203230000030002": "AMS D Slot4 filament has run out and automatically switched to the slot with the same filament.",
+  "1203300000010001": "AMS D Slot 1 RFID coil is broken or the RF hardware circuit has an error.",
+  "1203300000010004": "RFID cannot be read because of an encryption chip failure in AMS D.",
+  "1203300000020002": "The RFID-tag on AMS D Slot 1 is damaged.",
+  "1203300000030003": "AMS D Slot1 RFID cannot be read because of a structural error.",
+  "1203310000010001": "AMS D Slot 2 RFID coil is broken or the RF hardware circuit has an error.",
+  "1203310000020002": "The RFID-tag on AMS D Slot 2 is damaged.",
+  "1203310000030003": "AMS D Slot2 RFID cannot be read because of a structural error.",
+  "1203320000010001": "AMS D Slot 3 RFID coil is broken or the RF hardware circuit has an error.",
+  "1203320000020002": "The RFID-tag on AMS D Slot 3 is damaged.",
+  "1203320000030003": "AMS D Slot3 RFID cannot be read because of a structural error.",
+  "1203330000010001": "AMS D Slot 4 RFID coil is broken or the RF hardware circuit has an error.",
+  "1203330000020002": "The RFID-tag on AMS D Slot 4 is damaged.",
+  "1203330000030003": "AMS D Slot4 RFID cannot be read because of a structural error.",
+  "1203500000020001": "AMS D communication is abnormal; please check the connection cable.",
+  "1203700000010001": "AMS D Filament speed and length error: The slot 1 filament odometry may be faulty.",
+  "1203710000010001": "AMS D Filament speed and length error: The slot 2 filament odometry may be faulty.",
+  "1203720000010001": "AMS D Filament speed and length error: The slot 3 filament odometry may be faulty.",
+  "1203730000010001": "AMS D Filament speed and length error: The slot 4 filament odometry may be faulty.",
+  "1203800000020001": "AMS D Slot1 filament may be tangled or stuck.",
+  "1203810000020001": "AMS D Slot2 filament may be tangled or stuck.",
+  "1203820000020001": "AMS D Slot3 filament may be tangled or stuck.",
+  "1203830000020001": "AMS D Slot4 filament may be tangled or stuck.",
+  "12FF200000020001": "Filament at the spool holder has run out; please insert a new filament.",
+  "12FF200000020002": "Filament on the spool holder is empty; please insert a new filament.",
+  "12FF200000020004": "Please pull the filament on the spool holder out from the extruder.",
+  "12FF200000020005": "Filament may be broken in the tool head.",
+  "12FF200000020006": "Failed to extrude the filament; the extruder may be clogged.",
+  "12FF200000020007": "Failed to check the filament location in the tool head; please click for more help.",
+  "12FF200000030007": "Checking the filament location of all AMS slots, please wait.",
+  "12FF800000020001": "The filament on the spool holder may be tangled or stuck.",
+  "1800010000010001": "The AMS-HT A assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "1800010000010003": "The AMS-HT A assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1800010000010004": "The AMS-HT A assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "1800010000010005": "AMS-HT A The current sensor of assist motor may be faulty.",
+  "1800010000010011": "AMS-HT A The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1800010000020002": "The AMS-HT A assist motor is overloaded. The filament may be tangled or stuck.",
+  "1800010000020006": "AMS-HT A The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "1800010000020007": "AMS-HT A The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "1800010000020008": "AMS-HT A The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "1800010000020009": "AMS-HT A The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "1800010000020010": "AMS-HT A The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "1800010000020011": "AMS-HT A The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1800020000010001": "AMS-HT A Filament speed and length error: The filament odometry may be faulty.",
+  "1800020000020002": "AMS-HT A The odometer has no signal. The odometer connector may have poor contact.",
+  "1800020000020003": "AMS-HT A filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "1800100000010001": "The AMS-HT A slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1800100000010003": "The AMS-HT A slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1800100000020002": "The AMS-HT A slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "1800100000020004": "AMS-HT A The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1800110000010001": "The AMS-HT A slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1800110000010003": "The AMS-HT A slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1800110000020002": "The AMS-HT A slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "1800110000020004": "AMS-HT A The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1800120000010001": "The AMS-HT A slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1800120000010003": "The AMS-HT A slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1800120000020002": "The AMS-HT A slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "1800120000020004": "AMS-HT A The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1800130000010001": "The AMS-HT A slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1800130000010003": "The AMS-HT A slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1800130000020002": "The AMS-HT A slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "1800130000020004": "AMS-HT A The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1800200000010081": "Failed to read the filament information from AMS-HT A slot 1. The AMS main board may be malfunctioning.",
+  "1800200000010082": "Failed to read the filament information from AMS-HT A slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1800200000010083": "Failed to read the filament information from AMS-HT A slot 1. The RFID tag may be damaged.",
+  "1800200000010084": "Failed to read the filament information from AMS-HT A slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1800200000010085": "Failed to read the filament information from AMS-HT A slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1800200000010086": "Failed to read the filament information from AMS-HT A slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1800200000020001": "AMS-HT A Slot 1 filament has run out. Please insert a new filament.",
+  "1800200000020002": "AMS-HT A Slot 1 is empty; please insert a new filament.",
+  "1800200000020003": "AMS-HT A Slot 1's filament may be broken in AMS-HT.",
+  "1800200000020004": "AMS-HT A Slot 1 filament may be broken in the tool head.",
+  "1800200000020005": "AMS-HT A Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1800200000020006": "AMS-HT A has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1800200000020007": "AMS-HT A Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1800200000020008": "AMS-HT A Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1800200000020009": "Failed to extrude AMS-HT A Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180020000002000A": "Failed to adjust the filament position of AMS-HT A Slot 1. The filament or buffer may be stuck.",
+  "1800200000020010": "AMS-HT A slot 1 feeds filament out of AMS timeout.",
+  "1800200000020011": "AMS-HT A slot 1 pulls filament back to AMS timeout.",
+  "1800200000020012": "AMS-HT A slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "1800200000020013": "AMS-HT A slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1800200000020014": "AMS-HT A slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1800200000020015": "AMS-HT A slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1800200000020016": "AMS-HT A slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1800200000020017": "AMS-HT A slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1800200000020018": "AMS-HT A slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1800200000020019": "AMS-HT A slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1800200000020020": "AMS-HT A slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1800200000020021": "AMS-HT A slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1800200000020022": "AMS-HT A slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1800200000020023": "AMS-HT A slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1800200000020024": "AMS-HT A slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1800200000020025": "AMS-HT A slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1800200000020026": "AMS-HT A assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "1800200000020027": "AMS-HT A assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "1800200000030001": "AMS-HT A Slot 1 filament has run out. Please wait while old filament is purged.",
+  "1800200000030002": "AMS-HT A Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "1800210000010081": "Failed to read the filament information from AMS-HT A slot 2. The AMS main board may be malfunctioning.",
+  "1800210000010082": "Failed to read the filament information from AMS-HT A slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1800210000010083": "Failed to read the filament information from AMS-HT A slot 2. The RFID tag may be damaged.",
+  "1800210000010084": "Failed to read the filament information from AMS-HT A slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1800210000010085": "Failed to read the filament information from AMS-HT A slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1800210000010086": "Failed to read the filament information from AMS-HT A slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1800210000020001": "AMS-HT A Slot 2 filament has run out. Please insert a new filament.",
+  "1800210000020002": "AMS-HT A Slot 2 is empty; please insert a new filament.",
+  "1800210000020003": "AMS-HT A Slot 2's filament may be broken in AMS-HT.",
+  "1800210000020004": "AMS-HT A Slot 2 filament may be broken in the tool head.",
+  "1800210000020005": "AMS-HT A Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1800210000020006": "AMS-HT A has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1800210000020007": "AMS-HT A Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1800210000020008": "AMS-HT A Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1800210000020009": "Failed to extrude AMS-HT A Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180021000002000A": "Failed to adjust the filament position of AMS-HT A Slot 2. The filament or buffer may be stuck.",
+  "1800210000020010": "AMS-HT A slot 2 feeds filament out of AMS timeout.",
+  "1800210000020011": "AMS-HT A slot 2 pulls filament back to AMS timeout.",
+  "1800210000020012": "AMS-HT A slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "1800210000020013": "AMS-HT A slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1800210000020014": "AMS-HT A slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1800210000020015": "AMS-HT A slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1800210000020016": "AMS-HT A slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1800210000020017": "AMS-HT A slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1800210000020018": "AMS-HT A slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1800210000020019": "AMS-HT A slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1800210000020020": "AMS-HT A slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1800210000020021": "AMS-HT A slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1800210000020022": "AMS-HT A slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1800210000020023": "AMS-HT A slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1800210000020024": "AMS-HT A slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1800210000020025": "AMS-HT A slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1800210000030001": "AMS-HT A Slot 2 filament has run out. Please wait while old filament is purged.",
+  "1800210000030002": "AMS-HT A Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "1800220000010081": "Failed to read the filament information from AMS-HT A slot 3. The AMS main board may be malfunctioning.",
+  "1800220000010082": "Failed to read the filament information from AMS-HT A slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1800220000010083": "Failed to read the filament information from AMS-HT A slot 3. The RFID tag may be damaged.",
+  "1800220000010084": "Failed to read the filament information from AMS-HT A slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1800220000010085": "Failed to read the filament information from AMS-HT A slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1800220000010086": "Failed to read the filament information from AMS-HT A slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1800220000020001": "AMS-HT A Slot 3 filament has run out. Please insert a new filament.",
+  "1800220000020002": "AMS-HT A Slot 3 is empty; please insert a new filament.",
+  "1800220000020003": "AMS-HT A Slot 3's filament may be broken in AMS-HT.",
+  "1800220000020004": "AMS-HT A Slot 3 filament may be broken in the tool head.",
+  "1800220000020005": "AMS-HT A Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1800220000020006": "AMS-HT A has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1800220000020007": "AMS-HT A Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1800220000020008": "AMS-HT A Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1800220000020009": "Failed to extrude AMS-HT A Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180022000002000A": "Failed to adjust the filament position of AMS-HT A Slot 3. The filament or buffer may be stuck.",
+  "1800220000020010": "AMS-HT A slot 3 feeds filament out of AMS timeout.",
+  "1800220000020011": "AMS-HT A slot 3 pulls filament back to AMS timeout.",
+  "1800220000020012": "AMS-HT A slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "1800220000020013": "AMS-HT A slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1800220000020014": "AMS-HT A slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1800220000020015": "AMS-HT A slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1800220000020016": "AMS-HT A slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1800220000020017": "AMS-HT A slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1800220000020018": "AMS-HT A slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1800220000020019": "AMS-HT A slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1800220000020020": "AMS-HT A slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1800220000020021": "AMS-HT A slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1800220000020022": "AMS-HT A slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1800220000020023": "AMS-HT A slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1800220000020024": "AMS-HT A slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1800220000020025": "AMS-HT A slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1800220000030001": "AMS-HT A Slot 3 filament has run out. Please wait while old filament is purged.",
+  "1800220000030002": "AMS-HT A Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "1800230000010081": "Failed to read the filament information from AMS-HT A slot 4. The AMS main board may be malfunctioning.",
+  "1800230000010082": "Failed to read the filament information from AMS-HT A slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1800230000010083": "Failed to read the filament information from AMS-HT A slot 4. The RFID tag may be damaged.",
+  "1800230000010084": "Failed to read the filament information from AMS-HT A slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1800230000010085": "Failed to read the filament information from AMS-HT A slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1800230000010086": "Failed to read the filament information from AMS-HT A slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1800230000020001": "AMS-HT A Slot 4 filament has run out. Please insert a new filament.",
+  "1800230000020002": "AMS-HT A Slot 4 is empty; please insert a new filament.",
+  "1800230000020003": "AMS-HT A Slot 4's filament may be broken in AMS-HT.",
+  "1800230000020004": "AMS-HT A Slot 4 filament may be broken in the tool head.",
+  "1800230000020005": "AMS-HT A Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1800230000020006": "AMS-HT A has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1800230000020007": "AMS-HT A Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1800230000020008": "AMS-HT A Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1800230000020009": "Failed to extrude AMS-HT A Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180023000002000A": "Failed to adjust the filament position of AMS-HT A Slot 4. The filament or buffer may be stuck.",
+  "1800230000020010": "AMS-HT A slot 4 feeds filament out of AMS timeout.",
+  "1800230000020011": "AMS-HT A slot 4 pulls filament back to AMS timeout.",
+  "1800230000020012": "AMS-HT A slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "1800230000020013": "AMS-HT A slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1800230000020014": "AMS-HT A slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1800230000020015": "AMS-HT A slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1800230000020016": "AMS-HT A slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1800230000020017": "AMS-HT A slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1800230000020018": "AMS-HT A slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1800230000020019": "AMS-HT A slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1800230000020020": "AMS-HT A slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1800230000020021": "AMS-HT A slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1800230000020022": "AMS-HT A slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1800230000020023": "AMS-HT A slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1800230000020024": "AMS-HT A slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1800230000020025": "AMS-HT A slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1800230000030001": "AMS-HT A Slot 4 filament has run out. Please wait while old filament is purged.",
+  "1800230000030002": "AMS-HT A Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "1800240000010007": "AMS-HT A door detection is abnormal, the Hall sensor connection may be loose or disconnected.",
+  "1800240000020009": "AMS-HT A front cover is open. This may affect the drying performance or cause the filament to absorb moisture.",
+  "1800250000020001": "AMS-HT A uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "1800260000020001": "The induction hotend is heating up, and the drying power of AMS-HT A will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "1800300000010001": "The AMS-HT A RFID 1 board has an error.",
+  "1800300000010004": "Encryption chip failure",
+  "1800300000020002": "The RFID-tag on AMS-HT A Slot1 is damaged, or its content cannot be identified.",
+  "1800300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1800310000010001": "The AMS-HT A RFID 2 board has an error.",
+  "1800310000010004": "Encryption chip failure",
+  "1800310000020002": "The RFID-tag on AMS-HT A Slot2 is damaged, or its content cannot be identified.",
+  "1800310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1800320000020002": "The RFID-tag on AMS-HT A Slot3 is damaged, or its content cannot be identified.",
+  "1800330000020002": "The RFID-tag on AMS-HT A Slot4 is damaged, or its content cannot be identified.",
+  "1800350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "1800350000010002": "AMS-HT A The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "1800400000020001": "AMS-HT A Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "1800400000020002": "Filament buffer position signal error: the position sensor may be malfunctioning.",
+  "1800400000020003": "The AMS Hub communication is abnormal; the cable may be not well connected.",
+  "1800400000020004": "The filament buffer signal is abnormal; the spring may be stuck, or the filament may be tangled.",
+  "1800450000020001": "The filament cutter sensor is malfunctioning; please check whether the connector is properly plugged in.",
+  "1800450000020002": "The filament cutter's cutting distance is too large. The XY motor may lose steps.",
+  "1800450000020003": "The filament cutter handle has not been released. The handle or blade may be jammed, or there could be an issue with the filament sensor connection.",
+  "1800500000020001": "AMS-HT A communication is abnormal; please check the connection cable.",
+  "1800500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1800510000030001": "The AMS is disabled; please load filament from the spool holder.",
+  "1800550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "1800550000010003": "AMS-HT A was detected offline during the AMS initialization process.",
+  "1800550000010004": "The binding between AMS-HT A and the extruder is incorrect. Please run the AMS Setup.",
+  "1800550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "1800550000020001": "A new AMS-HT detected. Please set it up to check which extruder the AMS-HT is connected to.",
+  "1800560000030001": "AMS-HT A is undergoing dry cooling; please wait for it to cool down before operating.",
+  "1800600000020001": "The AMS-HT A Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1800610000020001": "The AMS-HT A Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1800620000020001": "The AMS-HT A Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1800630000020001": "The AMS-HT A Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1800700000020001": "Failed to pull out the AMS-HT A Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1800700000020002": "Failed to feed the AMS-HT A Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1800700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "1800700000020004": "Failed to pull back the AMS-HT A Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1800700000020005": "Failed to feed the AMS-HT A Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1800700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "1800700000020007": "AMS-HT filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "1800700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "180070000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "1800710000020001": "Failed to pull out the AMS-HT A Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1800710000020002": "Failed to feed the AMS-HT A Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1800710000020004": "Failed to pull back the AMS-HT A Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1800710000020005": "Failed to feed the AMS-HT A Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1800720000020001": "Failed to pull out the AMS-HT A Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1800720000020002": "Failed to feed the AMS-HT A Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1800720000020004": "Failed to pull back the AMS-HT A Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1800720000020005": "Failed to feed the AMS-HT A Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1800730000020001": "Failed to pull out the AMS-HT A Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1800730000020002": "Failed to feed the AMS-HT A Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1800730000020004": "Failed to pull back the AMS-HT A Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1800730000020005": "Failed to feed the AMS-HT A Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1800800000010001": "AMS-HT A The current sensor of heater 1 is abnormal.",
+  "1800800000010002": "AMS-HT A The heater 1 is disconnected, which may be due to poor connector contact.",
+  "1800800000010003": "AMS-HT A The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1800800000010004": "AMS-HT A The heater 1 is heating abnormally.",
+  "1800810000010001": "AMS-HT A The current sensor of heater 2 is abnormal.",
+  "1800810000010002": "AMS-HT A The heater 2 is disconnected, which may be due to poor connector contact.",
+  "1800810000010003": "AMS-HT A The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1800810000010004": "AMS-HT A The heater 2 is heating abnormally.",
+  "1800860000010004": "AMS-HT A is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "1800900000010002": "AMS-HT A The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "1800900000010003": "AMS-HT A The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "1800900000010004": "The current sensor of AMS-HT A exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1800900000020001": "AMS-HT A The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "1800910000010002": "AMS-HT A The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "1800910000010003": "AMS-HT A The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "1800910000010004": "The current sensor of AMS-HT A exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1800910000020001": "AMS-HT A The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "1800920000010001": "AMS-HT A The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "1800920000020002": "AMS-HT A The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "1800920000020003": "The AMS-HT A heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "1800930000010001": "AMS-HT A The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "1800930000020002": "AMS-HT A The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "1800930000020003": "The AMS-HT A heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "1800940000010001": "AMS-HT A The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "1800940000010002": "Temperature sensor 1 on the AMS-HT A heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1800950000010001": "AMS-HT A The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "1800950000010002": "Temperature sensor 2 on the AMS-HT A heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1800960000010001": "AMS-HT A The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "1800960000010003": "AMS-HT A Unable to start drying; please pull out the filament from filament hub and try again.",
+  "1800960000020002": "AMS-HT A Environmental temperature is too low, which will affect the drying capability.",
+  "1800960000020004": "AMS-HT A The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "1800970000030001": "AMS-HT A chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "1800980000020001": "AMS-HT A The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "1800980000020002": "AMS-HT A The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "1801010000010001": "The AMS-HT B assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "1801010000010003": "The AMS-HT B assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1801010000010004": "The AMS-HT B assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "1801010000010005": "AMS-HT B The current sensor of assist motor may be faulty.",
+  "1801010000010011": "AMS-HT B The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1801010000020002": "The AMS-HT B assist motor is overloaded. The filament may be tangled or stuck.",
+  "1801010000020006": "AMS-HT B The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "1801010000020007": "AMS-HT B The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "1801010000020008": "AMS-HT B The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "1801010000020009": "AMS-HT B The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "1801010000020010": "AMS-HT B The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "1801010000020011": "AMS-HT B The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1801020000010001": "AMS-HT B Filament speed and length error: The filament odometry may be faulty.",
+  "1801020000020002": "AMS-HT B The odometer has no signal. The odometer connector may have poor contact.",
+  "1801020000020003": "AMS-HT B filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "1801100000010001": "The AMS-HT B slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1801100000010003": "The AMS-HT B slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1801100000020002": "The AMS-HT B slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "1801100000020004": "AMS-HT B The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1801110000010001": "The AMS-HT B slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1801110000010003": "The AMS-HT B slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1801110000020002": "The AMS-HT B slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "1801110000020004": "AMS-HT B The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1801120000010001": "The AMS-HT B slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1801120000010003": "The AMS-HT B slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1801120000020002": "The AMS-HT B slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "1801120000020004": "AMS-HT B The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1801130000010001": "The AMS-HT B slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1801130000010003": "The AMS-HT B slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1801130000020002": "The AMS-HT B slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "1801130000020004": "AMS-HT B The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1801200000010081": "Failed to read the filament information from AMS-HT B slot 1. The AMS main board may be malfunctioning.",
+  "1801200000010082": "Failed to read the filament information from AMS-HT B slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1801200000010083": "Failed to read the filament information from AMS-HT B slot 1. The RFID tag may be damaged.",
+  "1801200000010084": "Failed to read the filament information from AMS-HT B slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1801200000010085": "Failed to read the filament information from AMS-HT B slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1801200000010086": "Failed to read the filament information from AMS-HT B slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1801200000020001": "AMS-HT B Slot 1 filament has run out. Please insert a new filament.",
+  "1801200000020002": "AMS-HT B Slot 1 is empty; please insert a new filament.",
+  "1801200000020003": "AMS-HT B Slot 1's filament may be broken in AMS-HT.",
+  "1801200000020004": "AMS-HT B Slot 1 filament may be broken in the tool head.",
+  "1801200000020005": "AMS-HT B Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1801200000020006": "AMS-HT B has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1801200000020007": "AMS-HT B Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1801200000020008": "AMS-HT B Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1801200000020009": "Failed to extrude AMS-HT B Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180120000002000A": "Failed to adjust the filament position of AMS-HT B Slot 1. The filament or buffer may be stuck.",
+  "1801200000020010": "AMS-HT B slot 1 feeds filament out of AMS timeout.",
+  "1801200000020011": "AMS-HT B slot 1 pulls filament back to AMS timeout.",
+  "1801200000020012": "AMS-HT B slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "1801200000020013": "AMS-HT B slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1801200000020014": "AMS-HT B slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1801200000020015": "AMS-HT B slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1801200000020016": "AMS-HT B slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1801200000020017": "AMS-HT B slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1801200000020018": "AMS-HT B slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1801200000020019": "AMS-HT B slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1801200000020020": "AMS-HT B slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1801200000020021": "AMS-HT B slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1801200000020022": "AMS-HT B slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1801200000020023": "AMS-HT B slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1801200000020024": "AMS-HT B slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1801200000020025": "AMS-HT B slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1801200000020026": "AMS-HT B assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "1801200000020027": "AMS-HT B assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "1801200000030001": "AMS-HT B Slot 1 filament has run out. Please wait while old filament is purged.",
+  "1801200000030002": "AMS-HT B Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "1801210000010081": "Failed to read the filament information from AMS-HT B slot 2. The AMS main board may be malfunctioning.",
+  "1801210000010082": "Failed to read the filament information from AMS-HT B slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1801210000010083": "Failed to read the filament information from AMS-HT B slot 2. The RFID tag may be damaged.",
+  "1801210000010084": "Failed to read the filament information from AMS-HT B slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1801210000010085": "Failed to read the filament information from AMS-HT B slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1801210000010086": "Failed to read the filament information from AMS-HT B slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1801210000020001": "AMS-HT B Slot 2 filament has run out. Please insert a new filament.",
+  "1801210000020002": "AMS-HT B Slot 2 is empty; please insert a new filament.",
+  "1801210000020003": "AMS-HT B Slot 2's filament may be broken in AMS-HT.",
+  "1801210000020004": "AMS-HT B Slot 2 filament may be broken in the tool head.",
+  "1801210000020005": "AMS-HT B Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1801210000020006": "AMS-HT B has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1801210000020007": "AMS-HT B Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1801210000020008": "AMS-HT B Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1801210000020009": "Failed to extrude AMS-HT B Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180121000002000A": "Failed to adjust the filament position of AMS-HT B Slot 2. The filament or buffer may be stuck.",
+  "1801210000020010": "AMS-HT B slot 2 feeds filament out of AMS timeout.",
+  "1801210000020011": "AMS-HT B slot 2 pulls filament back to AMS timeout.",
+  "1801210000020012": "AMS-HT B slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "1801210000020013": "AMS-HT B slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1801210000020014": "AMS-HT B slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1801210000020015": "AMS-HT B slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1801210000020016": "AMS-HT B slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1801210000020017": "AMS-HT B slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1801210000020018": "AMS-HT B slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1801210000020019": "AMS-HT B slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1801210000020020": "AMS-HT B slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1801210000020021": "AMS-HT B slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1801210000020022": "AMS-HT B slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1801210000020023": "AMS-HT B slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1801210000020024": "AMS-HT B slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1801210000020025": "AMS-HT B slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1801210000030001": "AMS-HT B Slot 2 filament has run out. Please wait while old filament is purged.",
+  "1801210000030002": "AMS-HT B Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "1801220000010081": "Failed to read the filament information from AMS-HT B slot 3. The AMS main board may be malfunctioning.",
+  "1801220000010082": "Failed to read the filament information from AMS-HT B slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1801220000010083": "Failed to read the filament information from AMS-HT B slot 3. The RFID tag may be damaged.",
+  "1801220000010084": "Failed to read the filament information from AMS-HT B slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1801220000010085": "Failed to read the filament information from AMS-HT B slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1801220000010086": "Failed to read the filament information from AMS-HT B slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1801220000020001": "AMS-HT B Slot 3 filament has run out. Please insert a new filament.",
+  "1801220000020002": "AMS-HT B Slot 3 is empty; please insert a new filament.",
+  "1801220000020003": "AMS-HT B Slot 3's filament may be broken in AMS-HT.",
+  "1801220000020004": "AMS-HT B Slot 3 filament may be broken in the tool head.",
+  "1801220000020005": "AMS-HT B Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1801220000020006": "AMS-HT B has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1801220000020007": "AMS-HT B Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1801220000020008": "AMS-HT B Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1801220000020009": "Failed to extrude AMS-HT B Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180122000002000A": "Failed to adjust the filament position of AMS-HT B Slot 3. The filament or buffer may be stuck.",
+  "1801220000020010": "AMS-HT B slot 3 feeds filament out of AMS timeout.",
+  "1801220000020011": "AMS-HT B slot 3 pulls filament back to AMS timeout.",
+  "1801220000020012": "AMS-HT B slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "1801220000020013": "AMS-HT B slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1801220000020014": "AMS-HT B slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1801220000020015": "AMS-HT B slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1801220000020016": "AMS-HT B slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1801220000020017": "AMS-HT B slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1801220000020018": "AMS-HT B slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1801220000020019": "AMS-HT B slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1801220000020020": "AMS-HT B slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1801220000020021": "AMS-HT B slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1801220000020022": "AMS-HT B slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1801220000020023": "AMS-HT B slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1801220000020024": "AMS-HT B slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1801220000020025": "AMS-HT B slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1801220000030001": "AMS-HT B Slot 3 filament has run out. Please wait while old filament is purged.",
+  "1801220000030002": "AMS-HT B Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "1801230000010081": "Failed to read the filament information from AMS-HT B slot 4. The AMS main board may be malfunctioning.",
+  "1801230000010082": "Failed to read the filament information from AMS-HT B slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1801230000010083": "Failed to read the filament information from AMS-HT B slot 4. The RFID tag may be damaged.",
+  "1801230000010084": "Failed to read the filament information from AMS-HT B slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1801230000010085": "Failed to read the filament information from AMS-HT B slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1801230000010086": "Failed to read the filament information from AMS-HT B slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1801230000020001": "AMS-HT B Slot 4 filament has run out. Please insert a new filament.",
+  "1801230000020002": "AMS-HT B Slot 4 is empty; please insert a new filament.",
+  "1801230000020003": "AMS-HT B Slot 4's filament may be broken in AMS-HT.",
+  "1801230000020004": "AMS-HT B Slot 4 filament may be broken in the tool head.",
+  "1801230000020005": "AMS-HT B Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1801230000020006": "AMS-HT B has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1801230000020007": "AMS-HT B Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1801230000020008": "AMS-HT B Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1801230000020009": "Failed to extrude AMS-HT B Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180123000002000A": "Failed to adjust the filament position of AMS-HT B Slot 4. The filament or buffer may be stuck.",
+  "1801230000020010": "AMS-HT B slot 4 feeds filament out of AMS timeout.",
+  "1801230000020011": "AMS-HT B slot 4 pulls filament back to AMS timeout.",
+  "1801230000020012": "AMS-HT B slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "1801230000020013": "AMS-HT B slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1801230000020014": "AMS-HT B slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1801230000020015": "AMS-HT B slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1801230000020016": "AMS-HT B slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1801230000020017": "AMS-HT B slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1801230000020018": "AMS-HT B slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1801230000020019": "AMS-HT B slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1801230000020020": "AMS-HT B slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1801230000020021": "AMS-HT B slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1801230000020022": "AMS-HT B slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1801230000020023": "AMS-HT B slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1801230000020024": "AMS-HT B slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1801230000020025": "AMS-HT B slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1801230000030001": "AMS-HT B Slot 4 filament has run out. Please wait while old filament is purged.",
+  "1801230000030002": "AMS-HT B Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "1801240000010007": "AMS-HT B door detection is abnormal, the Hall sensor connection may be loose or disconnected.",
+  "1801240000020009": "AMS-HT B front cover is open. This may affect the drying performance or cause the filament to absorb moisture.",
+  "1801250000020001": "AMS-HT B uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "1801260000020001": "The induction hotend is heating up, and the drying power of AMS-HT B will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "1801300000010001": "The AMS-HT B RFID 1 board has an error.",
+  "1801300000010004": "Encryption chip failure",
+  "1801300000020002": "The RFID-tag on AMS-HT B Slot1 is damaged, or its content cannot be identified.",
+  "1801300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1801310000010001": "The AMS-HT B RFID 2 board has an error.",
+  "1801310000010004": "Encryption chip failure",
+  "1801310000020002": "The RFID-tag on AMS-HT B Slot2 is damaged, or its content cannot be identified.",
+  "1801310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1801320000020002": "The RFID-tag on AMS-HT B Slot3 is damaged, or its content cannot be identified.",
+  "1801330000020002": "The RFID-tag on AMS-HT B Slot4 is damaged, or its content cannot be identified.",
+  "1801350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "1801350000010002": "AMS-HT B The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "1801400000020001": "AMS-HT B Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "1801500000020001": "AMS-HT B communication is abnormal; please check the connection cable.",
+  "1801500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1801550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "1801550000010003": "AMS-HT B was detected offline during the AMS initialization process.",
+  "1801550000010004": "The binding between AMS-HT B and the extruder is incorrect. Please run the AMS Setup.",
+  "1801550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "1801560000030001": "AMS-HT B is undergoing dry cooling; please wait for it to cool down before operating.",
+  "1801600000020001": "The AMS-HT B Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1801610000020001": "The AMS-HT B Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1801620000020001": "The AMS-HT B Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1801630000020001": "The AMS-HT B Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1801700000020001": "Failed to pull out the AMS-HT B Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1801700000020002": "Failed to feed the AMS-HT B Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1801700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "1801700000020004": "Failed to pull back the AMS-HT B Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1801700000020005": "Failed to feed the AMS-HT B Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1801700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "1801700000020007": "AMS-HT filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "1801700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "180170000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "1801710000020001": "Failed to pull out the AMS-HT B Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1801710000020002": "Failed to feed the AMS-HT B Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1801710000020004": "Failed to pull back the AMS-HT B Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1801710000020005": "Failed to feed the AMS-HT B Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1801720000020001": "Failed to pull out the AMS-HT B Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1801720000020002": "Failed to feed the AMS-HT B Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1801720000020004": "Failed to pull back the AMS-HT B Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1801720000020005": "Failed to feed the AMS-HT B Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1801730000020001": "Failed to pull out the AMS-HT B Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1801730000020002": "Failed to feed the AMS-HT B Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1801730000020004": "Failed to pull back the AMS-HT B Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1801730000020005": "Failed to feed the AMS-HT B Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1801800000010001": "AMS-HT B The current sensor of heater 1 is abnormal.",
+  "1801800000010002": "AMS-HT B The heater 1 is disconnected, which may be due to poor connector contact.",
+  "1801800000010003": "AMS-HT B The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1801800000010004": "AMS-HT B The heater 1 is heating abnormally.",
+  "1801810000010001": "AMS-HT B The current sensor of heater 2 is abnormal.",
+  "1801810000010002": "AMS-HT B The heater 2 is disconnected, which may be due to poor connector contact.",
+  "1801810000010003": "AMS-HT B The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1801810000010004": "AMS-HT B The heater 2 is heating abnormally.",
+  "1801860000010004": "AMS-HT B is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "1801900000010002": "AMS-HT B The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "1801900000010003": "AMS-HT B The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "1801900000010004": "The current sensor of AMS-HT B exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1801900000020001": "AMS-HT B The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "1801910000010002": "AMS-HT B The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "1801910000010003": "AMS-HT B The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "1801910000010004": "The current sensor of AMS-HT B exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1801910000020001": "AMS-HT B The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "1801920000010001": "AMS-HT B The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "1801920000020002": "AMS-HT B The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "1801920000020003": "The AMS-HT B heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "1801930000010001": "AMS-HT B The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "1801930000020002": "AMS-HT B The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "1801930000020003": "The AMS-HT B heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "1801940000010001": "AMS-HT B The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "1801940000010002": "Temperature sensor 1 on the AMS-HT B heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1801950000010001": "AMS-HT B The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "1801950000010002": "Temperature sensor 2 on the AMS-HT B heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1801960000010001": "AMS-HT B The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "1801960000010003": "AMS-HT B Unable to start drying; please pull out the filament from filament hub and try again.",
+  "1801960000020002": "AMS-HT B Environmental temperature is too low, which will affect the drying capability.",
+  "1801960000020004": "AMS-HT B The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "1801970000030001": "AMS-HT B chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "1801980000020001": "AMS-HT B The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "1801980000020002": "AMS-HT B The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "1802010000010001": "The AMS-HT C assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "1802010000010003": "The AMS-HT C assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1802010000010004": "The AMS-HT C assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "1802010000010005": "AMS-HT C The current sensor of assist motor may be faulty.",
+  "1802010000010011": "AMS-HT C The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1802010000020002": "The AMS-HT C assist motor is overloaded. The filament may be tangled or stuck.",
+  "1802010000020006": "AMS-HT C The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "1802010000020007": "AMS-HT C The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "1802010000020008": "AMS-HT C The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "1802010000020009": "AMS-HT C The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "1802010000020010": "AMS-HT C The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "1802010000020011": "AMS-HT C The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1802020000010001": "AMS-HT C Filament speed and length error: The filament odometry may be faulty.",
+  "1802020000020002": "AMS-HT C The odometer has no signal. The odometer connector may have poor contact.",
+  "1802020000020003": "AMS-HT C filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "1802100000010001": "The AMS-HT C slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1802100000010003": "The AMS-HT C slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1802100000020002": "The AMS-HT C slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "1802100000020004": "AMS-HT C The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1802110000010001": "The AMS-HT C slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1802110000010003": "The AMS-HT C slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1802110000020002": "The AMS-HT C slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "1802110000020004": "AMS-HT C The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1802120000010001": "The AMS-HT C slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1802120000010003": "The AMS-HT C slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1802120000020002": "The AMS-HT C slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "1802120000020004": "AMS-HT C The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1802130000010001": "The AMS-HT C slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1802130000010003": "The AMS-HT C slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1802130000020002": "The AMS-HT C slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "1802130000020004": "AMS-HT C The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1802200000010081": "Failed to read the filament information from AMS-HT C slot 1. The AMS main board may be malfunctioning.",
+  "1802200000010082": "Failed to read the filament information from AMS-HT C slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1802200000010083": "Failed to read the filament information from AMS-HT C slot 1. The RFID tag may be damaged.",
+  "1802200000010084": "Failed to read the filament information from AMS-HT C slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1802200000010085": "Failed to read the filament information from AMS-HT C slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1802200000010086": "Failed to read the filament information from AMS-HT C slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1802200000020001": "AMS-HT C Slot 1 filament has run out. Please insert a new filament.",
+  "1802200000020002": "AMS-HT C Slot 1 is empty; please insert a new filament.",
+  "1802200000020003": "AMS-HT C Slot 1's filament may be broken in AMS-HT.",
+  "1802200000020004": "AMS-HT C Slot 1 filament may be broken in the tool head.",
+  "1802200000020005": "AMS-HT C Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1802200000020006": "AMS-HT C has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1802200000020007": "AMS-HT C Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1802200000020008": "AMS-HT C Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1802200000020009": "Failed to extrude AMS-HT C Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180220000002000A": "Failed to adjust the filament position of AMS-HT C Slot 1. The filament or buffer may be stuck.",
+  "1802200000020010": "AMS-HT C slot 1 feeds filament out of AMS timeout.",
+  "1802200000020011": "AMS-HT C slot 1 pulls filament back to AMS timeout.",
+  "1802200000020012": "AMS-HT C slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "1802200000020013": "AMS-HT C slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1802200000020014": "AMS-HT C slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1802200000020015": "AMS-HT C slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1802200000020016": "AMS-HT C slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1802200000020017": "AMS-HT C slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1802200000020018": "AMS-HT C slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1802200000020019": "AMS-HT C slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1802200000020020": "AMS-HT C slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1802200000020021": "AMS-HT C slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1802200000020022": "AMS-HT C slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1802200000020023": "AMS-HT C slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1802200000020024": "AMS-HT C slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1802200000020025": "AMS-HT C slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1802200000020026": "AMS-HT C assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "1802200000020027": "AMS-HT C assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "1802200000030001": "AMS-HT C Slot 1 filament has run out. Please wait while old filament is purged.",
+  "1802200000030002": "AMS-HT C Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "1802210000010081": "Failed to read the filament information from AMS-HT C slot 2. The AMS main board may be malfunctioning.",
+  "1802210000010082": "Failed to read the filament information from AMS-HT C slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1802210000010083": "Failed to read the filament information from AMS-HT C slot 2. The RFID tag may be damaged.",
+  "1802210000010084": "Failed to read the filament information from AMS-HT C slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1802210000010085": "Failed to read the filament information from AMS-HT C slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1802210000010086": "Failed to read the filament information from AMS-HT C slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1802210000020001": "AMS-HT C Slot 2 filament has run out. Please insert a new filament.",
+  "1802210000020002": "AMS-HT C Slot 2 is empty; please insert a new filament.",
+  "1802210000020003": "AMS-HT C Slot 2's filament may be broken in AMS-HT.",
+  "1802210000020004": "AMS-HT C Slot 2 filament may be broken in the tool head.",
+  "1802210000020005": "AMS-HT C Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1802210000020006": "AMS-HT C has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1802210000020007": "AMS-HT C Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1802210000020008": "AMS-HT C Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1802210000020009": "Failed to extrude AMS-HT C Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180221000002000A": "Failed to adjust the filament position of AMS-HT C Slot 2. The filament or buffer may be stuck.",
+  "1802210000020010": "AMS-HT C slot 2 feeds filament out of AMS timeout.",
+  "1802210000020011": "AMS-HT C slot 2 pulls filament back to AMS timeout.",
+  "1802210000020012": "AMS-HT C slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "1802210000020013": "AMS-HT C slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1802210000020014": "AMS-HT C slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1802210000020015": "AMS-HT C slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1802210000020016": "AMS-HT C slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1802210000020017": "AMS-HT C slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1802210000020018": "AMS-HT C slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1802210000020019": "AMS-HT C slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1802210000020020": "AMS-HT C slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1802210000020021": "AMS-HT C slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1802210000020022": "AMS-HT C slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1802210000020023": "AMS-HT C slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1802210000020024": "AMS-HT C slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1802210000020025": "AMS-HT C slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1802210000030001": "AMS-HT C Slot 2 filament has run out. Please wait while old filament is purged.",
+  "1802210000030002": "AMS-HT C Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "1802220000010081": "Failed to read the filament information from AMS-HT C slot 3. The AMS main board may be malfunctioning.",
+  "1802220000010082": "Failed to read the filament information from AMS-HT C slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1802220000010083": "Failed to read the filament information from AMS-HT C slot 3. The RFID tag may be damaged.",
+  "1802220000010084": "Failed to read the filament information from AMS-HT C slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1802220000010085": "Failed to read the filament information from AMS-HT C slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1802220000010086": "Failed to read the filament information from AMS-HT C slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1802220000020001": "AMS-HT C Slot 3 filament has run out. Please insert a new filament.",
+  "1802220000020002": "AMS-HT C Slot 3 is empty; please insert a new filament.",
+  "1802220000020003": "AMS-HT C Slot 3's filament may be broken in AMS-HT.",
+  "1802220000020004": "AMS-HT C Slot 3 filament may be broken in the tool head.",
+  "1802220000020005": "AMS-HT C Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1802220000020006": "AMS-HT C has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1802220000020007": "AMS-HT C Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1802220000020008": "AMS-HT C Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1802220000020009": "Failed to extrude AMS-HT C Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180222000002000A": "Failed to adjust the filament position of AMS-HT C Slot 3. The filament or buffer may be stuck.",
+  "1802220000020010": "AMS-HT C slot 3 feeds filament out of AMS timeout.",
+  "1802220000020011": "AMS-HT C slot 3 pulls filament back to AMS timeout.",
+  "1802220000020012": "AMS-HT C slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "1802220000020013": "AMS-HT C slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1802220000020014": "AMS-HT C slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1802220000020015": "AMS-HT C slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1802220000020016": "AMS-HT C slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1802220000020017": "AMS-HT C slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1802220000020018": "AMS-HT C slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1802220000020019": "AMS-HT C slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1802220000020020": "AMS-HT C slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1802220000020021": "AMS-HT C slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1802220000020022": "AMS-HT C slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1802220000020023": "AMS-HT C slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1802220000020024": "AMS-HT C slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1802220000020025": "AMS-HT C slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1802220000030001": "AMS-HT C Slot 3 filament has run out. Please wait while old filament is purged.",
+  "1802220000030002": "AMS-HT C Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "1802230000010081": "Failed to read the filament information from AMS-HT C slot 4. The AMS main board may be malfunctioning.",
+  "1802230000010082": "Failed to read the filament information from AMS-HT C slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1802230000010083": "Failed to read the filament information from AMS-HT C slot 4. The RFID tag may be damaged.",
+  "1802230000010084": "Failed to read the filament information from AMS-HT C slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1802230000010085": "Failed to read the filament information from AMS-HT C slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1802230000010086": "Failed to read the filament information from AMS-HT C slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1802230000020001": "AMS-HT C Slot 4 filament has run out. Please insert a new filament.",
+  "1802230000020002": "AMS-HT C Slot 4 is empty; please insert a new filament.",
+  "1802230000020003": "AMS-HT C Slot 4's filament may be broken in AMS-HT.",
+  "1802230000020004": "AMS-HT C Slot 4 filament may be broken in the tool head.",
+  "1802230000020005": "AMS-HT C Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1802230000020006": "AMS-HT C has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1802230000020007": "AMS-HT C Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1802230000020008": "AMS-HT C Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1802230000020009": "Failed to extrude AMS-HT C Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180223000002000A": "Failed to adjust the filament position of AMS-HT C Slot 4. The filament or buffer may be stuck.",
+  "1802230000020010": "AMS-HT C slot 4 feeds filament out of AMS timeout.",
+  "1802230000020011": "AMS-HT C slot 4 pulls filament back to AMS timeout.",
+  "1802230000020012": "AMS-HT C slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "1802230000020013": "AMS-HT C slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1802230000020014": "AMS-HT C slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1802230000020015": "AMS-HT C slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1802230000020016": "AMS-HT C slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1802230000020017": "AMS-HT C slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1802230000020018": "AMS-HT C slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1802230000020019": "AMS-HT C slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1802230000020020": "AMS-HT C slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1802230000020021": "AMS-HT C slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1802230000020022": "AMS-HT C slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1802230000020023": "AMS-HT C slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1802230000020024": "AMS-HT C slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1802230000020025": "AMS-HT C slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1802230000030001": "AMS-HT C Slot 4 filament has run out. Please wait while old filament is purged.",
+  "1802230000030002": "AMS-HT C Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "1802240000010007": "AMS-HT C door detection is abnormal, the Hall sensor connection may be loose or disconnected.",
+  "1802240000020009": "AMS-HT C front cover is open. This may affect the drying performance or cause the filament to absorb moisture.",
+  "1802250000020001": "AMS-HT C uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "1802260000020001": "The induction hotend is heating up, and the drying power of AMS-HT C will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "1802300000010001": "The AMS-HT C RFID 1 board has an error.",
+  "1802300000010004": "Encryption chip failure",
+  "1802300000020002": "The RFID-tag on AMS-HT C Slot1 is damaged, or its content cannot be identified.",
+  "1802300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1802310000010001": "The AMS-HT C RFID 2 board has an error.",
+  "1802310000010004": "Encryption chip failure",
+  "1802310000020002": "The RFID-tag on AMS-HT C Slot2 is damaged, or its content cannot be identified.",
+  "1802310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1802320000020002": "The RFID-tag on AMS-HT C Slot3 is damaged, or its content cannot be identified.",
+  "1802330000020002": "The RFID-tag on AMS-HT C Slot4 is damaged, or its content cannot be identified.",
+  "1802350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "1802350000010002": "AMS-HT C The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "1802400000020001": "AMS-HT C Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "1802500000020001": "AMS-HT C communication is abnormal; please check the connection cable.",
+  "1802500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1802550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "1802550000010003": "AMS-HT C was detected offline during the AMS initialization process.",
+  "1802550000010004": "The binding between AMS-HT C and the extruder is incorrect. Please run the AMS Setup.",
+  "1802550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "1802560000030001": "AMS-HT C is undergoing dry cooling; please wait for it to cool down before operating.",
+  "1802600000020001": "The AMS-HT C Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1802610000020001": "The AMS-HT C Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1802620000020001": "The AMS-HT C Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1802630000020001": "The AMS-HT C Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1802700000020001": "Failed to pull out the AMS-HT C Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1802700000020002": "Failed to feed the AMS-HT C Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1802700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "1802700000020004": "Failed to pull back the AMS-HT C Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1802700000020005": "Failed to feed the AMS-HT C Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1802700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "1802700000020007": "AMS-HT filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "1802700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "180270000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "1802710000020001": "Failed to pull out the AMS-HT C Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1802710000020002": "Failed to feed the AMS-HT C Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1802710000020004": "Failed to pull back the AMS-HT C Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1802710000020005": "Failed to feed the AMS-HT C Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1802720000020001": "Failed to pull out the AMS-HT C Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1802720000020002": "Failed to feed the AMS-HT C Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1802720000020004": "Failed to pull back the AMS-HT C Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1802720000020005": "Failed to feed the AMS-HT C Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1802730000020001": "Failed to pull out the AMS-HT C Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1802730000020002": "Failed to feed the AMS-HT C Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1802730000020004": "Failed to pull back the AMS-HT C Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1802730000020005": "Failed to feed the AMS-HT C Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1802800000010001": "AMS-HT C The current sensor of heater 1 is abnormal.",
+  "1802800000010002": "AMS-HT C The heater 1 is disconnected, which may be due to poor connector contact.",
+  "1802800000010003": "AMS-HT C The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1802800000010004": "AMS-HT C The heater 1 is heating abnormally.",
+  "1802810000010001": "AMS-HT C The current sensor of heater 2 is abnormal.",
+  "1802810000010002": "AMS-HT C The heater 2 is disconnected, which may be due to poor connector contact.",
+  "1802810000010003": "AMS-HT C The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1802810000010004": "AMS-HT C The heater 2 is heating abnormally.",
+  "1802860000010004": "AMS-HT C is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "1802900000010002": "AMS-HT C The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "1802900000010003": "AMS-HT C The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "1802900000010004": "The current sensor of AMS-HT C exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1802900000020001": "AMS-HT C The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "1802910000010002": "AMS-HT C The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "1802910000010003": "AMS-HT C The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "1802910000010004": "The current sensor of AMS-HT C exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1802910000020001": "AMS-HT C The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "1802920000010001": "AMS-HT C The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "1802920000020002": "AMS-HT C The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "1802920000020003": "The AMS-HT C heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "1802930000010001": "AMS-HT C The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "1802930000020002": "AMS-HT C The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "1802930000020003": "The AMS-HT C heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "1802940000010001": "AMS-HT C The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "1802940000010002": "Temperature sensor 1 on the AMS-HT C heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1802950000010001": "AMS-HT C The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "1802950000010002": "Temperature sensor 2 on the AMS-HT C heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1802960000010001": "AMS-HT C The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "1802960000010003": "AMS-HT C Unable to start drying; please pull out the filament from filament hub and try again.",
+  "1802960000020002": "AMS-HT C Environmental temperature is too low, which will affect the drying capability.",
+  "1802960000020004": "AMS-HT C The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "1802970000030001": "AMS-HT C chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "1802980000020001": "AMS-HT C The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "1802980000020002": "AMS-HT C The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "1803010000010001": "The AMS-HT D assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "1803010000010003": "The AMS-HT D assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1803010000010004": "The AMS-HT D assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "1803010000010005": "AMS-HT D The current sensor of assist motor may be faulty.",
+  "1803010000010011": "AMS-HT D The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1803010000020002": "The AMS-HT D assist motor is overloaded. The filament may be tangled or stuck.",
+  "1803010000020006": "AMS-HT D The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "1803010000020007": "AMS-HT D The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "1803010000020008": "AMS-HT D The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "1803010000020009": "AMS-HT D The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "1803010000020010": "AMS-HT D The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "1803010000020011": "AMS-HT D The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1803020000010001": "AMS-HT D Filament speed and length error: The filament odometry may be faulty.",
+  "1803020000020002": "AMS-HT D The odometer has no signal. The odometer connector may have poor contact.",
+  "1803020000020003": "AMS-HT D filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "1803100000010001": "The AMS-HT D slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1803100000010003": "The AMS-HT D slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1803100000020002": "The AMS-HT D slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "1803100000020004": "AMS-HT D The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1803110000010001": "The AMS-HT D slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1803110000010003": "The AMS-HT D slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1803110000020002": "The AMS-HT D slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "1803110000020004": "AMS-HT D The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1803120000010001": "The AMS-HT D slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1803120000010003": "The AMS-HT D slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1803120000020002": "The AMS-HT D slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "1803120000020004": "AMS-HT D The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1803130000010001": "The AMS-HT D slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1803130000010003": "The AMS-HT D slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1803130000020002": "The AMS-HT D slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "1803130000020004": "AMS-HT D The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1803200000010081": "Failed to read the filament information from AMS-HT D slot 1. The AMS main board may be malfunctioning.",
+  "1803200000010082": "Failed to read the filament information from AMS-HT D slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1803200000010083": "Failed to read the filament information from AMS-HT D slot 1. The RFID tag may be damaged.",
+  "1803200000010084": "Failed to read the filament information from AMS-HT D slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1803200000010085": "Failed to read the filament information from AMS-HT D slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1803200000010086": "Failed to read the filament information from AMS-HT D slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1803200000020001": "AMS-HT D Slot 1 filament has run out. Please insert a new filament.",
+  "1803200000020002": "AMS-HT D Slot 1 is empty; please insert a new filament.",
+  "1803200000020003": "AMS-HT D Slot 1's filament may be broken in AMS-HT.",
+  "1803200000020004": "AMS-HT D Slot 1 filament may be broken in the tool head.",
+  "1803200000020005": "AMS-HT D Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1803200000020006": "AMS-HT D has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1803200000020007": "AMS-HT D Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1803200000020008": "AMS-HT D Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1803200000020009": "Failed to extrude AMS-HT D Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180320000002000A": "Failed to adjust the filament position of AMS-HT D Slot 1. The filament or buffer may be stuck.",
+  "1803200000020010": "AMS-HT D slot 1 feeds filament out of AMS timeout.",
+  "1803200000020011": "AMS-HT D slot 1 pulls filament back to AMS timeout.",
+  "1803200000020012": "AMS-HT D slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "1803200000020013": "AMS-HT D slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1803200000020014": "AMS-HT D slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1803200000020015": "AMS-HT D slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1803200000020016": "AMS-HT D slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1803200000020017": "AMS-HT D slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1803200000020018": "AMS-HT D slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1803200000020019": "AMS-HT D slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1803200000020020": "AMS-HT D slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1803200000020021": "AMS-HT D slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1803200000020022": "AMS-HT D slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1803200000020023": "AMS-HT D slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1803200000020024": "AMS-HT D slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1803200000020025": "AMS-HT D slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1803200000020026": "AMS-HT D assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "1803200000020027": "AMS-HT D assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "1803200000030001": "AMS-HT D Slot 1 filament has run out. Please wait while old filament is purged.",
+  "1803200000030002": "AMS-HT D Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "1803210000010081": "Failed to read the filament information from AMS-HT D slot 2. The AMS main board may be malfunctioning.",
+  "1803210000010082": "Failed to read the filament information from AMS-HT D slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1803210000010083": "Failed to read the filament information from AMS-HT D slot 2. The RFID tag may be damaged.",
+  "1803210000010084": "Failed to read the filament information from AMS-HT D slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1803210000010085": "Failed to read the filament information from AMS-HT D slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1803210000010086": "Failed to read the filament information from AMS-HT D slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1803210000020001": "AMS-HT D Slot 2 filament has run out. Please insert a new filament.",
+  "1803210000020002": "AMS-HT D Slot 2 is empty; please insert a new filament.",
+  "1803210000020003": "AMS-HT D Slot 2's filament may be broken in AMS-HT.",
+  "1803210000020004": "AMS-HT D Slot 2 filament may be broken in the tool head.",
+  "1803210000020005": "AMS-HT D Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1803210000020006": "AMS-HT D has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1803210000020007": "AMS-HT D Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1803210000020008": "AMS-HT D Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1803210000020009": "Failed to extrude AMS-HT D Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180321000002000A": "Failed to adjust the filament position of AMS-HT D Slot 2. The filament or buffer may be stuck.",
+  "1803210000020010": "AMS-HT D slot 2 feeds filament out of AMS timeout.",
+  "1803210000020011": "AMS-HT D slot 2 pulls filament back to AMS timeout.",
+  "1803210000020012": "AMS-HT D slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "1803210000020013": "AMS-HT D slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1803210000020014": "AMS-HT D slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1803210000020015": "AMS-HT D slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1803210000020016": "AMS-HT D slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1803210000020017": "AMS-HT D slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1803210000020018": "AMS-HT D slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1803210000020019": "AMS-HT D slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1803210000020020": "AMS-HT D slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1803210000020021": "AMS-HT D slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1803210000020022": "AMS-HT D slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1803210000020023": "AMS-HT D slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1803210000020024": "AMS-HT D slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1803210000020025": "AMS-HT D slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1803210000030001": "AMS-HT D Slot 2 filament has run out. Please wait while old filament is purged.",
+  "1803210000030002": "AMS-HT D Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "1803220000010081": "Failed to read the filament information from AMS-HT D slot 3. The AMS main board may be malfunctioning.",
+  "1803220000010082": "Failed to read the filament information from AMS-HT D slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1803220000010083": "Failed to read the filament information from AMS-HT D slot 3. The RFID tag may be damaged.",
+  "1803220000010084": "Failed to read the filament information from AMS-HT D slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1803220000010085": "Failed to read the filament information from AMS-HT D slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1803220000010086": "Failed to read the filament information from AMS-HT D slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1803220000020001": "AMS-HT D Slot 3 filament has run out. Please insert a new filament.",
+  "1803220000020002": "AMS-HT D Slot 3 is empty; please insert a new filament.",
+  "1803220000020003": "AMS-HT D Slot 3's filament may be broken in AMS-HT.",
+  "1803220000020004": "AMS-HT D Slot 3 filament may be broken in the tool head.",
+  "1803220000020005": "AMS-HT D Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1803220000020006": "AMS-HT D has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1803220000020007": "AMS-HT D Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1803220000020008": "AMS-HT D Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1803220000020009": "Failed to extrude AMS-HT D Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180322000002000A": "Failed to adjust the filament position of AMS-HT D Slot 3. The filament or buffer may be stuck.",
+  "1803220000020010": "AMS-HT D slot 3 feeds filament out of AMS timeout.",
+  "1803220000020011": "AMS-HT D slot 3 pulls filament back to AMS timeout.",
+  "1803220000020012": "AMS-HT D slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "1803220000020013": "AMS-HT D slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1803220000020014": "AMS-HT D slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1803220000020015": "AMS-HT D slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1803220000020016": "AMS-HT D slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1803220000020017": "AMS-HT D slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1803220000020018": "AMS-HT D slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1803220000020019": "AMS-HT D slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1803220000020020": "AMS-HT D slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1803220000020021": "AMS-HT D slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1803220000020022": "AMS-HT D slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1803220000020023": "AMS-HT D slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1803220000020024": "AMS-HT D slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1803220000020025": "AMS-HT D slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1803220000030001": "AMS-HT D Slot 3 filament has run out. Please wait while old filament is purged.",
+  "1803220000030002": "AMS-HT D Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "1803230000010081": "Failed to read the filament information from AMS-HT D slot 4. The AMS main board may be malfunctioning.",
+  "1803230000010082": "Failed to read the filament information from AMS-HT D slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1803230000010083": "Failed to read the filament information from AMS-HT D slot 4. The RFID tag may be damaged.",
+  "1803230000010084": "Failed to read the filament information from AMS-HT D slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1803230000010085": "Failed to read the filament information from AMS-HT D slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1803230000010086": "Failed to read the filament information from AMS-HT D slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1803230000020001": "AMS-HT D Slot 4 filament has run out. Please insert a new filament.",
+  "1803230000020002": "AMS-HT D Slot 4 is empty; please insert a new filament.",
+  "1803230000020003": "AMS-HT D Slot 4's filament may be broken in AMS-HT.",
+  "1803230000020004": "AMS-HT D Slot 4 filament may be broken in the tool head.",
+  "1803230000020005": "AMS-HT D Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1803230000020006": "AMS-HT D has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1803230000020007": "AMS-HT D Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1803230000020008": "AMS-HT D Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1803230000020009": "Failed to extrude AMS-HT D Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180323000002000A": "Failed to adjust the filament position of AMS-HT D Slot 4. The filament or buffer may be stuck.",
+  "1803230000020010": "AMS-HT D slot 4 feeds filament out of AMS timeout.",
+  "1803230000020011": "AMS-HT D slot 4 pulls filament back to AMS timeout.",
+  "1803230000020012": "AMS-HT D slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "1803230000020013": "AMS-HT D slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1803230000020014": "AMS-HT D slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1803230000020015": "AMS-HT D slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1803230000020016": "AMS-HT D slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1803230000020017": "AMS-HT D slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1803230000020018": "AMS-HT D slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1803230000020019": "AMS-HT D slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1803230000020020": "AMS-HT D slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1803230000020021": "AMS-HT D slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1803230000020022": "AMS-HT D slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1803230000020023": "AMS-HT D slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1803230000020024": "AMS-HT D slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1803230000020025": "AMS-HT D slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1803230000030001": "AMS-HT D Slot 4 filament has run out. Please wait while old filament is purged.",
+  "1803230000030002": "AMS-HT D Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "1803240000010007": "AMS-HT D door detection is abnormal, the Hall sensor connection may be loose or disconnected.",
+  "1803240000020009": "AMS-HT D front cover is open. This may affect the drying performance or cause the filament to absorb moisture.",
+  "1803250000020001": "AMS-HT D uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "1803260000020001": "The induction hotend is heating up, and the drying power of AMS-HT D will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "1803300000010001": "The AMS-HT D RFID 1 board has an error.",
+  "1803300000010004": "Encryption chip failure",
+  "1803300000020002": "The RFID-tag on AMS-HT D Slot1 is damaged, or its content cannot be identified.",
+  "1803300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1803310000010001": "The AMS-HT D RFID 2 board has an error.",
+  "1803310000010004": "Encryption chip failure",
+  "1803310000020002": "The RFID-tag on AMS-HT D Slot2 is damaged, or its content cannot be identified.",
+  "1803310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1803320000020002": "The RFID-tag on AMS-HT D Slot3 is damaged, or its content cannot be identified.",
+  "1803330000020002": "The RFID-tag on AMS-HT D Slot4 is damaged, or its content cannot be identified.",
+  "1803350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "1803350000010002": "AMS-HT D The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "1803400000020001": "AMS-HT D Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "1803500000020001": "AMS-HT D communication is abnormal; please check the connection cable.",
+  "1803500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1803550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "1803550000010003": "AMS-HT D was detected offline during the AMS initialization process.",
+  "1803550000010004": "The binding between AMS-HT D and the extruder is incorrect. Please run the AMS Setup.",
+  "1803550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "1803560000030001": "AMS-HT D is undergoing dry cooling; please wait for it to cool down before operating.",
+  "1803600000020001": "The AMS-HT D Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1803610000020001": "The AMS-HT D Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1803620000020001": "The AMS-HT D Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1803630000020001": "The AMS-HT D Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1803700000020001": "Failed to pull out the AMS-HT D Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1803700000020002": "Failed to feed the AMS-HT D Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1803700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "1803700000020004": "Failed to pull back the AMS-HT D Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1803700000020005": "Failed to feed the AMS-HT D Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1803700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "1803700000020007": "AMS-HT filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "1803700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "180370000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "1803710000020001": "Failed to pull out the AMS-HT D Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1803710000020002": "Failed to feed the AMS-HT D Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1803710000020004": "Failed to pull back the AMS-HT D Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1803710000020005": "Failed to feed the AMS-HT D Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1803720000020001": "Failed to pull out the AMS-HT D Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1803720000020002": "Failed to feed the AMS-HT D Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1803720000020004": "Failed to pull back the AMS-HT D Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1803720000020005": "Failed to feed the AMS-HT D Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1803730000020001": "Failed to pull out the AMS-HT D Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1803730000020002": "Failed to feed the AMS-HT D Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1803730000020004": "Failed to pull back the AMS-HT D Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1803730000020005": "Failed to feed the AMS-HT D Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1803800000010001": "AMS-HT D The current sensor of heater 1 is abnormal.",
+  "1803800000010002": "AMS-HT D The heater 1 is disconnected, which may be due to poor connector contact.",
+  "1803800000010003": "AMS-HT D The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1803800000010004": "AMS-HT D The heater 1 is heating abnormally.",
+  "1803810000010001": "AMS-HT D The current sensor of heater 2 is abnormal.",
+  "1803810000010002": "AMS-HT D The heater 2 is disconnected, which may be due to poor connector contact.",
+  "1803810000010003": "AMS-HT D The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1803810000010004": "AMS-HT D The heater 2 is heating abnormally.",
+  "1803860000010004": "AMS-HT D is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "1803900000010002": "AMS-HT D The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "1803900000010003": "AMS-HT D The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "1803900000010004": "The current sensor of AMS-HT D exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1803900000020001": "AMS-HT D The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "1803910000010002": "AMS-HT D The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "1803910000010003": "AMS-HT D The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "1803910000010004": "The current sensor of AMS-HT D exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1803910000020001": "AMS-HT D The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "1803920000010001": "AMS-HT D The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "1803920000020002": "AMS-HT D The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "1803920000020003": "The AMS-HT D heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "1803930000010001": "AMS-HT D The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "1803930000020002": "AMS-HT D The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "1803930000020003": "The AMS-HT D heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "1803940000010001": "AMS-HT D The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "1803940000010002": "Temperature sensor 1 on the AMS-HT D heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1803950000010001": "AMS-HT D The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "1803950000010002": "Temperature sensor 2 on the AMS-HT D heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1803960000010001": "AMS-HT D The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "1803960000010003": "AMS-HT D Unable to start drying; please pull out the filament from filament hub and try again.",
+  "1803960000020002": "AMS-HT D Environmental temperature is too low, which will affect the drying capability.",
+  "1803960000020004": "AMS-HT D The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "1803970000030001": "AMS-HT D chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "1803980000020001": "AMS-HT D The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "1803980000020002": "AMS-HT D The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "1804010000010001": "The AMS-HT E assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "1804010000010003": "The AMS-HT E assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1804010000010004": "The AMS-HT E assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "1804010000010005": "AMS-HT E The current sensor of assist motor may be faulty.",
+  "1804010000010011": "AMS-HT E The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1804010000020002": "The AMS-HT E assist motor is overloaded. The filament may be tangled or stuck.",
+  "1804010000020006": "AMS-HT E The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "1804010000020007": "AMS-HT E The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "1804010000020008": "AMS-HT E The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "1804010000020009": "AMS-HT E The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "1804010000020010": "AMS-HT E The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "1804010000020011": "AMS-HT E The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1804020000010001": "AMS-HT E Filament speed and length error: The filament odometry may be faulty.",
+  "1804020000020002": "AMS-HT E The odometer has no signal. The odometer connector may have poor contact.",
+  "1804020000020003": "AMS-HT E filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "1804100000010001": "The AMS-HT E slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1804100000010003": "The AMS-HT E slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1804100000020002": "The AMS-HT E slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "1804100000020004": "AMS-HT E The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1804110000010001": "The AMS-HT E slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1804110000010003": "The AMS-HT E slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1804110000020002": "The AMS-HT E slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "1804110000020004": "AMS-HT E The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1804120000010001": "The AMS-HT E slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1804120000010003": "The AMS-HT E slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1804120000020002": "The AMS-HT E slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "1804120000020004": "AMS-HT E The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1804130000010001": "The AMS-HT E slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1804130000010003": "The AMS-HT E slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1804130000020002": "The AMS-HT E slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "1804130000020004": "AMS-HT E The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1804200000010081": "Failed to read the filament information from AMS-HT E slot 1. The AMS main board may be malfunctioning.",
+  "1804200000010082": "Failed to read the filament information from AMS-HT E slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1804200000010083": "Failed to read the filament information from AMS-HT E slot 1. The RFID tag may be damaged.",
+  "1804200000010084": "Failed to read the filament information from AMS-HT E slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1804200000010085": "Failed to read the filament information from AMS-HT E slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1804200000010086": "Failed to read the filament information from AMS-HT E slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1804200000020001": "AMS-HT E Slot 1 filament has run out. Please insert a new filament.",
+  "1804200000020002": "AMS-HT E Slot 1 is empty; please insert a new filament.",
+  "1804200000020003": "AMS-HT E Slot 1's filament may be broken in AMS-HT.",
+  "1804200000020004": "AMS-HT E Slot 1 filament may be broken in the tool head.",
+  "1804200000020005": "AMS-HT E Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1804200000020006": "AMS-HT E has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1804200000020007": "AMS-HT E Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1804200000020008": "AMS-HT E Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1804200000020009": "Failed to extrude AMS-HT E Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180420000002000A": "Failed to adjust the filament position of AMS-HT E Slot 1. The filament or buffer may be stuck.",
+  "1804200000020010": "AMS-HT E slot 1 feeds filament out of AMS timeout.",
+  "1804200000020011": "AMS-HT E slot 1 pulls filament back to AMS timeout.",
+  "1804200000020012": "AMS-HT E slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "1804200000020013": "AMS-HT E slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1804200000020014": "AMS-HT E slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1804200000020015": "AMS-HT E slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1804200000020016": "AMS-HT E slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1804200000020017": "AMS-HT E slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1804200000020018": "AMS-HT E slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1804200000020019": "AMS-HT E slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1804200000020020": "AMS-HT E slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1804200000020021": "AMS-HT E slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1804200000020022": "AMS-HT E slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1804200000020023": "AMS-HT E slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1804200000020024": "AMS-HT E slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1804200000020025": "AMS-HT E slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1804200000020026": "AMS-HT E assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "1804200000020027": "AMS-HT E assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "1804200000030001": "AMS-HT E Slot 1 filament has run out. Please wait while old filament is purged.",
+  "1804200000030002": "AMS-HT E Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "1804210000010081": "Failed to read the filament information from AMS-HT E slot 2. The AMS main board may be malfunctioning.",
+  "1804210000010082": "Failed to read the filament information from AMS-HT E slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1804210000010083": "Failed to read the filament information from AMS-HT E slot 2. The RFID tag may be damaged.",
+  "1804210000010084": "Failed to read the filament information from AMS-HT E slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1804210000010085": "Failed to read the filament information from AMS-HT E slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1804210000010086": "Failed to read the filament information from AMS-HT E slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1804210000020001": "AMS-HT E Slot 2 filament has run out. Please insert a new filament.",
+  "1804210000020002": "AMS-HT E Slot 2 is empty; please insert a new filament.",
+  "1804210000020003": "AMS-HT E Slot 2's filament may be broken in AMS-HT.",
+  "1804210000020004": "AMS-HT E Slot 2 filament may be broken in the tool head.",
+  "1804210000020005": "AMS-HT E Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1804210000020006": "AMS-HT E has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1804210000020007": "AMS-HT E Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1804210000020008": "AMS-HT E Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1804210000020009": "Failed to extrude AMS-HT E Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180421000002000A": "Failed to adjust the filament position of AMS-HT E Slot 2. The filament or buffer may be stuck.",
+  "1804210000020010": "AMS-HT E slot 2 feeds filament out of AMS timeout.",
+  "1804210000020011": "AMS-HT E slot 2 pulls filament back to AMS timeout.",
+  "1804210000020012": "AMS-HT E slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "1804210000020013": "AMS-HT E slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1804210000020014": "AMS-HT E slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1804210000020015": "AMS-HT E slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1804210000020016": "AMS-HT E slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1804210000020017": "AMS-HT E slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1804210000020018": "AMS-HT E slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1804210000020019": "AMS-HT E slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1804210000020020": "AMS-HT E slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1804210000020021": "AMS-HT E slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1804210000020022": "AMS-HT E slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1804210000020023": "AMS-HT E slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1804210000020024": "AMS-HT E slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1804210000020025": "AMS-HT E slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1804210000030001": "AMS-HT E Slot 2 filament has run out. Please wait while old filament is purged.",
+  "1804210000030002": "AMS-HT E Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "1804220000010081": "Failed to read the filament information from AMS-HT E slot 3. The AMS main board may be malfunctioning.",
+  "1804220000010082": "Failed to read the filament information from AMS-HT E slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1804220000010083": "Failed to read the filament information from AMS-HT E slot 3. The RFID tag may be damaged.",
+  "1804220000010084": "Failed to read the filament information from AMS-HT E slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1804220000010085": "Failed to read the filament information from AMS-HT E slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1804220000010086": "Failed to read the filament information from AMS-HT E slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1804220000020001": "AMS-HT E Slot 3 filament has run out. Please insert a new filament.",
+  "1804220000020002": "AMS-HT E Slot 3 is empty; please insert a new filament.",
+  "1804220000020003": "AMS-HT E Slot 3's filament may be broken in AMS-HT.",
+  "1804220000020004": "AMS-HT E Slot 3 filament may be broken in the tool head.",
+  "1804220000020005": "AMS-HT E Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1804220000020006": "AMS-HT E has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1804220000020007": "AMS-HT E Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1804220000020008": "AMS-HT E Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1804220000020009": "Failed to extrude AMS-HT E Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180422000002000A": "Failed to adjust the filament position of AMS-HT E Slot 3. The filament or buffer may be stuck.",
+  "1804220000020010": "AMS-HT E slot 3 feeds filament out of AMS timeout.",
+  "1804220000020011": "AMS-HT E slot 3 pulls filament back to AMS timeout.",
+  "1804220000020012": "AMS-HT E slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "1804220000020013": "AMS-HT E slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1804220000020014": "AMS-HT E slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1804220000020015": "AMS-HT E slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1804220000020016": "AMS-HT E slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1804220000020017": "AMS-HT E slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1804220000020018": "AMS-HT E slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1804220000020019": "AMS-HT E slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1804220000020020": "AMS-HT E slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1804220000020021": "AMS-HT E slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1804220000020022": "AMS-HT E slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1804220000020023": "AMS-HT E slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1804220000020024": "AMS-HT E slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1804220000020025": "AMS-HT E slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1804220000030001": "AMS-HT E Slot 3 filament has run out. Please wait while old filament is purged.",
+  "1804220000030002": "AMS-HT E Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "1804230000010081": "Failed to read the filament information from AMS-HT E slot 4. The AMS main board may be malfunctioning.",
+  "1804230000010082": "Failed to read the filament information from AMS-HT E slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1804230000010083": "Failed to read the filament information from AMS-HT E slot 4. The RFID tag may be damaged.",
+  "1804230000010084": "Failed to read the filament information from AMS-HT E slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1804230000010085": "Failed to read the filament information from AMS-HT E slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1804230000010086": "Failed to read the filament information from AMS-HT E slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1804230000020001": "AMS-HT E Slot 4 filament has run out. Please insert a new filament.",
+  "1804230000020002": "AMS-HT E Slot 4 is empty; please insert a new filament.",
+  "1804230000020003": "AMS-HT E Slot 4's filament may be broken in AMS-HT.",
+  "1804230000020004": "AMS-HT E Slot 4 filament may be broken in the tool head.",
+  "1804230000020005": "AMS-HT E Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1804230000020006": "AMS-HT E has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1804230000020007": "AMS-HT E Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1804230000020008": "AMS-HT E Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1804230000020009": "Failed to extrude AMS-HT E Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180423000002000A": "Failed to adjust the filament position of AMS-HT E Slot 4. The filament or buffer may be stuck.",
+  "1804230000020010": "AMS-HT E slot 4 feeds filament out of AMS timeout.",
+  "1804230000020011": "AMS-HT E slot 4 pulls filament back to AMS timeout.",
+  "1804230000020012": "AMS-HT E slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "1804230000020013": "AMS-HT E slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1804230000020014": "AMS-HT E slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1804230000020015": "AMS-HT E slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1804230000020016": "AMS-HT E slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1804230000020017": "AMS-HT E slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1804230000020018": "AMS-HT E slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1804230000020019": "AMS-HT E slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1804230000020020": "AMS-HT E slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1804230000020021": "AMS-HT E slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1804230000020022": "AMS-HT E slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1804230000020023": "AMS-HT E slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1804230000020024": "AMS-HT E slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1804230000020025": "AMS-HT E slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1804230000030001": "AMS-HT E Slot 4 filament has run out. Please wait while old filament is purged.",
+  "1804230000030002": "AMS-HT E Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "1804240000010007": "AMS-HT E door detection is abnormal, the Hall sensor connection may be loose or disconnected.",
+  "1804240000020009": "AMS-HT E front cover is open. This may affect the drying performance or cause the filament to absorb moisture.",
+  "1804250000020001": "AMS-HT E uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "1804260000020001": "The induction hotend is heating up, and the drying power of AMS-HT E will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "1804300000010001": "The AMS-HT E RFID 1 board has an error.",
+  "1804300000010004": "Encryption chip failure",
+  "1804300000020002": "The RFID-tag on AMS-HT E Slot1 is damaged, or its content cannot be identified.",
+  "1804300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1804310000010001": "The AMS-HT E RFID 2 board has an error.",
+  "1804310000010004": "Encryption chip failure",
+  "1804310000020002": "The RFID-tag on AMS-HT E Slot2 is damaged, or its content cannot be identified.",
+  "1804310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1804320000020002": "The RFID-tag on AMS-HT E Slot3 is damaged, or its content cannot be identified.",
+  "1804330000020002": "The RFID-tag on AMS-HT E Slot4 is damaged, or its content cannot be identified.",
+  "1804350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "1804350000010002": "AMS-HT E The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "1804400000020001": "AMS-HT E Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "1804500000020001": "AMS-HT E communication is abnormal; please check the connection cable.",
+  "1804500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1804550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "1804550000010003": "AMS-HT E was detected offline during the AMS initialization process.",
+  "1804550000010004": "The binding between AMS-HT E and the extruder is incorrect. Please run the AMS Setup.",
+  "1804550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "1804560000030001": "AMS-HT E is undergoing dry cooling; please wait for it to cool down before operating.",
+  "1804600000020001": "The AMS-HT E Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1804610000020001": "The AMS-HT E Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1804620000020001": "The AMS-HT E Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1804630000020001": "The AMS-HT E Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1804700000020001": "Failed to pull out the AMS-HT E Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1804700000020002": "Failed to feed the AMS-HT E Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1804700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "1804700000020004": "Failed to pull back the AMS-HT E Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1804700000020005": "Failed to feed the AMS-HT E Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1804700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "1804700000020007": "AMS-HT filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "1804700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "180470000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "1804710000020001": "Failed to pull out the AMS-HT E Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1804710000020002": "Failed to feed the AMS-HT E Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1804710000020004": "Failed to pull back the AMS-HT E Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1804710000020005": "Failed to feed the AMS-HT E Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1804720000020001": "Failed to pull out the AMS-HT E Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1804720000020002": "Failed to feed the AMS-HT E Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1804720000020004": "Failed to pull back the AMS-HT E Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1804720000020005": "Failed to feed the AMS-HT E Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1804730000020001": "Failed to pull out the AMS-HT E Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1804730000020002": "Failed to feed the AMS-HT E Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1804730000020004": "Failed to pull back the AMS-HT E Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1804730000020005": "Failed to feed the AMS-HT E Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1804800000010001": "AMS-HT E The current sensor of heater 1 is abnormal.",
+  "1804800000010002": "AMS-HT E The heater 1 is disconnected, which may be due to poor connector contact.",
+  "1804800000010003": "AMS-HT E The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1804800000010004": "AMS-HT E The heater 1 is heating abnormally.",
+  "1804810000010001": "AMS-HT E The current sensor of heater 2 is abnormal.",
+  "1804810000010002": "AMS-HT E The heater 2 is disconnected, which may be due to poor connector contact.",
+  "1804810000010003": "AMS-HT E The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1804810000010004": "AMS-HT E The heater 2 is heating abnormally.",
+  "1804860000010004": "AMS-HT E is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "1804900000010002": "AMS-HT E The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "1804900000010003": "AMS-HT E The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "1804900000010004": "The current sensor of AMS-HT E exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1804900000020001": "AMS-HT E The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "1804910000010002": "AMS-HT E The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "1804910000010003": "AMS-HT E The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "1804910000010004": "The current sensor of AMS-HT E exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1804910000020001": "AMS-HT E The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "1804920000010001": "AMS-HT E The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "1804920000020002": "AMS-HT E The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "1804920000020003": "The AMS-HT E heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "1804930000010001": "AMS-HT E The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "1804930000020002": "AMS-HT E The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "1804930000020003": "The AMS-HT E heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "1804940000010001": "AMS-HT E The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "1804940000010002": "Temperature sensor 1 on the AMS-HT E heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1804950000010001": "AMS-HT E The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "1804950000010002": "Temperature sensor 2 on the AMS-HT E heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1804960000010001": "AMS-HT E The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "1804960000010003": "AMS-HT E Unable to start drying; please pull out the filament from filament hub and try again.",
+  "1804960000020002": "AMS-HT E Environmental temperature is too low, which will affect the drying capability.",
+  "1804960000020004": "AMS-HT E The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "1804970000030001": "AMS-HT E chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "1804980000020001": "AMS-HT E The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "1804980000020002": "AMS-HT E The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "1805010000010001": "The AMS-HT F assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "1805010000010003": "The AMS-HT F assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1805010000010004": "The AMS-HT F assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "1805010000010005": "AMS-HT F The current sensor of assist motor may be faulty.",
+  "1805010000010011": "AMS-HT F The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1805010000020002": "The AMS-HT F assist motor is overloaded. The filament may be tangled or stuck.",
+  "1805010000020006": "AMS-HT F The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "1805010000020007": "AMS-HT F The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "1805010000020008": "AMS-HT F The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "1805010000020009": "AMS-HT F The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "1805010000020010": "AMS-HT F The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "1805010000020011": "AMS-HT F The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1805020000010001": "AMS-HT F Filament speed and length error: The filament odometry may be faulty.",
+  "1805020000020002": "AMS-HT F The odometer has no signal. The odometer connector may have poor contact.",
+  "1805020000020003": "AMS-HT F filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "1805100000010001": "The AMS-HT F slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1805100000010003": "The AMS-HT F slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1805100000020002": "The AMS-HT F slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "1805100000020004": "AMS-HT F The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1805110000010001": "The AMS-HT F slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1805110000010003": "The AMS-HT F slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1805110000020002": "The AMS-HT F slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "1805110000020004": "AMS-HT F The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1805120000010001": "The AMS-HT F slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1805120000010003": "The AMS-HT F slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1805120000020002": "The AMS-HT F slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "1805120000020004": "AMS-HT F The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1805130000010001": "The AMS-HT F slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1805130000010003": "The AMS-HT F slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1805130000020002": "The AMS-HT F slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "1805130000020004": "AMS-HT F The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1805200000010081": "Failed to read the filament information from AMS-HT F slot 1. The AMS main board may be malfunctioning.",
+  "1805200000010082": "Failed to read the filament information from AMS-HT F slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1805200000010083": "Failed to read the filament information from AMS-HT F slot 1. The RFID tag may be damaged.",
+  "1805200000010084": "Failed to read the filament information from AMS-HT F slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1805200000010085": "Failed to read the filament information from AMS-HT F slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1805200000010086": "Failed to read the filament information from AMS-HT F slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1805200000020001": "AMS-HT F Slot 1 filament has run out. Please insert a new filament.",
+  "1805200000020002": "AMS-HT F Slot 1 is empty; please insert a new filament.",
+  "1805200000020003": "AMS-HT F Slot 1's filament may be broken in AMS-HT.",
+  "1805200000020004": "AMS-HT F Slot 1 filament may be broken in the tool head.",
+  "1805200000020005": "AMS-HT F Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1805200000020006": "AMS-HT F has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1805200000020007": "AMS-HT F Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1805200000020008": "AMS-HT F Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1805200000020009": "Failed to extrude AMS-HT F Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180520000002000A": "Failed to adjust the filament position of AMS-HT F Slot 1. The filament or buffer may be stuck.",
+  "1805200000020010": "AMS-HT F slot 1 feeds filament out of AMS timeout.",
+  "1805200000020011": "AMS-HT F slot 1 pulls filament back to AMS timeout.",
+  "1805200000020012": "AMS-HT F slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "1805200000020013": "AMS-HT F slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1805200000020014": "AMS-HT F slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1805200000020015": "AMS-HT F slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1805200000020016": "AMS-HT F slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1805200000020017": "AMS-HT F slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1805200000020018": "AMS-HT F slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1805200000020019": "AMS-HT F slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1805200000020020": "AMS-HT F slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1805200000020021": "AMS-HT F slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1805200000020022": "AMS-HT F slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1805200000020023": "AMS-HT F slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1805200000020024": "AMS-HT F slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1805200000020025": "AMS-HT F slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1805200000020026": "AMS-HT F assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "1805200000020027": "AMS-HT F assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "1805200000030001": "AMS-HT F Slot 1 filament has run out. Please wait while old filament is purged.",
+  "1805200000030002": "AMS-HT F Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "1805210000010081": "Failed to read the filament information from AMS-HT F slot 2. The AMS main board may be malfunctioning.",
+  "1805210000010082": "Failed to read the filament information from AMS-HT F slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1805210000010083": "Failed to read the filament information from AMS-HT F slot 2. The RFID tag may be damaged.",
+  "1805210000010084": "Failed to read the filament information from AMS-HT F slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1805210000010085": "Failed to read the filament information from AMS-HT F slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1805210000010086": "Failed to read the filament information from AMS-HT F slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1805210000020001": "AMS-HT F Slot 2 filament has run out. Please insert a new filament.",
+  "1805210000020002": "AMS-HT F Slot 2 is empty; please insert a new filament.",
+  "1805210000020003": "AMS-HT F Slot 2's filament may be broken in AMS-HT.",
+  "1805210000020004": "AMS-HT F Slot 2 filament may be broken in the tool head.",
+  "1805210000020005": "AMS-HT F Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1805210000020006": "AMS-HT F has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1805210000020007": "AMS-HT F Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1805210000020008": "AMS-HT F Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1805210000020009": "Failed to extrude AMS-HT F Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180521000002000A": "Failed to adjust the filament position of AMS-HT F Slot 2. The filament or buffer may be stuck.",
+  "1805210000020010": "AMS-HT F slot 2 feeds filament out of AMS timeout.",
+  "1805210000020011": "AMS-HT F slot 2 pulls filament back to AMS timeout.",
+  "1805210000020012": "AMS-HT F slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "1805210000020013": "AMS-HT F slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1805210000020014": "AMS-HT F slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1805210000020015": "AMS-HT F slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1805210000020016": "AMS-HT F slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1805210000020017": "AMS-HT F slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1805210000020018": "AMS-HT F slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1805210000020019": "AMS-HT F slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1805210000020020": "AMS-HT F slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1805210000020021": "AMS-HT F slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1805210000020022": "AMS-HT F slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1805210000020023": "AMS-HT F slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1805210000020024": "AMS-HT F slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1805210000020025": "AMS-HT F slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1805210000030001": "AMS-HT F Slot 2 filament has run out. Please wait while old filament is purged.",
+  "1805210000030002": "AMS-HT F Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "1805220000010081": "Failed to read the filament information from AMS-HT F slot 3. The AMS main board may be malfunctioning.",
+  "1805220000010082": "Failed to read the filament information from AMS-HT F slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1805220000010083": "Failed to read the filament information from AMS-HT F slot 3. The RFID tag may be damaged.",
+  "1805220000010084": "Failed to read the filament information from AMS-HT F slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1805220000010085": "Failed to read the filament information from AMS-HT F slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1805220000010086": "Failed to read the filament information from AMS-HT F slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1805220000020001": "AMS-HT F Slot 3 filament has run out. Please insert a new filament.",
+  "1805220000020002": "AMS-HT F Slot 3 is empty; please insert a new filament.",
+  "1805220000020003": "AMS-HT F Slot 3's filament may be broken in AMS-HT.",
+  "1805220000020004": "AMS-HT F Slot 3 filament may be broken in the tool head.",
+  "1805220000020005": "AMS-HT F Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1805220000020006": "AMS-HT F has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1805220000020007": "AMS-HT F Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1805220000020008": "AMS-HT F Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1805220000020009": "Failed to extrude AMS-HT F Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180522000002000A": "Failed to adjust the filament position of AMS-HT F Slot 3. The filament or buffer may be stuck.",
+  "1805220000020010": "AMS-HT F slot 3 feeds filament out of AMS timeout.",
+  "1805220000020011": "AMS-HT F slot 3 pulls filament back to AMS timeout.",
+  "1805220000020012": "AMS-HT F slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "1805220000020013": "AMS-HT F slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1805220000020014": "AMS-HT F slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1805220000020015": "AMS-HT F slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1805220000020016": "AMS-HT F slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1805220000020017": "AMS-HT F slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1805220000020018": "AMS-HT F slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1805220000020019": "AMS-HT F slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1805220000020020": "AMS-HT F slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1805220000020021": "AMS-HT F slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1805220000020022": "AMS-HT F slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1805220000020023": "AMS-HT F slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1805220000020024": "AMS-HT F slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1805220000020025": "AMS-HT F slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1805220000030001": "AMS-HT F Slot 3 filament has run out. Please wait while old filament is purged.",
+  "1805220000030002": "AMS-HT F Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "1805230000010081": "Failed to read the filament information from AMS-HT F slot 4. The AMS main board may be malfunctioning.",
+  "1805230000010082": "Failed to read the filament information from AMS-HT F slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1805230000010083": "Failed to read the filament information from AMS-HT F slot 4. The RFID tag may be damaged.",
+  "1805230000010084": "Failed to read the filament information from AMS-HT F slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1805230000010085": "Failed to read the filament information from AMS-HT F slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1805230000010086": "Failed to read the filament information from AMS-HT F slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1805230000020001": "AMS-HT F Slot 4 filament has run out. Please insert a new filament.",
+  "1805230000020002": "AMS-HT F Slot 4 is empty; please insert a new filament.",
+  "1805230000020003": "AMS-HT F Slot 4's filament may be broken in AMS-HT.",
+  "1805230000020004": "AMS-HT F Slot 4 filament may be broken in the tool head.",
+  "1805230000020005": "AMS-HT F Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1805230000020006": "AMS-HT F has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1805230000020007": "AMS-HT F Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1805230000020008": "AMS-HT F Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1805230000020009": "Failed to extrude AMS-HT F Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180523000002000A": "Failed to adjust the filament position of AMS-HT F Slot 4. The filament or buffer may be stuck.",
+  "1805230000020010": "AMS-HT F slot 4 feeds filament out of AMS timeout.",
+  "1805230000020011": "AMS-HT F slot 4 pulls filament back to AMS timeout.",
+  "1805230000020012": "AMS-HT F slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "1805230000020013": "AMS-HT F slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1805230000020014": "AMS-HT F slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1805230000020015": "AMS-HT F slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1805230000020016": "AMS-HT F slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1805230000020017": "AMS-HT F slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1805230000020018": "AMS-HT F slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1805230000020019": "AMS-HT F slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1805230000020020": "AMS-HT F slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1805230000020021": "AMS-HT F slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1805230000020022": "AMS-HT F slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1805230000020023": "AMS-HT F slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1805230000020024": "AMS-HT F slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1805230000020025": "AMS-HT F slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1805230000030001": "AMS-HT F Slot 4 filament has run out. Please wait while old filament is purged.",
+  "1805230000030002": "AMS-HT F Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "1805240000010007": "AMS-HT F door detection is abnormal, the Hall sensor connection may be loose or disconnected.",
+  "1805240000020009": "AMS-HT F front cover is open. This may affect the drying performance or cause the filament to absorb moisture.",
+  "1805250000020001": "AMS-HT F uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "1805260000020001": "The induction hotend is heating up, and the drying power of AMS-HT F will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "1805300000010001": "The AMS-HT F RFID 1 board has an error.",
+  "1805300000010004": "Encryption chip failure",
+  "1805300000020002": "The RFID-tag on AMS-HT F Slot1 is damaged, or its content cannot be identified.",
+  "1805300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1805310000010001": "The AMS-HT F RFID 2 board has an error.",
+  "1805310000010004": "Encryption chip failure",
+  "1805310000020002": "The RFID-tag on AMS-HT F Slot2 is damaged, or its content cannot be identified.",
+  "1805310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1805320000020002": "The RFID-tag on AMS-HT F Slot3 is damaged, or its content cannot be identified.",
+  "1805330000020002": "The RFID-tag on AMS-HT F Slot4 is damaged, or its content cannot be identified.",
+  "1805350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "1805350000010002": "AMS-HT F The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "1805400000020001": "AMS-HT F Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "1805500000020001": "AMS-HT F communication is abnormal; please check the connection cable.",
+  "1805500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1805550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "1805550000010003": "AMS-HT F was detected offline during the AMS initialization process.",
+  "1805550000010004": "The binding between AMS-HT F and the extruder is incorrect. Please run the AMS Setup.",
+  "1805550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "1805560000030001": "AMS-HT F is undergoing dry cooling; please wait for it to cool down before operating.",
+  "1805600000020001": "The AMS-HT F Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1805610000020001": "The AMS-HT F Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1805620000020001": "The AMS-HT F Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1805630000020001": "The AMS-HT F Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1805700000020001": "Failed to pull out the AMS-HT F Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1805700000020002": "Failed to feed the AMS-HT F Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1805700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "1805700000020004": "Failed to pull back the AMS-HT F Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1805700000020005": "Failed to feed the AMS-HT F Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1805700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "1805700000020007": "AMS-HT filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "1805700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "180570000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "1805710000020001": "Failed to pull out the AMS-HT F Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1805710000020002": "Failed to feed the AMS-HT F Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1805710000020004": "Failed to pull back the AMS-HT F Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1805710000020005": "Failed to feed the AMS-HT F Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1805720000020001": "Failed to pull out the AMS-HT F Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1805720000020002": "Failed to feed the AMS-HT F Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1805720000020004": "Failed to pull back the AMS-HT F Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1805720000020005": "Failed to feed the AMS-HT F Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1805730000020001": "Failed to pull out the AMS-HT F Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1805730000020002": "Failed to feed the AMS-HT F Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1805730000020004": "Failed to pull back the AMS-HT F Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1805730000020005": "Failed to feed the AMS-HT F Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1805800000010001": "AMS-HT F The current sensor of heater 1 is abnormal.",
+  "1805800000010002": "AMS-HT F The heater 1 is disconnected, which may be due to poor connector contact.",
+  "1805800000010003": "AMS-HT F The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1805800000010004": "AMS-HT F The heater 1 is heating abnormally.",
+  "1805810000010001": "AMS-HT F The current sensor of heater 2 is abnormal.",
+  "1805810000010002": "AMS-HT F The heater 2 is disconnected, which may be due to poor connector contact.",
+  "1805810000010003": "AMS-HT F The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1805810000010004": "AMS-HT F The heater 2 is heating abnormally.",
+  "1805860000010004": "AMS-HT F is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "1805900000010002": "AMS-HT F The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "1805900000010003": "AMS-HT F The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "1805900000010004": "The current sensor of AMS-HT F exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1805900000020001": "AMS-HT F The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "1805910000010002": "AMS-HT F The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "1805910000010003": "AMS-HT F The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "1805910000010004": "The current sensor of AMS-HT F exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1805910000020001": "AMS-HT F The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "1805920000010001": "AMS-HT F The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "1805920000020002": "AMS-HT F The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "1805920000020003": "The AMS-HT F heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "1805930000010001": "AMS-HT F The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "1805930000020002": "AMS-HT F The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "1805930000020003": "The AMS-HT F heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "1805940000010001": "AMS-HT F The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "1805940000010002": "Temperature sensor 1 on the AMS-HT F heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1805950000010001": "AMS-HT F The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "1805950000010002": "Temperature sensor 2 on the AMS-HT F heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1805960000010001": "AMS-HT F The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "1805960000010003": "AMS-HT F Unable to start drying; please pull out the filament from filament hub and try again.",
+  "1805960000020002": "AMS-HT F Environmental temperature is too low, which will affect the drying capability.",
+  "1805960000020004": "AMS-HT F The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "1805970000030001": "AMS-HT F chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "1805980000020001": "AMS-HT F The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "1805980000020002": "AMS-HT F The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "1806010000010001": "The AMS-HT G assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "1806010000010003": "The AMS-HT G assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1806010000010004": "The AMS-HT G assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "1806010000010005": "AMS-HT G The current sensor of assist motor may be faulty.",
+  "1806010000010011": "AMS-HT G The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1806010000020002": "The AMS-HT G assist motor is overloaded. The filament may be tangled or stuck.",
+  "1806010000020006": "AMS-HT G The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "1806010000020007": "AMS-HT G The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "1806010000020008": "AMS-HT G The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "1806010000020009": "AMS-HT G The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "1806010000020010": "AMS-HT G The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "1806010000020011": "AMS-HT G The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1806020000010001": "AMS-HT G Filament speed and length error: The filament odometry may be faulty.",
+  "1806020000020002": "AMS-HT G The odometer has no signal. The odometer connector may have poor contact.",
+  "1806020000020003": "AMS-HT G filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "1806100000010001": "The AMS-HT G slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1806100000010003": "The AMS-HT G slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1806100000020002": "The AMS-HT G slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "1806100000020004": "AMS-HT G The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1806110000010001": "The AMS-HT G slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1806110000010003": "The AMS-HT G slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1806110000020002": "The AMS-HT G slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "1806110000020004": "AMS-HT G The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1806120000010001": "The AMS-HT G slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1806120000010003": "The AMS-HT G slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1806120000020002": "The AMS-HT G slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "1806120000020004": "AMS-HT G The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1806130000010001": "The AMS-HT G slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1806130000010003": "The AMS-HT G slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1806130000020002": "The AMS-HT G slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "1806130000020004": "AMS-HT G The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1806200000010081": "Failed to read the filament information from AMS-HT G slot 1. The AMS main board may be malfunctioning.",
+  "1806200000010082": "Failed to read the filament information from AMS-HT G slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1806200000010083": "Failed to read the filament information from AMS-HT G slot 1. The RFID tag may be damaged.",
+  "1806200000010084": "Failed to read the filament information from AMS-HT G slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1806200000010085": "Failed to read the filament information from AMS-HT G slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1806200000010086": "Failed to read the filament information from AMS-HT G slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1806200000020001": "AMS-HT G Slot 1 filament has run out. Please insert a new filament.",
+  "1806200000020002": "AMS-HT G Slot 1 is empty; please insert a new filament.",
+  "1806200000020003": "AMS-HT G Slot 1's filament may be broken in AMS-HT.",
+  "1806200000020004": "AMS-HT G Slot 1 filament may be broken in the tool head.",
+  "1806200000020005": "AMS-HT G Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1806200000020006": "AMS-HT G has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1806200000020007": "AMS-HT G Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1806200000020008": "AMS-HT G Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1806200000020009": "Failed to extrude AMS-HT G Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180620000002000A": "Failed to adjust the filament position of AMS-HT G Slot 1. The filament or buffer may be stuck.",
+  "1806200000020010": "AMS-HT G slot 1 feeds filament out of AMS timeout.",
+  "1806200000020011": "AMS-HT G slot 1 pulls filament back to AMS timeout.",
+  "1806200000020012": "AMS-HT G slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "1806200000020013": "AMS-HT G slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1806200000020014": "AMS-HT G slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1806200000020015": "AMS-HT G slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1806200000020016": "AMS-HT G slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1806200000020017": "AMS-HT G slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1806200000020018": "AMS-HT G slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1806200000020019": "AMS-HT G slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1806200000020020": "AMS-HT G slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1806200000020021": "AMS-HT G slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1806200000020022": "AMS-HT G slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1806200000020023": "AMS-HT G slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1806200000020024": "AMS-HT G slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1806200000020025": "AMS-HT G slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1806200000020026": "AMS-HT G assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "1806200000020027": "AMS-HT G assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "1806200000030001": "AMS-HT G Slot 1 filament has run out. Please wait while old filament is purged.",
+  "1806200000030002": "AMS-HT G Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "1806210000010081": "Failed to read the filament information from AMS-HT G slot 2. The AMS main board may be malfunctioning.",
+  "1806210000010082": "Failed to read the filament information from AMS-HT G slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1806210000010083": "Failed to read the filament information from AMS-HT G slot 2. The RFID tag may be damaged.",
+  "1806210000010084": "Failed to read the filament information from AMS-HT G slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1806210000010085": "Failed to read the filament information from AMS-HT G slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1806210000010086": "Failed to read the filament information from AMS-HT G slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1806210000020001": "AMS-HT G Slot 2 filament has run out. Please insert a new filament.",
+  "1806210000020002": "AMS-HT G Slot 2 is empty; please insert a new filament.",
+  "1806210000020003": "AMS-HT G Slot 2's filament may be broken in AMS-HT.",
+  "1806210000020004": "AMS-HT G Slot 2 filament may be broken in the tool head.",
+  "1806210000020005": "AMS-HT G Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1806210000020006": "AMS-HT G has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1806210000020007": "AMS-HT G Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1806210000020008": "AMS-HT G Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1806210000020009": "Failed to extrude AMS-HT G Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180621000002000A": "Failed to adjust the filament position of AMS-HT G Slot 2. The filament or buffer may be stuck.",
+  "1806210000020010": "AMS-HT G slot 2 feeds filament out of AMS timeout.",
+  "1806210000020011": "AMS-HT G slot 2 pulls filament back to AMS timeout.",
+  "1806210000020012": "AMS-HT G slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "1806210000020013": "AMS-HT G slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1806210000020014": "AMS-HT G slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1806210000020015": "AMS-HT G slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1806210000020016": "AMS-HT G slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1806210000020017": "AMS-HT G slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1806210000020018": "AMS-HT G slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1806210000020019": "AMS-HT G slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1806210000020020": "AMS-HT G slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1806210000020021": "AMS-HT G slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1806210000020022": "AMS-HT G slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1806210000020023": "AMS-HT G slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1806210000020024": "AMS-HT G slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1806210000020025": "AMS-HT G slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1806210000030001": "AMS-HT G Slot 2 filament has run out. Please wait while old filament is purged.",
+  "1806210000030002": "AMS-HT G Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "1806220000010081": "Failed to read the filament information from AMS-HT G slot 3. The AMS main board may be malfunctioning.",
+  "1806220000010082": "Failed to read the filament information from AMS-HT G slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1806220000010083": "Failed to read the filament information from AMS-HT G slot 3. The RFID tag may be damaged.",
+  "1806220000010084": "Failed to read the filament information from AMS-HT G slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1806220000010085": "Failed to read the filament information from AMS-HT G slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1806220000010086": "Failed to read the filament information from AMS-HT G slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1806220000020001": "AMS-HT G Slot 3 filament has run out. Please insert a new filament.",
+  "1806220000020002": "AMS-HT G Slot 3 is empty; please insert a new filament.",
+  "1806220000020003": "AMS-HT G Slot 3's filament may be broken in AMS-HT.",
+  "1806220000020004": "AMS-HT G Slot 3 filament may be broken in the tool head.",
+  "1806220000020005": "AMS-HT G Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1806220000020006": "AMS-HT G has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1806220000020007": "AMS-HT G Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1806220000020008": "AMS-HT G Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1806220000020009": "Failed to extrude AMS-HT G Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180622000002000A": "Failed to adjust the filament position of AMS-HT G Slot 3. The filament or buffer may be stuck.",
+  "1806220000020010": "AMS-HT G slot 3 feeds filament out of AMS timeout.",
+  "1806220000020011": "AMS-HT G slot 3 pulls filament back to AMS timeout.",
+  "1806220000020012": "AMS-HT G slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "1806220000020013": "AMS-HT G slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1806220000020014": "AMS-HT G slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1806220000020015": "AMS-HT G slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1806220000020016": "AMS-HT G slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1806220000020017": "AMS-HT G slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1806220000020018": "AMS-HT G slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1806220000020019": "AMS-HT G slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1806220000020020": "AMS-HT G slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1806220000020021": "AMS-HT G slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1806220000020022": "AMS-HT G slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1806220000020023": "AMS-HT G slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1806220000020024": "AMS-HT G slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1806220000020025": "AMS-HT G slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1806220000030001": "AMS-HT G Slot 3 filament has run out. Please wait while old filament is purged.",
+  "1806220000030002": "AMS-HT G Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "1806230000010081": "Failed to read the filament information from AMS-HT G slot 4. The AMS main board may be malfunctioning.",
+  "1806230000010082": "Failed to read the filament information from AMS-HT G slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1806230000010083": "Failed to read the filament information from AMS-HT G slot 4. The RFID tag may be damaged.",
+  "1806230000010084": "Failed to read the filament information from AMS-HT G slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1806230000010085": "Failed to read the filament information from AMS-HT G slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1806230000010086": "Failed to read the filament information from AMS-HT G slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1806230000020001": "AMS-HT G Slot 4 filament has run out. Please insert a new filament.",
+  "1806230000020002": "AMS-HT G Slot 4 is empty; please insert a new filament.",
+  "1806230000020003": "AMS-HT G Slot 4's filament may be broken in AMS-HT.",
+  "1806230000020004": "AMS-HT G Slot 4 filament may be broken in the tool head.",
+  "1806230000020005": "AMS-HT G Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1806230000020006": "AMS-HT G has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1806230000020007": "AMS-HT G Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1806230000020008": "AMS-HT G Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1806230000020009": "Failed to extrude AMS-HT G Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180623000002000A": "Failed to adjust the filament position of AMS-HT G Slot 4. The filament or buffer may be stuck.",
+  "1806230000020010": "AMS-HT G slot 4 feeds filament out of AMS timeout.",
+  "1806230000020011": "AMS-HT G slot 4 pulls filament back to AMS timeout.",
+  "1806230000020012": "AMS-HT G slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "1806230000020013": "AMS-HT G slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1806230000020014": "AMS-HT G slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1806230000020015": "AMS-HT G slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1806230000020016": "AMS-HT G slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1806230000020017": "AMS-HT G slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1806230000020018": "AMS-HT G slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1806230000020019": "AMS-HT G slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1806230000020020": "AMS-HT G slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1806230000020021": "AMS-HT G slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1806230000020022": "AMS-HT G slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1806230000020023": "AMS-HT G slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1806230000020024": "AMS-HT G slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1806230000020025": "AMS-HT G slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1806230000030001": "AMS-HT G Slot 4 filament has run out. Please wait while old filament is purged.",
+  "1806230000030002": "AMS-HT G Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "1806240000010007": "AMS-HT G door detection is abnormal, the Hall sensor connection may be loose or disconnected.",
+  "1806240000020009": "AMS-HT G front cover is open. This may affect the drying performance or cause the filament to absorb moisture.",
+  "1806250000020001": "AMS-HT G uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "1806260000020001": "The induction hotend is heating up, and the drying power of AMS-HT G will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "1806300000010001": "The AMS-HT G RFID 1 board has an error.",
+  "1806300000010004": "Encryption chip failure",
+  "1806300000020002": "The RFID-tag on AMS-HT G Slot1 is damaged, or its content cannot be identified.",
+  "1806300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1806310000010001": "The AMS-HT G RFID 2 board has an error.",
+  "1806310000010004": "Encryption chip failure",
+  "1806310000020002": "The RFID-tag on AMS-HT G Slot2 is damaged, or its content cannot be identified.",
+  "1806310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1806320000020002": "The RFID-tag on AMS-HT G Slot3 is damaged, or its content cannot be identified.",
+  "1806330000020002": "The RFID-tag on AMS-HT G Slot4 is damaged, or its content cannot be identified.",
+  "1806350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "1806350000010002": "AMS-HT G The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "1806400000020001": "AMS-HT G Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "1806500000020001": "AMS-HT G communication is abnormal; please check the connection cable.",
+  "1806500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1806550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "1806550000010003": "AMS-HT G was detected offline during the AMS initialization process.",
+  "1806550000010004": "The binding between AMS-HT G and the extruder is incorrect. Please run the AMS Setup.",
+  "1806550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "1806560000030001": "AMS-HT G is undergoing dry cooling; please wait for it to cool down before operating.",
+  "1806600000020001": "The AMS-HT G Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1806610000020001": "The AMS-HT G Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1806620000020001": "The AMS-HT G Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1806630000020001": "The AMS-HT G Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1806700000020001": "Failed to pull out the AMS-HT G Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1806700000020002": "Failed to feed the AMS-HT G Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1806700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "1806700000020004": "Failed to pull back the AMS-HT G Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1806700000020005": "Failed to feed the AMS-HT G Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1806700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "1806700000020007": "AMS-HT filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "1806700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "180670000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "1806710000020001": "Failed to pull out the AMS-HT G Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1806710000020002": "Failed to feed the AMS-HT G Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1806710000020004": "Failed to pull back the AMS-HT G Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1806710000020005": "Failed to feed the AMS-HT G Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1806720000020001": "Failed to pull out the AMS-HT G Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1806720000020002": "Failed to feed the AMS-HT G Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1806720000020004": "Failed to pull back the AMS-HT G Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1806720000020005": "Failed to feed the AMS-HT G Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1806730000020001": "Failed to pull out the AMS-HT G Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1806730000020002": "Failed to feed the AMS-HT G Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1806730000020004": "Failed to pull back the AMS-HT G Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1806730000020005": "Failed to feed the AMS-HT G Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1806800000010001": "AMS-HT G The current sensor of heater 1 is abnormal.",
+  "1806800000010002": "AMS-HT G The heater 1 is disconnected, which may be due to poor connector contact.",
+  "1806800000010003": "AMS-HT G The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1806800000010004": "AMS-HT G The heater 1 is heating abnormally.",
+  "1806810000010001": "AMS-HT G The current sensor of heater 2 is abnormal.",
+  "1806810000010002": "AMS-HT G The heater 2 is disconnected, which may be due to poor connector contact.",
+  "1806810000010003": "AMS-HT G The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1806810000010004": "AMS-HT G The heater 2 is heating abnormally.",
+  "1806860000010004": "AMS-HT G is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "1806900000010002": "AMS-HT G The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "1806900000010003": "AMS-HT G The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "1806900000010004": "The current sensor of AMS-HT G exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1806900000020001": "AMS-HT G The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "1806910000010002": "AMS-HT G The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "1806910000010003": "AMS-HT G The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "1806910000010004": "The current sensor of AMS-HT G exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1806910000020001": "AMS-HT G The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "1806920000010001": "AMS-HT G The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "1806920000020002": "AMS-HT G The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "1806920000020003": "The AMS-HT G heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "1806930000010001": "AMS-HT G The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "1806930000020002": "AMS-HT G The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "1806930000020003": "The AMS-HT G heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "1806940000010001": "AMS-HT G The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "1806940000010002": "Temperature sensor 1 on the AMS-HT G heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1806950000010001": "AMS-HT G The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "1806950000010002": "Temperature sensor 2 on the AMS-HT G heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1806960000010001": "AMS-HT G The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "1806960000010003": "AMS-HT G Unable to start drying; please pull out the filament from filament hub and try again.",
+  "1806960000020002": "AMS-HT G Environmental temperature is too low, which will affect the drying capability.",
+  "1806960000020004": "AMS-HT G The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "1806970000030001": "AMS-HT G chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "1806980000020001": "AMS-HT G The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "1806980000020002": "AMS-HT G The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "1807010000010001": "The AMS-HT H assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.",
+  "1807010000010003": "The AMS-HT H assist motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1807010000010004": "The AMS-HT H assist motor speed control is malfunctioning. The speed sensor may be faulty.",
+  "1807010000010005": "AMS-HT H The current sensor of assist motor may be faulty.",
+  "1807010000010011": "AMS-HT H The assist motor calibration parameter error. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1807010000020002": "The AMS-HT H assist motor is overloaded. The filament may be tangled or stuck.",
+  "1807010000020006": "AMS-HT H The assist motor three-phase wires are not connected. The assist motor connector may have poor contact.",
+  "1807010000020007": "AMS-HT H The assist motor encoder wires are not connected. The assist motor connector may have poor contact.",
+  "1807010000020008": "AMS-HT H The assist motor phase winding has an open circuit. The assist motor may be faulty.",
+  "1807010000020009": "AMS-HT H The assist motor has unbalanced tree-phase resistaance. The assist motor may be faulty.",
+  "1807010000020010": "AMS-HT H The assist motor resistance is abnormal. The assist motor may be faulty.",
+  "1807010000020011": "AMS-HT H The motor assist parameter is lost. Please pull out the filament from the filament hub and then restart the AMS.",
+  "1807020000010001": "AMS-HT H Filament speed and length error: The filament odometry may be faulty.",
+  "1807020000020002": "AMS-HT H The odometer has no signal. The odometer connector may have poor contact.",
+  "1807020000020003": "AMS-HT H filament odometer speed signal is abnormal. If you hear unusual noises or the filament fails to unload from the Auxiliary Extruder, please replace the filament odometer.",
+  "1807100000010001": "The AMS-HT H slot 1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1807100000010003": "The AMS-HT H slot 1 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1807100000020002": "The AMS-HT H slot 1 motor is overloaded. The filament may be tangled or stuck.",
+  "1807100000020004": "AMS-HT H The brushed motor 1 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1807110000010001": "The AMS-HT H slot 2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1807110000010003": "The AMS-HT H slot 2 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1807110000020002": "The AMS-HT H slot 2 motor is overloaded. The filament may be tangled or stuck.",
+  "1807110000020004": "AMS-HT H The brushed motor 2 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1807120000010001": "The AMS-HT H slot 3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1807120000010003": "The AMS-HT H slot 3 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1807120000020002": "The AMS-HT H slot 3 motor is overloaded. The filament may be tangled or stuck.",
+  "1807120000020004": "AMS-HT H The brushed motor 3 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1807130000010001": "The AMS-HT H slot 4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.",
+  "1807130000010003": "The AMS-HT H slot 4 motor torque control is malfunctioning. The current sensor may be faulty.",
+  "1807130000020002": "The AMS-HT H slot 4 motor is overloaded. The filament may be tangled or stuck.",
+  "1807130000020004": "AMS-HT H The brushed motor 4 has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1807200000010081": "Failed to read the filament information from AMS-HT H slot 1. The AMS main board may be malfunctioning.",
+  "1807200000010082": "Failed to read the filament information from AMS-HT H slot 1. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1807200000010083": "Failed to read the filament information from AMS-HT H slot 1. The RFID tag may be damaged.",
+  "1807200000010084": "Failed to read the filament information from AMS-HT H slot 1. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1807200000010085": "Failed to read the filament information from AMS-HT H slot 1. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1807200000010086": "Failed to read the filament information from AMS-HT H slot 1. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1807200000020001": "AMS-HT H Slot 1 filament has run out. Please insert a new filament.",
+  "1807200000020002": "AMS-HT H Slot 1 is empty; please insert a new filament.",
+  "1807200000020003": "AMS-HT H Slot 1's filament may be broken in AMS-HT.",
+  "1807200000020004": "AMS-HT H Slot 1 filament may be broken in the tool head.",
+  "1807200000020005": "AMS-HT H Slot 1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1807200000020006": "AMS-HT H has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1807200000020007": "AMS-HT H Slot 1 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1807200000020008": "AMS-HT H Slot 1 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1807200000020009": "Failed to extrude AMS-HT H Slot 1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180720000002000A": "Failed to adjust the filament position of AMS-HT H Slot 1. The filament or buffer may be stuck.",
+  "1807200000020010": "AMS-HT H slot 1 feeds filament out of AMS timeout.",
+  "1807200000020011": "AMS-HT H slot 1 pulls filament back to AMS timeout.",
+  "1807200000020012": "AMS-HT H slot 1 feeder unit motor is stalled, cannot rotate the spool.",
+  "1807200000020013": "AMS-HT H slot 1 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1807200000020014": "AMS-HT H slot 1 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1807200000020015": "AMS-HT H slot 1 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1807200000020016": "AMS-HT H slot 1 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1807200000020017": "AMS-HT H slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1807200000020018": "AMS-HT H slot 1 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1807200000020019": "AMS-HT H slot 1 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1807200000020020": "AMS-HT H slot 1 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1807200000020021": "AMS-HT H slot 1 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1807200000020022": "AMS-HT H slot 1 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1807200000020023": "AMS-HT H slot 1 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1807200000020024": "AMS-HT H slot 1 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1807200000020025": "AMS-HT H slot 1 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1807200000020026": "AMS-HT H assist motor overloaded. Excessive resistance in the filament tube between the AMS and the filament track switch.",
+  "1807200000020027": "AMS-HT H assist motor overloaded. Excessive resistance in the filament tube between the filament track switch and the filament buffer.",
+  "1807200000030001": "AMS-HT H Slot 1 filament has run out. Please wait while old filament is purged.",
+  "1807200000030002": "AMS-HT H Slot 1 filament has run out and automatically switched to the slot with the same filament.",
+  "1807210000010081": "Failed to read the filament information from AMS-HT H slot 2. The AMS main board may be malfunctioning.",
+  "1807210000010082": "Failed to read the filament information from AMS-HT H slot 2. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1807210000010083": "Failed to read the filament information from AMS-HT H slot 2. The RFID tag may be damaged.",
+  "1807210000010084": "Failed to read the filament information from AMS-HT H slot 2. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1807210000010085": "Failed to read the filament information from AMS-HT H slot 2. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1807210000010086": "Failed to read the filament information from AMS-HT H slot 2. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1807210000020001": "AMS-HT H Slot 2 filament has run out. Please insert a new filament.",
+  "1807210000020002": "AMS-HT H Slot 2 is empty; please insert a new filament.",
+  "1807210000020003": "AMS-HT H Slot 2's filament may be broken in AMS-HT.",
+  "1807210000020004": "AMS-HT H Slot 2 filament may be broken in the tool head.",
+  "1807210000020005": "AMS-HT H Slot 2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1807210000020006": "AMS-HT H has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1807210000020007": "AMS-HT H Slot 2 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1807210000020008": "AMS-HT H Slot 2 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1807210000020009": "Failed to extrude AMS-HT H Slot 2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180721000002000A": "Failed to adjust the filament position of AMS-HT H Slot 2. The filament or buffer may be stuck.",
+  "1807210000020010": "AMS-HT H slot 2 feeds filament out of AMS timeout.",
+  "1807210000020011": "AMS-HT H slot 2 pulls filament back to AMS timeout.",
+  "1807210000020012": "AMS-HT H slot 2 feeder unit motor is stalled, cannot rotate the spool.",
+  "1807210000020013": "AMS-HT H slot 2 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1807210000020014": "AMS-HT H slot 2 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1807210000020015": "AMS-HT H slot 2 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1807210000020016": "AMS-HT H slot 2 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1807210000020017": "AMS-HT H slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1807210000020018": "AMS-HT H slot 2 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1807210000020019": "AMS-HT H slot 2 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1807210000020020": "AMS-HT H slot 2 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1807210000020021": "AMS-HT H slot 2 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1807210000020022": "AMS-HT H slot 2 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1807210000020023": "AMS-HT H slot 2 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1807210000020024": "AMS-HT H slot 2 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1807210000020025": "AMS-HT H slot 2 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1807210000030001": "AMS-HT H Slot 2 filament has run out. Please wait while old filament is purged.",
+  "1807210000030002": "AMS-HT H Slot 2 filament has run out and automatically switched to the slot with the same filament.",
+  "1807220000010081": "Failed to read the filament information from AMS-HT H slot 3. The AMS main board may be malfunctioning.",
+  "1807220000010082": "Failed to read the filament information from AMS-HT H slot 3. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1807220000010083": "Failed to read the filament information from AMS-HT H slot 3. The RFID tag may be damaged.",
+  "1807220000010084": "Failed to read the filament information from AMS-HT H slot 3. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1807220000010085": "Failed to read the filament information from AMS-HT H slot 3. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1807220000010086": "Failed to read the filament information from AMS-HT H slot 3. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1807220000020001": "AMS-HT H Slot 3 filament has run out. Please insert a new filament.",
+  "1807220000020002": "AMS-HT H Slot 3 is empty; please insert a new filament.",
+  "1807220000020003": "AMS-HT H Slot 3's filament may be broken in AMS-HT.",
+  "1807220000020004": "AMS-HT H Slot 3 filament may be broken in the tool head.",
+  "1807220000020005": "AMS-HT H Slot 3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1807220000020006": "AMS-HT H has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1807220000020007": "AMS-HT H Slot 3 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1807220000020008": "AMS-HT H Slot 3 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1807220000020009": "Failed to extrude AMS-HT H Slot 3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180722000002000A": "Failed to adjust the filament position of AMS-HT H Slot 3. The filament or buffer may be stuck.",
+  "1807220000020010": "AMS-HT H slot 3 feeds filament out of AMS timeout.",
+  "1807220000020011": "AMS-HT H slot 3 pulls filament back to AMS timeout.",
+  "1807220000020012": "AMS-HT H slot 3 feeder unit motor is stalled, cannot rotate the spool.",
+  "1807220000020013": "AMS-HT H slot 3 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1807220000020014": "AMS-HT H slot 3 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1807220000020015": "AMS-HT H slot 3 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1807220000020016": "AMS-HT H slot 3 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1807220000020017": "AMS-HT H slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1807220000020018": "AMS-HT H slot 3 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1807220000020019": "AMS-HT H slot 3 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1807220000020020": "AMS-HT H slot 3 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1807220000020021": "AMS-HT H slot 3 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1807220000020022": "AMS-HT H slot 3 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1807220000020023": "AMS-HT H slot 3 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1807220000020024": "AMS-HT H slot 3 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1807220000020025": "AMS-HT H slot 3 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1807220000030001": "AMS-HT H Slot 3 filament has run out. Please wait while old filament is purged.",
+  "1807220000030002": "AMS-HT H Slot 3 filament has run out and automatically switched to the slot with the same filament.",
+  "1807230000010081": "Failed to read the filament information from AMS-HT H slot 4. The AMS main board may be malfunctioning.",
+  "1807230000010082": "Failed to read the filament information from AMS-HT H slot 4. A non-official RFID tag was detected. You can try to use Bambu Lab filament.",
+  "1807230000010083": "Failed to read the filament information from AMS-HT H slot 4. The RFID tag may be damaged.",
+  "1807230000010084": "Failed to read the filament information from AMS-HT H slot 4. The RFID tag may be damaged or positioned at the edge of the RFID detection device. Please remove 5cm filament and try again.",
+  "1807230000010085": "Failed to read the filament information from AMS-HT H slot 4. RFID tag verification failed. You can try to use Bambu Lab filament.",
+  "1807230000010086": "Failed to read the filament information from AMS-HT H slot 4. The RFID tag cannot rotate due to a jam during the filament loading or unloading. Please pull out the filament and try again.",
+  "1807230000020001": "AMS-HT H Slot 4 filament has run out. Please insert a new filament.",
+  "1807230000020002": "AMS-HT H Slot 4 is empty; please insert a new filament.",
+  "1807230000020003": "AMS-HT H Slot 4's filament may be broken in AMS-HT.",
+  "1807230000020004": "AMS-HT H Slot 4 filament may be broken in the tool head.",
+  "1807230000020005": "AMS-HT H Slot 4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.",
+  "1807230000020006": "AMS-HT H has detected a breakage of the PTFE tube during filament loading. Please check whether the PTFE tubes inside and outside the AMS have fallen off or been damaged.",
+  "1807230000020007": "AMS-HT H Slot 4 feed-out Hall sensor is disconnected. The connector may have poor contact.",
+  "1807230000020008": "AMS-HT H Slot 4 feed-in Hall sensor is disconnected, which may be due to poor connector contact.",
+  "1807230000020009": "Failed to extrude AMS-HT H Slot 4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.",
+  "180723000002000A": "Failed to adjust the filament position of AMS-HT H Slot 4. The filament or buffer may be stuck.",
+  "1807230000020010": "AMS-HT H slot 4 feeds filament out of AMS timeout.",
+  "1807230000020011": "AMS-HT H slot 4 pulls filament back to AMS timeout.",
+  "1807230000020012": "AMS-HT H slot 4 feeder unit motor is stalled, cannot rotate the spool.",
+  "1807230000020013": "AMS-HT H slot 4 feeder unit motor has no signal, which may be due to poor contact in the motor connector or a motor fault.",
+  "1807230000020014": "AMS-HT H slot 4 filament odometer has no signal, which may be due to poor contact in the odometer connector or a odometer fault.",
+  "1807230000020015": "AMS-HT H slot 4 filament status is abnormal, which may be due to a filament breakage inside the AMS.",
+  "1807230000020016": "AMS-HT H slot 4 assist motor has slipped. Please pull out the filament, cut off the worn part, and then try again.",
+  "1807230000020017": "AMS-HT H slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the printer.",
+  "1807230000020018": "AMS-HT H slot 4 assist motor is stalled,due to excessive resistance in the tube near AMS.",
+  "1807230000020019": "AMS-HT H slot 4 assist motor is stalled,due to excessive resistance in the tube between AMS and the filament buffer.",
+  "1807230000020020": "AMS-HT H slot 4 assist motor is stalled,due to excessive resistance in the tube near the filament buffer。",
+  "1807230000020021": "AMS-HT H slot 4 assist motor is stalled,due to excessive resistance in the tube between the filament buffer and the toolhead.",
+  "1807230000020022": "AMS-HT H slot 4 assist motor is stalled,due to excessive resistance in the tube near the toolhead.",
+  "1807230000020023": "AMS-HT H slot 4 the tube inside the AMS is broken, or feed-out hall sensor is faulty and cannot detect the filament.",
+  "1807230000020024": "AMS-HT H slot 4 failed to rotate the filament spool when pulling filament back to AMS.",
+  "1807230000020025": "AMS-HT H slot 4 feed resistance is too high. Please reduce the feed resistance, decrease the rotation resistance of the spool, and avoid having the filament tube too long and excessively bent.",
+  "1807230000030001": "AMS-HT H Slot 4 filament has run out. Please wait while old filament is purged.",
+  "1807230000030002": "AMS-HT H Slot 4 filament has run out and automatically switched to the slot with the same filament.",
+  "1807240000010007": "AMS-HT H door detection is abnormal, the Hall sensor connection may be loose or disconnected.",
+  "1807240000020009": "AMS-HT H front cover is open. This may affect the drying performance or cause the filament to absorb moisture.",
+  "1807250000020001": "AMS-HT H uses printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+  "1807260000020001": "The induction hotend is heating up, and the drying power of AMS-HT H will be reduced. Please connect the power adapter or wait until the hotend finishes heating.",
+  "1807300000010001": "The AMS-HT H RFID 1 board has an error.",
+  "1807300000010004": "Encryption chip failure",
+  "1807300000020002": "The RFID-tag on AMS-HT H Slot1 is damaged, or its content cannot be identified.",
+  "1807300000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1807310000010001": "The AMS-HT H RFID 2 board has an error.",
+  "1807310000010004": "Encryption chip failure",
+  "1807310000020002": "The RFID-tag on AMS-HT H Slot2 is damaged, or its content cannot be identified.",
+  "1807310000030003": "RFID cannot be read because of a hardware or structural error.",
+  "1807320000020002": "The RFID-tag on AMS-HT H Slot3 is damaged, or its content cannot be identified.",
+  "1807330000020002": "The RFID-tag on AMS-HT H Slot4 is damaged, or its content cannot be identified.",
+  "1807350000010001": "The temperature and humidity sensor has an error. The chip may be faulty.",
+  "1807350000010002": "AMS-HT H The humidity sensor is disconnected, which may be due to poor connector contact.",
+  "1807400000020001": "AMS-HT H Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+  "1807500000020001": "AMS-HT H communication is abnormal; please check the connection cable.",
+  "1807500000020002": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1807550000010002": "The PTFE tube is incorrectly connected between the toolhead and the buffer. Please connect the buffer's top to the right extruder and the bottom to the left extruder.",
+  "1807550000010003": "AMS-HT H was detected offline during the AMS initialization process.",
+  "1807550000010004": "The binding between AMS-HT H and the extruder is incorrect. Please run the AMS Setup.",
+  "1807550000010005": "Please connect the PTFE tube along the correct path, from the AMS through the buffer and then to the toolhead. Bypassing the buffer may cause the AMS to fail to feed filament properly.",
+  "1807560000030001": "AMS-HT H is undergoing dry cooling; please wait for it to cool down before operating.",
+  "1807600000020001": "The AMS-HT H Slot 1 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1807610000020001": "The AMS-HT H Slot 2 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1807620000020001": "The AMS-HT H Slot 3 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1807630000020001": "The AMS-HT H Slot 4 is overloaded. The filament may be tangled or the filament buffer may be stuck.",
+  "1807700000020001": "Failed to pull out the AMS-HT H Slot 1 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1807700000020002": "Failed to feed the AMS-HT H Slot 1 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1807700000020003": "Failed to extrude the filament. Possible cause: extruder or nozzle clog.",
+  "1807700000020004": "Failed to pull back the AMS-HT H Slot 1 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1807700000020005": "Failed to feed the AMS-HT H Slot 1 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1807700000020006": "Timeout purging old filament. Possible cause: filament stuck or the extruder/nozzle clog.",
+  "1807700000020007": "AMS-HT filament ran out. Please put a new filament into the same slot in AMS and resume.",
+  "1807700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "180770000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "1807710000020001": "Failed to pull out the AMS-HT H Slot 2 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1807710000020002": "Failed to feed the AMS-HT H Slot 2 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1807710000020004": "Failed to pull back the AMS-HT H Slot 2 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1807710000020005": "Failed to feed the AMS-HT H Slot 2 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1807720000020001": "Failed to pull out the AMS-HT H Slot 3 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1807720000020002": "Failed to feed the AMS-HT H Slot 3 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1807720000020004": "Failed to pull back the AMS-HT H Slot 3 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1807720000020005": "Failed to feed the AMS-HT H Slot 3 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1807730000020001": "Failed to pull out the AMS-HT H Slot 4 filament from the extruder. Possible cause: clogged extruder or broken filament inside the extruder.",
+  "1807730000020002": "Failed to feed the AMS-HT H Slot 4 filament into the Toolhead. Possible cause: filament or spool stuck.",
+  "1807730000020004": "Failed to pull back the AMS-HT H Slot 4 filament from the Toolhead. Possible cause: filament or spool stuck.",
+  "1807730000020005": "Failed to feed the AMS-HT H Slot 4 filament. Please trim the end of the filament and check if the spool is stuck.",
+  "1807800000010001": "AMS-HT H The current sensor of heater 1 is abnormal.",
+  "1807800000010002": "AMS-HT H The heater 1 is disconnected, which may be due to poor connector contact.",
+  "1807800000010003": "AMS-HT H The heater 1 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1807800000010004": "AMS-HT H The heater 1 is heating abnormally.",
+  "1807810000010001": "AMS-HT H The current sensor of heater 2 is abnormal.",
+  "1807810000010002": "AMS-HT H The heater 2 is disconnected, which may be due to poor connector contact.",
+  "1807810000010003": "AMS-HT H The heater 2 is short-circuited, which may be due to a wiring short or heater damage.",
+  "1807810000010004": "AMS-HT H The heater 2 is heating abnormally.",
+  "1807860000010004": "AMS-HT H is not correctly bound to the filament track switch. Please run AMS setup again.",
+  "1807900000010002": "AMS-HT H The coil resistance of exhaust valve 1 is abnormal, which may be due to abnormal wiring or damage.",
+  "1807900000010003": "AMS-HT H The exhaust valve 1 is not connected, which may be due to poor connector contact.",
+  "1807900000010004": "The current sensor of AMS-HT H exhaust valve 1 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1807900000020001": "AMS-HT H The operation of the exhaust valve 1 is abnormal, which may be due to excessive resistance.",
+  "1807910000010002": "AMS-HT H The coil resistance of exhaust valve 2 is abnormal, which may be due to abnormal wiring or damage.",
+  "1807910000010003": "AMS-HT H The exhaust valve 2 is not connected, which may be due to poor connector contact.",
+  "1807910000010004": "The current sensor of AMS-HT H exhaust valve 2 is abnormal; please get in touch with customer support to replace the AMS-HT mainboard.",
+  "1807910000020001": "AMS-HT H The operation of the exhaust valve 2 is abnormal, which may be due to excessive resistance.",
+  "1807920000010001": "AMS-HT H The cooling fan of heater 1 is blocked, which may be due to the fan being stuck.",
+  "1807920000020002": "AMS-HT H The cooling fan speed of heater 1 is too low, which could be due to excessive fan resistance.",
+  "1807920000020003": "The AMS-HT H heater 1 cooling fan cannot start because the power adapter is not connected.",
+  "1807930000010001": "AMS-HT H The cooling fan of heater 2 is blocked, which may be due to the fan being stuck.",
+  "1807930000020002": "AMS-HT H The cooling fan speed of heater 2 is too low, which could be due to excessive fan resistance.",
+  "1807930000020003": "The AMS-HT H heater 2 cooling fan cannot start because the power adapter is not connected.",
+  "1807940000010001": "AMS-HT H The temperature sensor of heater 1 is malfunctioning, which may be due to poor connector contact.",
+  "1807940000010002": "Temperature sensor 1 on the AMS-HT H heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1807950000010001": "AMS-HT H The temperature sensor of heater 2 is malfunctioning, which may be due to poor connector contact.",
+  "1807950000010002": "Temperature sensor 2 on the AMS-HT H heater has malfunctioned, resulting in abnormal temperature readings.",
+  "1807960000010001": "AMS-HT H The drying process may experience thermal runaway. Please turn off the AMS power supply.",
+  "1807960000010003": "AMS-HT H Unable to start drying; please pull out the filament from filament hub and try again.",
+  "1807960000020002": "AMS-HT H Environmental temperature is too low, which will affect the drying capability.",
+  "1807960000020004": "AMS-HT H The temperature control error is too large, which may be due to the lid being open or an abnormality with the heater.",
+  "1807970000030001": "AMS-HT H chamber temperature is too high; auxiliary feeding or RFID reading is currently not allowed.",
+  "1807980000020001": "AMS-HT H The power adapter voltage is too low, which may result in insufficient drying temperature. Please replace the power adapter.",
+  "1807980000020002": "AMS-HT H The power adapter voltage is too high, which may damage the heater circuit. Please replace the power adapter.",
+  "1880200000020058": "The RFID-tag on AMS-HT A cannot be identified.",
+  "1881200000020058": "The RFID-tag on AMS-HT B cannot be identified.",
+  "1882200000020058": "The RFID-tag on AMS-HT C cannot be identified.",
+  "1883200000020058": "The RFID-tag on AMS-HT D cannot be identified.",
+  "1884200000020058": "The RFID-tag on AMS-HT E cannot be identified.",
+  "1885200000020058": "The RFID-tag on AMS-HT F cannot be identified.",
+  "1886200000020058": "The RFID-tag on AMS-HT G cannot be identified.",
+  "1887200000020058": "The RFID-tag on AMS-HT H cannot be identified.",
+  "18FE200000020001": "External filament of left extruder has run out; please load a new filament.",
+  "18FE200000020002": "No filament was detected in the left extruder from the external spool; please load the new filament.",
+  "18FE200000020004": "Please pull the external filament from the left extruder.",
+  "18FE450000020002": "The filament cutter's cutting distance is too large. Possible causes include the filament cutter stopper skipping teeth, motor losing steps, or the XY axis not being homed.",
+  "18FE450000020003": "The filament cutter handle has not been released. The handle or blade may be jammed, or there could be an issue with the filament sensor connection.",
+  "18FE600000020001": "External spool connected to left extruder may be tangled or jammed.",
+  "18FE700000020003": "Please check if the material is coming out of the left nozzle. If not, gently push the material and try to extrude again.",
+  "18FE700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18FE70000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "18FE800000010001": "The toolhead lift motor works abnormally. Please check whether the connecting cable is loose.",
+  "18FE800000010002": "The Toolhead Lifting Motor position hall sensor has an open circuit; please check whether the connecting cable is loose.",
+  "18FE800000010003": "The hall signal of the Toolhead Lifting Motor is abnormal, possibly due to internal communication failure in the Toolhead module.",
+  "18FE800000010004": "The Toolhead Lifting Motor position hall sensor is short-circuited; please check if the Hall sensor is malfunctioning.",
+  "18FE800000010005": "The extruder's switching limit angle has drifted significantly. Please check if the flow blocker is jammed or if filament is stuck in the toolhead.",
+  "18FE800000010006": "The TH board disconnected during the extruder switching process. Please check if the connecting cable is loose.",
+  "18FE800000020001": "The lifting action is abnormal during the extruder switch. Please check whether the flow blocker is stuck or there is filament stuck in the toolhead.",
+  "18FE800000020002": "The position of left hotend is abnormal during printing. Please check whether the flow blocker scratches the printed model.",
+  "18FE800000020003": "The deviation in the positioning calibration value of the extruder is too large; please recalibrate.",
+  "18FE810000010001": "The extruder switching motor is working abnormally. Please check whether the connecting cable is loose.",
+  "18FE810000010002": "The position hall sensor of the Extruder Switching Motor has an open circuit. Please check whether the connecting cable is loose.",
+  "18FE810000010003": "The hall signal of the Extruder Switching Motor is abnormal, possibly due to internal communication failure in the Toolhead module.",
+  "18FE810000010004": "The position hall sensor of the Extruder Switching Motor has a short circuit; please check if the Hall sensor is malfunctioning.",
+  "18FE810000010006": "The TH board disconnected during the extruder switching process. Please check if the connecting cable is loose.",
+  "18FE810000020001": "The extruder switching action is abnormal. Please check whether there is something stuck in the toolhead.",
+  "18FF200000020001": "External filament has run out; please load a new filament.",
+  "18FF200000020002": "External filament is missing; please load a new filament.",
+  "18FF200000020004": "Please pull the external filament from the extruder.",
+  "18FF450000020002": "The filament cutter's cutting distance is too large. Possible causes include the filament cutter stopper skipping teeth, motor losing steps, or the XY axis not being homed.",
+  "18FF450000020003": "The filament cutter handle has not been released. The handle or blade may be jammed, or there could be an issue with the filament sensor connection.",
+  "18FF600000020001": "External spool may be tangled or jammed.",
+  "18FF700000020003": "Please check if the material is coming out of the right nozzle. If not, gently push the material and try to extrude again.",
+  "18FF700000020008": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18FF70000002000A": "Failed to get filament-hotend mapping table from the slicing file.",
+  "18FF800000010001": "The toolhead lift motor works abnormally. Please check whether the connecting cable is loose.",
+  "18FF800000010002": "The Toolhead Lifting Motor position hall sensor has an open circuit; please check whether the connecting cable is loose.",
+  "18FF800000010003": "The hall signal of the Toolhead Lifting Motor is abnormal, possibly due to internal communication failure in the Toolhead module.",
+  "18FF800000010004": "The Toolhead Lifting Motor position hall sensor is short-circuited; please check if the Hall sensor is malfunctioning.",
+  "18FF800000010005": "The extruder's switching limit angle has drifted significantly. Please check if the flow blocker is jammed or if filament is stuck in the toolhead.",
+  "18FF800000010006": "The TH board disconnected during the extruder switching process. Please check if the connecting cable is loose.",
+  "18FF800000020001": "The lifting action is abnormal during the extruder switch. Please check whether the flow blocker is stuck or there is filament stuck in the toolhead.",
+  "18FF800000020002": "The position of left hotend is abnormal during printing. Please check whether the flow blocker scratches the printed model.",
+  "18FF800000020003": "The deviation in the positioning calibration value of the extruder is too large; please recalibrate.",
+  "18FF810000010001": "The extruder switching motor is working abnormally. Please check whether the connecting cable is loose.",
+  "18FF810000010002": "The position hall sensor of the Extruder Switching Motor has an open circuit. Please check whether the connecting cable is loose.",
+  "18FF810000010003": "The hall signal of the Extruder Switching Motor is abnormal, possibly due to internal communication failure in the Toolhead module.",
+  "18FF810000010004": "The position hall sensor of the Extruder Switching Motor has a short circuit; please check if the Hall sensor is malfunctioning.",
+  "18FF810000010006": "The TH board disconnected during the extruder switching process. Please check if the connecting cable is loose.",
+  "18FF810000020001": "The extruder switching action is abnormal. Please check whether there is something stuck in the toolhead.",
+  "1A00010000020001": "Error fetching/nesting the hotend in rack slot 1. Ensure the hotend holder has no hotend installed, the corresponding rack slot contains a hotend, and manually move the toolhead to the machine center before continuing.",
+  "1A00010000020002": "Error fetching/nesting the hotend in rack slot 1. Ensure the hotend holder has a hotend installed, the corresponding rack slot is empty, and manually move the toolhead to the machine center before continuing.",
+  "1A00010000020003": "Error detected before induction hotend replacement: no hotend installed in slot 1 on the rack.",
+  "1A00010000020004": "Error detected before induction hotend replacement: a hotend is already installed in slot 1 on the rack. Please remove it before continuing.",
+  "1A00010000020020": "Abnormal condition detected while nesting the hotend in slot 1: a hotend is already installed. Please remove the existing one before clicking “Continue.”",
+  "1A00010000020021": "Abnormal condition detected while nesting the hotend in slot 1: no hotend detected on the toolhead. Please mount the hotend onto the toolhead before clicking “Continue.”",
+  "1A00010000020022": "Abnormal condition detected while nesting the hotend in slot 1. Please ensure no hotend is mounted on the toolhead and that a hotend is present in slot 1 on the rack before clicking “Continue.”",
+  "1A00010000020023": "Abnormal condition detected while nesting the hotend in slot 1: the rack failed to detect the hotend on the toolhead. Please check whether there is any debris on the hotend heatsink surface or if the front magnet has fallen off. If not, recalibrate the induction hotend rack.",
+  "1A00010000020024": "Abnormal condition detected while nesting the hotend in slot 1: the rack failed to detach the hotend from the toolhead. Please make sure the locking lever is in the released position.",
+  "1A00010000020025": "Abnormal condition detected while nesting the hotend in slot 1: the hotend has dropped. Please check for excessive filament remaining at the top of the hotend, trim the residue, reinstall the hotend into slot 1 on the rack, and then click “Continue.”",
+  "1A00010000020026": "Abnormal condition detected while nesting the hotend in slot 1: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00010000020027": "Abnormal condition detected while nesting the hotend in slot 1: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00010000020030": "Abnormal condition detected while fetching the hotend from slot 1: no hotend found in this position. Please install a hotend in the corresponding slot before clicking “Continue.”",
+  "1A00010000020031": "Abnormal condition detected while fetching the hotend from slot 1: a hotend is already mounted on the toolhead. Please remove the hotend from the toolhead before clicking “Continue.”",
+  "1A00010000020032": "Error detected while fetching the hotend from slot 1: the toolhead failed to detect the hotend on the rack. Please check for any debris in the induction hotend mounting slot on the toolhead. If none is found, recalibrate the induction hotend rack.",
+  "1A00010000020033": "Error detected while fetching the hotend from slot 1: the rack failed to mount the hotend properly. Please check for excessive filament at the top of the hotend, debris in the contact area between the toolhead and the hotend, or if the locking lever is in the released position. After resolving the issue, click “Continue.”",
+  "1A00010000020034": "Error detected while fetching the hotend from slot 1: the hotend has dropped. Please check for excessive filament or other debris at the top of the hotend, trim or remove it, then reinstall the hotend into slot 1 on the rack and click “Continue.”",
+  "1A00010000020035": "Error detected while fetching the hotend from slot 1: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00010000020036": "Error detected while fetching the hotend from slot 1: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00020000020001": "Error fetching/nesting the hotend in rack slot 2. Ensure the hotend holder has no hotend installed, the corresponding rack slot contains a hotend, and manually move the toolhead to the machine center before continuing.",
+  "1A00020000020002": "Error fetching/nesting the hotend in rack slot 2. Ensure the hotend holder has a hotend installed, the corresponding rack slot is empty, and manually move the toolhead to the machine center before continuing.",
+  "1A00020000020003": "Error detected before induction hotend replacement: no hotend installed in slot 2 on the rack.",
+  "1A00020000020004": "Error detected before induction hotend replacement: a hotend is already installed in slot 2 on the rack. Please remove it before continuing.",
+  "1A00020000020020": "Abnormal condition detected while nesting the hotend in slot 2: a hotend is already installed. Please remove the existing one before clicking “Continue.”",
+  "1A00020000020021": "Abnormal condition detected while nesting the hotend in slot 2: no hotend detected on the toolhead. Please mount the hotend onto the toolhead before clicking “Continue.”",
+  "1A00020000020022": "Abnormal condition detected while nesting the hotend in slot 2. Please ensure no hotend is mounted on the toolhead and that a hotend is present in slot 2 on the rack before clicking “Continue.”",
+  "1A00020000020023": "Abnormal condition detected while nesting the hotend in slot 2: the rack failed to detect the hotend on the toolhead. Please check whether there is any debris on the hotend heatsink surface or if the front magnet has fallen off. If not, recalibrate the induction hotend rack.",
+  "1A00020000020024": "Abnormal condition detected while nesting the hotend in slot 2: the rack failed to detach the hotend from the toolhead. Please make sure the locking lever is in the released position.",
+  "1A00020000020025": "Abnormal condition detected while nesting the hotend in slot 2: the hotend has dropped. Please check for excessive filament remaining at the top of the hotend, trim the residue, reinstall the hotend into slot 2 on the rack, and then click “Continue.”",
+  "1A00020000020026": "Abnormal condition detected while nesting the hotend in slot 2: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00020000020027": "Abnormal condition detected while nesting the hotend in slot 2: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00020000020030": "Abnormal condition detected while fetching the hotend from slot 2: no hotend found in this position. Please install a hotend in the corresponding slot before clicking “Continue.”",
+  "1A00020000020031": "Abnormal condition detected while fetching the hotend from slot 2: a hotend is already mounted on the toolhead. Please remove the hotend from the toolhead before clicking “Continue.”",
+  "1A00020000020032": "Error detected while fetching the hotend from slot 2: the toolhead failed to detect the hotend on the rack. Please check for any debris in the induction hotend mounting slot on the toolhead. If none is found, recalibrate the induction hotend rack.",
+  "1A00020000020033": "Error detected while fetching the hotend from slot 2: the rack failed to mount the hotend properly. Please check for excessive filament at the top of the hotend, debris in the contact area between the toolhead and the hotend, or if the locking lever is in the released position. After resolving the issue, click “Continue.”",
+  "1A00020000020034": "Error detected while fetching the hotend from slot 2: the hotend has dropped. Please check for excessive filament or other debris at the top of the hotend, trim or remove it, then reinstall the hotend into slot 2 on the rack and click “Continue.”",
+  "1A00020000020035": "Error detected while fetching the hotend from slot 2: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00020000020036": "Error detected while fetching the hotend from slot 2: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00030000020001": "Error fetching/nesting the hotend in rack slot 3. Ensure the hotend holder has no hotend installed, the corresponding rack slot contains a hotend, and manually move the toolhead to the machine center before continuing.",
+  "1A00030000020002": "Error fetching/nesting the hotend in rack slot 3. Ensure the hotend holder has a hotend installed, the corresponding rack slot is empty, and manually move the toolhead to the machine center before continuing.",
+  "1A00030000020003": "Error detected before induction hotend replacement: no hotend installed in slot 3 on the rack.",
+  "1A00030000020004": "Error detected before induction hotend replacement: a hotend is already installed in slot 3 on the rack. Please remove it before continuing.",
+  "1A00030000020020": "Abnormal condition detected while nesting the hotend in slot 3: a hotend is already installed. Please remove the existing one before clicking “Continue.”",
+  "1A00030000020021": "Abnormal condition detected while nesting the hotend in slot 3: no hotend detected on the toolhead. Please mount the hotend onto the toolhead before clicking “Continue.”",
+  "1A00030000020022": "Abnormal condition detected while nesting the hotend in slot 3. Please ensure no hotend is mounted on the toolhead and that a hotend is present in slot 3 on the rack before clicking “Continue.”",
+  "1A00030000020023": "Abnormal condition detected while nesting the hotend in slot 3: the rack failed to detect the hotend on the toolhead. Please check whether there is any debris on the hotend heatsink surface or if the front magnet has fallen off. If not, recalibrate the induction hotend rack.",
+  "1A00030000020024": "Abnormal condition detected while nesting the hotend in slot 3: the rack failed to detach the hotend from the toolhead. Please make sure the locking lever is in the released position.",
+  "1A00030000020025": "Abnormal condition detected while nesting the hotend in slot 3: the hotend has dropped. Please check for excessive filament remaining at the top of the hotend, trim the residue, reinstall the hotend into slot 3 on the rack, and then click “Continue.”",
+  "1A00030000020026": "Abnormal condition detected while nesting the hotend in slot 3: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00030000020027": "Abnormal condition detected while nesting the hotend in slot 3: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00030000020030": "Abnormal condition detected while fetching the hotend from slot 3: no hotend found in this position. Please install a hotend in the corresponding slot before clicking “Continue.”",
+  "1A00030000020031": "Abnormal condition detected while fetching the hotend from slot 3: a hotend is already mounted on the toolhead. Please remove the hotend from the toolhead before clicking “Continue.”",
+  "1A00030000020032": "Error detected while fetching the hotend from slot 3: the toolhead failed to detect the hotend on the rack. Please check for any debris in the induction hotend mounting slot on the toolhead. If none is found, recalibrate the induction hotend rack.",
+  "1A00030000020033": "Error detected while fetching the hotend from slot 3: the rack failed to mount the hotend properly. Please check for excessive filament at the top of the hotend, debris in the contact area between the toolhead and the hotend, or if the locking lever is in the released position. After resolving the issue, click “Continue.”",
+  "1A00030000020034": "Error detected while fetching the hotend from slot 3: the hotend has dropped. Please check for excessive filament or other debris at the top of the hotend, trim or remove it, then reinstall the hotend into slot 3 on the rack and click “Continue.”",
+  "1A00030000020035": "Error detected while fetching the hotend from slot 3: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00030000020036": "Error detected while fetching the hotend from slot 3: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00040000020001": "Error fetching/nesting the hotend in rack slot 4. Ensure the hotend holder has no hotend installed, the corresponding rack slot contains a hotend, and manually move the toolhead to the machine center before continuing.",
+  "1A00040000020002": "Error fetching/nesting the hotend in rack slot 4. Ensure the hotend holder has a hotend installed, the corresponding rack slot is empty, and manually move the toolhead to the machine center before continuing.",
+  "1A00040000020003": "Error detected before induction hotend replacement: no hotend installed in slot 4 on the rack.",
+  "1A00040000020004": "Error detected before induction hotend replacement: a hotend is already installed in slot 4 on the rack. Please remove it before continuing.",
+  "1A00040000020020": "Abnormal condition detected while nesting the hotend in slot 4: a hotend is already installed. Please remove the existing one before clicking “Continue.”",
+  "1A00040000020021": "Abnormal condition detected while nesting the hotend in slot 4: no hotend detected on the toolhead. Please mount the hotend onto the toolhead before clicking “Continue.”",
+  "1A00040000020022": "Abnormal condition detected while nesting the hotend in slot 4. Please ensure no hotend is mounted on the toolhead and that a hotend is present in slot 4 on the rack before clicking “Continue.”",
+  "1A00040000020023": "Abnormal condition detected while nesting the hotend in slot 4: the rack failed to detect the hotend on the toolhead. Please check whether there is any debris on the hotend heatsink surface or if the front magnet has fallen off. If not, recalibrate the induction hotend rack.",
+  "1A00040000020024": "Abnormal condition detected while nesting the hotend in slot 4: the rack failed to detach the hotend from the toolhead. Please make sure the locking lever is in the released position.",
+  "1A00040000020025": "Abnormal condition detected while nesting the hotend in slot 4: the hotend has dropped. Please check for excessive filament remaining at the top of the hotend, trim the residue, reinstall the hotend into slot 4 on the rack, and then click “Continue.”",
+  "1A00040000020026": "Abnormal condition detected while nesting the hotend in slot 4: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00040000020027": "Abnormal condition detected while nesting the hotend in slot 4: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00040000020030": "Abnormal condition detected while fetching the hotend from slot 4: no hotend found in this position. Please install a hotend in the corresponding slot before clicking “Continue.”",
+  "1A00040000020031": "Abnormal condition detected while fetching the hotend from slot 4: a hotend is already mounted on the toolhead. Please remove the hotend from the toolhead before clicking “Continue.”",
+  "1A00040000020032": "Error detected while fetching the hotend from slot 4: the toolhead failed to detect the hotend on the rack. Please check for any debris in the induction hotend mounting slot on the toolhead. If none is found, recalibrate the induction hotend rack.",
+  "1A00040000020033": "Error detected while fetching the hotend from slot 4: the rack failed to mount the hotend properly. Please check for excessive filament at the top of the hotend, debris in the contact area between the toolhead and the hotend, or if the locking lever is in the released position. After resolving the issue, click “Continue.”",
+  "1A00040000020034": "Error detected while fetching the hotend from slot 4: the hotend has dropped. Please check for excessive filament or other debris at the top of the hotend, trim or remove it, then reinstall the hotend into slot 4 on the rack and click “Continue.”",
+  "1A00040000020035": "Error detected while fetching the hotend from slot 4: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00040000020036": "Error detected while fetching the hotend from slot 4: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00050000020001": "Error fetching/nesting the hotend in rack slot 5. Ensure the hotend holder has no hotend installed, the corresponding rack slot contains a hotend, and manually move the toolhead to the machine center before continuing.",
+  "1A00050000020002": "Error fetching/nesting the hotend in rack slot 5. Ensure the hotend holder has a hotend installed, the corresponding rack slot is empty, and manually move the toolhead to the machine center before continuing.",
+  "1A00050000020003": "Error detected before induction hotend replacement: no hotend installed in slot 5 on the rack.",
+  "1A00050000020004": "Error detected before induction hotend replacement: a hotend is already installed in slot 5 on the rack. Please remove it before continuing.",
+  "1A00050000020020": "Abnormal condition detected while nesting the hotend in slot 5: a hotend is already installed. Please remove the existing one before clicking “Continue.”",
+  "1A00050000020021": "Abnormal condition detected while nesting the hotend in slot 5: no hotend detected on the toolhead. Please mount the hotend onto the toolhead before clicking “Continue.”",
+  "1A00050000020022": "Abnormal condition detected while nesting the hotend in slot 5. Please ensure no hotend is mounted on the toolhead and that a hotend is present in slot 5 on the rack before clicking “Continue.”",
+  "1A00050000020023": "Abnormal condition detected while nesting the hotend in slot 5: the rack failed to detect the hotend on the toolhead. Please check whether there is any debris on the hotend heatsink surface or if the front magnet has fallen off. If not, recalibrate the induction hotend rack.",
+  "1A00050000020024": "Abnormal condition detected while nesting the hotend in slot 5: the rack failed to detach the hotend from the toolhead. Please make sure the locking lever is in the released position.",
+  "1A00050000020025": "Abnormal condition detected while nesting the hotend in slot 5: the hotend has dropped. Please check for excessive filament remaining at the top of the hotend, trim the residue, reinstall the hotend into slot 5 on the rack, and then click “Continue.”",
+  "1A00050000020026": "Abnormal condition detected while nesting the hotend in slot 5: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00050000020027": "Abnormal condition detected while nesting the hotend in slot 5: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00050000020030": "Abnormal condition detected while fetching the hotend from slot 5: no hotend found in this position. Please install a hotend in the corresponding slot before clicking “Continue.”",
+  "1A00050000020031": "Abnormal condition detected while fetching the hotend from slot 5: a hotend is already mounted on the toolhead. Please remove the hotend from the toolhead before clicking “Continue.”",
+  "1A00050000020032": "Error detected while fetching the hotend from slot 5: the toolhead failed to detect the hotend on the rack. Please check for any debris in the induction hotend mounting slot on the toolhead. If none is found, recalibrate the induction hotend rack.",
+  "1A00050000020033": "Error detected while fetching the hotend from slot 5: the rack failed to mount the hotend properly. Please check for excessive filament at the top of the hotend, debris in the contact area between the toolhead and the hotend, or if the locking lever is in the released position. After resolving the issue, click “Continue.”",
+  "1A00050000020034": "Error detected while fetching the hotend from slot 5: the hotend has dropped. Please check for excessive filament or other debris at the top of the hotend, trim or remove it, then reinstall the hotend into slot 5 on the rack and click “Continue.”",
+  "1A00050000020035": "Error detected while fetching the hotend from slot 5: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00050000020036": "Error detected while fetching the hotend from slot 5: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00060000020001": "Error fetching/nesting the hotend in rack slot 6. Ensure the hotend holder has no hotend installed, the corresponding rack slot contains a hotend, and manually move the toolhead to the machine center before continuing.",
+  "1A00060000020002": "Error fetching/nesting the hotend in rack slot 6. Ensure the hotend holder has a hotend installed, the corresponding rack slot is empty, and manually move the toolhead to the machine center before continuing.",
+  "1A00060000020003": "Error detected before induction hotend replacement: no hotend installed in slot 6 on the rack.",
+  "1A00060000020004": "Error detected before induction hotend replacement: a hotend is already installed in slot 6 on the rack. Please remove it before continuing.",
+  "1A00060000020020": "Abnormal condition detected while nesting the hotend in slot 6: a hotend is already installed. Please remove the existing one before clicking “Continue.”",
+  "1A00060000020021": "Abnormal condition detected while nesting the hotend in slot 6: no hotend detected on the toolhead. Please mount the hotend onto the toolhead before clicking “Continue.”",
+  "1A00060000020022": "Abnormal condition detected while nesting the hotend in slot 6. Please ensure no hotend is mounted on the toolhead and that a hotend is present in slot 6 on the rack before clicking “Continue.”",
+  "1A00060000020023": "Abnormal condition detected while nesting the hotend in slot 6: the rack failed to detect the hotend on the toolhead. Please check whether there is any debris on the hotend heatsink surface or if the front magnet has fallen off. If not, recalibrate the induction hotend rack.",
+  "1A00060000020024": "Abnormal condition detected while nesting the hotend in slot 6: the rack failed to detach the hotend from the toolhead. Please make sure the locking lever is in the released position.",
+  "1A00060000020025": "Abnormal condition detected while nesting the hotend in slot 6: the hotend has dropped. Please check for excessive filament remaining at the top of the hotend, trim the residue, reinstall the hotend into slot 6 on the rack, and then click “Continue.”",
+  "1A00060000020026": "Abnormal condition detected while nesting the hotend in slot 6: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00060000020027": "Abnormal condition detected while nesting the hotend in slot 6: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00060000020030": "Abnormal condition detected while fetching the hotend from slot 6: no hotend found in this position. Please install a hotend in the corresponding slot before clicking “Continue.”",
+  "1A00060000020031": "Abnormal condition detected while fetching the hotend from slot 6: a hotend is already mounted on the toolhead. Please remove the hotend from the toolhead before clicking “Continue.”",
+  "1A00060000020032": "Error detected while fetching the hotend from slot 6: the toolhead failed to detect the hotend on the rack. Please check for any debris in the induction hotend mounting slot on the toolhead. If none is found, recalibrate the induction hotend rack.",
+  "1A00060000020033": "Error detected while fetching the hotend from slot 6: the rack failed to mount the hotend properly. Please check for excessive filament at the top of the hotend, debris in the contact area between the toolhead and the hotend, or if the locking lever is in the released position. After resolving the issue, click “Continue.”",
+  "1A00060000020034": "Error detected while fetching the hotend from slot 6: the hotend has dropped. Please check for excessive filament or other debris at the top of the hotend, trim or remove it, then reinstall the hotend into slot 6 on the rack and click “Continue.”",
+  "1A00060000020035": "Error detected while fetching the hotend from slot 6: rack movement is blocked. Please check for any foreign objects, remove them, and then click “Continue.”",
+  "1A00060000020036": "Error detected while fetching the hotend from slot 6: toolhead recentering retry failed. Please manually move the toolhead to the center of the build plate and then click “Continue.”",
+  "1A00100000020001": "Toolhead XY axis or hotend rack has not finished homing. Hotend replacement cannot proceed.",
+  "1A00110000020002": "Hotend rack coarse homing failed. Please check for any obstructions.",
+  "1A00110000020003": "Induction Hotend Rack coarse homing failed due to excessive travel. Please check the timing belt condition.",
+  "1A00120000020001": "Induction hotend rack calibration failed: camera position calibration error. Please clean any debris from the thermal paper surface at the rear of the build plate and remove residue from the nozzle.",
+  "1A00120000020002": "Induction hotend rack calibration failed: the white marker on the rack’s induction hotend docking module was not detected. Please refer to the Wiki to check if the marker is damaged or covered by dirt.",
+  "1A00120000020003": "Induction hotend rack calibration failed: abnormal calibration result. Please refer to the Wiki to check if the rack structure is deformed.",
+  "1A00120000020004": "Hotend rack height calibration is abnormal. Please check for any foreign objects that may be causing the rack to jam.",
+  "1A00120000020007": "The hotend rack motion timed out. Please check for any foreign objects or obstructions that may be causing the rack to jam.",
+  "1A00120000020008": "The hotend rack tool lock rod position is abnormal. Please check for any foreign objects or obstructions that may be causing the lock rod to jam.",
+  "1A00120000020009": "Induction hotend locking lever or rack lever release mechanism error. Please refer to the Wiki for inspection.",
+  "1A00120000020010": "The Induction Hotend Rack has not been set up. Please follow the instructions to complete the setup before using its functions.",
+  "1A00120000020011": "Induction hotend not detected before locking. Please check its installation on the toolhead.",
+  "1A00130000020001": "The hotend rack is fully occupied. Please remove at least one hotend before proceeding.",
+  "1A00200000020001": "Power outage occurred during hotend replacement. Please follow the Wiki guide to restore the setup, otherwise printing may fail.",
+  "1A00210000010001": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend back to slot 1 on the rack, then move the toolhead to the center of the build plate.",
+  "1A00210000010002": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend from slot 1 on the rack back onto the toolhead, then move the toolhead to the center of the build plate.",
+  "1A00210000020001": "Power loss occurred while fetching/nesting hotend. Check if the hotend is in slot 1 on the rack. If not, nest it back to that slot and move the toolhead to the center of the build plate.",
+  "1A00210000020002": "Power loss occurred while fetching/nesting the hotend. Check if it’s mounted on the toolhead. If not, fetch it from slot 1 on the rack, nest it onto the toolhead, and move the toolhead to the center of the build plate.",
+  "1A00220000010001": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend back to slot 2 on the rack, then move the toolhead to the center of the build plate.",
+  "1A00220000010002": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend from slot 2 on the rack back onto the toolhead, then move the toolhead to the center of the build plate.",
+  "1A00220000020001": "Power loss occurred while fetching/nesting hotend. Check if the hotend is in slot 2 on the rack. If not, nest it back to that slot and move the toolhead to the center of the build plate.",
+  "1A00220000020002": "Power loss occurred while fetching/nesting the hotend. Check if it’s mounted on the toolhead. If not, fetch it from slot 2 on the rack, nest it onto the toolhead, and move the toolhead to the center of the build plate.",
+  "1A00230000010001": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend back to slot 3 on the rack, then move the toolhead to the center of the build plate.",
+  "1A00230000010002": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend from slot 3 on the rack back onto the toolhead, then move the toolhead to the center of the build plate.",
+  "1A00230000020001": "Power loss occurred while fetching/nesting hotend. Check if the hotend is in slot 3 on the rack. If not, nest it back to that slot and move the toolhead to the center of the build plate.",
+  "1A00230000020002": "Power loss occurred while fetching/nesting the hotend. Check if it’s mounted on the toolhead. If not, fetch it from slot 3 on the rack, nest it onto the toolhead, and move the toolhead to the center of the build plate.",
+  "1A00240000010001": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend back to slot 4 on the rack, then move the toolhead to the center of the build plate.",
+  "1A00240000010002": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend from slot 4 on the rack back onto the toolhead, then move the toolhead to the center of the build plate.",
+  "1A00240000020001": "Power loss occurred while fetching/nesting hotend. Check if the hotend is in slot 4 on the rack. If not, nest it back to that slot and move the toolhead to the center of the build plate.",
+  "1A00240000020002": "Power loss occurred while fetching/nesting the hotend. Check if it’s mounted on the toolhead. If not, fetch it from slot 4 on the rack, nest it onto the toolhead, and move the toolhead to the center of the build plate.",
+  "1A00250000010001": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend back to slot 5 on the rack, then move the toolhead to the center of the build plate.",
+  "1A00250000010002": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend from slot 5 on the rack back onto the toolhead, then move the toolhead to the center of the build plate.",
+  "1A00250000020001": "Power loss occurred while fetching/nesting hotend. Check if the hotend is in slot 5 on the rack. If not, nest it back to that slot and move the toolhead to the center of the build plate.",
+  "1A00250000020002": "Power loss occurred while fetching/nesting the hotend. Check if it’s mounted on the toolhead. If not, fetch it from slot 5 on the rack, nest it onto the toolhead, and move the toolhead to the center of the build plate.",
+  "1A00260000010001": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend back to slot 6 on the rack, then move the toolhead to the center of the build plate.",
+  "1A00260000010002": "Power loss occurred while fetching/nesting the hotend. Please nest the hotend from slot 6 on the rack back onto the toolhead, then move the toolhead to the center of the build plate.",
+  "1A00260000020001": "Power loss occurred while fetching/nesting hotend. Check if the hotend is in slot 6 on the rack. If not, nest it back to that slot and move the toolhead to the center of the build plate.",
+  "1A00260000020002": "Power loss occurred while fetching/nesting the hotend. Check if it’s mounted on the toolhead. If not, fetch it from slot 6 on the rack, nest it onto the toolhead, and move the toolhead to the center of the build plate.",
+  "1A00E00000010005": "The Induction Hotend Rack is blocked by an obstacle during homing. Please clear any obstructions from its movement path.",
+  "1A00E00000010006": "Induction Hotend Rack homing distance is too long. The timing belt may be loose. Please check and tighten."
+ },
+ "hms_by_model": {
+  "093": {
+   "0300020000010006": "The nozzle temperature is abnormal; the sensor may have a short circuit. Please check whether the connector is properly plugged in.",
+   "0300020000010007": "The nozzle temperature is abnormal; the sensor may have an open circuit.",
+   "0300A40000010002": "Hotend temperature is too high. Please wait for it to cool down to room temperature before restarting the task.",
+   "050001000001000A": "",
+   "07FF450000020001": "The filament cutter sensor is malfunctioning; please check whether the connector is properly plugged in.",
+   "0C00010000010018": "",
+   "0C00020000010001": "The height measuring laser is not lit. Check for obstructions or a loose toolhead camera cable."
+  },
+  "094": {
+   "0300020000010001": "The right nozzle temperature is abnormal; the heater may have a short circuit.",
+   "0300020000010002": "The right nozzle temperature is abnormal; the heater may have an open circuit.",
+   "0300020000010003": "The right nozzle temperature is abnormal; the heater is over temperature.",
+   "0300020000010009": "The right nozzle temperature control is abnormal. The hot end may not be installed. To heat the heating assembly without the hotend, enable Maintenance Mode in the settings.",
+   "0300180000030009": "High heatbed temperature printing is performed for the first time. In order to obtain better first layer printing quality, high temperature bed leveling is being automatically performed.",
+   "03001E0000010003": "The left nozzle temperature is abnormal; the heater is over temperature.",
+   "0300250000010005": "Z axis motor rotation is obstructed; please check if there are foreign objects stuck in the Z slider or Z timing pulley.",
+   "0300260000010005": "Z axis motor rotation is obstructed; please check if there are foreign objects stuck in the Z slider or Z timing pulley.",
+   "0300290000010001": "Encoder pattern can not be recognized; possible reasons include: pattern distortion, light overexposure, plate misplacement...",
+   "0300910000010008": "Chamber heater 1 failed to rise to target temperature.",
+   "0300930000010005": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air inlet may have a short circuit.",
+   "0300930000010006": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air inlet may have an open circuit.",
+   "0300960000010002": "The front door Hall sensor 1 is abnormal; please check whether the connection wire is loose.",
+   "0300960000010003": "The front door Hall sensor 2 is abnormal; please check whether the connection wire is loose.",
+   "0300970000010002": "The top cover Hall sensor 1 is abnormal; please check whether the connection wire is loose.",
+   "0300970000010003": "The top cover Hall sensor 2 is abnormal; please check whether the connection wire is loose.",
+   "0300980000010002": "The left side window Hall sensor 1 is abnormal; please check whether the connection wire is loose.",
+   "0300980000010003": "The left side window Hall sensor 2 is abnormal; please check whether the connection wire is loose.",
+   "0300990000010002": "The right side window Hall sensor 1 is abnormal; please check whether the connection wire is loose.",
+   "0300990000010003": "The right side window Hall sensor 2 is abnormal; please check whether the connection wire is loose.",
+   "03009B0000010003": "Emergency stop button is not properly installed. Please follow the Wiki to install it.",
+   "050001000001000A": "",
+   "0500020000020008": "Time synchronization failed.",
+   "0500030000010004": "The AHB module is malfunctioning. Please restart the device.",
+   "0500040000020043": "The Nozzle Camera is dirty or obstructed; please clean it and continue.",
+   "0500050000010007": "MQTT Command verification failed, please update Studio or Handy.",
+   "050005000001000D": "BirdsEye Camera is not certified. Please use Bambu Lab accessories.",
+   "050005000001000E": "Laser Module is not certified. Please use Bambu Lab accessories.",
+   "050005000001000F": "Uncertified accessory detected. Please use official accessories.",
+   "0500050000010010": "Uncertified AMS A detected or AMS A firmware not upgraded. Unable to operate with this printer.",
+   "0500050000010011": "Uncertified Air Pump or Air Pump firmware not upgraded. Unable to operate with this printer.",
+   "0500050000010012": "Uncertified Cutting Module or Cutting Module firmware not upgraded. Unable to operate with this printer.",
+   "0500060000020001": "",
+   "0500060000020002": "",
+   "0500060000020003": "",
+   "0501050000010010": "Uncertified AMS B detected or AMS B firmware not upgraded. Unable to operate with this printer.",
+   "0502050000010010": "Uncertified AMS C detected or AMS C firmware not upgraded. Unable to operate with this printer.",
+   "0503050000010010": "Uncertified AMS D detected or AMS D firmware not upgraded. Unable to operate with this printer.",
+   "0504050000010010": "Uncertified AMS E detected or AMS E firmware not upgraded. Unable to operate with this printer.",
+   "0505050000010010": "Uncertified AMS F detected or AMS F firmware not upgraded. Unable to operate with this printer.",
+   "0506050000010010": "Uncertified AMS G detected or AMS G firmware not upgraded. Unable to operate with this printer.",
+   "0507050000010010": "Uncertified AMS H detected or AMS H firmware not upgraded. Unable to operate with this printer.",
+   "0580050000010010": "Uncertified AMS-HT A detected or AMS-HT A firmware not upgraded. Unable to operate with this printer.",
+   "0581050000010010": "Uncertified AMS-HT B detected or AMS-HT B firmware not upgraded. Unable to operate with this printer.",
+   "0582050000010010": "Uncertified AMS-HT C detected or AMS-HT C firmware not upgraded. Unable to operate with this printer.",
+   "0583050000010010": "Uncertified AMS-HT D detected or AMS-HT D firmware not upgraded. Unable to operate with this printer.",
+   "0584050000010010": "Uncertified AMS-HT E detected or AMS-HT E firmware not upgraded. Unable to operate with this printer.",
+   "0585050000010010": "Uncertified AMS-HT F detected or AMS-HT F firmware not upgraded. Unable to operate with this printer.",
+   "0586050000010010": "Uncertified AMS-HT G detected or AMS-HT G firmware not upgraded. Unable to operate with this printer.",
+   "0587050000010010": "Uncertified AMS-HT H detected or AMS-HT H firmware not upgraded. Unable to operate with this printer.",
+   "0700200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS A to the extruder is properly connected.",
+   "0700200000020012": "AMS A slot 1 feeder unit motor is stalled.",
+   "0700200000020013": "AMS A slot 1 feeder unit motor has no signal.",
+   "0700200000020014": "AMS A slot 1 filament odometer has no signal.",
+   "0700200000020015": "AMS A slot 1 filament sensor status is abnormal.",
+   "0700200000020016": "AMS A slot 1 assist motor has slipped.",
+   "0700200000020023": "AMS A slot 1 the tube inside the AMS is broken.",
+   "0700210000020012": "AMS A slot 2 feeder unit motor is stalled.",
+   "0700210000020013": "AMS A slot 2 feeder unit motor has no signal.",
+   "0700210000020014": "AMS A slot 2 filament odometer has no signal.",
+   "0700210000020015": "AMS A slot 2 filament sensor status is abnormal.",
+   "0700210000020016": "AMS A slot 2 assist motor has slipped.",
+   "0700210000020023": "AMS A slot 2 the tube inside the AMS is broken.",
+   "0700220000020012": "AMS A slot 3 feeder unit motor is stalled.",
+   "0700220000020013": "AMS A slot 3 feeder unit motor has no signal.",
+   "0700220000020014": "AMS A slot 3 filament odometer has no signal.",
+   "0700220000020015": "AMS A slot 3 filament sensor status is abnormal.",
+   "0700220000020016": "AMS A slot 3 assist motor has slipped.",
+   "0700220000020023": "AMS A slot 3 the tube inside the AMS is broken.",
+   "0700230000020012": "AMS A slot 4 feeder unit motor is stalled.",
+   "0700230000020013": "AMS A slot 4 feeder unit motor has no signal.",
+   "0700230000020014": "AMS A slot 4 filament odometer has no signal.",
+   "0700230000020015": "AMS A slot 4 filament sensor status is abnormal.",
+   "0700230000020016": "AMS A slot 4 assist motor has slipped.",
+   "0700230000020023": "AMS A slot 4 the tube inside the AMS is broken.",
+   "0700250000020001": "Detected AMS using printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+   "0700400000020001": "Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+   "0700600000020001": "The AMS A Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0700610000020001": "The AMS A Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0700620000020001": "The AMS A Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0700630000020001": "The AMS A Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0700900000010002": "AMS A The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0700910000010002": "AMS A The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0701200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS B to the extruder is properly connected.",
+   "0701200000020012": "AMS B slot 1 feeder unit motor is stalled.",
+   "0701200000020013": "AMS B slot 1 feeder unit motor has no signal.",
+   "0701200000020014": "AMS B slot 1 filament odometer has no signal.",
+   "0701200000020015": "AMS B slot 1 filament sensor status is abnormal.",
+   "0701200000020016": "AMS B slot 1 assist motor has slipped.",
+   "0701200000020023": "AMS B slot 1 the tube inside the AMS is broken.",
+   "0701210000020012": "AMS B slot 2 feeder unit motor is stalled.",
+   "0701210000020013": "AMS B slot 2 feeder unit motor has no signal.",
+   "0701210000020014": "AMS B slot 2 filament odometer has no signal.",
+   "0701210000020015": "AMS B slot 2 filament sensor status is abnormal.",
+   "0701210000020016": "AMS B slot 2 assist motor has slipped.",
+   "0701210000020023": "AMS B slot 2 the tube inside the AMS is broken.",
+   "0701220000020012": "AMS B slot 3 feeder unit motor is stalled.",
+   "0701220000020013": "AMS B slot 3 feeder unit motor has no signal.",
+   "0701220000020014": "AMS B slot 3 filament odometer has no signal.",
+   "0701220000020015": "AMS B slot 3 filament sensor status is abnormal.",
+   "0701220000020016": "AMS B slot 3 assist motor has slipped.",
+   "0701220000020023": "AMS B slot 3 the tube inside the AMS is broken.",
+   "0701230000020012": "AMS B slot 4 feeder unit motor is stalled.",
+   "0701230000020013": "AMS B slot 4 feeder unit motor has no signal.",
+   "0701230000020014": "AMS B slot 4 filament odometer has no signal.",
+   "0701230000020015": "AMS B slot 4 filament sensor status is abnormal.",
+   "0701230000020016": "AMS B slot 4 assist motor has slipped.",
+   "0701230000020023": "AMS B slot 4 the tube inside the AMS is broken.",
+   "0701250000020001": "Detected AMS using printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+   "0701600000020001": "The AMS B Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0701610000020001": "The AMS B Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0701620000020001": "The AMS B Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0701630000020001": "The AMS B Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0701900000010002": "AMS B The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0701910000010002": "AMS B The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0702200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS C to the extruder is properly connected.",
+   "0702200000020012": "AMS C slot 1 feeder unit motor is stalled.",
+   "0702200000020013": "AMS C slot 1 feeder unit motor has no signal.",
+   "0702200000020014": "AMS C slot 1 filament odometer has no signal.",
+   "0702200000020015": "AMS C slot 1 filament sensor status is abnormal.",
+   "0702200000020016": "AMS C slot 1 assist motor has slipped.",
+   "0702200000020023": "AMS C slot 1 the tube inside the AMS is broken.",
+   "0702210000020012": "AMS C slot 2 feeder unit motor is stalled.",
+   "0702210000020013": "AMS C slot 2 feeder unit motor has no signal.",
+   "0702210000020014": "AMS C slot 2 filament odometer has no signal.",
+   "0702210000020015": "AMS C slot 2 filament sensor status is abnormal.",
+   "0702210000020016": "AMS C slot 2 assist motor has slipped.",
+   "0702210000020023": "AMS C slot 2 the tube inside the AMS is broken.",
+   "0702220000020012": "AMS C slot 3 feeder unit motor is stalled.",
+   "0702220000020013": "AMS C slot 3 feeder unit motor has no signal.",
+   "0702220000020014": "AMS C slot 3 filament odometer has no signal.",
+   "0702220000020015": "AMS C slot 3 filament sensor status is abnormal.",
+   "0702220000020016": "AMS C slot 3 assist motor has slipped.",
+   "0702220000020023": "AMS C slot 3 the tube inside the AMS is broken.",
+   "0702230000020012": "AMS C slot 4 feeder unit motor is stalled.",
+   "0702230000020013": "AMS C slot 4 feeder unit motor has no signal.",
+   "0702230000020014": "AMS C slot 4 filament odometer has no signal.",
+   "0702230000020015": "AMS C slot 4 filament sensor status is abnormal.",
+   "0702230000020016": "AMS C slot 4 assist motor has slipped.",
+   "0702230000020023": "AMS C slot 4 the tube inside the AMS is broken.",
+   "0702250000020001": "Detected AMS using printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+   "0702600000020001": "The AMS C Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0702610000020001": "The AMS C Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0702620000020001": "The AMS C Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0702630000020001": "The AMS C Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0702900000010002": "AMS C The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0702910000010002": "AMS C The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0703200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS D to the extruder is properly connected.",
+   "0703200000020012": "AMS D slot 1 feeder unit motor is stalled.",
+   "0703200000020013": "AMS D slot 1 feeder unit motor has no signal.",
+   "0703200000020014": "AMS D slot 1 filament odometer has no signal.",
+   "0703200000020015": "AMS D slot 1 filament sensor status is abnormal.",
+   "0703200000020016": "AMS D slot 1 assist motor has slipped.",
+   "0703200000020023": "AMS D slot 1 the tube inside the AMS is broken.",
+   "0703210000020012": "AMS D slot 2 feeder unit motor is stalled.",
+   "0703210000020013": "AMS D slot 2 feeder unit motor has no signal.",
+   "0703210000020014": "AMS D slot 2 filament odometer has no signal.",
+   "0703210000020015": "AMS D slot 2 filament sensor status is abnormal.",
+   "0703210000020016": "AMS D slot 2 assist motor has slipped.",
+   "0703210000020023": "AMS D slot 2 the tube inside the AMS is broken.",
+   "0703220000020012": "AMS D slot 3 feeder unit motor is stalled.",
+   "0703220000020013": "AMS D slot 3 feeder unit motor has no signal.",
+   "0703220000020014": "AMS D slot 3 filament odometer has no signal.",
+   "0703220000020015": "AMS D slot 3 filament sensor status is abnormal.",
+   "0703220000020016": "AMS D slot 3 assist motor has slipped.",
+   "0703220000020023": "AMS D slot 3 the tube inside the AMS is broken.",
+   "0703230000020012": "AMS D slot 4 feeder unit motor is stalled.",
+   "0703230000020013": "AMS D slot 4 feeder unit motor has no signal.",
+   "0703230000020014": "AMS D slot 4 filament odometer has no signal.",
+   "0703230000020015": "AMS D slot 4 filament sensor status is abnormal.",
+   "0703230000020016": "AMS D slot 4 assist motor has slipped.",
+   "0703230000020023": "AMS D slot 4 the tube inside the AMS is broken.",
+   "0703250000020001": "Detected AMS using printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+   "0703600000020001": "The AMS D Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0703610000020001": "The AMS D Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0703620000020001": "The AMS D Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0703630000020001": "The AMS D Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0703900000010002": "AMS D The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0703910000010002": "AMS D The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0704200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS E to the extruder is properly connected.",
+   "0704200000020012": "AMS E slot 1 feeder unit motor is stalled.",
+   "0704200000020013": "AMS E slot 1 feeder unit motor has no signal.",
+   "0704200000020014": "AMS E slot 1 filament odometer has no signal.",
+   "0704200000020015": "AMS E slot 1 filament sensor status is abnormal.",
+   "0704200000020016": "AMS E slot 1 assist motor has slipped.",
+   "0704200000020023": "AMS E slot 1 the tube inside the AMS is broken.",
+   "0704210000020012": "AMS E slot 2 feeder unit motor is stalled.",
+   "0704210000020013": "AMS E slot 2 feeder unit motor has no signal.",
+   "0704210000020014": "AMS E slot 2 filament odometer has no signal.",
+   "0704210000020015": "AMS E slot 2 filament sensor status is abnormal.",
+   "0704210000020016": "AMS E slot 2 assist motor has slipped.",
+   "0704210000020023": "AMS E slot 2 the tube inside the AMS is broken.",
+   "0704220000020012": "AMS E slot 3 feeder unit motor is stalled.",
+   "0704220000020013": "AMS E slot 3 feeder unit motor has no signal.",
+   "0704220000020014": "AMS E slot 3 filament odometer has no signal.",
+   "0704220000020015": "AMS E slot 3 filament sensor status is abnormal.",
+   "0704220000020016": "AMS E slot 3 assist motor has slipped.",
+   "0704220000020023": "AMS E slot 3 the tube inside the AMS is broken.",
+   "0704230000020012": "AMS E slot 4 feeder unit motor is stalled.",
+   "0704230000020013": "AMS E slot 4 feeder unit motor has no signal.",
+   "0704230000020014": "AMS E slot 4 filament odometer has no signal.",
+   "0704230000020015": "AMS E slot 4 filament sensor status is abnormal.",
+   "0704230000020016": "AMS E slot 4 assist motor has slipped.",
+   "0704230000020023": "AMS E slot 4 the tube inside the AMS is broken.",
+   "0704250000020001": "Detected AMS using printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+   "0704600000020001": "The AMS E Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0704610000020001": "The AMS E Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0704620000020001": "The AMS E Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0704630000020001": "The AMS E Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0704900000010002": "AMS E The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0704910000010002": "AMS E The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0705200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS F to the extruder is properly connected.",
+   "0705200000020012": "AMS F slot 1 feeder unit motor is stalled.",
+   "0705200000020013": "AMS F slot 1 feeder unit motor has no signal.",
+   "0705200000020014": "AMS F slot 1 filament odometer has no signal.",
+   "0705200000020015": "AMS F slot 1 filament sensor status is abnormal.",
+   "0705200000020016": "AMS F slot 1 assist motor has slipped.",
+   "0705200000020023": "AMS F slot 1 the tube inside the AMS is broken.",
+   "0705210000020012": "AMS F slot 2 feeder unit motor is stalled.",
+   "0705210000020013": "AMS F slot 2 feeder unit motor has no signal.",
+   "0705210000020014": "AMS F slot 2 filament odometer has no signal.",
+   "0705210000020015": "AMS F slot 2 filament sensor status is abnormal.",
+   "0705210000020016": "AMS F slot 2 assist motor has slipped.",
+   "0705210000020023": "AMS F slot 2 the tube inside the AMS is broken.",
+   "0705220000020012": "AMS F slot 3 feeder unit motor is stalled.",
+   "0705220000020013": "AMS F slot 3 feeder unit motor has no signal.",
+   "0705220000020014": "AMS F slot 3 filament odometer has no signal.",
+   "0705220000020015": "AMS F slot 3 filament sensor status is abnormal.",
+   "0705220000020016": "AMS F slot 3 assist motor has slipped.",
+   "0705220000020023": "AMS F slot 3 the tube inside the AMS is broken.",
+   "0705230000020012": "AMS F slot 4 feeder unit motor is stalled.",
+   "0705230000020013": "AMS F slot 4 feeder unit motor has no signal.",
+   "0705230000020014": "AMS F slot 4 filament odometer has no signal.",
+   "0705230000020015": "AMS F slot 4 filament sensor status is abnormal.",
+   "0705230000020016": "AMS F slot 4 assist motor has slipped.",
+   "0705230000020023": "AMS F slot 4 the tube inside the AMS is broken.",
+   "0705250000020001": "Detected AMS using printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+   "0705600000020001": "The AMS F Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0705610000020001": "The AMS F Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0705620000020001": "The AMS F Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0705630000020001": "The AMS F Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0705900000010002": "AMS F The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0705910000010002": "AMS F The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0706200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS G to the extruder is properly connected.",
+   "0706200000020012": "AMS G slot 1 feeder unit motor is stalled.",
+   "0706200000020013": "AMS G slot 1 feeder unit motor has no signal.",
+   "0706200000020014": "AMS G slot 1 filament odometer has no signal.",
+   "0706200000020015": "AMS G slot 1 filament sensor status is abnormal.",
+   "0706200000020016": "AMS G slot 1 assist motor has slipped.",
+   "0706200000020023": "AMS G slot 1 the tube inside the AMS is broken.",
+   "0706210000020012": "AMS G slot 2 feeder unit motor is stalled.",
+   "0706210000020013": "AMS G slot 2 feeder unit motor has no signal.",
+   "0706210000020014": "AMS G slot 2 filament odometer has no signal.",
+   "0706210000020015": "AMS G slot 2 filament sensor status is abnormal.",
+   "0706210000020016": "AMS G slot 2 assist motor has slipped.",
+   "0706210000020023": "AMS G slot 2 the tube inside the AMS is broken.",
+   "0706220000020012": "AMS G slot 3 feeder unit motor is stalled.",
+   "0706220000020013": "AMS G slot 3 feeder unit motor has no signal.",
+   "0706220000020014": "AMS G slot 3 filament odometer has no signal.",
+   "0706220000020015": "AMS G slot 3 filament sensor status is abnormal.",
+   "0706220000020016": "AMS G slot 3 assist motor has slipped.",
+   "0706220000020023": "AMS G slot 3 the tube inside the AMS is broken.",
+   "0706230000020012": "AMS G slot 4 feeder unit motor is stalled.",
+   "0706230000020013": "AMS G slot 4 feeder unit motor has no signal.",
+   "0706230000020014": "AMS G slot 4 filament odometer has no signal.",
+   "0706230000020015": "AMS G slot 4 filament sensor status is abnormal.",
+   "0706230000020016": "AMS G slot 4 assist motor has slipped.",
+   "0706230000020023": "AMS G slot 4 the tube inside the AMS is broken.",
+   "0706250000020001": "Detected AMS using printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+   "0706600000020001": "The AMS G Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0706610000020001": "The AMS G Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0706620000020001": "The AMS G Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0706630000020001": "The AMS G Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0706900000010002": "AMS G The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0706910000010002": "AMS G The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0707200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS H to the extruder is properly connected.",
+   "0707200000020012": "AMS H slot 1 feeder unit motor is stalled.",
+   "0707200000020013": "AMS H slot 1 feeder unit motor has no signal.",
+   "0707200000020014": "AMS H slot 1 filament odometer has no signal.",
+   "0707200000020015": "AMS H slot 1 filament sensor status is abnormal.",
+   "0707200000020016": "AMS H slot 1 assist motor has slipped.",
+   "0707200000020023": "AMS H slot 1 the tube inside the AMS is broken.",
+   "0707210000020012": "AMS H slot 2 feeder unit motor is stalled.",
+   "0707210000020013": "AMS H slot 2 feeder unit motor has no signal.",
+   "0707210000020014": "AMS H slot 2 filament odometer has no signal.",
+   "0707210000020015": "AMS H slot 2 filament sensor status is abnormal.",
+   "0707210000020016": "AMS H slot 2 assist motor has slipped.",
+   "0707210000020023": "AMS H slot 2 the tube inside the AMS is broken.",
+   "0707220000020012": "AMS H slot 3 feeder unit motor is stalled.",
+   "0707220000020013": "AMS H slot 3 feeder unit motor has no signal.",
+   "0707220000020014": "AMS H slot 3 filament odometer has no signal.",
+   "0707220000020015": "AMS H slot 3 filament sensor status is abnormal.",
+   "0707220000020016": "AMS H slot 3 assist motor has slipped.",
+   "0707220000020023": "AMS H slot 3 the tube inside the AMS is broken.",
+   "0707230000020012": "AMS H slot 4 feeder unit motor is stalled.",
+   "0707230000020013": "AMS H slot 4 feeder unit motor has no signal.",
+   "0707230000020014": "AMS H slot 4 filament odometer has no signal.",
+   "0707230000020015": "AMS H slot 4 filament sensor status is abnormal.",
+   "0707230000020016": "AMS H slot 4 assist motor has slipped.",
+   "0707230000020023": "AMS H slot 4 the tube inside the AMS is broken.",
+   "0707250000020001": "Detected AMS using printer power for drying during loading/printing. For better drying performance, please connect a power adapter.",
+   "0707600000020001": "The AMS H Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0707610000020001": "The AMS H Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0707620000020001": "The AMS H Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0707630000020001": "The AMS H Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "0707900000010002": "AMS H The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "0707910000010002": "AMS H The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "07FE450000020002": "The filament cutter's cutting distance is too large. The XY motor may lose steps.",
+   "07FEA00000020001": "The left nozzle cold pulling process has timed out. Please click \"Retry\" and then manually pull out the filament.",
+   "07FF200000020001": "External filament of right extruder has run out; please load a new filament.",
+   "07FF200000020002": "No filament was detected in the right extruder from the external spool; please load the new filament.",
+   "07FF200000020004": "Please pull the external filament from the right extruder.",
+   "07FF450000020002": "The filament cutter's cutting distance is too large. The XY motor may lose steps.",
+   "07FF600000020001": "External spool connected to right extruder may be tangled or jammed.",
+   "07FF700000020003": "Please check if the material is coming out of the right nozzle. If not, gently push the material and try to extrude again.",
+   "07FFA00000020001": "The right nozzle cold pulling process has timed out. Please click \"Retry\" and then manually pull out the filament.",
+   "0C00030000020014": "It's recommended to calibrate the Live View Camera to improve the accuracy of foreign object detection. Tap \"Settings > Calibration\" on the printer screen. If a laser or cutting module is installed, please remove it before calibration.",
+   "0C00030000020015": "Foreign object detection is not working. Detected the Live View Camera has been replaced. If a laser or cutting module is installed, please uninstall the module, tab \"Settings>Calibration\" on printer screen and re-calibrate the Live View Camera.",
+   "0C00040000020017": "Visual marker not detected; please re-paste the material in the correct position. Meanwhile, please clean the Toolhead Camera to prevent contamination and remove any objects that may obstruct its view.",
+   "0C00040000020018": "Hand-Eye calibration failed, and cutting accuracy may have decreased. Please check if the cutting blade tip is worn. Meanwhile, please clean the Toolhead Camera to prevent contamination and remove any objects that may obstruct its view.",
+   "0C00040000020023": "Thickness measurement failed, the Toolhead Camera was unable to detect the material surface. Meanwhile, please clean the Toolhead Camera to prevent contamination and remove any objects that may obstruct its view.",
+   "0C00040000030018": "The toolhead camera calibration failed, probably because the object was not tightly adhered. The default parameters will be used for this cutting.",
+   "1800200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS-HT A to the extruder is properly connected.",
+   "1800200000020012": "AMS-HT A slot 1 feeder unit motor is stalled.",
+   "1800200000020013": "AMS-HT A slot 1 feeder unit motor has no signal.",
+   "1800200000020014": "AMS-HT A slot 1 filament odometer has no signal.",
+   "1800200000020015": "AMS-HT A slot 1 filament sensor status is abnormal.",
+   "1800200000020016": "AMS-HT A slot 1 assist motor has slipped.",
+   "1800200000020023": "AMS-HT A slot 1 the tube inside the AMS is broken.",
+   "1800210000020012": "AMS-HT A slot 2 feeder unit motor is stalled.",
+   "1800210000020013": "AMS-HT A slot 2 feeder unit motor has no signal.",
+   "1800210000020014": "AMS-HT A slot 2 filament odometer has no signal.",
+   "1800210000020015": "AMS-HT A slot 2 filament sensor status is abnormal.",
+   "1800210000020016": "AMS-HT A slot 2 assist motor has slipped.",
+   "1800210000020023": "AMS-HT A slot 2 the tube inside the AMS is broken.",
+   "1800220000020012": "AMS-HT A slot 3 feeder unit motor is stalled.",
+   "1800220000020013": "AMS-HT A slot 3 feeder unit motor has no signal.",
+   "1800220000020014": "AMS-HT A slot 3 filament odometer has no signal.",
+   "1800220000020015": "AMS-HT A slot 3 filament sensor status is abnormal.",
+   "1800220000020016": "AMS-HT A slot 3 assist motor has slipped.",
+   "1800220000020023": "AMS-HT A slot 3 the tube inside the AMS is broken.",
+   "1800230000020012": "AMS-HT A slot 4 feeder unit motor is stalled.",
+   "1800230000020013": "AMS-HT A slot 4 feeder unit motor has no signal.",
+   "1800230000020014": "AMS-HT A slot 4 filament odometer has no signal.",
+   "1800230000020015": "AMS-HT A slot 4 filament sensor status is abnormal.",
+   "1800230000020016": "AMS-HT A slot 4 assist motor has slipped.",
+   "1800230000020023": "AMS-HT A slot 4 the tube inside the AMS is broken.",
+   "1800400000020001": "Filament buffer position signal lost: the cable or position sensor may be malfunctioning.",
+   "1800600000020001": "The AMS-HT A Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1800610000020001": "The AMS-HT A Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1800620000020001": "The AMS-HT A Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1800630000020001": "The AMS-HT A Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1800900000010002": "AMS-HT A The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1800910000010002": "AMS-HT A The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1801200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS-HT B to the extruder is properly connected.",
+   "1801200000020012": "AMS-HT B slot 1 feeder unit motor is stalled.",
+   "1801200000020013": "AMS-HT B slot 1 feeder unit motor has no signal.",
+   "1801200000020014": "AMS-HT B slot 1 filament odometer has no signal.",
+   "1801200000020015": "AMS-HT B slot 1 filament sensor status is abnormal.",
+   "1801200000020016": "AMS-HT B slot 1 assist motor has slipped.",
+   "1801200000020023": "AMS-HT B slot 1 the tube inside the AMS is broken.",
+   "1801210000020012": "AMS-HT B slot 2 feeder unit motor is stalled.",
+   "1801210000020013": "AMS-HT B slot 2 feeder unit motor has no signal.",
+   "1801210000020014": "AMS-HT B slot 2 filament odometer has no signal.",
+   "1801210000020015": "AMS-HT B slot 2 filament sensor status is abnormal.",
+   "1801210000020016": "AMS-HT B slot 2 assist motor has slipped.",
+   "1801210000020023": "AMS-HT B slot 2 the tube inside the AMS is broken.",
+   "1801220000020012": "AMS-HT B slot 3 feeder unit motor is stalled.",
+   "1801220000020013": "AMS-HT B slot 3 feeder unit motor has no signal.",
+   "1801220000020014": "AMS-HT B slot 3 filament odometer has no signal.",
+   "1801220000020015": "AMS-HT B slot 3 filament sensor status is abnormal.",
+   "1801220000020016": "AMS-HT B slot 3 assist motor has slipped.",
+   "1801220000020023": "AMS-HT B slot 3 the tube inside the AMS is broken.",
+   "1801230000020012": "AMS-HT B slot 4 feeder unit motor is stalled.",
+   "1801230000020013": "AMS-HT B slot 4 feeder unit motor has no signal.",
+   "1801230000020014": "AMS-HT B slot 4 filament odometer has no signal.",
+   "1801230000020015": "AMS-HT B slot 4 filament sensor status is abnormal.",
+   "1801230000020016": "AMS-HT B slot 4 assist motor has slipped.",
+   "1801230000020023": "AMS-HT B slot 4 the tube inside the AMS is broken.",
+   "1801600000020001": "The AMS-HT B Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1801610000020001": "The AMS-HT B Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1801620000020001": "The AMS-HT B Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1801630000020001": "The AMS-HT B Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1801900000010002": "AMS-HT B The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1801910000010002": "AMS-HT B The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1802200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS-HT C to the extruder is properly connected.",
+   "1802200000020012": "AMS-HT C slot 1 feeder unit motor is stalled.",
+   "1802200000020013": "AMS-HT C slot 1 feeder unit motor has no signal.",
+   "1802200000020014": "AMS-HT C slot 1 filament odometer has no signal.",
+   "1802200000020015": "AMS-HT C slot 1 filament sensor status is abnormal.",
+   "1802200000020016": "AMS-HT C slot 1 assist motor has slipped.",
+   "1802200000020023": "AMS-HT C slot 1 the tube inside the AMS is broken.",
+   "1802210000020012": "AMS-HT C slot 2 feeder unit motor is stalled.",
+   "1802210000020013": "AMS-HT C slot 2 feeder unit motor has no signal.",
+   "1802210000020014": "AMS-HT C slot 2 filament odometer has no signal.",
+   "1802210000020015": "AMS-HT C slot 2 filament sensor status is abnormal.",
+   "1802210000020016": "AMS-HT C slot 2 assist motor has slipped.",
+   "1802210000020023": "AMS-HT C slot 2 the tube inside the AMS is broken.",
+   "1802220000020012": "AMS-HT C slot 3 feeder unit motor is stalled.",
+   "1802220000020013": "AMS-HT C slot 3 feeder unit motor has no signal.",
+   "1802220000020014": "AMS-HT C slot 3 filament odometer has no signal.",
+   "1802220000020015": "AMS-HT C slot 3 filament sensor status is abnormal.",
+   "1802220000020016": "AMS-HT C slot 3 assist motor has slipped.",
+   "1802220000020023": "AMS-HT C slot 3 the tube inside the AMS is broken.",
+   "1802230000020012": "AMS-HT C slot 4 feeder unit motor is stalled.",
+   "1802230000020013": "AMS-HT C slot 4 feeder unit motor has no signal.",
+   "1802230000020014": "AMS-HT C slot 4 filament odometer has no signal.",
+   "1802230000020015": "AMS-HT C slot 4 filament sensor status is abnormal.",
+   "1802230000020016": "AMS-HT C slot 4 assist motor has slipped.",
+   "1802230000020023": "AMS-HT C slot 4 the tube inside the AMS is broken.",
+   "1802600000020001": "The AMS-HT C Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1802610000020001": "The AMS-HT C Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1802620000020001": "The AMS-HT C Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1802630000020001": "The AMS-HT C Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1802900000010002": "AMS-HT C The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1802910000010002": "AMS-HT C The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1803200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS-HT D to the extruder is properly connected.",
+   "1803200000020012": "AMS-HT D slot 1 feeder unit motor is stalled.",
+   "1803200000020013": "AMS-HT D slot 1 feeder unit motor has no signal.",
+   "1803200000020014": "AMS-HT D slot 1 filament odometer has no signal.",
+   "1803200000020015": "AMS-HT D slot 1 filament sensor status is abnormal.",
+   "1803200000020016": "AMS-HT D slot 1 assist motor has slipped.",
+   "1803200000020023": "AMS-HT D slot 1 the tube inside the AMS is broken.",
+   "1803210000020012": "AMS-HT D slot 2 feeder unit motor is stalled.",
+   "1803210000020013": "AMS-HT D slot 2 feeder unit motor has no signal.",
+   "1803210000020014": "AMS-HT D slot 2 filament odometer has no signal.",
+   "1803210000020015": "AMS-HT D slot 2 filament sensor status is abnormal.",
+   "1803210000020016": "AMS-HT D slot 2 assist motor has slipped.",
+   "1803210000020023": "AMS-HT D slot 2 the tube inside the AMS is broken.",
+   "1803220000020012": "AMS-HT D slot 3 feeder unit motor is stalled.",
+   "1803220000020013": "AMS-HT D slot 3 feeder unit motor has no signal.",
+   "1803220000020014": "AMS-HT D slot 3 filament odometer has no signal.",
+   "1803220000020015": "AMS-HT D slot 3 filament sensor status is abnormal.",
+   "1803220000020016": "AMS-HT D slot 3 assist motor has slipped.",
+   "1803220000020023": "AMS-HT D slot 3 the tube inside the AMS is broken.",
+   "1803230000020012": "AMS-HT D slot 4 feeder unit motor is stalled.",
+   "1803230000020013": "AMS-HT D slot 4 feeder unit motor has no signal.",
+   "1803230000020014": "AMS-HT D slot 4 filament odometer has no signal.",
+   "1803230000020015": "AMS-HT D slot 4 filament sensor status is abnormal.",
+   "1803230000020016": "AMS-HT D slot 4 assist motor has slipped.",
+   "1803230000020023": "AMS-HT D slot 4 the tube inside the AMS is broken.",
+   "1803600000020001": "The AMS-HT D Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1803610000020001": "The AMS-HT D Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1803620000020001": "The AMS-HT D Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1803630000020001": "The AMS-HT D Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1803900000010002": "AMS-HT D The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1803910000010002": "AMS-HT D The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1804200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS-HT E to the extruder is properly connected.",
+   "1804200000020012": "AMS-HT E slot 1 feeder unit motor is stalled.",
+   "1804200000020013": "AMS-HT E slot 1 feeder unit motor has no signal.",
+   "1804200000020014": "AMS-HT E slot 1 filament odometer has no signal.",
+   "1804200000020015": "AMS-HT E slot 1 filament sensor status is abnormal.",
+   "1804200000020016": "AMS-HT E slot 1 assist motor has slipped.",
+   "1804200000020023": "AMS-HT E slot 1 the tube inside the AMS is broken.",
+   "1804210000020012": "AMS-HT E slot 2 feeder unit motor is stalled.",
+   "1804210000020013": "AMS-HT E slot 2 feeder unit motor has no signal.",
+   "1804210000020014": "AMS-HT E slot 2 filament odometer has no signal.",
+   "1804210000020015": "AMS-HT E slot 2 filament sensor status is abnormal.",
+   "1804210000020016": "AMS-HT E slot 2 assist motor has slipped.",
+   "1804210000020023": "AMS-HT E slot 2 the tube inside the AMS is broken.",
+   "1804220000020012": "AMS-HT E slot 3 feeder unit motor is stalled.",
+   "1804220000020013": "AMS-HT E slot 3 feeder unit motor has no signal.",
+   "1804220000020014": "AMS-HT E slot 3 filament odometer has no signal.",
+   "1804220000020015": "AMS-HT E slot 3 filament sensor status is abnormal.",
+   "1804220000020016": "AMS-HT E slot 3 assist motor has slipped.",
+   "1804220000020023": "AMS-HT E slot 3 the tube inside the AMS is broken.",
+   "1804230000020012": "AMS-HT E slot 4 feeder unit motor is stalled.",
+   "1804230000020013": "AMS-HT E slot 4 feeder unit motor has no signal.",
+   "1804230000020014": "AMS-HT E slot 4 filament odometer has no signal.",
+   "1804230000020015": "AMS-HT E slot 4 filament sensor status is abnormal.",
+   "1804230000020016": "AMS-HT E slot 4 assist motor has slipped.",
+   "1804230000020023": "AMS-HT E slot 4 the tube inside the AMS is broken.",
+   "1804600000020001": "The AMS-HT E Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1804610000020001": "The AMS-HT E Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1804620000020001": "The AMS-HT E Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1804630000020001": "The AMS-HT E Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1804900000010002": "AMS-HT E The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1804910000010002": "AMS-HT E The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1805200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS-HT F to the extruder is properly connected.",
+   "1805200000020012": "AMS-HT F slot 1 feeder unit motor is stalled.",
+   "1805200000020013": "AMS-HT F slot 1 feeder unit motor has no signal.",
+   "1805200000020014": "AMS-HT F slot 1 filament odometer has no signal.",
+   "1805200000020015": "AMS-HT F slot 1 filament sensor status is abnormal.",
+   "1805200000020016": "AMS-HT F slot 1 assist motor has slipped.",
+   "1805200000020023": "AMS-HT F slot 1 the tube inside the AMS is broken.",
+   "1805210000020012": "AMS-HT F slot 2 feeder unit motor is stalled.",
+   "1805210000020013": "AMS-HT F slot 2 feeder unit motor has no signal.",
+   "1805210000020014": "AMS-HT F slot 2 filament odometer has no signal.",
+   "1805210000020015": "AMS-HT F slot 2 filament sensor status is abnormal.",
+   "1805210000020016": "AMS-HT F slot 2 assist motor has slipped.",
+   "1805210000020023": "AMS-HT F slot 2 the tube inside the AMS is broken.",
+   "1805220000020012": "AMS-HT F slot 3 feeder unit motor is stalled.",
+   "1805220000020013": "AMS-HT F slot 3 feeder unit motor has no signal.",
+   "1805220000020014": "AMS-HT F slot 3 filament odometer has no signal.",
+   "1805220000020015": "AMS-HT F slot 3 filament sensor status is abnormal.",
+   "1805220000020016": "AMS-HT F slot 3 assist motor has slipped.",
+   "1805220000020023": "AMS-HT F slot 3 the tube inside the AMS is broken.",
+   "1805230000020012": "AMS-HT F slot 4 feeder unit motor is stalled.",
+   "1805230000020013": "AMS-HT F slot 4 feeder unit motor has no signal.",
+   "1805230000020014": "AMS-HT F slot 4 filament odometer has no signal.",
+   "1805230000020015": "AMS-HT F slot 4 filament sensor status is abnormal.",
+   "1805230000020016": "AMS-HT F slot 4 assist motor has slipped.",
+   "1805230000020023": "AMS-HT F slot 4 the tube inside the AMS is broken.",
+   "1805600000020001": "The AMS-HT F Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1805610000020001": "The AMS-HT F Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1805620000020001": "The AMS-HT F Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1805630000020001": "The AMS-HT F Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1805900000010002": "AMS-HT F The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1805910000010002": "AMS-HT F The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1806200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS-HT G to the extruder is properly connected.",
+   "1806200000020012": "AMS-HT G slot 1 feeder unit motor is stalled.",
+   "1806200000020013": "AMS-HT G slot 1 feeder unit motor has no signal.",
+   "1806200000020014": "AMS-HT G slot 1 filament odometer has no signal.",
+   "1806200000020015": "AMS-HT G slot 1 filament sensor status is abnormal.",
+   "1806200000020016": "AMS-HT G slot 1 assist motor has slipped.",
+   "1806200000020023": "AMS-HT G slot 1 the tube inside the AMS is broken.",
+   "1806210000020012": "AMS-HT G slot 2 feeder unit motor is stalled.",
+   "1806210000020013": "AMS-HT G slot 2 feeder unit motor has no signal.",
+   "1806210000020014": "AMS-HT G slot 2 filament odometer has no signal.",
+   "1806210000020015": "AMS-HT G slot 2 filament sensor status is abnormal.",
+   "1806210000020016": "AMS-HT G slot 2 assist motor has slipped.",
+   "1806210000020023": "AMS-HT G slot 2 the tube inside the AMS is broken.",
+   "1806220000020012": "AMS-HT G slot 3 feeder unit motor is stalled.",
+   "1806220000020013": "AMS-HT G slot 3 feeder unit motor has no signal.",
+   "1806220000020014": "AMS-HT G slot 3 filament odometer has no signal.",
+   "1806220000020015": "AMS-HT G slot 3 filament sensor status is abnormal.",
+   "1806220000020016": "AMS-HT G slot 3 assist motor has slipped.",
+   "1806220000020023": "AMS-HT G slot 3 the tube inside the AMS is broken.",
+   "1806230000020012": "AMS-HT G slot 4 feeder unit motor is stalled.",
+   "1806230000020013": "AMS-HT G slot 4 feeder unit motor has no signal.",
+   "1806230000020014": "AMS-HT G slot 4 filament odometer has no signal.",
+   "1806230000020015": "AMS-HT G slot 4 filament sensor status is abnormal.",
+   "1806230000020016": "AMS-HT G slot 4 assist motor has slipped.",
+   "1806230000020023": "AMS-HT G slot 4 the tube inside the AMS is broken.",
+   "1806600000020001": "The AMS-HT G Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1806610000020001": "The AMS-HT G Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1806620000020001": "The AMS-HT G Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1806630000020001": "The AMS-HT G Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1806900000010002": "AMS-HT G The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1806910000010002": "AMS-HT G The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1807200000020006": "PTFE tube disconnection detected during filament loading. Please check if the PTFE tube from AMS-HT H to the extruder is properly connected.",
+   "1807200000020012": "AMS-HT H slot 1 feeder unit motor is stalled.",
+   "1807200000020013": "AMS-HT H slot 1 feeder unit motor has no signal.",
+   "1807200000020014": "AMS-HT H slot 1 filament odometer has no signal.",
+   "1807200000020015": "AMS-HT H slot 1 filament sensor status is abnormal.",
+   "1807200000020016": "AMS-HT H slot 1 assist motor has slipped.",
+   "1807200000020023": "AMS-HT H slot 1 the tube inside the AMS is broken.",
+   "1807210000020012": "AMS-HT H slot 2 feeder unit motor is stalled.",
+   "1807210000020013": "AMS-HT H slot 2 feeder unit motor has no signal.",
+   "1807210000020014": "AMS-HT H slot 2 filament odometer has no signal.",
+   "1807210000020015": "AMS-HT H slot 2 filament sensor status is abnormal.",
+   "1807210000020016": "AMS-HT H slot 2 assist motor has slipped.",
+   "1807210000020023": "AMS-HT H slot 2 the tube inside the AMS is broken.",
+   "1807220000020012": "AMS-HT H slot 3 feeder unit motor is stalled.",
+   "1807220000020013": "AMS-HT H slot 3 feeder unit motor has no signal.",
+   "1807220000020014": "AMS-HT H slot 3 filament odometer has no signal.",
+   "1807220000020015": "AMS-HT H slot 3 filament sensor status is abnormal.",
+   "1807220000020016": "AMS-HT H slot 3 assist motor has slipped.",
+   "1807220000020023": "AMS-HT H slot 3 the tube inside the AMS is broken.",
+   "1807230000020012": "AMS-HT H slot 4 feeder unit motor is stalled.",
+   "1807230000020013": "AMS-HT H slot 4 feeder unit motor has no signal.",
+   "1807230000020014": "AMS-HT H slot 4 filament odometer has no signal.",
+   "1807230000020015": "AMS-HT H slot 4 filament sensor status is abnormal.",
+   "1807230000020016": "AMS-HT H slot 4 assist motor has slipped.",
+   "1807230000020023": "AMS-HT H slot 4 the tube inside the AMS is broken.",
+   "1807600000020001": "The AMS-HT H Slot 1 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1807610000020001": "The AMS-HT H Slot 2 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1807620000020001": "The AMS-HT H Slot 3 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1807630000020001": "The AMS-HT H Slot 4 is overloaded. The filament may be tangled or the spool may be stuck.",
+   "1807900000010002": "AMS-HT H The coil of exhaust valve 1 is short-circuited, which may be due to abnormal wiring or damage.",
+   "1807910000010002": "AMS-HT H The coil of exhaust valve 2 is short-circuited, which may be due to abnormal wiring or damage.",
+   "18FE450000020002": "The filament cutter's cutting distance is too large. The XY motor may lose steps.",
+   "18FF200000020001": "External filament of right extruder has run out; please load a new filament.",
+   "18FF200000020002": "No filament was detected in the right extruder from the external spool; please load the new filament.",
+   "18FF200000020004": "Please pull the external filament from the right extruder.",
+   "18FF450000020002": "The filament cutter's cutting distance is too large. The XY motor may lose steps.",
+   "18FF600000020001": "External spool connected to right extruder may be tangled or jammed."
+  },
+  "20P": {
+   "0300020000010001": "The right nozzle temperature is abnormal; the heater may have a short circuit.",
+   "0300020000010002": "The right nozzle temperature is abnormal; the heater may have an open circuit.",
+   "0300020000010003": "The right nozzle temperature is abnormal; the heater is overheated.",
+   "0300020000010008": "The right extruder nozzle temperature is abnormal and cannot reach the set value. This may be caused by an improperly installed nozzle silicone sock.",
+   "0300020000010009": "The right nozzle temperature control is abnormal. The hot end may not be installed. To heat the heating assembly without the hotend, enable Maintenance Mode.",
+   "0300030000010001": "The right hotend cooling fan speed is too slow or stopped. The fan may be stuck or the connector may not be plugged in properly.",
+   "0300030000020002": "The right hotend cooling fan speed is slow. It may be stuck and need cleaning.",
+   "03001E0000010009": "The left nozzle temperature control is abnormal; the hot end may not be installed. If you want to heat the hot end without it being installed, please turn on maintenance mode.",
+   "0300250000010001": "Low signal frequency detected on the right extruder eddy current sensor. The nozzle may not be installed, or the nozzle heat sink may be too far away from the sensor.",
+   "0300250000010002": "Low sensitivity detected in the right extruder eddy current sensor. Check the nozzle installation.",
+   "0300250000010003": "Unable to read data from the right extruder eddy current sensor; there may be a communication break or sensor damage.",
+   "0300250000010004": "The eddy current sensor signal of the right extruder is abnormal. The sensor may be damaged, or the MC-TH communication may be abnormal.",
+   "0300250000010007": "High signal frequency detected on the right extruder eddy current sensor. The sensor may be damaged, or the nozzle heat sink may be too close to the sensor.",
+   "0300250000010008": "The right nozzle touches the heating bed abnormally. Please check whether there is filament residue on the nozzle, foreign matter at the point where the nozzle contacts the bed, or severe deformation of the flow blocker.",
+   "0300260000010001": "Low signal frequency detected on the left extruder eddy current sensor. The sensor may be installed too far, or the sensor may be loose.",
+   "0300260000010002": "Left extruder eddy current sensor sensitivity is low. Check sensor installation.",
+   "0300260000010003": "Unable to read data from the left extruder eddy current sensor; there may be a communication break or sensor damage.",
+   "0300260000010004": "The eddy current sensor signal of the left extruder is abnormal. The sensor may be damaged, or the MC-TH communication may be abnormal.",
+   "0300260000010007": "High signal frequency detected on the left extruder eddy current sensor. The sensor may be installed too close, or the sensor may be loose.",
+   "0300260000010008": "The left nozzle touches the heating bed abnormally. Please check whether there is filament residue on the nozzle, foreign matter at the point where the nozzle contacts the bed, or severe deformation of the flow blocker.",
+   "0300320000010001": "The Left Side (Auxiliary Component Cooling-Chamber heating) Fan speed is too slow or stopped. It may be stuck, or the connector may not be plugged in properly.",
+   "0300320000020002": "The Left Side (Auxiliary Component Cooling-Chamber heating) Fan speed is slow. It may be stuck and need cleaning.",
+   "0300910000010007": "The chamber heater control is abnormal. The left chamber heater vent is likely blocked, activating its over-temperature protection.",
+   "0300910000010008": "The chamber heater 1 failed to reach the target temperature.",
+   "0300920000010008": "The chamber heater 2 failed to reach the target temperature.",
+   "0300C00000010001": "The chamber temperature switching flap operation failed. It may get stuck.",
+   "0300C30000010001": "The current sensor for the chamber temperature switching flap is abnormal, which may be due to a loose flap wiring harness or a fault in the hardware sampling circuit.",
+   "050001000001000A": "",
+   "0500010000030004": "Not enough space on the USB flash drive. Video recording and timelapse recording cannot be performed. Please clear some space.",
+   "0500010000030007": "No USB drive detected. The liveview camera cannot record a time-lapse.",
+   "0500030000010025": "The current firmware version is abnormal and printing cannot start. Please ensure the printer is connected to the network and go to the “Firmware” page to update.",
+   "0500050000010021": "Time-lapse kit communication error. Please reconnect the cable or restart the printer.",
+   "0500050000020023": "",
+   "070020000002000A": "Failed to adjust the buffer position. The AMS A Slot 1 filament or the buffer itself may be jammed.",
+   "0700200000020025": "AMS A slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070021000002000A": "Failed to adjust the buffer position. The AMS A Slot 2 filament or the buffer itself may be jammed.",
+   "0700210000020025": "AMS A slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070022000002000A": "Failed to adjust the buffer position. The AMS A Slot 3 filament or the buffer itself may be jammed.",
+   "0700220000020025": "AMS A slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070023000002000A": "Failed to adjust the buffer position. The AMS A Slot 4 filament or the buffer itself may be jammed.",
+   "0700230000020025": "AMS A slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "0700550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "0700800000010001": "AMS A Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0700810000010001": "AMS A Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0700940000010001": "AMS A The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0700950000010001": "AMS A The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "070120000002000A": "Failed to adjust the buffer position. The AMS B Slot 1 filament or the buffer itself may be jammed.",
+   "0701200000020025": "AMS B slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070121000002000A": "Failed to adjust the buffer position. The AMS B Slot 2 filament or the buffer itself may be jammed.",
+   "0701210000020025": "AMS B slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070122000002000A": "Failed to adjust the buffer position. The AMS B Slot 3 filament or the buffer itself may be jammed.",
+   "0701220000020025": "AMS B slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070123000002000A": "Failed to adjust the buffer position. The AMS B Slot 4 filament or the buffer itself may be jammed.",
+   "0701230000020025": "AMS B slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "0701550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "0701800000010001": "AMS B Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0701810000010001": "AMS B Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0701940000010001": "AMS B The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0701950000010001": "AMS B The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "070220000002000A": "Failed to adjust the buffer position. The AMS C Slot 1 filament or the buffer itself may be jammed.",
+   "0702200000020025": "AMS C slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070221000002000A": "Failed to adjust the buffer position. The AMS C Slot 2 filament or the buffer itself may be jammed.",
+   "0702210000020025": "AMS C slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070222000002000A": "Failed to adjust the buffer position. The AMS C Slot 3 filament or the buffer itself may be jammed.",
+   "0702220000020025": "AMS C slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070223000002000A": "Failed to adjust the buffer position. The AMS C Slot 4 filament or the buffer itself may be jammed.",
+   "0702230000020025": "AMS C slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "0702550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "0702800000010001": "AMS C Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0702810000010001": "AMS C Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0702940000010001": "AMS C The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0702950000010001": "AMS C The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "070320000002000A": "Failed to adjust the buffer position. The AMS D Slot 1 filament or the buffer itself may be jammed.",
+   "0703200000020025": "AMS D slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070321000002000A": "Failed to adjust the buffer position. The AMS D Slot 2 filament or the buffer itself may be jammed.",
+   "0703210000020025": "AMS D slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070322000002000A": "Failed to adjust the buffer position. The AMS D Slot 3 filament or the buffer itself may be jammed.",
+   "0703220000020025": "AMS D slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070323000002000A": "Failed to adjust the buffer position. The AMS D Slot 4 filament or the buffer itself may be jammed.",
+   "0703230000020025": "AMS D slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "0703550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "0703800000010001": "AMS D Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0703810000010001": "AMS D Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0703940000010001": "AMS D The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0703950000010001": "AMS D The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "070420000002000A": "Failed to adjust the buffer position. The AMS E Slot 1 filament or the buffer itself may be jammed.",
+   "0704200000020025": "AMS E slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070421000002000A": "Failed to adjust the buffer position. The AMS E Slot 2 filament or the buffer itself may be jammed.",
+   "0704210000020025": "AMS E slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070422000002000A": "Failed to adjust the buffer position. The AMS E Slot 3 filament or the buffer itself may be jammed.",
+   "0704220000020025": "AMS E slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070423000002000A": "Failed to adjust the buffer position. The AMS E Slot 4 filament or the buffer itself may be jammed.",
+   "0704230000020025": "AMS E slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "0704550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "0704800000010001": "AMS E Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0704810000010001": "AMS E Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0704940000010001": "AMS E The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0704950000010001": "AMS E The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "070520000002000A": "Failed to adjust the buffer position. The AMS F Slot 1 filament or the buffer itself may be jammed.",
+   "0705200000020025": "AMS F slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070521000002000A": "Failed to adjust the buffer position. The AMS F Slot 2 filament or the buffer itself may be jammed.",
+   "0705210000020025": "AMS F slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070522000002000A": "Failed to adjust the buffer position. The AMS F Slot 3 filament or the buffer itself may be jammed.",
+   "0705220000020025": "AMS F slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070523000002000A": "Failed to adjust the buffer position. The AMS F Slot 4 filament or the buffer itself may be jammed.",
+   "0705230000020025": "AMS F slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "0705550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "0705800000010001": "AMS F Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0705810000010001": "AMS F Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0705940000010001": "AMS F The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0705950000010001": "AMS F The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "070620000002000A": "Failed to adjust the buffer position. The AMS G Slot 1 filament or the buffer itself may be jammed.",
+   "0706200000020025": "AMS G slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070621000002000A": "Failed to adjust the buffer position. The AMS G Slot 2 filament or the buffer itself may be jammed.",
+   "0706210000020025": "AMS G slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070622000002000A": "Failed to adjust the buffer position. The AMS G Slot 3 filament or the buffer itself may be jammed.",
+   "0706220000020025": "AMS G slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070623000002000A": "Failed to adjust the buffer position. The AMS G Slot 4 filament or the buffer itself may be jammed.",
+   "0706230000020025": "AMS G slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "0706550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "0706800000010001": "AMS G Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0706810000010001": "AMS G Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0706940000010001": "AMS G The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0706950000010001": "AMS G The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "070720000002000A": "Failed to adjust the buffer position. The AMS H Slot 1 filament or the buffer itself may be jammed.",
+   "0707200000020025": "AMS H slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070721000002000A": "Failed to adjust the buffer position. The AMS H Slot 2 filament or the buffer itself may be jammed.",
+   "0707210000020025": "AMS H slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070722000002000A": "Failed to adjust the buffer position. The AMS H Slot 3 filament or the buffer itself may be jammed.",
+   "0707220000020025": "AMS H slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "070723000002000A": "Failed to adjust the buffer position. The AMS H Slot 4 filament or the buffer itself may be jammed.",
+   "0707230000020025": "AMS H slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "0707550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "0707800000010001": "AMS H Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0707810000010001": "AMS H Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0707940000010001": "AMS H The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0707950000010001": "AMS H The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "07FE800000010001": "Extruder switching abnormal. Please check whether the flow blocker is stuck or if filament is jammed inside the toolhead.",
+   "07FE800000010002": "The Hall sensor for detecting the extruder switching angle is open-circuited; please check if the hall sensor is malfunctioning.",
+   "07FE800000010003": "The hall signal for detecting the extruder switching angle is abnormal, possibly due to internal communication failure in the Toolhead module.",
+   "07FE800000010004": "The hall sensor for detecting the extruder switching angle is short-circuited; please check if the hall sensor is malfunctioning.",
+   "07FE800000020002": "Extruder switch lever failed to hit the rod, possibly due to the XY motor losing step. Please check for any obstructions blocking the toolhead movement, such as a PTFE tube.",
+   "07FF200000020001": "External filament of right (Aux) extruder has run out; please load a new filament.",
+   "07FF200000020002": "No filament was detected in the right (Aux) extruder from the external spool; please load the new filament.",
+   "07FF200000020004": "Please pull the external filament from the right (Aux) extruder.",
+   "07FF600000020001": "External spool connected to right (Aux) extruder may be tangled or jammed.",
+   "07FF700000020003": "Please check if the material is coming out of the right (Aux) nozzle. If not, gently push the material and try to extrude again.",
+   "07FF800000010001": "Extruder switching abnormal. Please check whether the flow blocker is stuck or if filament is jammed inside the toolhead.",
+   "07FF800000010002": "The Hall sensor for detecting the extruder switching angle is open-circuited; please check if the hall sensor is malfunctioning.",
+   "07FF800000010003": "The hall signal for detecting the extruder switching angle is abnormal, possibly due to internal communication failure in the Toolhead module.",
+   "07FF800000010004": "The hall sensor for detecting the extruder switching angle is short-circuited; please check if the hall sensor is malfunctioning.",
+   "07FF800000020002": "Extruder switch lever failed to hit the rod, possibly due to the XY motor losing step. Please check for any obstructions blocking the toolhead movement, such as a PTFE tube.",
+   "07FFA00000020001": "The right (Aux) nozzle cold pull process has timed out. Please click \"Retry\" and then manually pull out the filament.",
+   "0C00010000010018": "",
+   "0C00030000020012": "Foreign object detection is not working. The Live View Camera needs calibration. Please tap \"Settings > Calibration\" on the printer screen.",
+   "0C00030000020014": "Foreign object detection accuracy has decreased. If this occurs frequently, perform a Live View Camera calibration (“Settings” > “Calibration” on the printer screen).",
+   "0C00030000020015": "Foreign object detection is not working. Detected the Live View Camera has been replaced. Please tap \"Settings>Calibration\" on printer screen and re-calibrate the Live View Camera.",
+   "0C00030000030008": "A possible spaghetti failure has been detected. Please check the print quality and decide whether to stop the job. Cleaning the build plate or drying the filament can effectively reduce the risk of spaghetti failure.",
+   "0C0003000003001A": "",
+   "0C00040000010014": "Cutting Protection Base not detected, which may lead to heatbed damage. Please place it and continue.",
+   "180020000002000A": "Failed to adjust the buffer position. The AMS-HT A Slot 1 filament or the buffer itself may be jammed.",
+   "1800200000020025": "AMS-HT A slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180021000002000A": "Failed to adjust the buffer position. The AMS-HT A Slot 2 filament or the buffer itself may be jammed.",
+   "1800210000020025": "AMS-HT A slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180022000002000A": "Failed to adjust the buffer position. The AMS-HT A Slot 3 filament or the buffer itself may be jammed.",
+   "1800220000020025": "AMS-HT A slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180023000002000A": "Failed to adjust the buffer position. The AMS-HT A Slot 4 filament or the buffer itself may be jammed.",
+   "1800230000020025": "AMS-HT A slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "1800550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "1800800000010001": "AMS-HT A Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1800810000010001": "AMS-HT A Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1800940000010001": "AMS-HT A The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1800950000010001": "AMS-HT A The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "180120000002000A": "Failed to adjust the buffer position. The AMS-HT B Slot 1 filament or the buffer itself may be jammed.",
+   "1801200000020025": "AMS-HT B slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180121000002000A": "Failed to adjust the buffer position. The AMS-HT B Slot 2 filament or the buffer itself may be jammed.",
+   "1801210000020025": "AMS-HT B slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180122000002000A": "Failed to adjust the buffer position. The AMS-HT B Slot 3 filament or the buffer itself may be jammed.",
+   "1801220000020025": "AMS-HT B slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180123000002000A": "Failed to adjust the buffer position. The AMS-HT B Slot 4 filament or the buffer itself may be jammed.",
+   "1801230000020025": "AMS-HT B slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "1801550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "1801800000010001": "AMS-HT B Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1801810000010001": "AMS-HT B Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1801940000010001": "AMS-HT B The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1801950000010001": "AMS-HT B The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "180220000002000A": "Failed to adjust the buffer position. The AMS-HT C Slot 1 filament or the buffer itself may be jammed.",
+   "1802200000020025": "AMS-HT C slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180221000002000A": "Failed to adjust the buffer position. The AMS-HT C Slot 2 filament or the buffer itself may be jammed.",
+   "1802210000020025": "AMS-HT C slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180222000002000A": "Failed to adjust the buffer position. The AMS-HT C Slot 3 filament or the buffer itself may be jammed.",
+   "1802220000020025": "AMS-HT C slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180223000002000A": "Failed to adjust the buffer position. The AMS-HT C Slot 4 filament or the buffer itself may be jammed.",
+   "1802230000020025": "AMS-HT C slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "1802550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "1802800000010001": "AMS-HT C Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1802810000010001": "AMS-HT C Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1802940000010001": "AMS-HT C The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1802950000010001": "AMS-HT C The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "180320000002000A": "Failed to adjust the buffer position. The AMS-HT D Slot 1 filament or the buffer itself may be jammed.",
+   "1803200000020025": "AMS-HT D slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180321000002000A": "Failed to adjust the buffer position. The AMS-HT D Slot 2 filament or the buffer itself may be jammed.",
+   "1803210000020025": "AMS-HT D slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180322000002000A": "Failed to adjust the buffer position. The AMS-HT D Slot 3 filament or the buffer itself may be jammed.",
+   "1803220000020025": "AMS-HT D slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180323000002000A": "Failed to adjust the buffer position. The AMS-HT D Slot 4 filament or the buffer itself may be jammed.",
+   "1803230000020025": "AMS-HT D slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "1803550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "1803800000010001": "AMS-HT D Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1803810000010001": "AMS-HT D Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1803940000010001": "AMS-HT D The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1803950000010001": "AMS-HT D The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "180420000002000A": "Failed to adjust the buffer position. The AMS-HT E Slot 1 filament or the buffer itself may be jammed.",
+   "1804200000020025": "AMS-HT E slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180421000002000A": "Failed to adjust the buffer position. The AMS-HT E Slot 2 filament or the buffer itself may be jammed.",
+   "1804210000020025": "AMS-HT E slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180422000002000A": "Failed to adjust the buffer position. The AMS-HT E Slot 3 filament or the buffer itself may be jammed.",
+   "1804220000020025": "AMS-HT E slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180423000002000A": "Failed to adjust the buffer position. The AMS-HT E Slot 4 filament or the buffer itself may be jammed.",
+   "1804230000020025": "AMS-HT E slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "1804550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "1804800000010001": "AMS-HT E Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1804810000010001": "AMS-HT E Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1804940000010001": "AMS-HT E The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1804950000010001": "AMS-HT E The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "180520000002000A": "Failed to adjust the buffer position. The AMS-HT F Slot 1 filament or the buffer itself may be jammed.",
+   "1805200000020025": "AMS-HT F slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180521000002000A": "Failed to adjust the buffer position. The AMS-HT F Slot 2 filament or the buffer itself may be jammed.",
+   "1805210000020025": "AMS-HT F slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180522000002000A": "Failed to adjust the buffer position. The AMS-HT F Slot 3 filament or the buffer itself may be jammed.",
+   "1805220000020025": "AMS-HT F slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180523000002000A": "Failed to adjust the buffer position. The AMS-HT F Slot 4 filament or the buffer itself may be jammed.",
+   "1805230000020025": "AMS-HT F slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "1805550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "1805800000010001": "AMS-HT F Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1805810000010001": "AMS-HT F Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1805940000010001": "AMS-HT F The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1805950000010001": "AMS-HT F The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "180620000002000A": "Failed to adjust the buffer position. The AMS-HT G Slot 1 filament or the buffer itself may be jammed.",
+   "1806200000020025": "AMS-HT G slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180621000002000A": "Failed to adjust the buffer position. The AMS-HT G Slot 2 filament or the buffer itself may be jammed.",
+   "1806210000020025": "AMS-HT G slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180622000002000A": "Failed to adjust the buffer position. The AMS-HT G Slot 3 filament or the buffer itself may be jammed.",
+   "1806220000020025": "AMS-HT G slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180623000002000A": "Failed to adjust the buffer position. The AMS-HT G Slot 4 filament or the buffer itself may be jammed.",
+   "1806230000020025": "AMS-HT G slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "1806550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "1806800000010001": "AMS-HT G Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1806810000010001": "AMS-HT G Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1806940000010001": "AMS-HT G The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1806950000010001": "AMS-HT G The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "180720000002000A": "Failed to adjust the buffer position. The AMS-HT H Slot 1 filament or the buffer itself may be jammed.",
+   "1807200000020025": "AMS-HT H slot 1 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180721000002000A": "Failed to adjust the buffer position. The AMS-HT H Slot 2 filament or the buffer itself may be jammed.",
+   "1807210000020025": "AMS-HT H slot 2 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180722000002000A": "Failed to adjust the buffer position. The AMS-HT H Slot 3 filament or the buffer itself may be jammed.",
+   "1807220000020025": "AMS-HT H slot 3 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "180723000002000A": "Failed to adjust the buffer position. The AMS-HT H Slot 4 filament or the buffer itself may be jammed.",
+   "1807230000020025": "AMS-HT H slot 4 feed resistance is too high. Please reduce spool rotation resistance and avoid over-bent or over-long filament tubes.",
+   "1807550000010002": "The PTFE tube connection between the toolhead and buffer is incorrect. Please ensure the upper port of the buffer is connected to the left extruder, and the lower port is connected to the right extruder.",
+   "1807800000010001": "AMS-HT H Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1807810000010001": "AMS-HT H Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1807940000010001": "AMS-HT H The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1807950000010001": "AMS-HT H The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "18FE800000010001": "Extruder switching abnormal. Please check whether the flow blocker is stuck or if filament is jammed inside the toolhead.",
+   "18FE800000010002": "The Hall sensor for detecting the extruder switching angle is open-circuited; please check if the hall sensor is malfunctioning.",
+   "18FE800000010003": "The hall signal for detecting the extruder switching angle is abnormal, possibly due to internal communication failure in the Toolhead module.",
+   "18FE800000010004": "The hall sensor for detecting the extruder switching angle is short-circuited; please check if the hall sensor is malfunctioning.",
+   "18FE800000020002": "Extruder switch lever failed to hit the rod, possibly due to the XY motor losing step. Please check for any obstructions blocking the toolhead movement, such as a PTFE tube.",
+   "18FF200000020001": "External filament of right (Aux) extruder has run out; please load a new filament.",
+   "18FF200000020002": "No filament was detected in the right (Aux) extruder from the external spool; please load the new filament.",
+   "18FF200000020004": "Please pull the external filament from the right (Aux) extruder.",
+   "18FF600000020001": "External spool connected to right (Aux) extruder may be tangled or jammed.",
+   "18FF700000020003": "Please check if the material is coming out of the right (Aux) nozzle. If not, gently push the material and try to extrude again.",
+   "18FF800000010001": "Extruder switching abnormal. Please check whether the flow blocker is stuck or if filament is jammed inside the toolhead.",
+   "18FF800000010002": "The Hall sensor for detecting the extruder switching angle is open-circuited; please check if the hall sensor is malfunctioning.",
+   "18FF800000010003": "The hall signal for detecting the extruder switching angle is abnormal, possibly due to internal communication failure in the Toolhead module.",
+   "18FF800000010004": "The hall sensor for detecting the extruder switching angle is short-circuited; please check if the hall sensor is malfunctioning.",
+   "18FF800000020002": "Extruder switch lever failed to hit the rod, possibly due to the XY motor losing step. Please check for any obstructions blocking the toolhead movement, such as a PTFE tube."
+  },
+  "22E": {
+   "0300020000010006": "The nozzle temperature is abnormal; the sensor may have a short circuit. Please check whether the connector is properly plugged in.",
+   "0300020000010007": "The nozzle temperature is abnormal; the sensor may have an open circuit.",
+   "0300320000010001": "The Right Side (Auxiliary Component Cooling-Filtration) Fan speed is too slow or stopped. It may be stuck, or the connector may not be plugged in properly.",
+   "03003A0000010001": "The Left Side Auxiliary Component Cooling Fan speed is slow. It may be stuck and need cleaning.",
+   "03003A0000020002": "The Left Side Auxiliary Component Cooling Fan speed is slow. It may be stuck and need cleaning.",
+   "050001000001000A": "",
+   "0C00010000010018": "",
+   "0C00030000020012": "Foreign object detection is not working. The Live View Camera needs calibration. Please tap \"Settings > Calibration\" on the printer screen.",
+   "0C00030000020014": "Foreign object detection accuracy has decreased. If this occurs frequently, perform a Live View Camera calibration (“Settings” > “Calibration” on the printer screen).",
+   "0C00030000020015": "Foreign object detection is not working. Detected the Live View Camera has been replaced. Please tap \"Settings>Calibration\" on printer screen and re-calibrate the Live View Camera.",
+   "0C0003000003001A": "Foreign object detection failed, possibly due to manual interruption (e.g., clicking \"Stop\"). If not, try restarting the printer or updating the firmware."
+  },
+  "239": {
+   "0300020000010001": "The right nozzle temperature is abnormal; the heater may have a short circuit.",
+   "0300020000010002": "The right nozzle temperature is abnormal; the heater may have an open circuit.",
+   "0300020000010003": "The right nozzle temperature is abnormal; the heater is overheated.",
+   "0300020000010009": "The right nozzle temperature control is abnormal. The hot end may not be installed. To heat the heating assembly without the hotend, enable Maintenance Mode in the settings.",
+   "0300180000030009": "This is the printer's first print with a high heatbed temperature. In order to obtain better first layer printing quality, high temperature bed leveling is being automatically performed.",
+   "03002D0000030009": "This is the printer's first print with a high heatbed temperature. In order to obtain better first layer printing quality, high temperature bed leveling is being automatically performed.",
+   "0300910000010008": "Chamber heater 1 failed to rise to target temperature.",
+   "0300D10000010002": "Fire extinguisher motor has a short-circuit. It may have failed.",
+   "0300D30000020001": "Fire extinguisher not detected.",
+   "0300D40000010002": "Fire Extinguisher Cylinder presence detection sensor is disconnected. Please check the sensor connection.",
+   "0500030000020055": "User information has expired, please rebind the device.",
+   "07FF200000020001": "External filament of right extruder has run out; please load a new filament.",
+   "07FF200000020002": "No filament was detected in the right extruder from the external spool; please load the new filament.",
+   "07FF200000020004": "Please pull the external filament from the right extruder.",
+   "07FF600000020001": "External spool connected to right extruder may be tangled or jammed.",
+   "07FF700000020003": "Please check if the material is coming out of the right nozzle. If not, gently push the material and try to extrude again.",
+   "07FFA00000020001": "The right nozzle cold pull process has timed out. Please click \"Retry\" and then manually pull out the filament.",
+   "0C00010000010018": "The BirdsEye Camera is malfunctioning. Please try restarting the device. If the issue persists after multiple restarts, check the camera connection status or contact customer support.",
+   "18FF200000020001": "External filament of right extruder has run out; please load a new filament.",
+   "18FF200000020002": "No filament was detected in the right extruder from the external spool; please load the new filament.",
+   "18FF200000020004": "Please pull the external filament from the right extruder.",
+   "18FF600000020001": "External spool connected to right extruder may be tangled or jammed."
+  },
+  "26A": {
+   "0300020000010006": "The nozzle temperature is abnormal; the sensor may have a short circuit. Please check whether the connector is properly plugged in.",
+   "0300020000010007": "The nozzle temperature is abnormal; the sensor may have an open circuit.",
+   "0300110000020002": "The resonance frequency of the Y-axis differs greatly from the last calibration. Please clean the Y-axis liner rail and conduct a calibration after printing.",
+   "0300180000010001": "The value of extrusion force sensor is low, the nozzle seems to not be installed.",
+   "0300180000010004": "The data from extrusion force sensor is abnormal; the sensor is probably broken.",
+   "0300180000010005": "The Z axis motor got stuck during movement; please check if there is any foreign matter on the Z sliders or Z timing belt wheels, or if the extrusion force sensor is abnormal.",
+   "03001A0000020001": "A nozzle clumping has been detected. Please refer to the Wiki for troubleshooting.",
+   "0300200000010002": "Y axis homing abnormal: Please check if the heatbed is stuck or if the motion resistance is too high.",
+   "0300A10000010001": "The ambient temperature is too high; please lower it.",
+   "0500010000030004": "Not enough space on MicroSD Card; please clear some space.",
+   "0500010000030005": "The Micro SD card is in Read-Only mode. Video recording and Timelapse recording cannot be performed. Please refer to the Wiki for assistance.",
+   "0500010000030006": "Unformatted MicroSD Card: please format it.",
+   "0500010000030007": "Unable to record time-lapse photography without MicroSD card inserted.",
+   "0500030000010025": "The current firmware version is abnormal and printing cannot start. Please ensure the printer is connected to the network and go to the “Firmware” page to update.",
+   "0500030000020020": "Micro SD Card capacity is insufficient to cache print files.",
+   "0500050000010021": "Time-lapse kit communication error. Please reconnect the cable or restart the printer.",
+   "0700700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "0700800000010001": "AMS A Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0700810000010001": "AMS A Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0700940000010001": "AMS A The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0700950000010001": "AMS A The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "0701700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "0701800000010001": "AMS B Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0701810000010001": "AMS B Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0701940000010001": "AMS B The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0701950000010001": "AMS B The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "0702700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "0702800000010001": "AMS C Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0702810000010001": "AMS C Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0702940000010001": "AMS C The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0702950000010001": "AMS C The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "0703700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "0703800000010001": "AMS D Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0703810000010001": "AMS D Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0703940000010001": "AMS D The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0703950000010001": "AMS D The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "0704700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "0704800000010001": "AMS E Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0704810000010001": "AMS E Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0704940000010001": "AMS E The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0704950000010001": "AMS E The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "0705700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "0705800000010001": "AMS F Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0705810000010001": "AMS F Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0705940000010001": "AMS F The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0705950000010001": "AMS F The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "0706700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "0706800000010001": "AMS G Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0706810000010001": "AMS G Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0706940000010001": "AMS G The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0706950000010001": "AMS G The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "0707700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "0707800000010001": "AMS H Heater 1, heater malfunction or abnormal current sensor detected.",
+   "0707810000010001": "AMS H Heater 2, heater malfunction or abnormal current sensor detected.",
+   "0707940000010001": "AMS H The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "0707950000010001": "AMS H The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "1800700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "1800800000010001": "AMS-HT A Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1800810000010001": "AMS-HT A Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1800940000010001": "AMS-HT A The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1800950000010001": "AMS-HT A The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "1801700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "1801800000010001": "AMS-HT B Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1801810000010001": "AMS-HT B Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1801940000010001": "AMS-HT B The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1801950000010001": "AMS-HT B The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "1802700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "1802800000010001": "AMS-HT C Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1802810000010001": "AMS-HT C Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1802940000010001": "AMS-HT C The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1802950000010001": "AMS-HT C The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "1803700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "1803800000010001": "AMS-HT D Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1803810000010001": "AMS-HT D Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1803940000010001": "AMS-HT D The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1803950000010001": "AMS-HT D The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "1804700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "1804800000010001": "AMS-HT E Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1804810000010001": "AMS-HT E Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1804940000010001": "AMS-HT E The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1804950000010001": "AMS-HT E The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "1805700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "1805800000010001": "AMS-HT F Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1805810000010001": "AMS-HT F Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1805940000010001": "AMS-HT F The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1805950000010001": "AMS-HT F The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "1806700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "1806800000010001": "AMS-HT G Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1806810000010001": "AMS-HT G Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1806940000010001": "AMS-HT G The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1806950000010001": "AMS-HT G The temperature sensor of heater 2 is offline, which may be due to poor connector contact.",
+   "1807700000020002": "Failed to feed the filament into the toolhead. Possible cause: filament or spool stuck.",
+   "1807800000010001": "AMS-HT H Heater 1, heater malfunction or abnormal current sensor detected.",
+   "1807810000010001": "AMS-HT H Heater 2, heater malfunction or abnormal current sensor detected.",
+   "1807940000010001": "AMS-HT H The temperature sensor of heater 1 is offline, which may be due to poor connector contact.",
+   "1807950000010001": "AMS-HT H The temperature sensor of heater 2 is offline, which may be due to poor connector contact."
+  },
+  "31B": {
+   "0300020000010001": "Right extruder nozzle temperature error. The heater coil may be short-circuited. Please check if the insulation layer is intact.",
+   "0300020000010002": "Right extruder nozzle temperature error. The heater coil may be open-circuited. Please check if the connector is loose.",
+   "0300020000010003": "Right extruder nozzle temperature error. The heater core may be overheating.",
+   "0300020000010008": "The right nozzle temperature control is abnormal and is unable to heat up to the preset temperature.",
+   "0300020000010009": "The right nozzle temperature control is abnormal. The hotend is not installed.",
+   "030025000001000C": "An anomalous jump in the right extruder extrusion force sensor data has been detected, potentially caused by poor sensor contact or a faulty sensor.",
+   "030026000001000C": "An anomalous jump in the left extruder extrusion force sensor data has been detected, potentially caused by poor sensor contact or a faulty sensor.",
+   "050001000001000A": "",
+   "0501040000030004": "Please clean and lubricate the X-axis linear rail, YZ-axis linear rods, and the extruder lift linear rail.",
+   "07FF200000020001": "External filament of right extruder has run out; please load a new filament.",
+   "07FF200000020002": "No filament was detected in the right extruder from the external spool; please load the new filament.",
+   "07FF200000020004": "Please pull the external filament from the right extruder.",
+   "07FF600000020001": "External spool connected to right extruder may be tangled or jammed.",
+   "07FF700000020003": "Please check if the material is coming out of the right nozzle. If not, gently push the material and try to extrude again.",
+   "07FFA00000020001": "The right nozzle cold pull process has timed out. Please click \"Retry\" and then manually pull out the filament.",
+   "0C00010000010018": "",
+   "0C00020000010001": "The height measuring laser is not lit. Please check if it is blocked or if the connection cable is disconnected.",
+   "0C0003000003001A": "",
+   "18FF200000020001": "External filament of right extruder has run out; please load a new filament.",
+   "18FF200000020002": "No filament was detected in the right extruder from the external spool; please load the new filament.",
+   "18FF200000020004": "Please pull the external filament from the right extruder.",
+   "18FF600000020001": "External spool connected to right extruder may be tangled or jammed."
+  }
+ },
+ "error": {
+  "03004000": "Z axis homing failed; the task has been stopped.",
+  "03004001": "The printer timed out waiting for the nozzle to cool down before homing.",
+  "03004002": "Auto Bed Leveling failed; the task has been stopped.",
+  "03004005": "The hotend cooling fan speed is abnormal.",
+  "03004006": "The nozzle is clogged.",
+  "03004008": "The AMS failed to change filament.",
+  "03004009": "Homing XY axis failed.",
+  "0300400A": "Mechanical resonance frequency identification failed.",
+  "0300400B": "Internal communication exception",
+  "0300400C": "The task was canceled.",
+  "0300400D": "Resume failed after power loss.",
+  "0300400E": "The motor self-check failed.",
+  "0300400F": "The power supply voltage does not match the printer.",
+  "03004010": "Nozzle offset calibration failed.",
+  "03004011": "Flow Dynamics Calibration failed; please reinitiate printing or calibration.",
+  "03004013": "Printing cannot be initiated while AMS is drying.",
+  "03004014": "Homing Z axis failed: temperature control abnormality.",
+  "03004015": "Nozzle clumping detection calibration failed. Please go to \"Assistant\" for troubleshooting.",
+  "03004016": "Nozzle cleaning failed. Please click the Assistant for troubleshooting.",
+  "0300401F": "The right hotend is not installed, and the toolhead cannot perform homing. Please install the hotend and then continue.",
+  "03004020": "The nozzle presence detection failed. Please check the Assistant for details.",
+  "03004021": "Nozzle offset calibration sensor signal abnormality detected. Please check the sensor and retry.",
+  "03004030": "Z-axis homing failed. Remove the heated bed slider fixing screws and ensure no obstructions are blocking the heated bed's vertical movement. After resolving the issue, recalibrate on the device.",
+  "03004031": "The nozzle has not been installed. Please install the nozzle and recalibrate.",
+  "03004032": "XY homing failed. Please remove any foreign objects obstructing the toolhead movement and recalibrate.",
+  "03004033": "Z-axis homing failed due to a temperature control abnormality. Please refer to the help instructions to resolve the issue, then recalibrate on the device.",
+  "03004035": "Nozzle cleaning failed. Please refer to the help instructions to resolve the issue, then recalibrate on the device.",
+  "03004036": "Nozzle offset calibration failed. Please refer to the help instructions to resolve the issue, then recalibrate on the device.",
+  "03004037": "Nozzle clumping detection calibration failed. Please refer to the help instructions to resolve the issue, then recalibrate on the device.",
+  "03004038": "Heatbed leveling failed. Please refer to the help instructions to resolve the issue, then recalibrate on the device.",
+  "03004039": "The nozzle presence detection failed. Please refer to the help instructions to resolve the issue, then recalibrate on the device.",
+  "03004042": "The Laser Safety Window is not properly installed. The task has been stopped.",
+  "03004044": "The Flame Sensor is abnormal. The sensor may be short-circuited. Please troubleshoot the issue before starting a print job.",
+  "0300404B": "Task aborted because the front door or top cover is open.",
+  "0300404D": "The current temperature of the hotend, heatbed, or chamber is too high. Please wait for it to cool down to room temperature before restarting the task.",
+  "03004050": "Liveview Camera calibration timeout; please restart the printer.",
+  "03004052": "Blade Z-axis homing failed",
+  "03004057": "Z-axis step loss detected. The task has stopped. Please check if there are any obstructions beneath the heatbed.",
+  "03004066": "Calibration of motion precision failed.",
+  "03004067": "Calibration result is over the threshold.",
+  "03004068": "Step loss occurred during the motion accuracy enhancement process. Please try again.",
+  "03004070": "Please heat the nozzle to above 170°C.",
+  "03008000": "Printing was paused for unknown reason. You can select \"Resume\" to resume the print job.",
+  "03008001": "Printing paused due to the pause command added to the printing file.",
+  "03008002": "First layer defects were detected by the Micro Lidar. Please check the quality of the printed model before continuing your print.",
+  "03008003": "Spaghetti defects were detected by the AI Print Monitoring. Please check the quality of the printed model before continuing your print.",
+  "03008004": "Filament ran out. Please load new filament.",
+  "03008005": "Toolhead front cover fell off. Please remount the front cover and check to make sure your print is going okay.",
+  "03008006": "The build plate marker was not detected. Please confirm the build plate is correctly positioned on the heatbed with all four corners aligned, and the marker is visible.",
+  "03008007": "There was an unfinished print job when the printer lost power. If the model is still adhered to the build plate, you can try resuming the print job.",
+  "03008008": "Nozzle temperature malfunction",
+  "03008009": "Heatbed temperature malfunction",
+  "0300800A": "A Filament pile-up was detected by AI Print Monitoring. Please clean filament from the waste chute.",
+  "0300800B": "The cutter is stuck. Please make sure the cutter handle is out and check the filament sensor cable connection.",
+  "0300800C": "Skipped step detected: auto-recover complete; please resume print and check if there are any layer shift problems.",
+  "0300800D": "Detected that the extruder is not extruding normally. If the defects are acceptable, select \"Resume\" to resume the print job.",
+  "0300800E": "The print file is not available. Please check to see if the storage media has been removed.",
+  "0300800F": "The door seems to be open, so printing was paused.",
+  "03008010": "The hotend cooling fan speed is abnormal.",
+  "03008011": "Detected build plate is not the same as the Gcode file. Please adjust slicer settings or use the correct plate.",
+  "03008013": "Printing was paused by the user. You can select \"Resume\" to continue printing.",
+  "03008014": "The nozzle is covered with filament, or the build plate is installed incorrectly. Please cancel this print and clean the nozzle or adjust the build plate according to the actual status. You can also select \"Resume\" to resume the print job.",
+  "03008015": "The filament on external spool has run out; please load new filament. If the filament is loaded, please select \"Resume\".",
+  "03008016": "The nozzle is clogged with filament. Please cancel this print and clean the nozzle or select \"Resume\" to resume the print job.",
+  "03008017": "Foreign objects detected on heatbed. Please check and clean the heatbed. Then, select \"Resume\" to resume the print job.",
+  "03008018": "Chamber temperature malfunction.",
+  "03008019": "No build plate is placed.",
+  "0300801A": "Filament extrusion error; please check the assistant for troubleshooting. After resolving the issue, decide whether to cancel or resume the print job based on the actual print status.",
+  "0300801B": "Nozzle temperature problem detected. Refer to Assistant to re-connect the hotend connector. POWER OFF the printer before this operation to avoid short circuits.",
+  "0300801C": "The extrusion resistance is abnormal. The extruder may be clogged, please refer to the assistant, after trouble shooting, you can select \"Resume\" to resume the print job.",
+  "0300801D": "The extruder servo motor position sensor is malfunctioning. Please power off the printer first and check if the connection cable is loose.",
+  "0300801E": "The extrusion motor is overloaded, please check the Assistant for details.",
+  "03008021": "The nozzle may not be installed or not properly installed. Please ensure the nozzle is correctly installed before proceeding.",
+  "03008022": "The heatbed may be obstructed while moving downward. Please clear any objects beneath the heatbed and check for any resistance or jamming during its movement.",
+  "03008023": "XY motor homing failed during hotend holder homing. Please check for obstructions and retry.",
+  "03008024": "Hotend holder homing blocked. Please check for obstructions and retry.",
+  "03008025": "Hotend holder homing distance too long. Please inspect the timing belt and motor gear for looseness or damage.",
+  "03008026": "Laser/Calibration mode: Please remove all hotends from the holder before retrying.",
+  "03008027": "Hotend dropped from the holder during printing. Please return it to the original position before continuing.",
+  "03008028": "Nozzle offset calibration sensor error. If using a single hotend or the calibration function is disabled, you may ignore this and continue printing; otherwise, it is recommended to check the sensor before proceeding.",
+  "03008029": "MC–hotend rack communication error. Please check if the rack mainboard cable is loose.",
+  "0300802A": "Hotend rack motor position sensor error. If the issue persists after retrying, restart the printer.",
+  "0300802B": "Hotend rack motor error. Please check and remove any obstructions affecting rack movement before continuing.",
+  "0300802C": "A nozzle-clumping sensor error has been detected. Please cancel the print and service the sensor, or disable clumping detection function and continue printing.",
+  "03008041": "Platform detection timeout: please restart the printer.",
+  "03008042": "Task paused because the door or top cover is open.",
+  "03008043": "The laser module is abnormal.",
+  "03008044": "Fire was detected inside the chamber.",
+  "03008045": "Material detection timeout: please restart the printer.",
+  "03008046": "Foreign object detect timeout: please restart the printer.",
+  "03008047": "Quick-release lever detection time out: please restart the printer.",
+  "03008048": "Laser Module unlock has timed out, and the task cannot proceed. Please restart the printer and try again.",
+  "03008049": "The current plate is invalid.",
+  "0300804A": "Emergency stop button improperly installed. Please reinstall according to the Wiki before proceeding.",
+  "0300804B": "Task paused. The Laser Safety Window is open.",
+  "0300804E": "This is a printing task. Please detach the Laser/Cutting Module from the Toolhead.",
+  "0300804F": "The loading/unloading process is currently ongoing. Please stop the process or remove the laser/cutting module.",
+  "03008050": "This device does not support the 40W Laser Module. Please remove it or replace it with a 10W Laser Module.",
+  "03008051": "The cutting module has dropped or the cutting module cable is disconnected; please check the module.",
+  "03008053": "Laser module detected. Please install the right nozzle correctly to ensure proper Laser Module Mounting Calibration.",
+  "03008054": "Please place the paper required for Print Then Cut.",
+  "03008055": "The module mounted on the toolhead does not match the task. Please install the correct module.",
+  "03008056": "Laser module detected. Please remove the hotend from the hotend rack to prevent laser processing debris from affecting printing performance.",
+  "03008057": "The rotary attachment is disconnected. Please ensure it is properly installed and the cable is securely plugged in.",
+  "03008058": "The rotary attachment is detected. Please remove it before continuing.",
+  "03008060": "Please check whether the cutting platform is properly positioned and not shifted, then continue.",
+  "03008061": "The mode of Airflow System failed to activate; check the air door condition.",
+  "03008062": "The chamber temperature is too high. It may be due to high environmental temperature.",
+  "03008063": "The chamber temperature is too high. Please open the top cover and front door to cool down.",
+  "03008064": "The chamber temperature is too high. Please open the top cover and front door to cool down. (Open door detection for this print job will be set to \"Notification\" level)",
+  "03008065": "The temperature of the MC module is too high. Please check the Wiki for possible explanations.",
+  "0300806E": "Abnormal nozzle temperature control detected; the heating module may be damaged. Please disconnect the power immediately and stop using the device.",
+  "0300806F": "Abnormal temperature rise detected on the heatbed. The heating module may be damaged. Please power off the device immediately and stop using it.",
+  "03008070": "The chamber heater temperature is rising abnormally and the heating module may be damaged. Please power off the device immediately and stop using the device.",
+  "03008071": "The Toolhead Enhanced Cooling Fan module is malfunctioning.",
+  "0300807D": "Fire Extinguisher not detected, the automatic extinguishing function will be unavailable.",
+  "0300807E": "Fire Extinguisher not detected, the automatic extinguishing function will be unavailable.",
+  "0300807F": "Fire Extinguisher is malfunctioning.",
+  "03008080": "Fire extinguisher motor reset failed.",
+  "03008081": "Fire extinguisher cylinder not installed. Please confirm on the extinguisher page.",
+  "03008082": "The Fire Extinguisher Gas Cylinder is empty.",
+  "03008084": "The right hotend heatbreak temperature is too high, which may cause clogging. Please open the printer’s top cover and front door to reduce the chamber temperature, or lower the ambient temperature.",
+  "0300C012": "Please heat the nozzle to above 170°C.",
+  "0300C056": "A minor fire was detected inside the chamber, and the Auto Fire Extinguishing process has been aborted.",
+  "0300C070": "The fire extinguisher has been detected and is ready for use after the laser module is connected.",
+  "05004001": "Failed to connect to Bambu Cloud. Please check your network connection.",
+  "05004002": "Unsupported print file path or name. Please resend the print job.",
+  "05004003": "Printing stopped because the printer was unable to parse the file. Please resend your print job.",
+  "05004004": "Device is busy and cannot start new task. Please wait for current task to complete before sending new task.",
+  "05004005": "Print jobs are not allowed to be sent while updating firmware.",
+  "05004006": "There is not enough free storage space for the print job. Restoring to factory settings can free up available space.",
+  "05004007": "The device requires a repair upgrade, and printing is currently unavailable.",
+  "05004008": "Starting printing failed; please power cycle the printer and resend the print job.",
+  "05004009": "Print jobs are not allowed to be sent while updating logs.",
+  "0500400A": "The file name is not supported. Please rename and restart the print job.",
+  "0500400B": "There was a problem downloading a file. Please check your network connection and resend the print job.",
+  "0500400C": "Please insert a MicroSD card and restart the print job.",
+  "0500400D": "Please run a self-test and restart the print job.",
+  "0500400E": "Printing was cancelled.",
+  "0500400F": "AMS is initializing and cannot be upgraded at the moment. Please try again later.",
+  "05004010": "AMS is drying and cannot be upgraded at the moment. Please try again later.",
+  "05004011": "The printer is loading or unloading filament and cannot be upgraded at the moment. Please try again later.",
+  "05004012": "The device is printing and cannot be upgraded at the moment. Please try again later.",
+  "05004013": "AMS is in operation and cannot be upgraded at the moment. Please try again when it is idle.",
+  "05004014": "Slicing for the print job failed. Please check your settings and restart the print job.",
+  "05004015": "There is not enough free storage space for the print job. Please format or clear files from the MicroSD card to free up space.",
+  "05004016": "The MicroSD Card is write-protected. Please replace the MicroSD Card.",
+  "05004017": "Binding failed. Please retry or restart the printer and retry.",
+  "05004018": "Binding configuration information parsing failed; please try again.",
+  "05004019": "The printer has already been bound. Please unbind it and try again.",
+  "0500401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration before trying again.",
+  "0500401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "0500401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "0500401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+  "0500401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "0500401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.",
+  "05004020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "05004021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+  "05004022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "05004023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "05004024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration before you try again.",
+  "05004025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "05004026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "05004027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+  "05004028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "05004029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "0500402A": "Failed to connect to the router, which may be caused by wireless interference or being too far away from the router. Please try again or move the printer closer to the router and try again.",
+  "0500402B": "Router connection failed due to incorrect password. Please check the password and try again.",
+  "0500402C": "Failed to obtain IP address, which may be caused by wireless interference resulting in data transmission failure or the DHCP address pool of the router being full. Please move the printer closer to the router and try again. If the issue persists, please check router settings to see whether the IP addresses have been exhausted.",
+  "0500402D": "System exception",
+  "0500402E": "System does not support the file system currently used by the USB flash drive. Please replace or format the USB flash drive to FAT32.",
+  "0500402F": "USB flash drive sector data is damaged; please format it. If it still cannot be recognized, please replace the USB flash drive.",
+  "05004030": "The device is currently upgrading. Please try again when it is idle.",
+  "05004031": "The accessory firmware does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "05004032": "Hotend Change System is busy; please perform the upgrade when it is idle.",
+  "05004033": "The AMS firmware does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "05004034": "The Laser Module firmware does not match the printer. Please upgrade it on the \"Firmware\" page.",
+  "05004035": "The BirdsEye Camera is malfunctioning. Please try restarting the device. If the issue persists after multiple restarts, check the camera connection status or contact customer support.",
+  "05004037": "Your sliced file is not compatible with current printer model. This file can't be printed on this printer.",
+  "05004038": "The nozzle diameter in sliced file is not consistent with the current nozzle setting. This file can't be printed.",
+  "05004039": "The current task does not allow the installation of the laser/cutting module, and the task has been halted.",
+  "0500403A": "The current temperature is too low. In order to protect you and your printer, printing tasks, moving an axis and other operations are disabled. Please move the printer to an environment above 10 degrees Celsius.",
+  "0500403B": "Laser/cutting tasks cannot be initiated on the machine at the moment. Please use the computer software to start the task.",
+  "0500403C": "The nozzle setup does not match the slicing file. Please initiate the print after re-slicing.",
+  "0500403D": "The toolhead module is not set up. Please set it up before initiating the task.",
+  "0500403E": "The current tool head does not support initialization.",
+  "0500403F": "Failed to download print job; please check your network connection.",
+  "05004040": "The printer has reached its power limit. Please connect a dedicated power adapter to this AMS to enable drying.",
+  "05004041": "The AMS drying cannot be started during printing.",
+  "05004042": "Due to power limitations, starting AMS drying will pause current operations such as nozzle heating and fan running. Do you want to proceed with drying?",
+  "05004043": "Due to power limitations, only one AMS is allowed to use the device's power for drying.",
+  "05004044": "BirdsEye Camera malfunction: please contact customer support.",
+  "05004045": "Hotend check in progress. This operation is temporarily unavailable. Please wait.",
+  "05004046": "The print has stopped because the 3MF file is invalid. Please verify that the correct printer model was selected during slicing, or update Studio and re-slice the file.",
+  "05004047": "The print has stopped because the available hotend quantity or model does not match the sliced file. Please verify the hotend model and quantity before restarting the print job.",
+  "05004048": "The feeder module is offline. Please check if the feeder module connection cable is loose.",
+  "05004049": "A feeder module replacement is detected. Please ensure that the corresponding extrusion gear and nozzle have been replaced, and manually update the nozzle type in the printer.",
+  "05004050": "Error detected on the print board.",
+  "05004051": "Dynamic arc fitting failed. Please re-slice the model before starting the print.",
+  "05004052": "Error detected on the hot end.",
+  "05004054": "Error detected on the mat.",
+  "05004057": "The filament selected in the slicer requires a harder nozzle. Please replace the nozzle or adjust the filament settings before reprinting.",
+  "0500405D": "Laser module Serial Number error: unable to calibrate or make project.",
+  "05004065": "The task requires a Laser Platform, but the current one is a Cutting Platform. Please replace it, measure the material thickness in the software, and then restart the task.",
+  "05004070": "The laser or cutter module is connected, so the device cannot initiate a 3D printing task.",
+  "05004075": "No Laser Platform was detected, which may affect thickness measurement accuracy. Please place the laser platform correctly and ensure the rear markers are not blocked, then restart the thickness measurement in the software before initiating the task.",
+  "05004076": "Please place the Laser Platform correctly and ensure the rear markers are not blocked, then restart the thickness measurement in the software before initiating the task.",
+  "05004095": "No print plate detected. Please place it correctly and recalibrate on the device.",
+  "05004097": "The device cannot detect the Laser Module. Please reconnect the module cable or restart the printer.",
+  "05004098": "The device cannot detect AMS A. Please reconnect the AMS cable or restart the printer.",
+  "05004099": "The firmware of Cutting Module does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "0500409A": "The firmware of Air Pump does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "0500409B": "The firmware of Laser Module does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "0500409D": "The firmware of AMS A does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "0500409E": "The device cannot detect the Cutting Module. Please reconnect the module cable or restart the printer.",
+  "0500409F": "The device cannot detect the Air Pump.  Please reconnect the module cable or restart the printer.",
+  "050040A0": "The device cannot detect Laser Fourth Axis.  Please reconnect the module cable or restart the printer.",
+  "050040A1": "The device cannot detect Fire Extinguisher.  Please reconnect the module cable or restart the printer.",
+  "050040A2": "The device cannot detect the External Exhaust Fan.  Please restart the printer or reconnect the fan cable.",
+  "050040A3": "AMS(or AMS lite) A communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "050040A4": "The current firmware only supports 1 AMS Lite. Please remove all AMS units before reconnecting the supported AMS Lite device.",
+  "050040A5": "The current firmware only supports AMS/AMS 2 Pro/AMS HT, with a maximum of 4 units. Please remove all AMS units before reconnecting the supported one.",
+  "050040A6": "File download failed due to missing certificates. Please check the Fleet Hub certificate configuration and restart the printer before trying again",
+  "050040A7": "The device cannot detect the Filament Track Switch.  Please restart the printer or reconnect the cable.",
+  "050040A8": "The device firmware requires a repair upgrade, and the current operation cannot be performed. Please upgrade it on the \"Firmware\" page.",
+  "050040C0": "Communication error detected with AMS, AMS Lite or AMS HT. Please reconnect the module cable or restart the printer when it is idle.",
+  "05008013": "The print file is not available. Please check to see if the storage media has been removed.",
+  "05008030": "",
+  "05008036": "Your sliced file is not consistent with the current printer model. Continue?",
+  "0500803C": "The left and right nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
+  "05008040": "Toolhead front cover is detached. Moving the toolhead may damage the printer. Do you want to continue?",
+  "05008041": "The filament in hotend is too cold. Extrusion may damage the extruder. Still feeding in/out the filament?",
+  "05008048": "The module on the toolhead is not calibrated. Please cancel the task to perform calibration or switch to a calibrated module.",
+  "05008051": "Detected build plate is not the same as the Gcode file. Please adjust slicer settings or use the correct plate.",
+  "05008053": "The left and right nozzle is not matched with slicing file. Please initiate the print after re-slicing, or continue printing after replacing the correct nozzle. Caution: the hotend temperature is high.",
+  "05008055": "Laser module is installed, but a Cutting Platform is detected. Please place a Laser Platform and perform laser calibration.",
+  "05008056": "Cutting module is installed, but the laser platform is detected. Please place the cutting platform for calibration.",
+  "05008057": "The filament hardness selected in the slicer exceeds the current nozzle hardness. Continuing the print may cause nozzle wear, leading to leakage and unstable flow. Please proceed with caution.",
+  "05008058": "Please place the light grip cutting mat correctly and ensure the marker is exposed.",
+  "05008059": "Cutting platform base is not correctly aligned. Please ensure that the four corners of the platform are aligned with the heatbed.",
+  "0500805A": "Please place the cutting mat on cutting protection base.",
+  "0500805B": "The cutting mat type is unknown; please replace it with the correct cutting mat.",
+  "0500805C": "The grip cutting mat type does not match; please place a LightGrip cutting mat.",
+  "0500805E": "Cutting module Serial Number error: unable to calibrate or make project.",
+  "05008060": "The current module on toolhead does not meet requirements. Please replace the module as per the on-screen instructions.",
+  "05008061": "No print plate detected. Please make sure it is placed correctly.",
+  "05008062": "The print plate marker was not detected. Please confirm the print plate is correctly positioned on the heatbed with all four corners aligned, and the marker is visible. If strong light is shining on the print sheet, consider closing the front door and blocking external light sources.",
+  "05008063": "The platform is not detected during calibration; please make sure the Laser Platform is properly placed.",
+  "05008064": "Please place the Laser Platform correctly and ensure the rear markers are not blocked for laser calibration.",
+  "05008066": "The task requires a Cutting Platform, but the current one is a Laser Platform. Please replace it with a Cutting Platform (Cutting Protection Base + LightGrip cutting mat).",
+  "05008067": "Please place a LightGrip cutting mat on the cutting protection base.",
+  "05008068": "Please place the strong grip cutting mat correctly and ensure the marker is exposed.",
+  "05008069": "Unable to recognize the left and right hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please manually set the hotend type.",
+  "0500806A": "Unable to recognize the left and right hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please set hotend type on printer screen before next print.",
+  "0500806B": "Quick-release Lever is not locked. Please press down the external toolhead module to ensure it is properly seated, then push down the level to lock it in place.",
+  "0500806C": "Please place the cutting platform correctly and ensure the marker is exposed.",
+  "0500806D": "Material not detected. Please confirm placement and continue.",
+  "0500806E": "Foreign objects detected on heatbed; please check and clean up the heatbed.",
+  "0500806F": "The grip cutting mat type does not match; please place a StrongGrip cutting mat.",
+  "05008071": "No cutting platform was detected. Please confirm that it has been correctly placed.",
+  "05008072": "Live View camera is blocked",
+  "05008073": "Heatbed limit block is obstructed or contaminated. Please clean and ensure the limit block is visible, otherwise platform position offset detection may be inaccurate.",
+  "05008074": "The Laser Platform is offset. Please ensure that the four corners of the platform are aligned with the heatbed, and the marker is not obstructed.",
+  "05008077": "The visual marker was not detected. Please ensure the paper is properly placed.",
+  "05008078": "Current material does not match the sliced file settings. Please load the correct material and ensure the QR code on the material is not damaged or dirty.",
+  "05008079": "Please place the Laser Test Material (350g paperboard) and position support strips underneath to prevent material warping.",
+  "0500807A": "The foreign object detection function is not working. You can continue the task or check the assistant for troubleshooting.",
+  "0500807B": "Please place the cutting platform (cutting protection base + LightGrip cutting mat).",
+  "0500807C": "Please place the cutting platform (cutting protection base + StrongGrip cutting mat).",
+  "0500807D": "This task requires a Cutting Platform, but the current one is a Laser Platform. Please replace it with a Cutting Platform (Cutting Protection Base + StrongGrip Cutting Mat).",
+  "0500807E": "Please place a StrongGrip cutting mat on the cutting protection base.",
+  "05008080": "The left and right hotend is not installed.",
+  "05008081": "The left and right hotend is not installed.",
+  "05008082": "Please remove the protective film on the Opaque Glossy Acrylic before processing",
+  "05008083": "Material is not allowed in Mounting Calibration. Please remove the material from the platform.",
+  "05008084": "The Live View Camera is dirty; please clean it and continue.",
+  "05008085": "Toolhead camera is obstructed",
+  "05008086": "Toolhead Camera is dirty, which affects the AI function; please clean the lens surface.",
+  "05008087": "BirdsEye camera is obstructed",
+  "05008088": "The Birdseye Camera is dirty",
+  "05008089": "Task paused due to Presence Check failed. Please check the printer to continue.",
+  "0500808A": "The BirdsEye Camera is installed offset. Please refer to the assistant to reinstall it.",
+  "0500808B": "The BirdsEye Camera setup failed. Please remove all objects and the mat on the heatbed to ensure the heatbed markers are visible. Meanwhile, please ensure the BirdsEye Camera is installed correctly and remove any obstructions that may block the camera's view.",
+  "0500808C": "Detected build plate offset. Please align the build plate with the heatbed, and then continue.",
+  "0500808D": "The Cutting Module offset calibration failed, which may result in inaccurate cuts. Please ensure the cutting material is properly positioned and check whether the cutting blade tip is worn.",
+  "0500808E": "BirdsEye Camera initialization failed. The toolhead camera did not detect the Heatbed features. Please clean the Heatbed, remove all objects and pads, and ensure the bed markings are visible. Check Assistant for a detailed solution.",
+  "0500808F": "Nozzle camera lens is dirty, affecting AI monitoring. Clean the lens with a non-woven cloth and a small amount of alcohol. Beware of hotend heat; wait for it to cool before handling.",
+  "05008090": "Please attach the 80g White Printing Paper to the center area of the platform.",
+  "05008091": "The Cutting Module offset calibration failed, which may result in inaccurate cuts. Please ensure the 80g white printer paper(letter paper thickness) is properly positioned and check whether the cutting blade tip is worn.",
+  "05008092": "Toolhead Camera initialization failed. This print can still continue, but some AI functions will be disabled. If you encounter this issue again after restarting, please contact customer support.",
+  "05008093": "The nozzle silicone sleeve is not installed; there is a risk of temperature control failure. Please install it correctly and try again.",
+  "05008098": "No material detected. Please confirm material placement and continue.",
+  "05008099": "AI detected potential print shift or collapse. Check print status and take action. Clean build plate or apply adhesive to improve adhesion.",
+  "0500809A": "Please replace the Vision Encoder board with the print plate to avoid damage during calibration.",
+  "0500809B": "Build plate not properly positioned, may collide with the waste chute. Please reposition build plate and align with heatbed.",
+  "050080A0": "The visual encoder board was not detected. Please check if the board is properly placed and aligned at all four corners, and ensure the positioning markings are clear and free from wear.",
+  "0500C010": "USB flash drive read-write exception; please reinsert or replace it.",
+  "0500C011": "",
+  "0500C032": "Laser/Cutting module connected to the toolhead. The drying process has been automatically stopped.",
+  "0500C036": "This is a printing task. Please detach the Laser/Cutting Module from the Toolhead.",
+  "0500C04A": "AMS is calibrating, reading RFID or loading/unloading material, unable to initiate drying process, please wait.",
+  "0500C04B": "Filament in AMS outlet, the high drying temperature may cause AMS blockage. Drying cannot be started. Please unload the filament first.",
+  "0500C04C": "The AMS is currently drying. Please do not start the process again.",
+  "0500C04D": "The device is currently in laser or cutting mode and cannot start drying. Please do not initiate the drying process.",
+  "0500C04E": "Please connect a power adapter to the AMS-HT before starting the drying process.",
+  "0500C04F": "The AMS is drying and cannot perform this operation at the moment.",
+  "0500C07F": "Device is busy and cannot perform this operation. To proceed, please pause or stop the current task.",
+  "0500C080": "The extruder is currently running and cannot perform this operation. To proceed, please pause or stop the current task first.",
+  "05014017": "Binding failed. Please retry or restart the printer and retry.",
+  "05014018": "Binding configuration information parsing failed; please try again.",
+  "05014019": "The printer has already been bound. Please unbind it and try again.",
+  "0501401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration before trying again.",
+  "0501401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "0501401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "0501401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+  "0501401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "0501401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.",
+  "05014020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "05014021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+  "05014022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "05014023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "05014024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration before you try again.",
+  "05014025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "05014026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "05014027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+  "05014028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+  "05014029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
+  "05014031": "Device discovery binding is in progress, and the QR code cannot be displayed on the screen. You can wait for the binding to finish or abort the device discovery binding process in the APP/Studio and retry scanning the QR code on the screen for binding.",
+  "05014032": "QR code binding is in progress, so device discovery binding cannot be performed. You can scan the QR code on the screen for binding or exit the QR code display page on screen and try device discovery binding.",
+  "05014033": "Your APP region does not match with your printer; please download the APP in the corresponding region and register your account again.",
+  "05014034": "The slicing progress has not been updated for a long time, and the printing task has exited. Please confirm the parameters and reinitiate printing.",
+  "05014035": "The device is in the process of binding and cannot respond to new binding requests.",
+  "05014038": "The regional settings do not match the printer; please check the printer's regional settings.",
+  "05014039": "Device login has expired; please try to bind again.",
+  "05014098": "The device cannot detect AMS B. Please reconnect the AMS cable or restart the printer.",
+  "0501409D": "The firmware of AMS B does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "050140A3": "AMS(or AMS lite) B communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05024001": "Current filament will be used in this print job. Settings cannot be changed.",
+  "05024002": "Please go to “Settings > Calibration” to run the Motion Accuracy Enhancement Calibration before turning on Motion Accuracy Enhancement mode.",
+  "05024003": "The printer is currently printing and the motion accuracy enhancement feature cannot be turned on or off.",
+  "05024004": "Some features are not supported by the current device. Please check the Studio feature settings or update the firmware to the latest version.",
+  "05024005": "The AMS has not been calibrated yet, so printing cannot be initiated.",
+  "05024006": "Unknown module detected, please try updating the firmware to the latest version.",
+  "05024007": "Failed to start a new task: filament loading/unloading not completed.",
+  "05024008": "Failed to start a new task: The nozzle cold pulling was not completed.",
+  "05024009": "",
+  "0502400A": "",
+  "0502400B": "",
+  "0502400C": "",
+  "0502400D": "Failed to start a new task: filament loading/unloading not completed.",
+  "0502400E": "Failed to start a new task: The nozzle cold pull was not completed.",
+  "05024013": "This device is not compatible with the 40W laser module. Please replace it with a 10W laser module or remove it.",
+  "05024015": "Hotend authentication failed. Please switch the hotend or restart the printer.",
+  "05024016": "No available hotend was detected in the induction hotend holder or the hotend rack. Please install a hotend and try again.",
+  "05024017": "The task failed due to a timeout on the induction hotend switch. Please go to the \"Hotend & Rack\" page and click \"Read All\" to switch manually.",
+  "05024018": "The hotend rack has not been calibrated, and hotend switching cannot be performed. Please complete the rack calibration on the device.",
+  "05024019": "The device is busy and cannot perform hotend rack tasks at this moment. Please try again later.",
+  "05024020": "The hotend rack is fully occupied. Please remove one hotend before proceeding.",
+  "05024021": "Failed to read induction hotend information. Please replace the hotend or restart the printer.",
+  "05024023": "Induction hotend recognition error detected. Please go to the \"Hotends & Rack\" page and click \"Read All\" to re-detect. If the issue persists, try updating the firmware on the \"Firmware\" page.",
+  "05024027": "The current AMS does not support drying while printing. Please connect to the network and update the AMS firmware on the “Firmware” page.",
+  "05024029": "This filament is only supported for printing with the left extruder.",
+  "0502402D": "Printing stopped because the printer was unable to parse the 3mf file. Please resend your print job.",
+  "0502402E": "Printing stopped because the printer was unable to parse the 3mf file. Please resend your print job.",
+  "0502402F": "Printing stopped because the printer was unable to parse the 3mf file. Please resend your print job.",
+  "05024030": "Printing stopped because the printer was unable to parse the 3mf file. Please resend your print job.",
+  "05024035": "With the current nozzle diameter, This filament is only supported for printing with the left extruder.",
+  "05024098": "The device cannot detect AMS C. Please reconnect the AMS cable or restart the printer.",
+  "0502409D": "The firmware of AMS C does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "050240A3": "AMS(or AMS lite) C communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05028022": "Induction hotend verification failed. Please replace the hotend and try again.",
+  "0502802A": "Using this filament with the auxiliary extruder involves a high risk and may result in print failure or device malfunction.",
+  "0502802B": "Using this filament with the auxiliary extruder may affect print quality.",
+  "05028036": "With the current nozzle diameter, Using this filament with the auxiliary extruder involves a high risk and may result in print failure or device malfunction.",
+  "05028037": "With the current nozzle diameter, Using this filament with the auxiliary extruder may affect print quality.",
+  "0502C00F": "The device is busy and cannot perform nozzle identification.",
+  "0502C010": "Due to printer power limitations, printing, calibration, controls and other actions cannot be performed during AMS drying. Please stop the drying process before proceeding with any other operation.",
+  "0502C011": "Currently in 2D production mode. Please continue the operation on the printer",
+  "0502C012": "The task cannot be paused.",
+  "0502C014": "The AMS Remaining Filament Estimation is enabled by default and cannot be disabled.",
+  "0502C024": "The flow dynamic calibration records have exceeded the storage limit. Please delete some historical records in the slicer software before adding new calibration data.",
+  "0502C025": "A hotend information update was detected during nozzle offset calibration. Please go to the \"Hotends & Rack\" page and click “Read All” to reidentify. If the issue persists, try updating from the “Firmware” page.",
+  "0502C026": "The device is busy with the current task and cannot perform this operation for now. Please try again later.",
+  "0502C028": "The filament currently loaded in the extruder does not support manual feeding.",
+  "0502C031": "Please check and remove any printed parts or debris from the heatbed surface before continuing the cold pull.",
+  "0502C032": "Please check and remove any printed parts or debris from the heatbed surface and underside before continuing the drying process.",
+  "0502C033": "While retracting filament on the auxiliary extruder, manually collect it promptly to prevent it from tangling on the spool.",
+  "0502C034": "Extruder switch failed and the current action was not executed. Please go to the Extruder screen, check and complete the extruder switch, and then try again.",
+  "05034098": "The device cannot detect AMS D. Please reconnect the AMS cable or restart the printer.",
+  "0503409D": "The firmware of AMS D does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "050340A3": "AMS(or AMS lite) D communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05804096": "The device cannot detect AMS-HT A. Please reconnect the AMS-HT cable or restart the printer.",
+  "0580409C": "The firmware of AMS-HT A does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "058040A2": "AMS-HT A communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05814096": "The device cannot detect AMS-HT B. Please reconnect the AMS-HT cable or restart the printer.",
+  "0581409C": "The firmware of AMS-HT B does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "058140A2": "AMS-HT B communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05824096": "The device cannot detect AMS-HT C. Please reconnect the AMS-HT cable or restart the printer.",
+  "0582409C": "The firmware of AMS-HT C does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "058240A2": "AMS-HT C communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05834096": "The device cannot detect AMS-HT D. Please reconnect the AMS-HT cable or restart the printer.",
+  "0583409C": "The firmware of AMS-HT D does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "058340A2": "AMS-HT D communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05844096": "The device cannot detect AMS-HT F. Please reconnect the AMS-HT cable or restart the printer.",
+  "0584409C": "The firmware of AMS-HT E does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "058440A2": "AMS-HT E communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05854096": "The device cannot detect AMS-HT E. Please reconnect the AMS-HT cable or restart the printer.",
+  "0585409C": "The firmware of AMS-HT F does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "058540A2": "AMS-HT F communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05864096": "The device cannot detect AMS-HT G. Please reconnect the AMS-HT cable or restart the printer.",
+  "0586409C": "The firmware of AMS-HT G does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "058640A2": "AMS-HT G communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05874096": "The device cannot detect AMS-HT H. Please reconnect the AMS-HT cable or restart the printer.",
+  "0587409C": "The firmware of AMS-HT H does not match the printer; the device cannot continue working. Please upgrade it on the \"Firmware\" page.",
+  "058740A2": "AMS-HT H communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "05FE403C": "The left nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
+  "05FE4094": "The left hotend is not installed. Please install the hotend and recalibrate.",
+  "05FE803C": "The left nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
+  "05FE8053": "The left nozzle is not matched with slicing file. Please initiate the print after re-slicing, or continue printing after replacing the correct nozzle. Caution: the hotend temperature is high.",
+  "05FE8069": "Unable to recognize the left hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please manually set the hotend type.",
+  "05FE806A": "Unable to recognize the left hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please set hotend type on printer screen before next print.",
+  "05FE8080": "The left hotend is not installed.",
+  "05FE8081": "The left hotend is not installed.",
+  "05FF403C": "The right (Aux) nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
+  "05FF4094": "The right hotend is not installed. Please install the hotend and recalibrate.",
+  "05FF803C": "The right (Aux) nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
+  "05FF8053": "The right nozzle is not matched with slicing file. Please initiate the print after re-slicing, or continue printing after replacing the correct nozzle. Caution: the hotend temperature is high.",
+  "05FF8069": "Unable to recognize the right hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please manually set the hotend type.",
+  "05FF806A": "Unable to recognize the right hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please set hotend type on printer screen before next print.",
+  "05FF8080": "The right hotend is not installed.",
+  "05FF8081": "The right hotend is not installed.",
+  "07004001": "The AMS has been disabled for a print, but it still has filament loaded. Please unload the AMS filament and switch to the spool holder filament for printing.",
+  "07004025": "Failed to read the filament information.",
+  "07008001": "Failed to cut the filament. Please check the cutter.",
+  "07008002": "The cutter is stuck. Please make sure the cutter handle is out.",
+  "07008003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "07008004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "07008005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "07008006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+  "07008007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "0700800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS A to the extruder is properly connected.",
+  "07008010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "07008011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+  "07008012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07008013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "07008016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "07008017": "AMS A is drying. Please stop drying process before loading/unloading material.",
+  "07008018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07008021": "AMS setup failed; please refer to the assistant.",
+  "07008023": "AMS A cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "07008026": "AMS set up failed. Please check the assistant for details.",
+  "07008027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "07008028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "07008029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "0700802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "07008030": "Hotend pre-swap check failed.",
+  "07008031": "Failed to lock the induction hotend.",
+  "07008032": "Failed to unlock the induction hotend.",
+  "07008033": "Failed to nest hotend into slot 1 on the rack.",
+  "07008034": "Failed to fetch induction hotend from slot 1 on the rack.",
+  "07008035": "Hotend Rack coarse homing failed. Please check for obstructions or if the build plate is misaligned.",
+  "07008036": "Hotend rack coarse homing failed. Please check for obstructions or if the build plate is misaligned.",
+  "07008037": "Hotend type mismatch detected during printing. Please replace the hotend in slot 1 on the rack with the type specified in the slicer before continuing the print.",
+  "0700C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0700C069": "An error occurred during AMS A drying. Please go to Assistant for more details.",
+  "0700C06A": "AMS A is reading RFID. Unable to start drying. Please try again later.",
+  "0700C06B": "AMS A is changing filament. Unable to start drying. Please try again later.",
+  "0700C06C": "AMS A is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "0700C06D": "AMS A is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "0700C06E": "AMS A motor is performing self-test. Unable to start drying. Please try again later.",
+  "07014001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
+  "07014025": "Failed to read the filament information.",
+  "07018001": "Failed to cut the filament. Please check the cutter.",
+  "07018002": "The cutter is stuck. Please make sure the cutter handle is out.",
+  "07018003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "07018004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "07018005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "07018006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+  "07018007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "0701800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS B to the extruder is properly connected.",
+  "07018010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "07018011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+  "07018012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07018013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "07018016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "07018017": "AMS B is drying. Please stop drying process before loading/unloading material.",
+  "07018018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07018021": "AMS setup failed; please refer to the assistant.",
+  "07018023": "AMS B cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "07018026": "AMS set up failed. Please check the assistant for details.",
+  "07018027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "07018028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "07018029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "0701802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "07018030": "Hotend pre-swap check failed.",
+  "07018033": "Failed to nest hotend into slot 2 on the rack.",
+  "07018034": "Failed to fetch induction hotend from slot 2 on the rack.",
+  "07018037": "Hotend type mismatch detected during printing. Please replace the hotend in slot 2 on the rack with the type specified in the slicer before continuing the print.",
+  "0701C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0701C069": "An error occurred during AMS B drying. Please go to Assistant for more details.",
+  "0701C06A": "AMS B is reading RFID. Unable to start drying. Please try again later.",
+  "0701C06B": "AMS B is changing filament. Unable to start drying. Please try again later.",
+  "0701C06C": "AMS B is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "0701C06D": "AMS B is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "0701C06E": "AMS B motor is performing self-test. Unable to start drying. Please try again later.",
+  "07024001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
+  "07024025": "Failed to read the filament information.",
+  "07028001": "Failed to cut the filament. Please check the cutter.",
+  "07028002": "The cutter is stuck. Please make sure the cutter handle is out.",
+  "07028003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "07028004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "07028005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "07028006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+  "07028007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "0702800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS C to the extruder is properly connected.",
+  "07028010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "07028011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+  "07028012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07028013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "07028016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "07028017": "AMS C is drying. Please stop drying process before loading/unloading material.",
+  "07028018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07028021": "AMS setup failed; please refer to the assistant.",
+  "07028023": "AMS C cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "07028026": "AMS set up failed. Please check the assistant for details.",
+  "07028027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "07028028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "07028029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "0702802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "07028030": "Hotend pre-swap check failed.",
+  "07028033": "Failed to nest hotend into slot 3 on the rack.",
+  "07028034": "Failed to fetch induction hotend from slot 3 on the rack.",
+  "07028037": "Hotend type mismatch detected during printing. Please replace the hotend in slot 3 on the rack with the type specified in the slicer before continuing the print.",
+  "0702C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0702C069": "An error occurred during AMS C drying. Please go to Assistant for more details.",
+  "0702C06A": "AMS C is reading RFID. Unable to start drying. Please try again later.",
+  "0702C06B": "AMS C is changing filament. Unable to start drying. Please try again later.",
+  "0702C06C": "AMS C is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "0702C06D": "AMS C is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "0702C06E": "AMS C motor is performing self-test. Unable to start drying. Please try again later.",
+  "07034001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
+  "07034025": "Failed to read the filament information.",
+  "07038001": "Failed to cut the filament. Please check the cutter.",
+  "07038002": "The cutter is stuck. Please make sure the cutter handle is out.",
+  "07038003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "07038004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "07038005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "07038006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+  "07038007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "0703800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS D to the extruder is properly connected.",
+  "07038010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "07038011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+  "07038012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07038013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "07038016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "07038017": "AMS D is drying. Please stop drying process before loading/unloading material.",
+  "07038018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07038021": "AMS setup failed; please refer to the assistant.",
+  "07038023": "AMS D cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "07038026": "AMS set up failed. Please check the assistant for details.",
+  "07038027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "07038028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "07038029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "0703802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "07038030": "Hotend pre-swap check failed.",
+  "07038033": "Failed to nest hotend into slot 4 on the rack.",
+  "07038034": "Failed to fetch induction hotend from slot 4 on the rack.",
+  "07038037": "Hotend type mismatch detected during printing. Please replace the hotend in slot 4 on the rack with the type specified in the slicer before continuing the print.",
+  "0703C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "0703C069": "An error occurred during AMS D drying. Please go to Assistant for more details.",
+  "0703C06A": "AMS D is reading RFID. Unable to start drying. Please try again later.",
+  "0703C06B": "AMS D is changing filament. Unable to start drying. Please try again later.",
+  "0703C06C": "AMS D is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "0703C06D": "AMS D is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "0703C06E": "AMS D motor is performing self-test. Unable to start drying. Please try again later.",
+  "07044025": "Failed to read the filament information.",
+  "07048003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "07048004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "07048005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "07048006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+  "07048007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "0704800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS E to the extruder is properly connected.",
+  "07048010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "07048011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+  "07048012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07048013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "07048016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "07048018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07048021": "AMS setup failed; please refer to the assistant.",
+  "07048023": "AMS E cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "07048030": "Hotend pre-swap check failed.",
+  "07048033": "Failed to nest hotend into slot 5 on the rack.",
+  "07048034": "Failed to fetch induction hotend from slot 5 on the rack.",
+  "07048037": "Hotend type mismatch detected during printing. Please replace the hotend in slot 5 on the rack with the type specified in the slicer before continuing the print.",
+  "0704C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "07054025": "Failed to read the filament information.",
+  "07058003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "07058004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "07058005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "07058006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+  "07058007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "0705800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS F to the extruder is properly connected.",
+  "07058010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "07058011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+  "07058012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07058013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "07058016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "07058018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07058021": "AMS setup failed; please refer to the assistant.",
+  "07058023": "AMS F cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "07058030": "Hotend pre-swap check failed.",
+  "07058033": "Failed to nest hotend into slot 6 on the rack.",
+  "07058034": "Failed to fetch induction hotend from slot 6 on the rack.",
+  "07058037": "Hotend type mismatch detected during printing. Please replace the hotend in slot 6 on the rack with the type specified in the slicer before continuing the print.",
+  "0705C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "07064025": "Failed to read the filament information.",
+  "07068003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "07068004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "07068005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "07068006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+  "07068007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "0706800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS G to the extruder is properly connected.",
+  "07068010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "07068011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+  "07068012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07068013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "07068016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "07068018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07068021": "AMS setup failed; please refer to the assistant.",
+  "07068023": "AMS G cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "07068030": "Hotend pre-swap check failed.",
+  "0706C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "07074025": "Failed to read the filament information.",
+  "07078003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "07078004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "07078005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "07078006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+  "07078007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "0707800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS H to the extruder is properly connected.",
+  "07078010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "07078011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+  "07078012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07078013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "07078016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "07078018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07078021": "AMS setup failed; please refer to the assistant.",
+  "07078023": "AMS H cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "07078030": "Hotend pre-swap check failed.",
+  "0707C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "07FE8001": "Failed to cut the filament of the left extruder. Please check the cutter.",
+  "07FE8002": "The cutter of the left extruder is stuck. Please pull out the cutter handle.",
+  "07FE8003": "Please pull out the filament on the spool holder  of the left extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+  "07FE8004": "Failed to pull back the filament from the left extruder. Please check whether the filament is stuck inside the extruder.",
+  "07FE8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck.",
+  "07FE8006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+  "07FE8007": "Please observe the nozzle of the left extruder. If the filament has been extruded, select \"Continue\"; if it is not, please push the filament forward slightly, and then select \"Retry\".",
+  "07FE8010": "Check if the left external filament spool or filament is stuck.",
+  "07FE8011": "The external filament connected to the left extruder has run out; please load a new filament.",
+  "07FE8012": "Failed to get mapping table; please select \"Resume\" to retry.",
+  "07FE8013": "Timeout purging old filament of the left extruder: Please check if the filament is stuck or the extruder is clogged.",
+  "07FE8018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07FE8020": "Extruder change failed; please refer to the assistant.",
+  "07FE8021": "AMS setup failed; please refer to the assistant.",
+  "07FE8024": "Extruder position calibration failed; please refer to the assistant.",
+  "07FE8025": "Cold pull timed out. Please promptly operate or check whether the filament is broken inside the extruder, and click the Assistant for details.",
+  "07FE8030": "The filament specified in the slicer has been used up. Printing is paused. Please go to the machine to replace the material and resume printing.",
+  "07FEC003": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+  "07FEC006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+  "07FEC008": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+  "07FEC009": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+  "07FEC00A": "Please observe the nozzle of the left extruder. If the filament has been extruded, select \"Continue\"; if not, please push the filament forward slightly and then select \"Retry\".",
+  "07FEC010": "Insert the filament until it can not be pushed any farther. There might be slight smoke during purging, so please close the front door and top cover after insertion.",
+  "07FEC011": "Please manually and slowly pull out the filament from the extruder. Then click “Continue”.",
+  "07FEC012": "Press the black PTFE tube coupler and unplug the PTFE tube. After completing the operation, click \"Continue.\"",
+  "07FEC030": "The filament specified in the slicer has been used up. Printing is paused. Please go to the machine to replace the material and resume printing.",
+  "07FF4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
+  "07FF8001": "Failed to cut the filament. Please check the cutter.",
+  "07FF8002": "The cutter is stuck. Please make sure the cutter handle is out.",
+  "07FF8003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+  "07FF8004": "Failed to pull back the filament from the toolhead to AMS. Please check whether the filament or the spool is stuck.",
+  "07FF8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck.",
+  "07FF8006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
+  "07FF8007": "Please observe the nozzle of the right extruder. If the filament has been extruded, select \"Continue\"; if it is not, please push the filament forward slightly, and then select \"Retry\".",
+  "07FF800D": "Failed to feed material from the right extruder to the toolhead. Please refer to the assistant for troubleshooting. Remove the filament, resolve any abnormalities, and then continue.",
+  "07FF8010": "Check if the external filament spool or filament is stuck.",
+  "07FF8011": "External filament has run out; please load a new filament.",
+  "07FF8012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "07FF8013": "Timeout purging old filament of the right extruder: Please check if the filament is stuck or the extruder is clogged.",
+  "07FF8017": "Filament remains were detected in the PTFE tube between the Auxiliary Extruder and the Toolhead. Please follow the assistant to remove the filament, then click “Continue.”",
+  "07FF8018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "07FF8020": "Extruder change failed; please refer to the assistant.",
+  "07FF8021": "AMS setup failed; please refer to the assistant.",
+  "07FF8024": "Extruder position calibration failed; please refer to the assistant.",
+  "07FF8025": "Cold pull timed out. Please promptly operate or check whether the filament is broken inside the extruder, and click the Assistant for details.",
+  "07FF8030": "The filament specified in the slicer has been used up. Printing is paused. Please go to the machine to replace the material and resume printing.",
+  "07FFC003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+  "07FFC006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
+  "07FFC008": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS)",
+  "07FFC009": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
+  "07FFC00A": "Please observe the nozzle. If the filament has been extruded, select \"Continue\"; if not, please push the filament forward slightly and then select \"Retry\".",
+  "07FFC010": "Insert the filament (over 30cm long) until it stops. You might see slight smoke during flushing. After insertion, close the front door and top cover.",
+  "07FFC011": "Please manually and slowly pull out the filament from the extruder. Then click “Continue”.",
+  "07FFC012": "Press the black PTFE tube coupler and unplug the PTFE tube. After completing the operation, click \"Continue.\"",
+  "07FFC030": "The filament specified in the slicer has been used up. Printing is paused. Please go to the machine to replace the material and resume printing.",
+  "0C004020": "The setup of BirdsEye Camera failed. Please clear all objects and remove the mat. Make sure the marker is not obstructed. Meanwhile, clean both the BirdsEye Camera and Toolhead Camera, and remove any foreign objects blocking their view.",
+  "0C004021": "The setup of BirdsEye Camera failed; please reboot the printer.",
+  "0C004022": "The setup of BirdsEye Camera failed.  Please check if the laser module is working properly.",
+  "0C004024": "The Birdseye Camera is installed offset. Please refer to the assistant to reinstall it.",
+  "0C004025": "The Birdseye Camera is dirty. Please clean it and restart the process.",
+  "0C004026": "The Live View Camera initialization failed; please reboot the printer.",
+  "0C004027": "The Live View Camera calibration failed. Please refer to the assistant for details and recalibrate the camera after processing.",
+  "0C004029": "Material not detected. Please confirm placement and continue.",
+  "0C00402A": "The visual marker was not detected. Please re-paste the paper in the correct position.",
+  "0C00402C": "Device data link error. Please reboot the printer.",
+  "0C00402D": "The toolhead camera is not working properly; please reboot the device.",
+  "0C00403D": "The vision encoder plate was not detected. Please confirm it is correctly positioned on the heatbed.",
+  "0C00403E": "The high-precision nozzle offset calibration has failed, possibly due to a damaged pattern or the similarity of the colors of the two selected filaments. Please clear the printed pattern and replace the filaments with higher color contrast before re-calibrating.",
+  "0C004041": "Toolhead camera calibration failed. Please ensure the Calibration Marker on the heatbed or Height Calibration Marker on the homing area is clean and undamaged, then re-run the calibration process.",
+  "0C008001": "First layer defects were detected. If the defects are acceptable, select 'Resume' to resume the print job.",
+  "0C008002": "",
+  "0C008005": "Purged filament has piled up in the waste chute, which may cause a tool head collision.",
+  "0C008009": "Build plate localization marker was not found.",
+  "0C00800B": "The heatbed marker was not detected. Please clear all objects and remove the mat. Make sure the marker is not obstructed.",
+  "0C008015": "Objects detected on the platform; please clean them up in a timely manner.",
+  "0C008016": "The foreign object detection function is not working. You can continue the task or check assistant for solutions.",
+  "0C008017": "Foreign objects detected on the platform; please clean them up on time.",
+  "0C008018": "The foreign object detection function is not working. You can continue the task or view the assistant for troubleshooting.",
+  "0C008033": "Quick-release Lever is not locked. Please push it down to secure.",
+  "0C008034": "Liveview Camera initialization failed. This print can still continue, but some AI functions will be disabled. If you encounter this issue again after restarting, please contact customer support.",
+  "0C00803F": "AI detected nozzle clumping. Please check the nozzle condition. Refer to assistant for solutions.",
+  "0C008040": "AI detected air-printing defect. Please check the hotend extrusion status. Refer to assistant for solutions.",
+  "0C008042": "The AI print monitor has detected a spaghetti defect. Please check the print and take the necessary action.",
+  "0C008043": "AI detected nozzle clumping. Please check the nozzle condition. Refer to assistant for solutions.",
+  "0C00C003": "Possible defects were detected in the first layer.",
+  "0C00C004": "Possible spaghetti failure was detected.",
+  "0C00C006": "Purged filament may have piled up in the waste chute.",
+  "1000C001": "High bed temperature may lead to filament clogging in the nozzle. You may open the chamber door.",
+  "1000C002": "Printing CF material with stainless steel may cause nozzle damage.",
+  "1000C003": "Enabling Timelapse in traditional mode may cause defects; please activate this feature as needed.",
+  "10014001": "Timelapse is not supported as Spiral Vase mode is enabled in slicing presets.",
+  "10014002": "Timelapse is not supported as the Print sequence is set to \"By object\".",
+  "10018003": "The time-lapse mode is set to Traditional in the slicing file. This may cause surface defects. Would you like to enable it?",
+  "10018004": "Prime Tower is not enabled and time-lapse mode is set to Smooth in slicing file. This may cause surface defects. Would you like to enable it?",
+  "12004001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+  "12008001": "Cutting the filament failed. Please check to see if the cutter is stuck. Refer to the Assistant for solutions.",
+  "12008002": "The cutter is stuck. Please pull out the cutter handle.",
+  "12008003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
+  "12008004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+  "12008005": "The filament is not inserted. Please insert the filament.",
+  "12008006": "Unable to feed filament into the extruder. This could be due to tangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+  "12008007": "Failed to extrude the filament. This might be caused by clogged extruder or stuck filament. Refer to the Assistant for solutions.",
+  "12008010": "Filament or spool may be stuck.",
+  "12008011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+  "12008012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "12008013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+  "12008014": "The filament location in the toolhead was not found. Refer to the Assistant for solutions.",
+  "12008015": "Failed to pull out the filament from the toolhead. Please check if the filament is stuck, or if it is broken inside the extruder or PTFE tube.",
+  "12008016": "The extruder is not extruding normally. Refer to the Assistant for troubleshooting. There may be defects in this layer, but you may resume if the defects are acceptable.",
+  "12014001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+  "12018001": "Failed to cut the filament. Please check the cutter.",
+  "12018002": "The cutter is stuck. Please pull out the cutter handle.",
+  "12018003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
+  "12018004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+  "12018005": "Failed to feed the filament. Please load the filament and then select \"Retry\".",
+  "12018006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck.",
+  "12018007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS.",
+  "12018010": "Please check if the spool or filament is stuck.",
+  "12018011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+  "12018012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "12018013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+  "12018014": "Failed to check the filament location in the tool head, please refer to the HMS.",
+  "12018015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or the filament is broken inside the extruder.",
+  "12018016": "The extruder is not extruding normally, please refer to the HMS. After trouble shooting, if the defects are acceptable, please resume printing.",
+  "12024001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+  "12028001": "Failed to cut the filament. Please check the cutter.",
+  "12028002": "The cutter is stuck. Please pull out the cutter handle.",
+  "12028003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
+  "12028004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+  "12028005": "The filament is not inserted. Please insert the filament.",
+  "12028006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck.",
+  "12028007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS.",
+  "12028010": "Please check if the spool or filament is stuck.",
+  "12028011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+  "12028012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "12028013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+  "12028014": "Failed to check the filament location in the tool head, please refer to the HMS.",
+  "12028015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder.",
+  "12028016": "The extruder is not extruding normally, please refer to the HMS. After trouble shooting, if the defects are acceptable, please resume printing.",
+  "12034001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+  "12038001": "Failed to cut the filament. Please check the cutter.",
+  "12038002": "The cutter is stuck. Please pull out the cutter handle.",
+  "12038003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
+  "12038004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+  "12038005": "The filament is not inserted. Please insert the filament.",
+  "12038006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck.",
+  "12038007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS.",
+  "12038010": "Please check if the spool or filament is stuck.",
+  "12038011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+  "12038012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "12038013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+  "12038014": "Failed to check the filament location in the tool head; please refer to the HMS.",
+  "12038015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder.",
+  "12038016": "The extruder is not extruding normally, please refer to the HMS. After trouble shooting, if the defects are acceptable, please resume printing.",
+  "12FF4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+  "12FF8001": "Failed to cut the filament. Please check the cutter.",
+  "12FF8002": "The cutter is stuck. Please pull out the cutter handle.",
+  "12FF8003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+  "12FF8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+  "12FF8005": "The filament is not inserted. Please insert the filament.",
+  "12FF8006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
+  "12FF8007": "Check nozzle. Select \"Done\" if filament was extruded, otherwise push filament forward slightly and select \"Retry.\"",
+  "12FF8010": "Please check if the filament or the spool is stuck.",
+  "12FF8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+  "12FF8012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "12FF8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+  "12FFC003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE Tube. (Connect a PTFE tube if you are about to use an AMS.)",
+  "12FFC006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
+  "18004025": "Failed to read the filament information.",
+  "18008003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "18008004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "18008005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "18008006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+  "18008007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "1800800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT A to the extruder is properly connected.",
+  "18008010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "18008011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+  "18008012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18008013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "18008016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "18008017": "AMS-HT A is drying. Please stop drying process before loading/unloading material.",
+  "18008018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18008021": "AMS setup failed; please refer to the assistant.",
+  "18008023": "AMS-HT A cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "18008026": "AMS set up failed. Please check the assistant for details.",
+  "18008027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "18008028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "18008029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "1800802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "18008030": "Hotend pre-swap check failed.",
+  "18008031": "Failed to lock the induction hotend.",
+  "18008032": "Failed to unlock the induction hotend.",
+  "18008033": "Failed to nest hotend into slot 1 on the rack.",
+  "18008034": "Failed to fetch induction hotend from slot 1 on the rack.",
+  "18008035": "Hotend Rack coarse homing failed. Please check for obstructions or if the build plate is misaligned.",
+  "18008036": "Hotend rack coarse homing failed. Please check for obstructions or if the build plate is misaligned.",
+  "1800C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1800C069": "An error occurred during AMS-HT A drying. Please go to Assistant for more details.",
+  "1800C06A": "AMS-HT A is reading RFID. Unable to start drying. Please try again later.",
+  "1800C06B": "AMS-HT A is changing filament. Unable to start drying. Please try again later.",
+  "1800C06C": "AMS-HT A is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "1800C06D": "AMS-HT A is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "1800C06E": "AMS-HT A motor is performing self-test. Unable to start drying. Please try again later.",
+  "18014025": "Failed to read the filament information.",
+  "18018003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "18018004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "18018005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "18018006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+  "18018007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "1801800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT B to the extruder is properly connected.",
+  "18018010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "18018011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+  "18018012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18018013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "18018016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "18018017": "AMS-HT B is drying. Please stop drying process before loading/unloading material.",
+  "18018018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18018021": "AMS setup failed; please refer to the assistant.",
+  "18018023": "AMS-HT B cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "18018026": "AMS set up failed. Please check the assistant for details.",
+  "18018027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "18018028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "18018029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "1801802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "18018030": "Hotend pre-swap check failed.",
+  "18018033": "Failed to nest hotend into slot 2 on the rack.",
+  "18018034": "Failed to fetch induction hotend from slot 2 on the rack.",
+  "1801C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1801C069": "An error occurred during AMS-HT B drying. Please go to Assistant for more details.",
+  "1801C06A": "AMS-HT B is reading RFID. Unable to start drying. Please try again later.",
+  "1801C06B": "AMS-HT B is changing filament. Unable to start drying. Please try again later.",
+  "1801C06C": "AMS-HT B is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "1801C06D": "AMS-HT B is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "1801C06E": "AMS-HT B motor is performing self-test. Unable to start drying. Please try again later.",
+  "18024025": "Failed to read the filament information.",
+  "18028003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "18028004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "18028005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "18028006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+  "18028007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "1802800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT C to the extruder is properly connected.",
+  "18028010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "18028011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+  "18028012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18028013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "18028016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "18028017": "AMS-HT C is drying. Please stop drying process before loading/unloading material.",
+  "18028018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18028021": "AMS setup failed; please refer to the assistant.",
+  "18028023": "AMS-HT C cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "18028026": "AMS set up failed. Please check the assistant for details.",
+  "18028027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "18028028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "18028029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "1802802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "18028030": "Hotend pre-swap check failed.",
+  "18028033": "Failed to nest hotend into slot 3 on the rack.",
+  "18028034": "Failed to fetch induction hotend from slot 3 on the rack.",
+  "1802C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1802C069": "An error occurred during AMS-HT C drying. Please go to Assistant for more details.",
+  "1802C06A": "AMS-HT C is reading RFID. Unable to start drying. Please try again later.",
+  "1802C06B": "AMS-HT C is changing filament. Unable to start drying. Please try again later.",
+  "1802C06C": "AMS-HT C is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "1802C06D": "AMS-HT C is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "1802C06E": "AMS-HT C motor is performing self-test. Unable to start drying. Please try again later.",
+  "18034025": "Failed to read the filament information.",
+  "18038003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "18038004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "18038005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "18038006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+  "18038007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "1803800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT D to the extruder is properly connected.",
+  "18038010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "18038011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+  "18038012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18038013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "18038016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "18038017": "AMS-HT D is drying. Please stop drying process before loading/unloading material.",
+  "18038018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18038021": "AMS setup failed; please refer to the assistant.",
+  "18038023": "AMS-HT D cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "18038026": "AMS set up failed. Please check the assistant for details.",
+  "18038027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "18038028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "18038029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "1803802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "18038030": "Hotend pre-swap check failed.",
+  "18038033": "Failed to nest hotend into slot 4 on the rack.",
+  "18038034": "Failed to fetch induction hotend from slot 4 on the rack.",
+  "1803C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1803C069": "An error occurred during AMS-HT D drying. Please go to Assistant for more details.",
+  "1803C06A": "AMS-HT D is reading RFID. Unable to start drying. Please try again later.",
+  "1803C06B": "AMS-HT D is changing filament. Unable to start drying. Please try again later.",
+  "1803C06C": "AMS-HT D is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "1803C06D": "AMS-HT D is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "1803C06E": "AMS-HT D motor is performing self-test. Unable to start drying. Please try again later.",
+  "18044025": "Failed to read the filament information.",
+  "18048003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "18048004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "18048005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "18048006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+  "18048007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "1804800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT E to the extruder is properly connected.",
+  "18048010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "18048011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+  "18048012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18048013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "18048016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "18048018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18048021": "AMS setup failed; please refer to the assistant.",
+  "18048023": "AMS-HT E cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "18048026": "AMS set up failed. Please check the assistant for details.",
+  "18048027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "18048028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "18048029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "1804802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "18048030": "Hotend pre-swap check failed.",
+  "18048033": "Failed to nest hotend into slot 5 on the rack.",
+  "18048034": "Failed to fetch induction hotend from slot 5 on the rack.",
+  "1804C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1804C069": "An error occurred during AMS-HT E drying. Please go to Assistant for more details.",
+  "1804C06A": "AMS-HT E is reading RFID. Unable to start drying. Please try again later.",
+  "1804C06B": "AMS-HT E is changing filament. Unable to start drying. Please try again later.",
+  "1804C06C": "AMS-HT E is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "1804C06D": "AMS-HT E is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "1804C06E": "AMS-HT E motor is performing self-test. Unable to start drying. Please try again later.",
+  "18054025": "Failed to read the filament information.",
+  "18058003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "18058004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "18058005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "18058006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+  "18058007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "1805800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT F to the extruder is properly connected.",
+  "18058010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "18058011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+  "18058012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18058013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "18058016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "18058018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18058021": "AMS setup failed; please refer to the assistant.",
+  "18058023": "AMS-HT F cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "18058026": "AMS set up failed. Please check the assistant for details.",
+  "18058027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "18058028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "18058029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "1805802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "18058030": "Hotend pre-swap check failed.",
+  "18058033": "Failed to nest hotend into slot 6 on the rack.",
+  "18058034": "Failed to fetch induction hotend from slot 6 on the rack.",
+  "1805C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1805C069": "An error occurred during AMS-HT F drying. Please go to Assistant for more details.",
+  "1805C06A": "AMS-HT F is reading RFID. Unable to start drying. Please try again later.",
+  "1805C06B": "AMS-HT F is changing filament. Unable to start drying. Please try again later.",
+  "1805C06C": "AMS-HT F is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "1805C06D": "AMS-HT F is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "1805C06E": "AMS-HT F motor is performing self-test. Unable to start drying. Please try again later.",
+  "18064025": "Failed to read the filament information.",
+  "18068003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "18068004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "18068005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "18068006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+  "18068007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "1806800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT G to the extruder is properly connected.",
+  "18068010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "18068011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+  "18068012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18068013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "18068016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "18068018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18068021": "AMS setup failed; please refer to the assistant.",
+  "18068023": "AMS-HT G cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "18068026": "AMS set up failed. Please check the assistant for details.",
+  "18068027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "18068028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "18068029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "1806802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "18068030": "Hotend pre-swap check failed.",
+  "1806C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1806C069": "An error occurred during AMS-HT G drying. Please go to Assistant for more details.",
+  "1806C06A": "AMS-HT G is reading RFID. Unable to start drying. Please try again later.",
+  "1806C06B": "AMS-HT G is changing filament. Unable to start drying. Please try again later.",
+  "1806C06C": "AMS-HT G is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "1806C06D": "AMS-HT G is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "1806C06E": "AMS-HT G motor is performing self-test. Unable to start drying. Please try again later.",
+  "18074025": "Failed to read the filament information.",
+  "18078003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+  "18078004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+  "18078005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+  "18078006": "Unable to feed filament into the extruder. This could be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+  "18078007": "Failed to extrude the filament. Please check if the extruder clogged.",
+  "1807800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT H to the extruder is properly connected.",
+  "18078010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+  "18078011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+  "18078012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18078013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+  "18078016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+  "18078018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18078021": "AMS setup failed; please refer to the assistant.",
+  "18078023": "AMS-HT H cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+  "18078026": "AMS set up failed. Please check the assistant for details.",
+  "18078027": "Filament track channel switching failed. Possible causes include filament jamming or component damage.",
+  "18078028": "Failed to feed filament to the extruder. Possible causes include an incorrect AMS–filament track switch binding, tangled filament, a stuck spool, or a disconnected PTFE tube. Please check the assistant for details.",
+  "18078029": "This print requires a Filament Track Switch, but it is currently offline. Please connect the Filament Track Switch and retry.",
+  "1807802A": "The Filament Track Switch was detected while the printer was not idle. Please unplug the Filament Track Switch and reconnect it after the printer enters the idle state.",
+  "18078030": "Hotend pre-swap check failed.",
+  "1807C008": "AMS used for the current print is disconnected. Check the connection. Printing will resume automatically after reconnection.",
+  "1807C069": "An error occurred during AMS-HT H drying. Please go to Assistant for more details.",
+  "1807C06A": "AMS-HT H is reading RFID. Unable to start drying. Please try again later.",
+  "1807C06B": "AMS-HT H is changing filament. Unable to start drying. Please try again later.",
+  "1807C06C": "AMS-HT H is in Feed Assist Mode. Unable to start drying. Please try again later.",
+  "1807C06D": "AMS-HT H is assisting in filament insertion. Unable to start drying. Please try again later.",
+  "1807C06E": "AMS-HT H motor is performing self-test. Unable to start drying. Please try again later.",
+  "18FE8001": "Failed to cut the filament of the left extruder. Please check the cutter.",
+  "18FE8002": "The cutter of the left extruder is stuck. Please pull out the cutter handle.",
+  "18FE8003": "Please pull out the filament on the spool holder  of the left extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+  "18FE8004": "Failed to pull back the filament from the left extruder. Please check whether the filament is stuck inside the extruder.",
+  "18FE8005": "Failed to feed the filament outside the AMS-HT. Please clip the end of the filament flat and check to see if the spool is stuck.",
+  "18FE8006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+  "18FE8007": "Please observe the nozzle of the left extruder. If the filament has been extruded, select \"Continue\"; if it is not, please push the filament forward slightly, and then select \"Retry\".",
+  "18FE8011": "The external filament connected to the left extruder has run out; please load a new filament.",
+  "18FE8012": "Failed to get mapping table; please select \"Resume\" to retry.",
+  "18FE8013": "Timeout purging old filament of the left extruder: Please check if the filament is stuck or the extruder is clogged.",
+  "18FE8018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18FE8020": "Extruder change failed; please refer to the assistant.",
+  "18FE8021": "AMS setup failed; please refer to the assistant.",
+  "18FE8024": "Extruder position calibration failed; please refer to the assistant.",
+  "18FEC003": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+  "18FEC006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+  "18FEC008": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+  "18FEC009": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+  "18FEC00A": "Please observe the nozzle of the left extruder. If the filament has been extruded, select \"Continue\"; if not, please push the filament forward slightly and then select \"Retry\".",
+  "18FF8001": "Failed to cut the filament of the right extruder. Please check the cutter.",
+  "18FF8002": "The cutter of the right extruder is stuck. Please pull out the cutter handle.",
+  "18FF8003": "Please pull out the filament on the spool holder  of the right extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+  "18FF8004": "Failed to pull back the filament from the right extruder. Please check whether the filament is stuck inside the extruder.",
+  "18FF8005": "Failed to feed the filament outside the AMS-HT. Please clip the end of the filament flat and check to see if the spool is stuck.",
+  "18FF8006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+  "18FF8007": "Please observe the nozzle of the right extruder. If the filament has been extruded, select \"Continue\"; if it is not, please push the filament forward slightly, and then select \"Retry\".",
+  "18FF8011": "The external filament connected to the right extruder has run out; please load a new filament.",
+  "18FF8012": "Failed to get AMS mapping table; please select \"Resume\" to retry.",
+  "18FF8013": "Timeout purging old filament of the right extruder: Please check if the filament is stuck or the extruder is clogged.",
+  "18FF8018": "Failed to get filament-hotend mapping table from the slicing file, please retry.",
+  "18FF8020": "Extruder change failed; please refer to the assistant.",
+  "18FF8021": "AMS setup failed; please refer to the assistant.",
+  "18FF8024": "Extruder position calibration failed; please refer to the assistant.",
+  "18FFC003": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+  "18FFC006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+  "18FFC008": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+  "18FFC009": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+  "18FFC00A": "Please observe the nozzle of the right extruder. If the filament has been extruded, select \"Continue\"; if not, please push the filament forward slightly and then select \"Retry\".",
+  "1A004001": "Hotend rack coarse homing failed. Printing has been canceled. Please check if any obstruction is blocking the rack movement.",
+  "1A004007": "Induction hotend rack calibration failed, please check the Assistant.",
+  "1A004008": "The hotend rack is fully occupied, and hotend switching cannot be performed. Please remove one hotend before proceeding.",
+  "1A004009": "The hotend rack has not been set up. Please complete the setup before performing a hotend replacement.",
+  "1A008002": "Induction Hotend Rack homing failed due to an obstruction. Please remove the obstacle before resuming printing.",
+  "1A008003": "The Hotend Rack homing distance is abnormally long. Please check whether the timing belt is loose or broken.",
+  "1A008004": "Before using Laser Mode or calibrating the Hotend Rack position, please remove all hotends from the rack."
+ },
+ "error_by_model": {
+  "093": {
+   "0300801E": "The extrusion motor is overloaded, please check if the extruder is clogged or the filament is stuck in the tool head.",
+   "05008053": "Nozzle mismatch was detected during printing. Please initiate the print after re-slicing, or continue printing after replacing the correct nozzle. Caution: the hotend temperature is high.",
+   "05024007": "",
+   "05024008": "",
+   "07FF8007": "Please observe the nozzle. If the filament has been extruded, select \"Continue\"; if it is not, please push the filament forward slightly, and then select \"Retry\".",
+   "07FFC011": "Hold the driven wheel bracket, slowly pull the filament from the extruder, then press 'Continue'.",
+   "18FF8001": "Failed to cut the filament. Please check the cutter."
+  },
+  "094": {
+   "0300801E": "The extrusion motor is overloaded, please check if the extruder is clogged or the filament is stuck in the tool head.",
+   "05004007": "Print jobs are not allowed to be sent while force updating or when repair updating is required.",
+   "05004031": "An uncertified accessory was detected, and the device cannot continue working. Please use Bambu Lab accessories or upgrade to the latest firmware.",
+   "05004033": "Uncertified AMS detected or AMS firmware not upgraded: the device cannot continue working.",
+   "05004034": "Laser Module is not certified; the device cannot continue working. Please use Bambu Lab accessories or upgrade to the latest firmware.",
+   "05004035": "BirdsEye Camera is not certified; the device cannot continue working. Please use Bambu Lab accessories or upgrade to the latest firmware.",
+   "05004040": "Due to power limitations, the AMS is not allowed to use the device's power for drying during printing. Should the drying be stopped?",
+   "05004041": "Due to power limitations, only one AMS is allowed to use the device's power for drying during printing. Should the drying be stopped?",
+   "05004042": "The AMS is currently being used for printing and cannot initiate drying.",
+   "05008062": "The print plate type is unknown; please replace it with the correct plate.",
+   "0500806A": "Unable to recognize the left and right hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please set hotend type before next print.",
+   "0500806B": "Quick-release Lever is not locked. Please press it down to secure it.",
+   "05008079": "Please follow the instruction to place the Laser Test Material(350g paperboard).",
+   "05008085": "Nozzle camera is obstructed",
+   "05008086": "Nozzle camera lens is dirty, which may affect the AI monitoring functionality. Please clean the surface of the nozzle camera lens as soon as possible.",
+   "0500808B": "The BirdsEye Camera setup failed. Please clear the heated bed, remove all objects and the mat, and ensure the heatbed markers are visible. Meanwhile, clean BirdsEye Camera and Toolhead Camera, and remove any obstructions that may block the cameras.",
+   "05024002": "The printer does not have motion precision enhancement calibration data; the motion precision enhancement cannot be turned on.",
+   "05FE806A": "Unable to recognize the left hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please set hotend type before next print.",
+   "05FF806A": "Unable to recognize the right hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please set hotend type before next print.",
+   "07008006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07008023": "The cooling process after drying has failed in AMS A.",
+   "07018006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07018023": "The cooling process after drying has failed in AMS B.",
+   "07028006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07028023": "The cooling process after drying has failed in AMS C.",
+   "07038006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07038023": "The cooling process after drying has failed in AMS D.",
+   "07048006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07048023": "The cooling process after drying has failed in AMS E.",
+   "07058006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07058023": "The cooling process after drying has failed in AMS F.",
+   "07068006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07068023": "The cooling process after drying has failed in AMS G.",
+   "07078006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07078023": "The cooling process after drying has failed in AMS H.",
+   "07FE8025": "Cold pulling timed out. Please promptly operate or check whether the filament is broken inside the extruder, and click the Assistant for details.",
+   "07FF8001": "Failed to cut the filament of the right extruder. Please check the cutter.",
+   "07FF8002": "The cutter of the right extruder is stuck. Please pull out the cutter handle.",
+   "07FF8003": "Please pull out the filament on the spool holder  of the right extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+   "07FF8004": "Failed to pull back the filament from the right extruder. Please check whether the filament is stuck inside the extruder.",
+   "07FF8006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+   "07FF8010": "Check if the right external filament spool or filament is stuck.",
+   "07FF8011": "The external filament connected to the right extruder has run out; please load a new filament.",
+   "07FF8025": "Cold pulling timed out. Please promptly operate or check whether the filament is broken inside the extruder, and click the Assistant for details.",
+   "07FFC003": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "07FFC006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+   "07FFC008": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "07FFC009": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+   "07FFC00A": "Please observe the nozzle of the right extruder. If the filament has been extruded, select \"Continue\"; if not, please push the filament forward slightly and then select \"Retry\".",
+   "07FFC010": "Insert the filament until it can not be pushed any farther. There might be slight smoke during purging, so please close the front door and top cover after insertion.",
+   "0C00402A": "The visual marker was not detected. Please re-paste the paper in the correct position. Meanwhile, please clean the Toolhead Camera to prevent contamination and remove any objects that may obstruct its view.",
+   "18008006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18008023": "The cooling process after drying has failed in AMS-HT A.",
+   "18018006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18018023": "The cooling process after drying has failed in AMS-HT B.",
+   "18028006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18028023": "The cooling process after drying has failed in AMS-HT C.",
+   "18038006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18038023": "The cooling process after drying has failed in AMS-HT D.",
+   "18048006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18048023": "The cooling process after drying has failed in AMS-HT E.",
+   "18058006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18058023": "The cooling process after drying has failed in AMS-HT F.",
+   "18068006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18068023": "The cooling process after drying has failed in AMS-HT G.",
+   "18078006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18078023": "The cooling process after drying has failed in AMS-HT H."
+  },
+  "20P": {
+   "03008001": "Printing was paused by the user. You can select \"Resume\" to continue printing.",
+   "03008003": "Spaghetti defects were detected by the AI Print Monitoring. Please check the quality of the printed model before continuing your print. Cleaning the build plate or drying the filament can effectively reduce the risk of spaghetti failure.",
+   "03008008": "Nozzle temperature malfunction. Check the Assistant page and resolve the issue before retrying heating.",
+   "0300800C": "Multiple motion steps lost detected. Please check if the toolhead is blocked by obstructions or making abnormal noise. You may continue printing to observe. If the print shows layer shifting, refer to the Wiki for maintenance.",
+   "03008013": "Printing paused due to the pause command added to the printing file.",
+   "03008019": "Build plate not detected. Please ensure there are no bulges or debris on the plate and that the nozzle is clean.",
+   "0300801C": "The extrusion resistance is abnormal. The extruder may be clogged; please refer to the assistant. After trouble shooting, you can select \"Resume\" to resume the print job.",
+   "03008022": "During subsequent printing, the heatbed may encounter resistance while lowering. Please clear any foreign objects from beneath the heatbed and observe whether its movement is obstructed.",
+   "03008042": "Task paused because the door is open.",
+   "0500403C": "The left and right nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
+   "05004042": "Printing and calibration cannot be performed while the AMS is drying. Please stop the drying process or connect a power adapter to any AMS unit not used for the current print, then try again.",
+   "05008072": "The Live View Camera is obstructed. Please remove the obstruction before continuing.",
+   "05008091": "The Cutting Module offset calibration failed, which may result in inaccurate cuts. Please ensure the 80g white printer paper(letter paper thickness) is properly positioned; if the blade tip is worn, replace it. If the issue persists after checking, restart the device and try again.",
+   "05024007": "",
+   "05024008": "",
+   "0502C010": "Due to power limits, printing or calibration can’t run during AMS drying. Stop drying or connect a power adapter to continue.",
+   "05844096": "The device cannot detect AMS-HT E. Please reconnect the AMS-HT cable or restart the printer.",
+   "05854096": "The device cannot detect AMS-HT F. Please reconnect the AMS-HT cable or restart the printer.",
+   "05FF8053": "The right (Aux) nozzle is not matched with slicing file. Please initiate the print after re-slicing, or continue printing after replacing the correct nozzle. Caution: the hotend temperature is high.",
+   "05FF8069": "Unable to recognize the right (Aux) hotend. It might be a non-official hotend, or the hotend mark could be dirty. Please manually set the hotend type.",
+   "05FF8080": "The right (Aux) hotend is not installed.",
+   "05FF8081": "The right (Aux) hotend is not installed.",
+   "07008006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07008007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07018006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07018007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07028006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07028007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07038006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07038007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07048006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07048007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07058006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07058007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07068006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07068007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07078006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07078007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07FE8003": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+   "07FEC010": "Insert the filament (over 30cm long) until it stops. You might see slight smoke during flushing. After insertion, close the front door and top cover.",
+   "07FF8001": "Failed to cut the filament of the right (Aux) extruder. Please check the cutter.",
+   "07FF8002": "The cutter of the right (Aux) extruder is stuck. Please pull out the cutter handle.",
+   "07FF8003": "Please pull out the filament on the spool holder of the right (Aux) extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+   "07FF8004": "Failed to pull back the filament from the right (Aux) extruder. Please check whether the filament is stuck inside the extruder.",
+   "07FF8006": "Please feed filament into the PTFE tube of the right (Aux) extruder until it can not be pushed any farther.",
+   "07FF8007": "Please observe the nozzle of the right (Aux) extruder. If the filament has been extruded, select \"Continue\"; if it is not, please push the filament forward slightly, and then select \"Retry\".",
+   "07FF8010": "Check if the right (Aux) external filament spool or filament is stuck.",
+   "07FF8011": "The external filament connected to the right (Aux) extruder has run out; please load a new filament.",
+   "07FF8013": "Timeout purging old filament of the right (Aux) extruder: Please check if the filament is stuck or the extruder is clogged.",
+   "07FFC003": "Please pull out the filament on the spool holder of the right (Aux) extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "07FFC006": "Please feed filament into the PTFE tube of the right (Aux) extruder until it can not be pushed any farther.",
+   "07FFC008": "Please pull out the filament on the spool holder of the right (Aux) extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "07FFC009": "Please feed filament into the PTFE tube of the right (Aux) extruder until it can not be pushed any farther.",
+   "07FFC00A": "Please observe the nozzle of the right (Aux) extruder. If the filament has been extruded, select \"Continue\"; if not, please push the filament forward slightly and then select \"Retry\".",
+   "07FFC010": "Insert filament (longer than 90 cm) into the Auxiliary Extruder inlet until it cannot be pushed further. During flushing, slight smoke may occur. Please close the front door and top cover after insertion.",
+   "07FFC012": "Please press the PTFE tube coupler onto the auxiliary extruder's filament inlet and pull the PTFE tube out. Click \"Continue\" after the operation.",
+   "0C00C004": "Possible spaghetti failure was detected. Cleaning the build plate or drying the filament can effectively reduce the risk of spaghetti failure.",
+   "18008006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18008007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18018006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18018007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18028006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18028007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18038006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18038007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18048006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18048007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18058006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18058007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18068006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18068007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18078006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18078007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18FE8003": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+   "18FF8001": "Failed to cut the filament of the right (Aux) extruder. Please check the cutter.",
+   "18FF8002": "The cutter of the right (Aux) extruder is stuck. Please pull out the cutter handle.",
+   "18FF8003": "Please pull out the filament on the spool holder of the right (Aux) extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+   "18FF8004": "Failed to pull back the filament from the right (Aux) extruder. Please check whether the filament is stuck inside the extruder.",
+   "18FF8006": "Please feed filament into the PTFE tube of the right (Aux) extruder until it can not be pushed any farther.",
+   "18FF8007": "Please observe the nozzle of the right (Aux) extruder. If the filament has been extruded, select \"Continue\"; if it is not, please push the filament forward slightly, and then select \"Retry\".",
+   "18FF8011": "The external filament connected to the right (Aux) extruder has run out; please load a new filament.",
+   "18FF8013": "Timeout purging old filament of the right (Aux) extruder: Please check if the filament is stuck or the extruder is clogged.",
+   "18FFC003": "Please pull out the filament on the spool holder of the right (Aux) extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "18FFC006": "Please feed filament into the PTFE tube of the right (Aux) extruder until it can not be pushed any farther.",
+   "18FFC008": "Please pull out the filament on the spool holder of the right (Aux) extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "18FFC009": "Please feed filament into the PTFE tube of the right (Aux) extruder until it can not be pushed any farther.",
+   "18FFC00A": "Please observe the nozzle of the right (Aux) extruder. If the filament has been extruded, select \"Continue\"; if not, please push the filament forward slightly and then select \"Retry\"."
+  },
+  "22E": {
+   "0300401F": "The hotend is not installed, and the toolhead cannot perform homing. Please install the hotend and then continue.",
+   "03008042": "Task paused because the door is open.",
+   "0500803C": "The current nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
+   "05008053": "Nozzle mismatch was detected during printing. Please initiate the print after re-slicing, or continue printing after replacing the correct nozzle. Caution: the hotend temperature is high.",
+   "05024007": "",
+   "05024008": "",
+   "07008007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07018007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07028007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07038007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07048007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07058007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07068007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07078007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07FF8007": "Please observe the nozzle. If the filament has been extruded, select \"Continue\"; if it is not, please push the filament forward slightly, and then select \"Retry\".",
+   "07FFC012": "Remove the front cover and hang it on the X-axis. Press the black PTFE tube coupler and unplug the PTFE tube. After completing the operation, click \"Continue.\"",
+   "18008007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18018007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18028007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18038007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18048007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18058007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18068007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18078007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18FF8001": "Failed to cut the filament. Please check the cutter."
+  },
+  "239": {
+   "0300801E": "The extrusion motor is overloaded, please check if the extruder is clogged or the filament is stuck in the tool head.",
+   "05004097": "Uncertified Laser Module detected; the device cannot continue working. Please reconnect the module cable or restart the printer.",
+   "05004098": "Uncertified AMS A detected; the device cannot continue working. Please reconnect the AMS cable or restart the printer.",
+   "0500409E": "Uncertified Cutting Module detected; the device cannot continue working. Please reconnect the module cable or restart the printer.",
+   "0500409F": "Uncertified Air Pump detected; the device cannot continue working.  Please reconnect the module cable or restart the printer.",
+   "05008062": "The print plate marker was not detected. Please confirm the print plate is correctly positioned on the heatbed with all four corners aligned, and the marker is visible.",
+   "0500808B": "The BirdsEye Camera setup failed. Please clear the heatbed, remove all objects and the mat, and ensure the heatbed markers are visible. Meanwhile, clean the BirdsEye Camera and Toolhead Camera, and remove any obstructions that may block the cameras.",
+   "0500808D": "The Cutting Module offset calibration failed, which may result in inaccurate cuts. Please ensure the 80g white printer paper is properly positioned and check whether the cutting blade tip is worn.",
+   "05014098": "Uncertified AMS B detected; the device cannot continue working. Please reconnect the AMS cable or restart the printer.",
+   "05024002": "The printer does not have motion precision enhancement calibration data; the motion precision enhancement cannot be turned on.",
+   "05024007": "",
+   "05024008": "",
+   "05024098": "Uncertified AMS C detected; the device cannot continue working. Please reconnect the AMS cable or restart the printer.",
+   "05034098": "Uncertified AMS D detected; the device cannot continue working. Please reconnect the AMS cable or restart the printer.",
+   "05804096": "Uncertified AMS-HT A detected; the device cannot continue working. Please reconnect the AMS-HT cable or restart the printer.",
+   "05814096": "Uncertified AMS-HT B detected; the device cannot continue working. Please reconnect the AMS-HT cable or restart the printer.",
+   "05824096": "Uncertified AMS-HT C detected; the device cannot continue working. Please reconnect the AMS-HT cable or restart the printer.",
+   "05834096": "Uncertified AMS-HT D detected; the device cannot continue working. Please reconnect the AMS-HT cable or restart the printer.",
+   "05844096": "Uncertified AMS-HT F detected; the device cannot continue working. Please reconnect the AMS-HT cable or restart the printer.",
+   "05854096": "Uncertified AMS-HT E detected; the device cannot continue working. Please reconnect the AMS-HT cable or restart the printer.",
+   "05864096": "Uncertified AMS-HT G detected; the device cannot continue working. Please reconnect the AMS-HT cable or restart the printer.",
+   "05874096": "Uncertified AMS-HT H detected; the device cannot continue working. Please reconnect the AMS-HT cable or restart the printer.",
+   "07008006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07008023": "The cooling process after drying has failed in AMS A.",
+   "07018006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07018023": "The cooling process after drying has failed in AMS B.",
+   "07028006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07028023": "The cooling process after drying has failed in AMS C.",
+   "07038006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07038023": "The cooling process after drying has failed in AMS D.",
+   "07048006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07048023": "The cooling process after drying has failed in AMS E.",
+   "07058006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07058023": "The cooling process after drying has failed in AMS F.",
+   "07068006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07068023": "The cooling process after drying has failed in AMS G.",
+   "07078006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07078023": "The cooling process after drying has failed in AMS H.",
+   "07FF8001": "Failed to cut the filament of the right extruder. Please check the cutter.",
+   "07FF8002": "The cutter of the right extruder is stuck. Please pull out the cutter handle.",
+   "07FF8003": "Please pull out the filament on the spool holder  of the right extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+   "07FF8004": "Failed to pull back the filament from the right extruder. Please check whether the filament is stuck inside the extruder.",
+   "07FF8006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+   "07FF8010": "Check if the right external filament spool or filament is stuck.",
+   "07FF8011": "The external filament connected to the right extruder has run out; please load a new filament.",
+   "07FFC003": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "07FFC006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+   "07FFC008": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "07FFC009": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+   "07FFC00A": "Please observe the nozzle of the right extruder. If the filament has been extruded, select \"Continue\"; if not, please push the filament forward slightly and then select \"Retry\".",
+   "07FFC010": "Insert the filament until it can not be pushed any farther. There might be slight smoke during purging, so please close the front door and top cover after insertion.",
+   "18008006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18008023": "The cooling process after drying has failed in AMS-HT A.",
+   "18018006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18018023": "The cooling process after drying has failed in AMS-HT B.",
+   "18028006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18028023": "The cooling process after drying has failed in AMS-HT C.",
+   "18038006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18038023": "The cooling process after drying has failed in AMS-HT D.",
+   "18048006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18048023": "The cooling process after drying has failed in AMS-HT E.",
+   "18058006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18058023": "The cooling process after drying has failed in AMS-HT F.",
+   "18068006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18068023": "The cooling process after drying has failed in AMS-HT G.",
+   "18078006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18078023": "The cooling process after drying has failed in AMS-HT H."
+  },
+  "26A": {
+   "0300401F": "The hotend is not installed, and the toolhead cannot perform homing. Please install the hotend and then continue.",
+   "03004030": "Z-axis homing failed. Remove the heated bed slider fixing screws and ensure no obstructions are blocking the heated bed's vertical movement. After resolving the issue, recalibrate the system.",
+   "03004033": "Z-axis homing failed due to a temperature control abnormality. Scan the QR code for assistance. After resolving the issue, recalibrate the device.",
+   "03004035": "Nozzle cleaning failed. Please scan the QR code for assistance. After resolving the issue, recalibrate.",
+   "03004038": "Heatbed leveling failed. Please scan the QR code for assistance. After resolving the issue, recalibrate.",
+   "03008001": "Printing was paused by the user. You can select \"Resume\" to continue printing.",
+   "03008013": "Printing paused due to the pause command added to the printing file.",
+   "03008014": "Nozzle clumping detected. If clumping is confirmed, cancel the print and clean the nozzle; if not, click “Resume”.",
+   "03008019": "Build plate not detected. Please ensure there are no bulges or debris on the plate and that the nozzle is clean.",
+   "0300801C": "The extrusion resistance is abnormal. The extruder may be clogged; please refer to the assistant. After trouble shooting, you can select \"Resume\" to resume the print job.",
+   "03008062": "The ambient temperature is too high; please lower it.",
+   "0500402E": "The system does not support the file system currently used by the MicroSD card. Please replace the MicroSD card or format it to FAT32.",
+   "0500402F": "The MicroSD card sector data is damaged. Please use the SD card repair tool to repair or format it. If it still cannot be identified, please replace the MicroSD card.",
+   "05004042": "Printing and calibration cannot be performed while the AMS is drying. Please stop the drying process or connect a power adapter to any AMS unit not used for the current print, then try again.",
+   "05004095": "No print plate detected. Please place it correctly and recalibrate.",
+   "0500C010": "MicroSD Card read/write exception: please reinsert or replace the MicroSD Card.",
+   "05024007": "",
+   "05024008": "",
+   "07008007": "Extruding filament failed. The extruder might be clogged.",
+   "07018007": "Extruding filament failed. The extruder might be clogged.",
+   "07028007": "Extruding filament failed. The extruder might be clogged.",
+   "07038007": "Extruding filament failed. The extruder might be clogged.",
+   "07048007": "Extruding filament failed. The extruder might be clogged.",
+   "07058007": "Extruding filament failed. The extruder might be clogged.",
+   "07068007": "Extruding filament failed. The extruder might be clogged.",
+   "07078007": "Extruding filament failed. The extruder might be clogged.",
+   "07FF8007": "Please observe the nozzle. If the filament has been extruded, select \"Done\"; if not, please push the filament forward slightly, and then select \"Retry\".",
+   "07FF8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+   "18008007": "Extruding filament failed. The extruder might be clogged.",
+   "18018007": "Extruding filament failed. The extruder might be clogged.",
+   "18028007": "Extruding filament failed. The extruder might be clogged.",
+   "18038007": "Extruding filament failed. The extruder might be clogged.",
+   "18048007": "Extruding filament failed. The extruder might be clogged.",
+   "18058007": "Extruding filament failed. The extruder might be clogged.",
+   "18068007": "Extruding filament failed. The extruder might be clogged.",
+   "18078007": "Extruding filament failed. The extruder might be clogged."
+  },
+  "31B": {
+   "03008001": "Printing was paused by the user. You can select \"Resume\" to continue printing.",
+   "03008013": "Printing paused due to the pause command added to the printing file.",
+   "05024007": "",
+   "05024008": "",
+   "07008006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07008007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07018006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07018007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07028006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07028007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07038006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07038007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07048006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07048007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07058006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07058007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07068006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07068007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07078006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+   "07078007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "07FEC010": "Insert the filament (over 30cm long) until it stops. You might see slight smoke during flushing. After insertion, close the front door and top cover.",
+   "07FF8001": "Failed to cut the filament of the right extruder. Please check the cutter.",
+   "07FF8002": "The cutter of the right extruder is stuck. Please pull out the cutter handle.",
+   "07FF8003": "Please pull out the filament on the spool holder  of the right extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are about to use an AMS.)",
+   "07FF8004": "Failed to pull back the filament from the right extruder. Please check whether the filament is stuck inside the extruder.",
+   "07FF8006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+   "07FF8010": "Check if the right external filament spool or filament is stuck.",
+   "07FF8011": "The external filament connected to the right extruder has run out; please load a new filament.",
+   "07FFC003": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "07FFC006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+   "07FFC008": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to use an AMS)",
+   "07FFC009": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+   "07FFC00A": "Please observe the nozzle of the right extruder. If the filament has been extruded, select \"Continue\"; if not, please push the filament forward slightly and then select \"Retry\".",
+   "18008006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18008007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18018006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18018007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18028006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18028007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18038006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18038007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18048006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18048007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18058006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18058007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18068006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18068007": "Failed to extrude the filament. Please check Assistant for more details.",
+   "18078006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please check if the AMS-HT PTFE tube is connected.",
+   "18078007": "Failed to extrude the filament. Please check Assistant for more details."
+  }
+ }
+}

+ 29 - 0
backend/app/data/hms_catalog_extra.json

@@ -0,0 +1,29 @@
+{
+ "_comment": "print_error texts Bambu Studio does not ship, kept from the ha-bambulab table Bambuddy used before #2728. scripts/generate_hms_catalog.py adds them only where Studio has no entry. Keys are the 8-hex print_error value.",
+ "error": {
+  "03008002": "First layer defects were detected by the Micro Lidar. Please check the quality of the printed model before continuing your print.",
+  "03008004": "Filament ran out. Please load new filament.",
+  "03008006": "The build plate marker was not detected. Please confirm the build plate is correctly positioned on the heatbed with all four corners aligned, and the marker is visible.",
+  "03008011": "Detected build plate is not the same as the Gcode file. Please adjust slicer settings or use the correct plate.",
+  "0300801B": "Nozzle temperature problem detected. Refer to Assistant to re-connect the hotend connector. POWER OFF the printer before this operation to avoid short circuits.",
+  "03008057": "The rotary attachment is disconnected. Please ensure it is properly installed and the cable is securely plugged in.",
+  "03008058": "The rotary attachment is detected. Please remove it before continuing.",
+  "050040A3": "AMS(or AMS lite) A communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "050040A4": "The current firmware only supports 1 AMS Lite. Please remove all AMS units before reconnecting the supported AMS Lite device.",
+  "050040A5": "The current firmware only supports AMS/AMS 2 Pro/AMS HT, with a maximum of 4 units. Please remove all AMS units before reconnecting the supported one.",
+  "05008048": "The module on the toolhead is not calibrated. Please cancel the task to perform calibration or switch to a calibrated module.",
+  "050140A3": "AMS(or AMS lite) B communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "050240A3": "AMS(or AMS lite) C communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "050340A3": "AMS(or AMS lite) D communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "058040A2": "AMS-HT A communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "058140A2": "AMS-HT B communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "058240A2": "AMS-HT C communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "058340A2": "AMS-HT D communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "058440A2": "AMS-HT E communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "058540A2": "AMS-HT F communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "058640A2": "AMS-HT G communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "058740A2": "AMS-HT H communication is abnormal. Please reconnect the module cable or restart the printer.",
+  "0C008001": "First layer defects were detected. If the defects are acceptable, select 'Resume' to resume the print job.",
+  "0C00C003": "Possible defects were detected in the first layer."
+ }
+}

File diff suppressed because it is too large
+ 688 - 94
backend/app/main.py


+ 10 - 0
backend/app/models/__init__.py

@@ -1,9 +1,11 @@
 from backend.app.models.ams_history import AMSSensorHistory
 from backend.app.models.ams_label import AmsLabel
+from backend.app.models.announcement import Announcement, AnnouncementRead
 from backend.app.models.api_key import APIKey
 from backend.app.models.archive import PrintArchive
 from backend.app.models.auth_ephemeral import AuthEphemeralToken, AuthRateLimitEvent
 from backend.app.models.color_catalog import ColorCatalogEntry
+from backend.app.models.connected_app import ConnectedApp, ConnectedAppGrant
 from backend.app.models.filament import Filament
 from backend.app.models.github_backup import GitHubBackupConfig, GitHubBackupLog
 from backend.app.models.group import Group, user_groups
@@ -38,6 +40,7 @@ from backend.app.models.spool_filament_preset import SpoolFilamentPreset, Spoolm
 from backend.app.models.spool_k_profile import SpoolKProfile
 from backend.app.models.spool_usage_history import SpoolUsageHistory
 from backend.app.models.spoolbuddy_device import SpoolBuddyDevice
+from backend.app.models.supplier import SpoolmanSpoolSupplier, SpoolSupplier, Supplier
 from backend.app.models.user import User
 from backend.app.models.user_email_pref import UserEmailPreference
 from backend.app.models.user_otp_code import UserOTPCode
@@ -89,13 +92,20 @@ __all__ = [
     "SpoolAssignment",
     "SpoolCatalogEntry",
     "SpoolUsageHistory",
+    "Supplier",
+    "SpoolSupplier",
+    "SpoolmanSpoolSupplier",
     "ColorCatalogEntry",
     "SpoolBuddyDevice",
     "SponsorToastState",
+    "Announcement",
+    "AnnouncementRead",
     "UserEmailPreference",
     "UserOTPCode",
     "UserTOTP",
     "AuthEphemeralToken",
     "AuthRateLimitEvent",
     "LongLivedToken",
+    "ConnectedApp",
+    "ConnectedAppGrant",
 ]

+ 51 - 0
backend/app/models/announcement.py

@@ -0,0 +1,51 @@
+"""Announcements from the Bambuddy maintainers, and who has read them.
+
+Fetched from a signed feed on GitHub (services/announcements.py). Only the ones
+that target this install are stored, and the feed is authoritative: a message
+withdrawn upstream is deleted here on the next fetch, together with its read
+markers.
+
+``AnnouncementRead.user_id`` is nullable: with authentication off there are no
+users, and the install's read state is a single NULL-keyed row per message.
+"""
+
+from __future__ import annotations
+
+from datetime import datetime
+
+from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func
+from sqlalchemy.orm import Mapped, mapped_column
+
+from backend.app.core.database import Base
+
+
+class Announcement(Base):
+    __tablename__ = "announcements"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    # The feed's id for the message. Stable across edits upstream, so a corrected
+    # typo does not come back as unread.
+    public_id: Mapped[str] = mapped_column(String(64), unique=True, index=True)
+    level: Mapped[str] = mapped_column(String(16))  # info | important | critical
+    # JSON {"en": {"title", "body", "link_label"?}, ...}. Text for SQLite/Postgres
+    # uniformity; the service serialises with json.dumps.
+    texts: Mapped[str] = mapped_column(Text)
+    link_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
+    # Naive UTC, like every other timestamp in Bambuddy.
+    published_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
+
+
+class AnnouncementRead(Base):
+    __tablename__ = "announcement_reads"
+    __table_args__ = (UniqueConstraint("announcement_id", "user_id", name="uq_announcement_reads_user"),)
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    announcement_id: Mapped[int] = mapped_column(
+        Integer, ForeignKey("announcements.id", ondelete="CASCADE"), index=True
+    )
+    user_id: Mapped[int | None] = mapped_column(
+        Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True, index=True
+    )
+    read_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())

+ 4 - 0
backend/app/models/api_key.py

@@ -51,6 +51,10 @@ class APIKey(Base):
     # granting full SETTINGS_UPDATE (which is denied for API keys because it
     # could rewrite SMTP/LDAP/MQTT credentials).
     can_update_energy_cost: Mapped[bool] = mapped_column(Boolean, default=False)
+    # Send a message through the notification channels that accept app
+    # messages (POST /notifications/app-message). Nothing else: no reading or
+    # changing the channels themselves.
+    can_send_notifications: Mapped[bool] = mapped_column(Boolean, default=False)
 
     # Optional scope limits
     printer_ids: Mapped[list | None] = mapped_column(JSON, nullable=True)  # null = all printers

+ 35 - 0
backend/app/models/archive.py

@@ -106,6 +106,41 @@ class PrintArchive(Base):
     failure_reason: Mapped[str | None] = mapped_column(String(100))  # For failed prints
     quantity: Mapped[int] = mapped_column(Integer, default=1)  # Number of items printed
 
+    # Post-print outcome confirmation (#1898). user_verdict is the USER's
+    # quality judgement ('good' / 'reject'), deliberately orthogonal to the
+    # machine-reported `status`: completed + reject means "printer finished
+    # it, part is scrap". confirm_requested is copied from the queue item's
+    # opt-in flag at dispatch (like plate_id) and drives the prompt + the
+    # "unconfirmed" badge; confirm_token is a per-archive capability for the
+    # one-tap verdict links in push notifications, minted when the prompt
+    # fires. A landed verdict RETIRES the token by stamping
+    # confirm_token_used_at rather than clearing the value: the link stays
+    # resolvable so a second tap can say "already answered, here is what was
+    # recorded" instead of the bare "invalid or already used" 404.
+    # user_verdict_source records how the verdict arrived ('dialog', 'link',
+    # 'plate_clear', 'printer_card', 'api', 'reaction') so the UI can explain
+    # a verdict nobody remembers giving.
+    user_verdict: Mapped[str | None] = mapped_column(String(10), nullable=True)
+    user_verdict_source: Mapped[str | None] = mapped_column(String(16), nullable=True)
+    # When the verdict on file was recorded (#1898). Written with every verdict,
+    # unlike `confirm_token_used_at`, which marks the one moment the one-tap
+    # capability was spent — a verdict changed later in the app must not be
+    # dated by that older event.
+    user_verdict_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    # Nullable to match the ALTER that adds it to an existing install: a fresh
+    # database would otherwise get NOT NULL while an upgraded one gets a
+    # nullable column, and the two would disagree about the same table. The
+    # default still means every row written by Bambuddy is True or False; only
+    # `is_(True)` and truthiness read it, both of which treat NULL as off.
+    confirm_requested: Mapped[bool | None] = mapped_column(Boolean, nullable=True, default=False, server_default="0")
+    # Indexed and unique: the one-tap route is reachable with no credential
+    # at all, so an unindexed lookup would let anyone turn a stream of
+    # garbage tokens into a stream of full scans of this table. Uniqueness
+    # costs nothing (the only writer is secrets.token_urlsafe(32)) and keeps
+    # scalar_one_or_none from ever raising MultipleResultsFound.
+    confirm_token: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True, index=True)
+    confirm_token_used_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None)
+
     # Energy tracking
     energy_kwh: Mapped[float | None] = mapped_column(Float)  # Energy consumed in kWh
     energy_cost: Mapped[float | None] = mapped_column(Float)  # Cost of energy consumed

+ 27 - 0
backend/app/models/auth_ephemeral.py

@@ -42,6 +42,7 @@ class TokenType(str, Enum):
     PASSWORD_RESET = "password_reset"
     EMAIL_OTP_SETUP = "email_otp_setup"
     SLICER_DOWNLOAD = "slicer_download"
+    CONNECT_CODE = "connect_code"
 
 
 class EventType(str, Enum):
@@ -57,6 +58,8 @@ class EventType(str, Enum):
     LOGIN_IP = "login_ip"
     PASSWORD_RESET_SEND = "password_reset_send"
     PASSWORD_RESET_IP = "password_reset_ip"
+    CONNECT_TOKEN_CLIENT = "connect_client"
+    CONNECT_TOKEN_IP = "connect_ip"
 
 
 class AuthEphemeralToken(Base):
@@ -183,6 +186,30 @@ class AuthEphemeralToken(Base):
             expires_at=expires_at,
         )
 
+    @classmethod
+    def new_connect_code(
+        cls,
+        code_hash: str,
+        username: str,
+        app_id: int,
+        code_challenge: str,
+        expires_at: datetime,
+    ) -> AuthEphemeralToken:
+        """Create a connected-app authorization code.
+
+        Only the SHA-256 of the code is stored (``token``), so a database read
+        does not yield a usable code. Field reuse: ``provider_id`` holds the
+        connected app's id and ``nonce`` the PKCE S256 challenge.
+        """
+        return cls(
+            token=code_hash,
+            token_type=TokenType.CONNECT_CODE,
+            username=username,
+            provider_id=app_id,
+            nonce=code_challenge,
+            expires_at=expires_at,
+        )
+
 
 class AuthRateLimitEvent(Base):
     """Timestamped events used for sliding-window rate limiting."""

+ 48 - 0
backend/app/models/connected_app.py

@@ -0,0 +1,48 @@
+"""Connected apps: external applications that sign users in with Bambuddy.
+
+A connected app is registered by an admin with one exact callback URL. It
+signs a user in through a minimal OAuth 2.0 authorization-code flow with PKCE
+(see ``api/routes/connected_apps.py``): Bambuddy hands the app a single-use,
+60-second code, and the app's server swaps it for the user's identity and
+permissions. The app never sees the user's Bambuddy login token.
+"""
+
+from datetime import datetime
+
+from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, func
+from sqlalchemy.orm import Mapped, mapped_column
+
+from backend.app.core.database import Base
+
+
+class ConnectedApp(Base):
+    __tablename__ = "connected_apps"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    name: Mapped[str] = mapped_column(String(100))
+    # Public identifier the app sends on every request.
+    client_id: Mapped[str] = mapped_column(String(64), unique=True, index=True)
+    # bcrypt hash; the secret itself is shown once, at creation or rotation.
+    client_secret_hash: Mapped[str] = mapped_column(String(255))
+    # Codes are only ever delivered here. Compared exactly, never by prefix.
+    redirect_uri: Mapped[str] = mapped_column(String(500))
+    enabled: Mapped[bool] = mapped_column(Boolean, default=True)
+    created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
+    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
+    last_used_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+
+
+class ConnectedAppGrant(Base):
+    """A user's consent for one app, so the consent screen is shown only once.
+
+    Deleting the app or the user removes the grant, and the next sign-in asks
+    again.
+    """
+
+    __tablename__ = "connected_app_grants"
+    __table_args__ = (Index("uq_connected_app_grants_app_user", "app_id", "user_id", unique=True),)
+
+    id: Mapped[int] = mapped_column(Integer, primary_key=True)
+    app_id: Mapped[int] = mapped_column(ForeignKey("connected_apps.id", ondelete="CASCADE"), index=True)
+    user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
+    granted_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())

+ 6 - 0
backend/app/models/library.py

@@ -147,6 +147,12 @@ class LibraryFile(Base):
     # User notes
     notes: Mapped[str | None] = mapped_column(Text, nullable=True)
 
+    # User-provided link (Printables, Thingiverse, ...) and photos of the printed
+    # result (#3077) — the same trio archives carry. ``photos`` is a list of
+    # stored filenames under ``library_paths.library_photos_dir(id)``.
+    external_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
+    photos: Mapped[list | None] = mapped_column(JSON, nullable=True)
+
     # Provenance — when the file was imported from an external source (e.g.
     # MakerWorld), ``source_type`` identifies the source and ``source_url`` is
     # the canonical public URL. Used for "already imported" detection and

+ 54 - 0
backend/app/models/notification.py

@@ -59,6 +59,11 @@ class NotificationProvider(Base):
     # Provider-specific configuration stored as JSON string
     config = Column(Text, nullable=False)
 
+    # Attach a camera snapshot when one was captured for the event. Defaults
+    # True so ntfy/Pushover/Telegram/Discord keep behaving as they always
+    # did; this is the opt-out for whoever doesn't want it.
+    attach_photo = Column(Boolean, default=True)
+
     # Event triggers - print lifecycle
     on_print_start = Column(Boolean, default=False)
     on_print_complete = Column(Boolean, default=True)
@@ -101,9 +106,23 @@ class NotificationProvider(Base):
     # Off by default: fires after every print, alongside the print-complete alert (#2525)
     on_plate_clear_required = Column(Boolean, default=False)  # Print ended, queue gated until plate is confirmed clear
 
+    # Print asked for an outcome verdict (#1898). Defaults ON: it only ever
+    # fires for prints where the user opted in per-job, so the provider-level
+    # toggle exists to silence a channel, not to enable the feature.
+    on_print_confirm_request = Column(Boolean, default=True)
+    # How a Telegram provider collects that verdict (#3046): inline URL
+    # buttons ("buttons", the original behaviour), a thumbs-up/down reaction
+    # on the message ("reactions"), or both. Reactions need no inbound
+    # connectivity — the poller fetches them — so they work without an
+    # external_url. Ignored for every other provider type.
+    telegram_verdict_mode = Column(String(16), default="buttons")
+
     # Event triggers - Bed cooled after print
     on_bed_cooled = Column(Boolean, default=False)  # Bed cooled below threshold after print
     on_first_layer_complete = Column(Boolean, default=False)  # First layer finished printing
+    # Messages another application sends through Bambuddy (POST /notifications/app-message),
+    # e.g. Bambuddy Orders' "an order needs you". Off by default: nothing new arrives on upgrade.
+    on_app_message = Column(Boolean, default=False)
 
     # Event triggers - Inventory stock alerts
     on_stock_reorder_alert = Column(Boolean, default=False)  # SKU hits reorder point
@@ -143,3 +162,38 @@ class NotificationProvider(Base):
     printer = relationship("Printer", back_populates="notification_providers")
     logs = relationship("NotificationLog", back_populates="provider", cascade="all, delete-orphan")
     digest_queue = relationship("NotificationDigestQueue", back_populates="provider", cascade="all, delete-orphan")
+    # ORM-level cascade like the two above: SQLite ships with foreign keys
+    # off, so the column's ON DELETE CASCADE alone would leave rows behind.
+    pending_verdicts = relationship("TelegramPendingVerdict", back_populates="provider", cascade="all, delete-orphan")
+
+
+class TelegramPendingVerdict(Base):
+    """A Telegram outcome prompt still waiting for a thumbs-up/down reaction (#3046).
+
+    One row per delivered ``print_confirm_request`` message on a provider in
+    "reactions" or "both" mode. The reaction poller matches incoming
+    ``message_reaction`` updates against (provider, chat, message) and deletes
+    the row once a verdict landed; rows older than a week are pruned.
+    """
+
+    __tablename__ = "telegram_pending_verdicts"
+
+    id = Column(Integer, primary_key=True, index=True)
+    provider_id = Column(Integer, ForeignKey("notification_providers.id", ondelete="CASCADE"), nullable=False)
+    chat_id = Column(String(64), nullable=False)
+    message_id = Column(Integer, nullable=False)
+    archive_id = Column(Integer, ForeignKey("print_archives.id", ondelete="CASCADE"), nullable=False)
+    # The archive's confirm_token when the prompt went out. A reaction counts
+    # only while that is still the archive's live token, i.e. exactly as long
+    # as the prompt's one-tap links would: a reprint clears the token, so an
+    # earlier run's message can no longer answer the new run.
+    confirm_token = Column(String(64), nullable=True)
+    # sendPhoto messages carry the prompt as a caption, which is edited with a
+    # different Bot API method than a plain text message.
+    has_caption = Column(Boolean, default=False)
+    # The Markdown we sent, so the confirmation edit can append to it — the
+    # reaction update does not carry the message body.
+    message_text = Column(Text, nullable=True)
+    created_at = Column(DateTime, default=datetime.utcnow, index=True)
+
+    provider = relationship("NotificationProvider", back_populates="pending_verdicts")

+ 16 - 2
backend/app/models/notification_template.py

@@ -55,6 +55,20 @@ DEFAULT_TEMPLATES = [
         "title_template": "Print {progress}% Complete",
         "body_template": "{printer}: {filename}\nRemaining: {remaining_time}",
     },
+    {
+        # Post-print outcome confirmation (#1898). The body carries
+        # {confirm_url}, which only opens the archive in Bambuddy: anything
+        # that walks a URL out of a message body — a preview card, a mail
+        # gateway, a proxy — then reaches a page that changes nothing.
+        # {good_url} / {reject_url} are the single-use capability links and
+        # stay available as variables, but they travel by default only in the
+        # affordances nothing prefetches: the ntfy action buttons and the
+        # Telegram inline keyboard, both built in notification_service.
+        "event_type": "print_confirm_request",
+        "name": "Print Outcome Confirmation",
+        "title_template": "How did your print come out?",
+        "body_template": "{printer}: {filename}\nConfirm: {confirm_url}",
+    },
     {
         "event_type": "print_missing_spool_assignment",
         "name": "Missing Spool Assignment",
@@ -221,13 +235,13 @@ DEFAULT_TEMPLATES = [
         "event_type": "stock_reorder_alert",
         "name": "Stock Reorder Alert",
         "title_template": "Reorder Alert: {material}",
-        "body_template": "{material} ({brand}) has reached the reorder point.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Days left: {days_left}d\nReorder now to avoid a stock break.",
+        "body_template": "{material} {subtype} {color} ({brand}) has reached the reorder point.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Days left: {days_left}d\nReorder now to avoid a stock break.",
     },
     {
         "event_type": "stock_break_alert",
         "name": "Stock Break Alert",
         "title_template": "Stock Break Risk: {material}",
-        "body_template": "{material} ({brand}) will run out before replenishment arrives.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Lead time: {lead_time_days}d\nOnly {days_left}d of stock remaining — order immediately.",
+        "body_template": "{material} {subtype} {color} ({brand}) will run out before replenishment arrives.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Lead time: {lead_time_days}d\nOnly {days_left}d of stock remaining — order immediately.",
     },
     # User email notification templates (sent to the print job owner).
     # Names include " Email" so they aren't confused with the provider-level

+ 13 - 0
backend/app/models/oidc_provider.py

@@ -3,6 +3,7 @@ from __future__ import annotations
 from datetime import datetime
 
 from sqlalchemy import (
+    JSON,
     Boolean,
     CheckConstraint,
     DateTime,
@@ -103,6 +104,18 @@ class OIDCProvider(Base):
     default_group_id: Mapped[int | None] = mapped_column(
         Integer, ForeignKey("groups.id", ondelete="SET NULL"), nullable=True, default=None
     )
+    # #3107 — JWT claim name the group sync reads IdP groups from. Defaults to
+    # "groups". Providers put groups in different claims (Keycloak: "groups"
+    # as a JSON array after a client mapper; Authentik: "groups" as an array,
+    # but some setups ship "roles" or a custom claim), so it is configurable
+    # like email_claim. Same character rules enforced by _validate_email_claim_name.
+    group_claim: Mapped[str] = mapped_column(String(64), default="groups", server_default="groups")
+    # #3107 — mapping of IdP group value -> Bambuddy group name. Empty dict
+    # (the default) disables group sync entirely: the provider behaves exactly
+    # as it did before the column existed. Only groups named in the values are
+    # managed by the sync; everything else on the user is a manual assignment
+    # and survives logins (#1292 semantics, same as the LDAP sync).
+    group_mapping: Mapped[dict[str, str]] = mapped_column(JSON, default=dict, server_default="{}")
     # Optional icon URL the admin entered. The actual image bytes are fetched
     # server-side and cached in icon_data — the SPA never hotlinks this URL
     # (would require loosening img-src CSP; see PR #1333 / issue #1333).

+ 10 - 1
backend/app/models/print_batch.py

@@ -1,6 +1,6 @@
 from datetime import datetime
 
-from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func
+from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func
 from sqlalchemy.orm import Mapped, mapped_column, relationship
 
 from backend.app.core.database import Base
@@ -21,6 +21,7 @@ class PrintBatch(Base):
     """
 
     __tablename__ = "print_batches"
+    __table_args__ = (Index("uq_print_batches_external", "external_source", "external_ref", unique=True),)
 
     id: Mapped[int] = mapped_column(primary_key=True)
     name: Mapped[str] = mapped_column(String(255))
@@ -45,6 +46,14 @@ class PrintBatch(Base):
     due_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
     notes: Mapped[str | None] = mapped_column(Text, nullable=True)
 
+    # Link to the record in another system that asked for this batch, e.g. a
+    # shop order an integration turned into prints. ``external_ref`` identifies
+    # this batch within ``external_source`` and is unique there, so a client
+    # that retries a create after a lost response gets a 409 instead of a
+    # second batch printing the same order twice. Both set or both null.
+    external_source: Mapped[str | None] = mapped_column(String(32), nullable=True)
+    external_ref: Mapped[str | None] = mapped_column(String(255), nullable=True)
+
     # Timestamps
     created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
     completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

+ 4 - 0
backend/app/models/print_log.py

@@ -45,6 +45,10 @@ class PrintLogEntry(Base):
     energy_kwh: Mapped[float | None] = mapped_column(Float)
     energy_cost: Mapped[float | None] = mapped_column(Float)
     failure_reason: Mapped[str | None] = mapped_column(String(100))
+    # User quality verdict ('good' / 'reject'), mirrored from the archive on
+    # PATCH exactly like status/failure_reason (#1444 mirror) so verdict-aware
+    # statistics can stay on this table (#1898).
+    user_verdict: Mapped[str | None] = mapped_column(String(10), nullable=True)
     thumbnail_path: Mapped[str | None] = mapped_column(String(500))
     created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
     created_by_username: Mapped[str | None] = mapped_column(String(100))

+ 3 - 0
backend/app/models/print_queue.py

@@ -118,6 +118,9 @@ class PrintQueueItem(Base):
     use_ams: Mapped[bool] = mapped_column(Boolean, default=True)
     # Nozzle offset calibration — dual-nozzle printers only, MQTT-gated (#1682)
     nozzle_offset_cali: Mapped[str] = mapped_column(String(8), default="auto")
+    # Ask for a post-print outcome verdict when this job completes (#1898).
+    # Copied onto the archive as confirm_requested at dispatch, like plate_id.
+    confirm_outcome: Mapped[bool] = mapped_column(Boolean, default=False, server_default="0")
 
     # Preheat / heat-soak override (#1468). 'inherit' uses the global
     # preheat_enabled setting; 'on' / 'off' force the per-item decision. The

+ 4 - 0
backend/app/models/scheduled_drying.py

@@ -33,6 +33,10 @@ class ScheduledDrying(Base):
     status: Mapped[str] = mapped_column(String(20), default="pending")
     waiting_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
     error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
+    # Why a failed run failed, as a code the UI translates (screen_only /
+    # unsupported / did_not_start). error_message keeps the English text for the
+    # API and logs; a row without a code falls back to showing that.
+    error_code: Mapped[str | None] = mapped_column(String(32), nullable=True)
 
     created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
     started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

+ 6 - 0
backend/app/models/slot_preset.py

@@ -25,6 +25,12 @@ class SlotPresetMapping(Base):
     preset_id: Mapped[str] = mapped_column(String(100))  # Cloud preset setting_id
     preset_name: Mapped[str] = mapped_column(String(200))  # Preset name for display
     preset_source: Mapped[str] = mapped_column(String(20), default="cloud")  # cloud or local
+    # The filament id the slot was configured with alongside this preset. When
+    # the printer later reports a different one, something else (the slicer's
+    # Device tab, the printer screen) re-configured the slot and this row no
+    # longer describes it (#3216). None for rows written before it was recorded
+    # and by callers that do not know it.
+    tray_info_idx: Mapped[str | None] = mapped_column(String(32), nullable=True)
     created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
     updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
 

+ 15 - 0
backend/app/models/spool.py

@@ -57,6 +57,15 @@ class Spool(Base):
     # spools with a lower one without changing the global default.
     low_stock_threshold_pct: Mapped[int | None] = mapped_column(Integer)
 
+    # Internal material / article number (#2870): the identifier a business
+    # purchases and costs by (e.g. "15" = Bambu Lab PLA Basic), distinct from
+    # `category` (production grouping) and `note` (free text). Free text, no
+    # uniqueness — several spools of the same product share the number, which
+    # is exactly what makes it a sort/filter/statistics key. New spools of a
+    # matching product inherit it on creation (services/material_number.py,
+    # applied by the spool create routes and the RFID auto-add).
+    material_number: Mapped[str | None] = mapped_column(String(64))
+
     # Cost tracking
     cost_per_kg: Mapped[float | None] = mapped_column(Float)  # Cost per kilogram
 
@@ -83,9 +92,15 @@ class Spool(Base):
     )
     assignments: Mapped[list["SpoolAssignment"]] = relationship(back_populates="spool", cascade="all, delete-orphan")
     location: Mapped["Location | None"] = relationship(back_populates="spools")
+    # Supplier assignments (#2988): where this product can be bought, with
+    # per-assignment article number / price and a purchase-source marker.
+    # Default loader like every other relationship here — the handful of
+    # routes that embed them ask for selectinload() at the query site.
+    supplier_links: Mapped[list["SpoolSupplier"]] = relationship(back_populates="spool", cascade="all, delete-orphan")
 
 
 from backend.app.models.location import Location  # noqa: E402
 from backend.app.models.spool_assignment import SpoolAssignment  # noqa: E402
 from backend.app.models.spool_filament_preset import SpoolFilamentPreset  # noqa: E402
 from backend.app.models.spool_k_profile import SpoolKProfile  # noqa: E402
+from backend.app.models.supplier import SpoolSupplier  # noqa: E402

+ 133 - 0
backend/app/models/supplier.py

@@ -0,0 +1,133 @@
+from __future__ import annotations
+
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from sqlalchemy import (
+    Boolean,
+    DateTime,
+    Float,
+    ForeignKey,
+    Integer,
+    String,
+    UniqueConstraint,
+    func,
+)
+from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
+
+from backend.app.core.database import Base
+
+if TYPE_CHECKING:
+    from backend.app.models.spool import Spool
+
+
+def supplier_name_key(name: str) -> str:
+    """Case-insensitive lookup key stored on ``Supplier.name_key``.
+
+    Folded in Python, not in SQL: SQLite's ``lower()`` folds ASCII only, so a
+    unique index on ``lower(name)`` lets “Ökofilament” and “ökofilament” both
+    through — and the CSV import, which folds in Python, then collapses them
+    onto one map entry and resolves to whichever row it built last. Every
+    caller goes through this one function so the two folds cannot drift.
+    """
+    return name.strip().lower()
+
+
+class Supplier(Base):
+    """Managed supplier master list (#2988).
+
+    A supplier is *where filament is bought* — distinct from ``Spool.brand``,
+    which is who made it. One supplier carries many brands, and the same
+    product is available from several suppliers, hence the n:m assignment
+    below instead of a free-text field that drifts in spelling.
+    """
+
+    __tablename__ = "suppliers"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    name: Mapped[str] = mapped_column(String(200), index=True)
+    # Case-insensitive uniqueness -- the whole feature keys on the name. The
+    # CSV import resolves a column of names against this table and the master
+    # list is what a rename re-points, so "Extrudr" and "extrudr" must not be
+    # two rows. Stored key rather than a unique index on lower(name), for the
+    # reason in supplier_name_key: only the Python fold is Unicode-correct on
+    # every backend, and it is the same fold the import map uses.
+    name_key: Mapped[str] = mapped_column(String(200), nullable=False, unique=True, index=True)
+    # Shop / website URL, purely informational.
+    website: Mapped[str | None] = mapped_column(String(500))
+    # The business's own customer number AT this supplier.
+    customer_number: Mapped[str | None] = mapped_column(String(100))
+    note: Mapped[str | None] = mapped_column(String(500))
+    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
+    updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
+
+    spool_links: Mapped[list[SpoolSupplier]] = relationship(back_populates="supplier")
+
+    @validates("name")
+    def _sync_name_key(self, _key: str, value: str) -> str:
+        """Derive name_key from every name write, so no insert path can skip it."""
+        self.name_key = supplier_name_key(value)
+        return value
+
+
+class SpoolSupplier(Base):
+    """Spool-to-supplier assignment with per-assignment attributes (#2988).
+
+    Modelled after ``CostCenterMember`` (surrogate id + UNIQUE pair) rather
+    than a bare association table, because the assignment carries data: the
+    supplier's own article number for the product (NOT the internal material
+    number from #2870), the price at this supplier, and whether this concrete
+    spool was actually bought there — the other rows are alternative sources.
+    """
+
+    __tablename__ = "spool_suppliers"
+    __table_args__ = (UniqueConstraint("spool_id", "supplier_id", name="uq_spool_suppliers_spool_supplier"),)
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    spool_id: Mapped[int] = mapped_column(ForeignKey("spool.id", ondelete="CASCADE"), index=True)
+    supplier_id: Mapped[int] = mapped_column(ForeignKey("suppliers.id"), index=True)
+    # The supplier's article number for this product.
+    supplier_article_number: Mapped[str | None] = mapped_column(String(100))
+    # QUOTED price per kg at this supplier, for comparing sources. Named so it
+    # can never be read as actual cost: ``spool.cost_per_kg`` is the cost basis
+    # for every print and is never written from here.
+    quoted_price_per_kg: Mapped[float | None] = mapped_column(Float)
+    # True on the assignment this spool was actually purchased from.
+    is_purchase_source: Mapped[bool] = mapped_column(Boolean, default=False)
+    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
+
+    supplier: Mapped[Supplier] = relationship(back_populates="spool_links")
+    spool: Mapped[Spool] = relationship(back_populates="supplier_links")
+
+    @property
+    def supplier_name(self) -> str:
+        """Flattened for SpoolSupplierResponse. Every route that embeds links
+        chains selectinload() onto ``supplier``, so this never lazy-loads."""
+        return self.supplier.name if self.supplier else ""
+
+
+class SpoolmanSpoolSupplier(Base):
+    """``SpoolSupplier`` for a Spoolman-managed spool.
+
+    Mirrors ``SpoolmanKProfile``: Spoolman owns the spool, Bambuddy owns the
+    supplier assignment, so the row is local and keyed by the remote spool id
+    with no foreign key to enforce it. Suppliers are Bambuddy-side on purpose —
+    Spoolman's ``vendor`` is the manufacturer, not the seller.
+    """
+
+    __tablename__ = "spoolman_spool_suppliers"
+    __table_args__ = (UniqueConstraint("spoolman_spool_id", "supplier_id", name="uq_spoolman_spool_suppliers_pair"),)
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    spoolman_spool_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
+    supplier_id: Mapped[int] = mapped_column(ForeignKey("suppliers.id"), index=True)
+    supplier_article_number: Mapped[str | None] = mapped_column(String(100))
+    quoted_price_per_kg: Mapped[float | None] = mapped_column(Float)
+    is_purchase_source: Mapped[bool] = mapped_column(Boolean, default=False)
+    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
+
+    supplier: Mapped[Supplier] = relationship()
+
+    @property
+    def supplier_name(self) -> str:
+        return self.supplier.name if self.supplier else ""

+ 3 - 0
backend/app/schemas/api_key.py

@@ -19,6 +19,7 @@ class APIKeyCreate(BaseModel):
     can_manage_projects: bool = True  # Create/update/delete projects + membership (add archives) (#1893)
     can_access_cloud: bool = False  # Read /cloud/* on the creator's behalf — default off (#1182)
     can_update_energy_cost: bool = False  # POST /settings/electricity-price only (#1356)
+    can_send_notifications: bool = False  # POST /notifications/app-message only
     printer_ids: list[int] | None = None  # null = all printers
     expires_at: datetime | None = None
 
@@ -37,6 +38,7 @@ class APIKeyUpdate(BaseModel):
     can_manage_projects: bool | None = None
     can_access_cloud: bool | None = None
     can_update_energy_cost: bool | None = None
+    can_send_notifications: bool | None = None
     printer_ids: list[int] | None = None
     enabled: bool | None = None
     expires_at: datetime | None = None
@@ -59,6 +61,7 @@ class APIKeyResponse(BaseModel):
     can_manage_projects: bool
     can_access_cloud: bool
     can_update_energy_cost: bool
+    can_send_notifications: bool
     printer_ids: list[int] | None
     enabled: bool
     last_used: datetime | None

+ 14 - 0
backend/app/schemas/archive.py

@@ -32,6 +32,14 @@ class ArchiveUpdate(ArchiveBase):
     project_id: int | None = None
     # Allow changing status (e.g., clearing failed flag)
     status: str | None = None
+    # Post-print quality verdict (#1898): 'good' / 'reject'; null clears it.
+    user_verdict: str | None = Field(default=None, pattern="^(good|reject)$")
+    # Which surface the verdict came from, for the "recorded when the plate was
+    # cleared" hint. Only the sources a client can honestly claim are accepted;
+    # 'link', 'plate_clear' and 'reaction' are stamped server-side by the paths
+    # that own them and must not be forgeable over this route. Omitted means
+    # 'api' — some script or integration did it.
+    user_verdict_source: str | None = Field(default=None, pattern="^(dialog|printer_card|api)$")
     # Editable because a print archived without its 3MF has no figure at all,
     # and nothing else can supply one after the fact -- rescan needs a file
     # this archive does not have (#1820). Bounded because it feeds the filament
@@ -109,6 +117,12 @@ class ArchiveResponse(BaseModel):
     failure_reason: str | None
     quantity: int = 1  # Number of items printed
 
+    # Post-print outcome confirmation (#1898)
+    user_verdict: str | None = None
+    user_verdict_source: str | None = None
+    user_verdict_at: datetime | None = None
+    confirm_requested: bool = False
+
     # Energy tracking
     energy_kwh: float | None = None
     energy_cost: float | None = None

+ 118 - 0
backend/app/schemas/auth.py

@@ -343,6 +343,68 @@ def _validate_email_claim_name(v: str) -> str:
     return v
 
 
+def _validate_group_claim_name(v: str) -> str:
+    """#3107 — like _validate_email_claim_name, but also allows one slash.
+
+    Auth0 (and Auth0-compatible providers) only expose custom claims under a
+    non-reserved namespace, e.g. ``https://example.com/roles`` or ``app/roles``,
+    so the email-claim charset would refuse every valid Auth0 group claim.
+    The slash is structurally safe here: the value never reaches a URL, a
+    path or SQL — it is only a JWT claim lookup key inside ``claims.get`` —
+    so the wider charset does not widen any injection surface. The 64-char
+    cap and the "starts with a letter" rule are kept. The full-URL form of
+    an Auth0 namespace exceeds 64 chars, but that is Auth0's documented
+    short-namespace territory; the limit matches email_claim and keeps the
+    column bound meaningful.
+    """
+    if not re.fullmatch(r"[a-zA-Z][a-zA-Z0-9_\-/]{0,63}", v):
+        raise ValueError("Invalid claim name")
+    return v
+
+
+def _validate_group_mapping(v: dict[str, str]) -> dict[str, str]:
+    """#3107 — normalise and bound an IdP-group -> Bambuddy-group mapping.
+
+    Values must reference Bambuddy group names; existence is checked against
+    the database in the route handlers (same split as default_group_id), since
+    the schema layer has no session. Keys are left as-is apart from stripping:
+    IdP group values are opaque strings (DNs, UUIDs, names) and must match the
+    claim byte-for-byte, so any normalisation beyond whitespace would silently
+    break the lookup.
+
+    Keys colliding case-insensitively are rejected: the sync matches the IdP
+    side case-insensitively, so {"Admins": "Administrators", "admins":
+    "Viewers"} would silently collapse to whichever entry the dict happens to
+    keep last — a member of "Admins" could end up in Viewers. Rejecting the
+    pair at save time (here, so the env path gets the same answer) turns an
+    undiagnosable runtime behaviour into a form error. Two keys differing only
+    by case and mapping to the SAME group are pointless but harmless, and are
+    rejected too for the same reason: they read as a mistake.
+    """
+    if not isinstance(v, dict):
+        raise ValueError("group_mapping must be a JSON object")
+    if len(v) > 100:
+        raise ValueError("group_mapping must have at most 100 entries")
+    cleaned: dict[str, str] = {}
+    seen_ci: dict[str, str] = {}
+    for key, value in v.items():
+        if not isinstance(key, str) or not key.strip():
+            raise ValueError("group_mapping keys must be non-empty strings")
+        if not isinstance(value, str) or not value.strip():
+            raise ValueError("group_mapping values must be non-empty group names")
+        k = key.strip()
+        ci = k.lower()
+        if ci in seen_ci:
+            raise ValueError(
+                f"group_mapping has two IdP groups differing only by case: "
+                f"'{seen_ci[ci]}' and '{k}' — the sync matches case-insensitively, "
+                f"so both cannot be honored"
+            )
+        seen_ci[ci] = k
+        cleaned[k] = value.strip()
+    return cleaned
+
+
 def _validate_icon_url(v: str | None) -> str | None:
     """Reject non-HTTPS icon URLs and SSRF-unsafe hosts.
 
@@ -436,6 +498,9 @@ class OIDCProviderCreate(BaseModel):
     auto_link_existing_accounts: bool = False  # M-2: conservative default, opt-in only
     email_claim: str = Field(default="email", max_length=64)
     require_email_verified: bool = True
+    # #3107 — group sync config. group_mapping empty (default) = no sync.
+    group_claim: str = Field(default="groups", max_length=64)
+    group_mapping: dict[str, str] = Field(default_factory=dict)
     icon_url: str | None = None
     default_group_id: int | None = None
     is_autologin: bool = False  # #1589 — at most one provider may carry this
@@ -461,6 +526,17 @@ class OIDCProviderCreate(BaseModel):
     def validate_email_claim(cls, v: str) -> str:
         return _validate_email_claim_name(v)
 
+    @field_validator("group_claim")
+    @classmethod
+    def validate_group_claim(cls, v: str) -> str:
+        # Namespaced claims allowed here (Auth0 et al) — see _validate_group_claim_name.
+        return _validate_group_claim_name(v)
+
+    @field_validator("group_mapping")
+    @classmethod
+    def validate_group_mapping(cls, v: dict[str, str]) -> dict[str, str]:
+        return _validate_group_mapping(v)
+
     @field_validator("icon_url")
     @classmethod
     def validate_icon_url(cls, v: str | None) -> str | None:
@@ -493,6 +569,10 @@ class OIDCProviderUpdate(BaseModel):
     auto_link_existing_accounts: bool | None = None
     email_claim: str | None = Field(default=None, max_length=64)
     require_email_verified: bool | None = None
+    # #3107 — group sync config. None = leave unchanged, same as every other
+    # optional field here; an explicit {} clears the mapping and disables sync.
+    group_claim: str | None = Field(default=None, max_length=64)
+    group_mapping: dict[str, str] | None = None
     icon_url: str | None = None
     default_group_id: int | None = None
     is_autologin: bool | None = None  # #1589
@@ -509,6 +589,20 @@ class OIDCProviderUpdate(BaseModel):
             return None
         return _validate_email_claim_name(v)
 
+    @field_validator("group_claim")
+    @classmethod
+    def validate_group_claim(cls, v: str | None) -> str | None:
+        if v is None:
+            return None
+        return _validate_group_claim_name(v)
+
+    @field_validator("group_mapping")
+    @classmethod
+    def validate_group_mapping(cls, v: dict[str, str] | None) -> dict[str, str] | None:
+        if v is None:
+            return None
+        return _validate_group_mapping(v)
+
     @field_validator("icon_url")
     @classmethod
     def validate_icon_url(cls, v: str | None) -> str | None:
@@ -540,6 +634,9 @@ class OIDCProviderResponse(BaseModel):
     auto_link_existing_accounts: bool = False
     email_claim: str = "email"
     require_email_verified: bool = True
+    # #3107 — group sync config, echoed back so the settings UI can render it.
+    group_claim: str = "groups"
+    group_mapping: dict[str, str] = {}
     icon_url: str | None = None
     default_group_id: int | None = None
     is_autologin: bool = False  # #1589
@@ -557,6 +654,27 @@ class OIDCProviderResponse(BaseModel):
         from_attributes = True
 
 
+class OIDCProviderPublicResponse(BaseModel):
+    """#3107 — what the unauthenticated login page is allowed to see.
+
+    GET /oidc/providers is public so the login page can render the SSO
+    buttons, and it needs exactly four fields: id + name for the button,
+    has_icon for the avatar, is_autologin for the redirect-on-mount (#1589).
+    The full OIDCProviderResponse carries group_claim / group_mapping —
+    which IdP group name maps to which Bambuddy group, including
+    Administrators — and leaking that to anonymous visitors would tell
+    anyone who can reach the login page exactly which IdP group to aim for.
+    """
+
+    id: int
+    name: str
+    has_icon: bool
+    is_autologin: bool = False
+
+    class Config:
+        from_attributes = True
+
+
 class OIDCAuthorizeResponse(BaseModel):
     auth_url: str
 

+ 109 - 0
backend/app/schemas/connected_app.py

@@ -0,0 +1,109 @@
+from datetime import datetime
+from typing import Literal
+from urllib.parse import urlsplit
+
+from pydantic import BaseModel, Field, field_validator
+
+from backend.app.schemas.print_queue import UTCDatetime
+
+# RFC 7636: 43-128 chars from the unreserved set. The challenge is the
+# base64url SHA-256 of the verifier, so it is always exactly 43 chars.
+_PKCE_VERIFIER_PATTERN = r"^[A-Za-z0-9\-._~]{43,128}$"
+_PKCE_CHALLENGE_PATTERN = r"^[A-Za-z0-9\-_]{43}$"
+
+
+def _validate_redirect_uri(value: str) -> str:
+    """Accept only an absolute http(s) URL without a fragment.
+
+    Plain http is allowed: connected apps typically run on the same LAN as
+    Bambuddy, often without TLS. What matters is that codes can only ever be
+    delivered to the one URL an admin registered, and that is enforced by an
+    exact match at authorize and token time.
+    """
+    value = value.strip()
+    parts = urlsplit(value)
+    if parts.scheme not in ("http", "https") or not parts.netloc:
+        raise ValueError("Callback URL must be an absolute http:// or https:// URL")
+    if parts.fragment:
+        raise ValueError("Callback URL must not contain a #fragment")
+    if parts.username or parts.password:
+        raise ValueError("Callback URL must not contain credentials")
+    return value
+
+
+class ConnectedAppCreate(BaseModel):
+    name: str = Field(min_length=1, max_length=100)
+    redirect_uri: str = Field(min_length=1, max_length=500)
+
+    _check_redirect = field_validator("redirect_uri")(_validate_redirect_uri)
+
+
+class ConnectedAppUpdate(BaseModel):
+    name: str | None = Field(default=None, min_length=1, max_length=100)
+    redirect_uri: str | None = Field(default=None, min_length=1, max_length=500)
+    enabled: bool | None = None
+
+    @field_validator("redirect_uri")
+    @classmethod
+    def _check_redirect(cls, value: str | None) -> str | None:
+        return None if value is None else _validate_redirect_uri(value)
+
+
+class ConnectedAppResponse(BaseModel):
+    id: int
+    name: str
+    client_id: str
+    redirect_uri: str
+    enabled: bool
+    created_at: UTCDatetime
+    last_used_at: UTCDatetime | None = None
+
+    class Config:
+        from_attributes = True
+
+
+class ConnectedAppSecretResponse(ConnectedAppResponse):
+    """Returned by create and rotate only: the one time the secret is shown."""
+
+    client_secret: str
+
+
+class ConnectAuthorizeInfo(BaseModel):
+    app_name: str
+    username: str
+    already_granted: bool
+
+
+class ConnectAuthorizeRequest(BaseModel):
+    client_id: str = Field(min_length=1, max_length=64)
+    redirect_uri: str = Field(min_length=1, max_length=500)
+    code_challenge: str = Field(pattern=_PKCE_CHALLENGE_PATTERN)
+    code_challenge_method: Literal["S256"]
+
+
+class ConnectAuthorizeResponse(BaseModel):
+    code: str
+    redirect_uri: str
+
+
+class ConnectTokenRequest(BaseModel):
+    grant_type: Literal["authorization_code"]
+    code: str = Field(min_length=1, max_length=128)
+    redirect_uri: str = Field(min_length=1, max_length=500)
+    client_id: str = Field(min_length=1, max_length=64)
+    client_secret: str = Field(min_length=1, max_length=128)
+    code_verifier: str = Field(pattern=_PKCE_VERIFIER_PATTERN)
+
+
+class ConnectedUser(BaseModel):
+    id: int
+    username: str
+    email: str | None = None
+    is_admin: bool
+    groups: list[str]
+    permissions: list[str]
+
+
+class ConnectTokenResponse(BaseModel):
+    user: ConnectedUser
+    issued_at: datetime

+ 56 - 1
backend/app/schemas/library.py

@@ -2,7 +2,7 @@
 
 from datetime import datetime
 
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, field_validator
 
 # ============ Folder Schemas ============
 
@@ -122,6 +122,20 @@ class FileUpdate(BaseModel):
     folder_id: int | None = None
     project_id: int | None = None
     notes: str | None = None
+    # Empty string clears the link, like ``notes`` (#3077).
+    external_url: str | None = Field(None, max_length=500)
+
+    @field_validator("external_url")
+    @classmethod
+    def validate_external_url(cls, v: str | None) -> str | None:
+        # The link is rendered as an href for every reader of the library, so
+        # only web URLs are accepted (no javascript:/data: schemes).
+        if v is None:
+            return None
+        v = v.strip()
+        if v and not v.lower().startswith(("http://", "https://")):
+            raise ValueError("external_url must start with http:// or https://")
+        return v
 
 
 class FileDuplicate(BaseModel):
@@ -157,6 +171,11 @@ class FileResponse(BaseModel):
     last_printed_at: datetime | None
 
     notes: str | None
+    # User link + photos of the printed result (#3077); ``source_url`` is the
+    # read-only import provenance (MakerWorld) shown next to it.
+    external_url: str | None = None
+    photos: list[str] = []
+    source_url: str | None = None
 
     # Duplicate detection
     duplicates: list[FileDuplicate] | None = None
@@ -227,6 +246,12 @@ class FileListResponse(BaseModel):
     variant_group_id: int | None = None
     variant_count: int = 0
 
+    # Metadata indicators (#3077). The list never ships the notes text itself —
+    # ``has_notes`` is enough for the card badge; the details modal loads the rest.
+    external_url: str | None = None
+    has_notes: bool = False
+    photo_count: int = 0
+
     class Config:
         from_attributes = True
 
@@ -301,6 +326,26 @@ class FileUploadResponse(BaseModel):
     metadata: dict | None = None
 
 
+# ============ Combine ============
+
+
+class CombineItem(BaseModel):
+    """One source model in a combine request."""
+
+    file_id: int
+    copies: int = Field(default=1, ge=1, le=100)
+
+
+class CombineFilesRequest(BaseModel):
+    """Combine STL library files into one multi-object 3MF on a single plate."""
+
+    items: list[CombineItem] = Field(..., min_length=1, max_length=100)
+    # Name of the new library file; ``.3mf`` is appended when missing.
+    filename: str = Field(..., min_length=1, max_length=255)
+    # Destination folder; None = library root.
+    folder_id: int | None = None
+
+
 # ============ Bulk Operations ============
 
 
@@ -413,6 +458,16 @@ class BatchThumbnailResponse(BaseModel):
     results: list[BatchThumbnailResult]
 
 
+class ClientThumbnailResponse(BaseModel):
+    """Schema for the client-rendered preview thumbnail upload response (#2976).
+
+    ``updated`` is false when the file already had a thumbnail — the upload is
+    skipped so a stored thumbnail is never silently replaced.
+    """
+
+    updated: bool
+
+
 # ============ Variant Group Schemas (#671 / #2570) ============
 
 

+ 65 - 1
backend/app/schemas/notification.py

@@ -1,7 +1,7 @@
 """Pydantic schemas for notification providers."""
 
 from datetime import datetime
-from typing import Any
+from typing import Any, Literal
 
 from pydantic import BaseModel, Field, field_validator, model_validator
 
@@ -29,6 +29,9 @@ class NotificationProviderBase(BaseModel):
     provider_type: ProviderType = Field(..., description="Type of notification provider")
     enabled: bool = Field(default=True, description="Whether notifications are enabled")
     config: dict[str, Any] = Field(..., description="Provider-specific configuration")
+    attach_photo: bool = Field(
+        default=True, description="Attach a camera snapshot to this provider's notifications when one is available"
+    )
 
     # Event triggers - print lifecycle
     on_print_start: bool = Field(default=False, description="Notify on print start")
@@ -82,12 +85,26 @@ class NotificationProviderBase(BaseModel):
         default=False, description="Notify when a finished print is waiting for plate-clear confirmation"
     )
 
+    # Event triggers - Post-print outcome confirmation (#1898)
+    on_print_confirm_request: bool = Field(
+        default=True,
+        description="Notify with one-tap verdict links when a print that opted in asks for its outcome",
+    )
+    # How a Telegram provider collects the verdict (#3046). Ignored elsewhere.
+    telegram_verdict_mode: Literal["buttons", "reactions", "both"] = Field(
+        default="buttons",
+        description="Telegram only: answer the outcome prompt via inline buttons, a thumbs reaction, or both",
+    )
+
     # Event triggers - Bed cooled
     on_bed_cooled: bool = Field(default=False, description="Notify when bed cools after print")
 
     # Event triggers - First layer complete
     on_first_layer_complete: bool = Field(default=False, description="Notify when first layer completes")
 
+    # Messages from connected apps (POST /notifications/app-message)
+    on_app_message: bool = Field(default=False, description="Deliver messages other applications send")
+
     # Event triggers - Inventory stock alerts
     # Missing from this schema until now, so every payload naming them was
     # dropped silently: the UI's toggles round-tripped as 200 OK and the row
@@ -152,6 +169,7 @@ class NotificationProviderUpdate(BaseModel):
     provider_type: ProviderType | None = None
     enabled: bool | None = None
     config: dict[str, Any] | None = None
+    attach_photo: bool | None = None
 
     # Event triggers - print lifecycle
     on_print_start: bool | None = None
@@ -188,12 +206,19 @@ class NotificationProviderUpdate(BaseModel):
     on_plate_not_empty: bool | None = None
     on_plate_clear_required: bool | None = None
 
+    # Event triggers - Post-print outcome confirmation (#1898)
+    on_print_confirm_request: bool | None = None
+    telegram_verdict_mode: Literal["buttons", "reactions", "both"] | None = None
+
     # Event triggers - Bed cooled
     on_bed_cooled: bool | None = None
 
     # Event triggers - First layer complete
     on_first_layer_complete: bool | None = None
 
+    # Messages from connected apps
+    on_app_message: bool | None = None
+
     # Event triggers - Inventory stock alerts
     on_stock_reorder_alert: bool | None = None
     on_stock_break_alert: bool | None = None
@@ -270,11 +295,50 @@ class NotificationProviderResponse(NotificationProviderBase):
         from_attributes = True
 
 
+class AppMessage(BaseModel):
+    """A message another application sends through Bambuddy's notification channels."""
+
+    title: str = Field(min_length=1, max_length=120)
+    message: str = Field(min_length=1, max_length=2000)
+    url: str | None = Field(default=None, max_length=500, description="A link the message points to (http or https)")
+
+    @field_validator("title", "message")
+    @classmethod
+    def _plain_text(cls, value: str) -> str:
+        # Plain text: no control characters beyond line breaks and tabs.
+        cleaned = "".join(ch for ch in value if ch in "\n\t" or ch.isprintable()).strip()
+        if not cleaned:
+            raise ValueError("must not be empty")
+        return cleaned
+
+    @field_validator("url")
+    @classmethod
+    def _http_url(cls, value: str | None) -> str | None:
+        if value is None or value.strip() == "":
+            return None
+        value = value.strip()
+        if not value.lower().startswith(("http://", "https://")) or any(c.isspace() for c in value):
+            raise ValueError("must be an http or https address")
+        return value
+
+
+class AppMessageResult(BaseModel):
+    channels: int = Field(description="How many channels the message was handed to")
+
+
+class AppMessageChannel(BaseModel):
+    name: str
+    provider_type: str
+
+
 class NotificationTestRequest(BaseModel):
     """Schema for testing notification configuration."""
 
     provider_type: ProviderType
     config: dict[str, Any]
+    attach_photo: bool = Field(
+        default=True, description="Include a sample photo in the test, mirroring the provider's own toggle"
+    )
 
 
 class NotificationTestResponse(BaseModel):

+ 101 - 3
backend/app/schemas/notification_template.py

@@ -76,6 +76,9 @@ EVENT_VARIABLES: dict[str, list[str]] = {
     "billing_charge_failed": ["printer", "filename", "archive_id", "error", "timestamp", "app_name"],
     "printer_offline": ["printer", "timestamp", "app_name"],
     "printer_error": ["printer", "error_type", "error_detail", "timestamp", "app_name"],
+    "ai_failure_detection": ["printer", "task_name", "confidence", "action", "timestamp", "app_name"],
+    "plate_not_empty": ["printer", "difference_percent", "timestamp", "app_name"],
+    "plate_clear_required": ["printer", "timestamp", "app_name"],
     "filament_low": ["printer", "slot", "remaining_percent", "color", "timestamp", "app_name"],
     "maintenance_due": ["printer", "items", "timestamp", "app_name"],
     "ams_humidity_high": ["printer", "ams_label", "humidity", "threshold", "timestamp", "app_name"],
@@ -92,6 +95,7 @@ EVENT_VARIABLES: dict[str, list[str]] = {
     "bed_cooled": ["printer", "bed_temp", "threshold", "filename", "timestamp", "app_name"],
     "ha_sensor_alert": ["printer", "sensor", "state", "timestamp", "app_name"],
     "location_ha_sensor_alert": ["location", "sensor", "state", "timestamp", "app_name"],
+    "first_layer_complete": ["printer", "filename", "total_layers", "timestamp", "app_name"],
     "test": ["app_name", "timestamp"],
     # Queue notifications
     "queue_job_added": ["job_name", "target", "timestamp", "app_name"],
@@ -101,14 +105,55 @@ EVENT_VARIABLES: dict[str, list[str]] = {
     "queue_job_skipped": ["printer", "job_name", "reason", "timestamp", "app_name"],
     "queue_job_failed": ["printer", "job_name", "reason", "timestamp", "app_name"],
     "queue_completed": ["completed_count", "timestamp", "app_name"],
+    "stock_reorder_alert": [
+        "material",
+        "subtype",
+        "brand",
+        "color",
+        "stock_g",
+        "rate_g_day",
+        "days_left",
+        "timestamp",
+        "app_name",
+    ],
+    "stock_break_alert": [
+        "material",
+        "subtype",
+        "brand",
+        "color",
+        "stock_g",
+        "rate_g_day",
+        "days_left",
+        "lead_time_days",
+        "timestamp",
+        "app_name",
+    ],
     # User management notifications
     "user_created": ["username", "password", "login_url", "app_name", "timestamp"],
     "password_reset": ["username", "password", "login_url", "app_name", "timestamp"],
     # User email print notifications
     "user_print_start": ["username", "printer", "filename", "timestamp", "app_name"],
-    "user_print_complete": ["username", "printer", "filename", "timestamp", "app_name"],
-    "user_print_failed": ["username", "printer", "filename", "timestamp", "app_name"],
-    "user_print_stopped": ["username", "printer", "filename", "timestamp", "app_name"],
+    "user_print_complete": ["username", "printer", "filename", "finish_photo_url", "timestamp", "app_name"],
+    "user_print_failed": ["username", "printer", "filename", "finish_photo_url", "timestamp", "app_name"],
+    "user_print_stopped": ["username", "printer", "filename", "finish_photo_url", "timestamp", "app_name"],
+}
+
+# Events whose real notification can carry a camera snapshot — mirrors which
+# NotificationService.on_* methods accept image_data.
+PHOTO_CAPABLE_EVENTS: set[str] = {
+    "print_start",
+    "print_complete",
+    "print_failed",
+    "print_stopped",
+    "print_progress",
+    "printer_error",
+    "ai_failure_detection",
+    "plate_not_empty",
+    "first_layer_complete",
+    "user_print_complete",
+    "user_print_failed",
+    "user_print_stopped",
+    "test",
 }
 
 # Sample data for previewing templates
@@ -190,6 +235,25 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
         "timestamp": "2024-01-15 14:30",
         "app_name": "Bambuddy",
     },
+    "ai_failure_detection": {
+        "printer": "Bambu X1C",
+        "task_name": "Benchy.3mf",
+        "confidence": "0.87",
+        "action": "pause",
+        "timestamp": "2024-01-15 15:10",
+        "app_name": "Bambuddy",
+    },
+    "plate_not_empty": {
+        "printer": "Bambu X1C",
+        "difference_percent": "12.4",
+        "timestamp": "2024-01-15 09:00",
+        "app_name": "Bambuddy",
+    },
+    "plate_clear_required": {
+        "printer": "Bambu X1C",
+        "timestamp": "2024-01-15 15:48",
+        "app_name": "Bambuddy",
+    },
     "filament_low": {
         "printer": "Bambu X1C",
         "slot": "1",
@@ -251,6 +315,13 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
         "timestamp": "2024-01-15 14:30",
         "app_name": "Bambuddy",
     },
+    "first_layer_complete": {
+        "printer": "Bambu X1C",
+        "filename": "Benchy.3mf",
+        "total_layers": "184",
+        "timestamp": "2024-01-15 14:35",
+        "app_name": "Bambuddy",
+    },
     "test": {
         "app_name": "Bambuddy",
         "timestamp": "2024-01-15 14:30",
@@ -303,6 +374,29 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
         "timestamp": "2024-01-15 18:30",
         "app_name": "Bambuddy",
     },
+    "stock_reorder_alert": {
+        "material": "PLA",
+        "subtype": "Matte",
+        "brand": "Bambu Lab",
+        "color": "Charcoal",
+        "stock_g": "420",
+        "rate_g_day": "18.5",
+        "days_left": "22",
+        "timestamp": "2024-01-15 14:30",
+        "app_name": "Bambuddy",
+    },
+    "stock_break_alert": {
+        "material": "PLA",
+        "subtype": "Matte",
+        "brand": "Bambu Lab",
+        "color": "Charcoal",
+        "stock_g": "160",
+        "rate_g_day": "18.5",
+        "days_left": "8",
+        "lead_time_days": "14",
+        "timestamp": "2024-01-15 14:30",
+        "app_name": "Bambuddy",
+    },
     # User management notifications
     "user_created": {
         "username": "john_doe",
@@ -330,6 +424,7 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
         "username": "john_doe",
         "printer": "Bambu X1C",
         "filename": "Benchy.3mf",
+        "finish_photo_url": "/api/v1/archives/123/photos/finish_20240115_154800_abc12345.jpg",
         "timestamp": "2024-01-15 15:48",
         "app_name": "Bambuddy",
     },
@@ -337,6 +432,7 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
         "username": "john_doe",
         "printer": "Bambu X1C",
         "filename": "Benchy.3mf",
+        "finish_photo_url": "/api/v1/archives/123/photos/finish_20240115_151500_def67890.jpg",
         "timestamp": "2024-01-15 15:15",
         "app_name": "Bambuddy",
     },
@@ -344,6 +440,7 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
         "username": "john_doe",
         "printer": "Bambu X1C",
         "filename": "Benchy.3mf",
+        "finish_photo_url": "/api/v1/archives/123/photos/finish_20240115_150000_ghi11223.jpg",
         "timestamp": "2024-01-15 15:15",
         "app_name": "Bambuddy",
     },
@@ -391,6 +488,7 @@ class EventVariablesResponse(BaseModel):
     event_type: str
     event_name: str
     variables: list[str]
+    supports_photo: bool = False
 
 
 class TemplatePreviewRequest(BaseModel):

+ 1 - 0
backend/app/schemas/print_log.py

@@ -28,6 +28,7 @@ class PrintLogEntrySchema(BaseModel):
     energy_kwh: float | None = None
     energy_cost: float | None = None
     failure_reason: str | None = None
+    user_verdict: str | None = None  # Post-print quality verdict (#1898)
     thumbnail_path: str | None = None
     created_by_id: int | None = None
     created_by_username: str | None = None

+ 21 - 1
backend/app/schemas/print_queue.py

@@ -80,7 +80,7 @@ class PrintQueueItemCreate(BaseModel):
     require_previous_success: bool = False
     auto_off_after: bool = False  # Power off printer after print completes
     manual_start: bool = False  # Requires manual trigger to start (staged)
-    insert_at_top: bool = False  # Insert ahead of other pending items in the same queue scope
+    insert_at_top: bool = False  # Insert ahead of other pending items (one queue order across all printers, #3200)
     insert_position: int | None = None  # 1-indexed insertion position for priority queueing
     # Persistent "Print Anyway" acknowledgement (#1698-followup). When set,
     # PrintModal already showed the deficit warning and the user confirmed,
@@ -103,6 +103,8 @@ class PrintQueueItemCreate(BaseModel):
     # Nozzle offset calibration — dual-nozzle printers only (#1682). The MQTT
     # layer ignores the value on single-nozzle printers so the wire stays "skip".
     nozzle_offset_cali: TriState = "auto"
+    # Ask for a post-print outcome verdict when this job completes (#1898)
+    confirm_outcome: bool = False
     # Preheat / heat-soak per-item override (#1468). 'inherit' uses the global
     # preheat_enabled setting; 'on' / 'off' force the decision. The chamber
     # target falls through: this override → max(filament-map[loaded tray]) → 0.
@@ -155,6 +157,7 @@ class PrintQueueItemUpdate(BaseModel):
     timelapse: bool | None = None
     use_ams: bool | None = None
     nozzle_offset_cali: TriState | None = None
+    confirm_outcome: bool | None = None
     preheat_override: Literal["inherit", "on", "off"] | None = None
     preheat_chamber_target_override: int | None = Field(default=None, ge=0, le=MAX_CHAMBER_TEMP_C)
     # Auto-print G-code injection
@@ -215,6 +218,7 @@ class PrintQueueItemResponse(BaseModel):
     timelapse: bool = False
     use_ams: bool = True
     nozzle_offset_cali: TriState = "auto"
+    confirm_outcome: bool = False
     preheat_override: Literal["inherit", "on", "off"] = "inherit"
     preheat_chamber_target_override: int | None = None
     status: Literal["pending", "printing", "completed", "failed", "skipped", "cancelled"]
@@ -339,6 +343,7 @@ class PrintQueueBulkUpdate(BaseModel):
     timelapse: bool | None = None
     use_ams: bool | None = None
     nozzle_offset_cali: TriState | None = None
+    confirm_outcome: bool | None = None
     preheat_override: Literal["inherit", "on", "off"] | None = None
     preheat_chamber_target_override: int | None = Field(default=None, ge=0, le=MAX_CHAMBER_TEMP_C)
     # Auto-print G-code injection
@@ -389,6 +394,19 @@ class PrintBatchCreate(BaseModel):
     project_id: int | None = None
     due_date: datetime | None = None
     notes: str | None = None
+    # The external record this batch fulfils, for integrations. ``external_ref``
+    # is unique within ``external_source``: creating a second batch with the
+    # same pair is a 409, which makes a retried create safe.
+    external_source: str | None = Field(default=None, min_length=1, max_length=32, pattern=r"^[a-z0-9_-]+$")
+    external_ref: str | None = Field(default=None, min_length=1, max_length=255)
+
+    @model_validator(mode="after")
+    def _external_link_is_complete(self) -> "PrintBatchCreate":
+        # A ref without its source can't be looked up, and a source without a
+        # ref would escape the uniqueness guarantee (NULLs never collide).
+        if (self.external_source is None) != (self.external_ref is None):
+            raise ValueError("external_source and external_ref must be given together")
+        return self
 
 
 class PrintBatchUpdate(BaseModel):
@@ -465,6 +483,8 @@ class PrintBatchResponse(BaseModel):
     project_id: int | None = None
     due_date: UTCDatetime | None = None
     notes: str | None = None
+    external_source: str | None = None
+    external_ref: str | None = None
     # Derived counts
     pending_count: int = 0
     printing_count: int = 0

+ 31 - 6
backend/app/schemas/printer.py

@@ -160,7 +160,7 @@ class HMSErrorResponse(BaseModel):
     code: str
     attr: int = 0  # Attribute value for constructing wiki URL
     module: int
-    severity: int  # 1=fatal, 2=serious, 3=common, 4=info
+    severity: int  # Bambu alert level: 1 error (stopped), 2 warning (paused), 3 notification, 0 invalid
     actions: list[str] = []  # List of user-facing action keys (e.g. "CHECK_FILAMENT")
     job_id: str | None = None  # Optional job ID for actions that require it (e.g. "CHECK_ASSISTANT")
     # Canonical hex identifier the firmware uses to match HMS-related commands.
@@ -173,14 +173,35 @@ class HMSErrorResponse(BaseModel):
     # The bundled catalogue's sentence for this fault, so a client does not have
     # to carry its own copy of the same table to tell a user why a print halted
     # (#2926). English only and not localized — the catalogue ships one language.
-    # None when the catalogue does not cover the code, which is common for
-    # `hms[]`-array faults: those resolve through a lossy collapse of their
-    # 16-char identifier and many land on no key at all (#2728). A client should
-    # treat null as "no text available", never as "no fault" — `full_code` is
-    # what identifies the fault, and it is always present.
+    # Generated from Bambu Studio's HMS files, keyed by `full_code` and the
+    # printer model (#2728). None when Bambu publishes no text for the code,
+    # which it does for some codes it lists. A client should treat null as "no
+    # text available", never as "no fault" — `full_code` is what identifies the
+    # fault, and it is always present.
     description: str | None = None
 
 
+def hms_error_responses(errors) -> list[HMSErrorResponse]:
+    """A printer's live HMS faults (``PrinterState.hms_errors``) as API rows.
+
+    Shared by the printer status route and the webhook status route, so a
+    fault reads the same to the UI and to an API-key client (#2919).
+    """
+    return [
+        HMSErrorResponse(
+            code=e.code,
+            attr=e.attr,
+            module=e.module,
+            severity=e.severity,
+            actions=e.actions,
+            job_id=e.job_id,
+            full_code=e.full_code,
+            description=e.description,
+        )
+        for e in (errors or [])
+    ]
+
+
 class AMSTray(BaseModel):
     id: int
     tray_color: str | None = None
@@ -213,6 +234,10 @@ class AMSUnit(BaseModel):
     serial_number: str = ""  # AMS unit serial number (sn from MQTT)
     sw_ver: str = ""  # AMS firmware version (from get_version info.module)
     dry_time: int = 0  # Minutes remaining (0 = not drying, >0 = drying active)
+    # True when dry_time > 0 but the countdown has not ticked for over
+    # DRY_COUNTDOWN_STALL_SECONDS with no active dry_status phase: the timer is
+    # set but no cycle is running (never started, or paused partway).
+    dry_countdown_stalled: bool = False
     dry_status: int = 0  # 0=Off, 1=Checking, 2=Drying, 3=Cooling, 4=Stopping, 5=Error
     dry_sub_status: int = 0  # 0=Off, 1=Heating, 2=Dehumidify
     dry_sf_reason: list[int] = []  # Cannot-dry reasons from firmware (see CannotDryReason)

+ 1 - 0
backend/app/schemas/scheduled_drying.py

@@ -36,6 +36,7 @@ class ScheduledDryingResponse(BaseModel):
     status: str
     waiting_reason: str | None
     error_message: str | None
+    error_code: str | None = None
     created_at: UTCDatetime
     started_at: UTCDatetime
     completed_at: UTCDatetime

+ 40 - 0
backend/app/schemas/settings.py

@@ -96,6 +96,12 @@ class AppSettings(BaseModel):
     check_updates: bool = Field(default=True, description="Automatically check for updates on startup")
     check_printer_firmware: bool = Field(default=True, description="Check for printer firmware updates from Bambu Lab")
     include_beta_updates: bool = Field(default=False, description="Include beta/prerelease versions in update checks")
+    announcements_enabled: bool = Field(
+        default=True, description="Fetch announcements from the Bambuddy maintainers (a signed file on GitHub)"
+    )
+    announcements_all_users: bool = Field(
+        default=False, description="Show announcements to every signed-in user, not only administrators"
+    )
 
     # Language
     language: str = Field(default="en", description="UI language (en, de, fr, ja, it, pt-BR)")
@@ -144,6 +150,16 @@ class AppSettings(BaseModel):
         default=False,
         description="Automatically dry AMS filament on idle printers when humidity exceeds threshold, regardless of queue",
     )
+    ambient_drying_sustained_minutes: int = Field(
+        default=0,
+        ge=0,
+        le=240,
+        description=(
+            "Minutes the humidity must stay above the threshold before an ambient "
+            "auto-dry starts (0 = start immediately). Rides out the reading spike "
+            "from opening the AMS lid instead of buying a dry cycle for it."
+        ),
+    )
     print_drying_enabled: bool = Field(
         default=False,
         description=(
@@ -397,6 +413,24 @@ class AppSettings(BaseModel):
         default="auto",
         description="Default nozzle offset calibration option for new prints (dual-nozzle printers only)",
     )
+    default_confirm_outcome: bool = Field(
+        default=False,
+        description="Default for asking for a post-print outcome verdict on new prints (#1898)",
+    )
+    confirm_outcome_external_prints: bool = Field(
+        default=False,
+        description=(
+            "Also ask for the outcome of prints Bambuddy archived but did not dispatch — started at "
+            "the printer, in Bambu Studio or in the Handy app (#1898)"
+        ),
+    )
+    confirm_default_good_on_plate_clear: bool = Field(
+        default=False,
+        description=(
+            "When the build plate is released (manual acknowledgment or next dispatch) with the "
+            "outcome prompt still unanswered, record the print as a good part (#1898)"
+        ),
+    )
 
     # Staggered batch start for multi-printer jobs
     stagger_group_size: int = Field(
@@ -668,6 +702,8 @@ class AppSettingsUpdate(BaseModel):
     check_updates: bool | None = None
     check_printer_firmware: bool | None = None
     include_beta_updates: bool | None = None
+    announcements_enabled: bool | None = None
+    announcements_all_users: bool | None = None
     local_login_enabled: bool | None = None
     language: str | None = None
     notification_language: str | None = None
@@ -682,6 +718,7 @@ class AppSettingsUpdate(BaseModel):
     queue_drying_enabled: bool | None = None
     queue_drying_block: bool | None = None
     ambient_drying_enabled: bool | None = None
+    ambient_drying_sustained_minutes: int | None = Field(default=None, ge=0, le=240)
     print_drying_enabled: bool | None = None
     drying_presets: str | None = None
     ams_humidity_thresholds: str | None = None
@@ -737,6 +774,9 @@ class AppSettingsUpdate(BaseModel):
     default_layer_inspect: bool | None = None
     default_timelapse: bool | None = None
     default_nozzle_offset_cali: TriState | None = None
+    default_confirm_outcome: bool | None = None
+    confirm_outcome_external_prints: bool | None = None
+    confirm_default_good_on_plate_clear: bool | None = None
     stagger_group_size: int | None = Field(default=None, ge=1, le=50)
     stagger_interval_minutes: int | None = Field(default=None, ge=1, le=60)
     billing_enabled: bool | None = None

+ 58 - 0
backend/app/schemas/spool.py

@@ -2,6 +2,8 @@ from datetime import datetime
 
 from pydantic import BaseModel, Field, field_validator
 
+from backend.app.schemas.supplier import SpoolSupplierResponse
+
 # Visual variant applied to a spool's swatch — purely cosmetic, does not
 # affect MQTT/firmware. Kept independent of `subtype` so users can override
 # the rendering hint without touching Bambu's categorical filament label.
@@ -63,6 +65,19 @@ def normalize_extra_colors(value: str | None) -> str | None:
     return ",".join(tokens)
 
 
+def normalize_material_number(value: str | None) -> str | None:
+    """Trim the material number and treat a blank one as unset (#2870).
+
+    Every write path lands here (form, bulk edit, CSV import, direct API), so
+    "15" and "15 " can never become two groups in the statistics aggregate or
+    two entries in the inventory filter. Blank collapses to NULL rather than
+    "", which keeps "has no number" a single state to query for.
+    """
+    if value is None:
+        return None
+    return value.strip() or None
+
+
 def normalize_effect_type(value: str | None) -> str | None:
     if value is None:
         return None
@@ -121,6 +136,17 @@ class SpoolBase(BaseModel):
     # User-defined category + per-spool low-stock threshold override (#729).
     category: str | None = Field(default=None, max_length=50)
     low_stock_threshold_pct: int | None = Field(default=None, ge=1, le=99)
+    # Internal material / article number (#2870) — the purchasing identifier,
+    # shared by all spools of the same product. Free text, no uniqueness.
+    material_number: str | None = Field(default=None, max_length=64)
+
+    # mode="before": trim first, so a padded value is held to the 64
+    # characters it will store, not to the length it arrived with.
+    @field_validator("material_number", mode="before")
+    @classmethod
+    def _validate_material_number(cls, v):
+        return normalize_material_number(v) if isinstance(v, str) else v
+
     # Free-text storage location, distinct from `location` (AMS slot
     # assignment). Column has lived on the ORM since the inventory rework
     # but was missing from this schema, so writes were silently dropped (#1291).
@@ -174,6 +200,16 @@ class SpoolUpdate(BaseModel):
     # User-defined category + per-spool low-stock threshold override (#729).
     category: str | None = Field(default=None, max_length=50)
     low_stock_threshold_pct: int | None = Field(default=None, ge=1, le=99)
+    # Internal material / article number (#2870).
+    material_number: str | None = Field(default=None, max_length=64)
+
+    # mode="before": trim first, so a padded value is held to the 64
+    # characters it will store, not to the length it arrived with.
+    @field_validator("material_number", mode="before")
+    @classmethod
+    def _validate_material_number(cls, v):
+        return normalize_material_number(v) if isinstance(v, str) else v
+
     storage_location: str | None = Field(default=None, max_length=255)
     location_id: int | None = Field(default=None, gt=0)
 
@@ -241,9 +277,31 @@ class SpoolResponse(SpoolBase):
     created_at: datetime
     updated_at: datetime
     k_profiles: list[SpoolKProfileResponse] = []
+    # Supplier assignments (#2988): where this product can be bought, with
+    # per-assignment article number / price and a purchase-source marker.
+    # Reads the ORM relationship `supplier_links`, which every route
+    # answering with this schema loads explicitly (see spool_response_loads).
+    suppliers: list[SpoolSupplierResponse] = Field(default=[], validation_alias="supplier_links")
 
     class Config:
         from_attributes = True
+        populate_by_name = True
+
+
+class MaterialNumberStats(BaseModel):
+    """Per-material-number inventory aggregate (#2870).
+
+    ``spool_count`` and ``remaining_g`` cover active (non-archived) spools;
+    ``consumed_g`` and ``cost`` sum the recorded usage history of every spool
+    carrying the number, archived included — consumption doesn't disappear
+    when a spool is archived.
+    """
+
+    material_number: str
+    spool_count: int
+    remaining_g: float
+    consumed_g: float
+    cost: float
 
 
 class SpoolAssignmentCreate(BaseModel):

+ 112 - 0
backend/app/schemas/supplier.py

@@ -0,0 +1,112 @@
+"""Pydantic schemas for the supplier master list (#2988)."""
+
+from datetime import datetime
+
+from pydantic import BaseModel, Field, field_validator
+
+# The CSV `suppliers` column joins names with "; " (#2988). A name carrying
+# the separator would split into unknown names on import and silently drop the
+# assignments, so it is refused at the edge rather than escaped — the column
+# stays readable and hand-editable.
+SUPPLIER_NAME_SEPARATOR = ";"
+
+
+def validate_supplier_name(value: str) -> str:
+    trimmed = value.strip()
+    if not trimmed:
+        raise ValueError("name must not be empty")
+    if SUPPLIER_NAME_SEPARATOR in trimmed:
+        raise ValueError("name must not contain ';' — it separates suppliers in the CSV export")
+    return trimmed
+
+
+class SupplierBase(BaseModel):
+    name: str = Field(..., min_length=1, max_length=200)
+    website: str | None = Field(default=None, max_length=500)
+    # The business's own customer number AT this supplier.
+    customer_number: str | None = Field(default=None, max_length=100)
+    note: str | None = Field(default=None, max_length=500)
+
+
+class SupplierCreate(SupplierBase):
+    # Only on the write schemas: SupplierResponse inherits SupplierBase, and a
+    # row written before the rule existed must still be readable. mode="before"
+    # so the name is trimmed before max_length sees it — a padded name at the
+    # cap is not too long once stripped. Non-strings pass through to the type
+    # check.
+    @field_validator("name", mode="before")
+    @classmethod
+    def _normalize_name(cls, value: object) -> object:
+        return validate_supplier_name(value) if isinstance(value, str) else value
+
+
+class SupplierUpdate(BaseModel):
+    # Optional so a PATCH can leave the name alone. An explicit null is a 422
+    # here instead of a NOT NULL violation surfacing as a 500.
+    name: str | None = Field(default=None, min_length=1, max_length=200)
+    website: str | None = Field(default=None, max_length=500)
+    customer_number: str | None = Field(default=None, max_length=100)
+    note: str | None = Field(default=None, max_length=500)
+
+    @field_validator("name", mode="before")
+    @classmethod
+    def _normalize_name(cls, value: object) -> object:
+        if value is None:
+            raise ValueError("name must not be null")
+        return validate_supplier_name(value) if isinstance(value, str) else value
+
+
+class SupplierResponse(SupplierBase):
+    id: int
+    # Number of spools referencing this supplier — shown in the settings list
+    # and the reason a delete is refused (409) instead of orphaning links.
+    spool_count: int = 0
+    created_at: datetime
+    updated_at: datetime
+
+    class Config:
+        from_attributes = True
+
+
+class SpoolSupplierLinkInput(BaseModel):
+    """One spool-to-supplier assignment as written by the spool dialog."""
+
+    supplier_id: int = Field(..., gt=0)
+    # The supplier's own article number for the product — NOT the internal
+    # material number (#2870).
+    supplier_article_number: str | None = Field(default=None, max_length=100)
+    # Quoted price per kg at this supplier, for comparing sources. Never a
+    # cost basis — ``spool.cost_per_kg`` stays authoritative and is not
+    # written from assignments.
+    quoted_price_per_kg: float | None = Field(default=None, ge=0)
+    # Marks where this concrete spool was actually bought; the other
+    # assignments are alternative sources.
+    is_purchase_source: bool = False
+
+
+class SpoolSupplierResponse(BaseModel):
+    id: int
+    supplier_id: int
+    supplier_name: str
+    supplier_article_number: str | None = None
+    quoted_price_per_kg: float | None = None
+    is_purchase_source: bool = False
+
+    class Config:
+        from_attributes = True
+
+
+class SupplierStats(BaseModel):
+    """Per-supplier inventory aggregate (#2988), purchase-source spools only.
+
+    ``spool_count`` and ``remaining_g`` cover active spools bought at this
+    supplier; ``consumed_g`` and ``cost`` sum the recorded usage history of
+    every purchase-source spool, archived included.
+    """
+
+    supplier_id: int
+    supplier_name: str
+    spool_count: int
+    remaining_g: float
+    consumed_g: float
+    cost: float

+ 476 - 0
backend/app/services/announcements.py

@@ -0,0 +1,476 @@
+"""Announcements from the Bambuddy maintainers: fetch, verify, keep what applies.
+
+Bambuddy fetches one file, ``feed.json``, from the public
+``maziggy/bambuddy-notifications`` repo on raw.githubusercontent.com -- the host
+the update check already talks to. No Bambuddy server is contacted and nothing
+about this install is sent: whether a message applies here (version range,
+channel, install type) is decided below, locally.
+
+The file is written by the maintainers' registrar and signed with Ed25519:
+
+    {"format": 1, "key_id": "...", "signature": "<base64>", "payload": {...}}
+
+The signature covers ``canonical(payload)``. A file that does not verify against
+a key in ``TRUSTED_KEYS`` is ignored, so neither a copy of the repo nor anyone in
+the middle can make Bambuddy show a message. The payload's ``serial`` only goes
+up; a feed older than one already accepted is refused, so an old signed file
+cannot be re-served to bring back a withdrawn message.
+
+The feed is the full current list. On every accepted fetch the stored set is
+replaced: a message withdrawn upstream disappears here with its read markers.
+Any failure keeps the last good list.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import base64
+import json
+import logging
+import random
+import re
+from dataclasses import dataclass
+from datetime import datetime, timezone
+from urllib.parse import urlsplit
+
+import httpx
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+from sqlalchemy import delete, select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.core.config import APP_VERSION
+from backend.app.models.announcement import Announcement, AnnouncementRead
+from backend.app.models.settings import Settings
+
+logger = logging.getLogger(__name__)
+
+FEED_URL = "https://raw.githubusercontent.com/maziggy/bambuddy-notifications/main/feed.json"
+FEED_FORMAT = 1
+
+# key_id (first 16 hex of sha256 of the raw public key) -> base64 raw public key.
+# A list so the key can be rotated: ship the new key next to the old one first.
+TRUSTED_KEYS: dict[str, str] = {
+    "d70b3bf207fdfd59": "uQqUAYrInmXQ1nIwOIY/95L7tD5o/HZMmZfhAlULJ/w=",
+}
+
+# Where a message may link to. The registrar refuses anything else too; this is
+# the side that counts.
+LINK_HOSTS = ("github.com", "bambuddy.cool")
+
+LEVELS = ("info", "important", "critical")
+MAX_FEED_BYTES = 512 * 1024
+MAX_TITLE = 120
+MAX_BODY = 2000
+MAX_LINK_LABEL = 40
+
+FETCH_INTERVAL_SECONDS = 6 * 3600
+FETCH_JITTER_SECONDS = 30 * 60
+# Let startup settle before the first fetch.
+FIRST_FETCH_DELAY_SECONDS = 60
+
+ENABLED_KEY = "announcements_enabled"
+ALL_USERS_KEY = "announcements_all_users"
+# Internal state, never part of the settings API.
+SERIAL_KEY = "announcements_feed_serial"
+LAST_FETCH_KEY = "announcements_last_fetch"
+
+
+class FeedRejected(Exception):
+    """The fetched file is not a feed this install accepts. Nothing changes."""
+
+
+# ---- verification -------------------------------------------------------------------
+
+
+def canonical(payload: dict) -> bytes:
+    return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
+
+
+def verify_feed(content: bytes, trusted_keys: dict[str, str] | None = None) -> dict:
+    """The payload of a correctly signed feed, or FeedRejected."""
+    keys = TRUSTED_KEYS if trusted_keys is None else trusted_keys
+    try:
+        envelope = json.loads(content)
+    except (ValueError, UnicodeDecodeError) as exc:
+        raise FeedRejected("not JSON") from exc
+    if not isinstance(envelope, dict) or envelope.get("format") != FEED_FORMAT:
+        raise FeedRejected("unknown feed format")
+    public = keys.get(str(envelope.get("key_id")))
+    if public is None:
+        raise FeedRejected(f"signed with an unknown key ({envelope.get('key_id')!r})")
+    payload = envelope.get("payload")
+    if not isinstance(payload, dict):
+        raise FeedRejected("no payload")
+    try:
+        signature = base64.b64decode(str(envelope.get("signature")), validate=True)
+        Ed25519PublicKey.from_public_bytes(base64.b64decode(public)).verify(signature, canonical(payload))
+    except (ValueError, InvalidSignature) as exc:
+        raise FeedRejected("signature does not verify") from exc
+    if payload.get("format") != FEED_FORMAT:
+        raise FeedRejected("unknown payload format")
+    serial = payload.get("serial")
+    if not isinstance(serial, int) or isinstance(serial, bool) or serial < 1:
+        raise FeedRejected("no serial")
+    if not isinstance(payload.get("announcements"), list):
+        raise FeedRejected("no announcement list")
+    return payload
+
+
+# ---- what this install is ------------------------------------------------------------
+
+
+@dataclass(frozen=True)
+class InstallFacts:
+    version: str
+    channel: str  # stable | beta
+    install_type: str  # docker | native | ha_addon | windows
+
+
+def _version_key(version: str) -> tuple:
+    """Sortable form that puts a release above its own betas (1.2.6 > 1.2.6b3)."""
+    from backend.app.api.routes.updates import parse_version
+
+    parsed = parse_version(version)
+    major, minor, patch, micro = (parsed + (0, 0, 0, 0))[:4]
+    is_prerelease = parsed[4] if len(parsed) > 4 else 0
+    prerelease_num = parsed[5] if len(parsed) > 5 else 0
+    return (major, minor, patch, micro, 1 - is_prerelease, prerelease_num)
+
+
+def _install_type() -> str:
+    from backend.app.api.routes import updates
+
+    if updates._is_windows_installer_install():
+        return "windows"
+    if updates._is_ha_addon():
+        return "ha_addon"
+    if updates._is_docker_environment():
+        return "docker"
+    return "native"
+
+
+async def install_facts(db: AsyncSession) -> InstallFacts:
+    beta_setting = (
+        await db.execute(select(Settings.value).where(Settings.key == "include_beta_updates"))
+    ).scalar_one_or_none()
+    # Beta testers are the installs that asked for betas, and the ones running one.
+    prerelease = bool(re.search(r"[a-zA-Z]", APP_VERSION.lstrip("v")))
+    beta = prerelease or (beta_setting or "").lower() == "true"
+    return InstallFacts(version=APP_VERSION, channel="beta" if beta else "stable", install_type=_install_type())
+
+
+def targets(entry: dict, facts: InstallFacts) -> bool:
+    target = entry.get("target") or {}
+    if not isinstance(target, dict):
+        return False
+    try:
+        here = _version_key(facts.version)
+        low, high = target.get("min_version"), target.get("max_version")
+        if low and here < _version_key(str(low)):
+            return False
+        if high and here > _version_key(str(high)):
+            return False
+    except (TypeError, ValueError):
+        return False
+    channels = target.get("channels") or []
+    if channels and facts.channel not in channels:
+        return False
+    install_types = target.get("install_types") or []
+    return not (install_types and facts.install_type not in install_types)
+
+
+# ---- one entry, defensively ---------------------------------------------------------
+
+
+def link_allowed(url: str) -> bool:
+    try:
+        parts = urlsplit(url)
+        port = parts.port
+    except ValueError:
+        return False
+    host = (parts.hostname or "").lower()
+    if parts.scheme != "https" or not host or parts.username or parts.password:
+        return False
+    if port not in (None, 443):
+        return False
+    return any(host == allowed or host.endswith("." + allowed) for allowed in LINK_HOSTS)
+
+
+def _parse_time(value: object) -> datetime | None:
+    if not isinstance(value, str) or not value:
+        return None
+    try:
+        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
+    except ValueError:
+        return None
+    if parsed.tzinfo is not None:
+        parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None)
+    return parsed
+
+
+def _clean_texts(texts: object) -> dict[str, dict[str, str]] | None:
+    """Plain strings, cut to length. None when there is no usable English text."""
+    if not isinstance(texts, dict):
+        return None
+    cleaned: dict[str, dict[str, str]] = {}
+    for lang, text in texts.items():
+        if not isinstance(lang, str) or len(lang) > 10 or not isinstance(text, dict):
+            continue
+        title, body = text.get("title"), text.get("body")
+        if not isinstance(title, str) or not isinstance(body, str) or not title.strip() or not body.strip():
+            continue
+        entry = {"title": title.strip()[:MAX_TITLE], "body": body.strip()[:MAX_BODY]}
+        label = text.get("link_label")
+        if isinstance(label, str) and label.strip():
+            entry["link_label"] = label.strip()[:MAX_LINK_LABEL]
+        cleaned[lang] = entry
+    return cleaned if "en" in cleaned else None
+
+
+@dataclass
+class Entry:
+    public_id: str
+    level: str
+    texts: dict[str, dict[str, str]]
+    link_url: str | None
+    published_at: datetime | None
+    expires_at: datetime | None
+
+
+def parse_entry(raw: object) -> Entry | None:
+    """One feed entry, or None if it can't be shown safely. Never raises."""
+    if not isinstance(raw, dict):
+        return None
+    public_id = raw.get("id")
+    if not isinstance(public_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", public_id):
+        return None
+    level = raw.get("level") if raw.get("level") in LEVELS else "info"
+    texts = _clean_texts(raw.get("texts"))
+    if texts is None:
+        return None
+    link = raw.get("link_url")
+    link = link if isinstance(link, str) and link_allowed(link) else None
+    published_at = _parse_time(raw.get("published_at"))
+    expires_at = _parse_time(raw.get("expires_at"))
+    # History is whatever has expired; the feed's ``archived`` flag says the same
+    # thing for messages the registrar kept after their expiry. One flagged but
+    # still in date by this install's clock is history anyway: expire it now.
+    now = datetime.now(timezone.utc).replace(tzinfo=None)
+    if raw.get("archived") is True and (expires_at is None or expires_at > now):
+        expires_at = now
+    return Entry(
+        public_id=public_id,
+        level=level,
+        texts=texts,
+        link_url=link,
+        published_at=published_at,
+        expires_at=expires_at,
+    )
+
+
+# ---- store ---------------------------------------------------------------------------
+
+
+async def _get(db: AsyncSession, key: str) -> str | None:
+    return (await db.execute(select(Settings.value).where(Settings.key == key))).scalar_one_or_none()
+
+
+async def _set(db: AsyncSession, key: str, value: str) -> None:
+    from backend.app.core.db_dialect import upsert_setting
+
+    await upsert_setting(db, Settings, key, value)
+
+
+async def is_enabled(db: AsyncSession) -> bool:
+    return (await _get(db, ENABLED_KEY) or "true").lower() != "false"
+
+
+async def apply_payload(db: AsyncSession, payload: dict, facts: InstallFacts) -> int:
+    """Replace the stored announcements with the ones in ``payload`` that apply here.
+
+    Refuses (FeedRejected) a serial below the highest already accepted. The same
+    serial again is the same feed fetched twice, and is fine. Returns how many are
+    stored. The caller commits.
+    """
+    serial = payload["serial"]
+    seen = int(await _get(db, SERIAL_KEY) or 0)
+    if serial < seen:
+        raise FeedRejected(f"serial {serial} is older than {seen}, already accepted")
+
+    wanted: dict[str, Entry] = {}
+    for raw in payload["announcements"]:
+        entry = parse_entry(raw)
+        if entry is not None and targets(raw, facts):
+            wanted[entry.public_id] = entry
+
+    existing = {a.public_id: a for a in (await db.execute(select(Announcement))).scalars().all()}
+    gone = [a.id for pid, a in existing.items() if pid not in wanted]
+    if gone:
+        # Read markers first: SQLite enforces no foreign keys unless asked to, so
+        # ON DELETE CASCADE alone would leave them behind.
+        await db.execute(delete(AnnouncementRead).where(AnnouncementRead.announcement_id.in_(gone)))
+        await db.execute(delete(Announcement).where(Announcement.id.in_(gone)))
+    for pid, entry in wanted.items():
+        row = existing.get(pid) or Announcement(public_id=pid)
+        row.level = entry.level
+        row.texts = json.dumps(entry.texts, ensure_ascii=False)
+        row.link_url = entry.link_url
+        row.published_at = entry.published_at
+        row.expires_at = entry.expires_at
+        if pid not in existing:
+            db.add(row)
+    await _set(db, SERIAL_KEY, str(max(serial, seen)))
+    return len(wanted)
+
+
+async def _download() -> bytes:
+    # No version and no install identity in the request: the generic agent says
+    # what is asking, nothing about which install.
+    headers = {"User-Agent": "Bambuddy-Announcements", "Cache-Control": "no-cache"}
+    async with (
+        httpx.AsyncClient(timeout=15, follow_redirects=False) as client,
+        client.stream("GET", FEED_URL, headers=headers) as response,
+    ):
+        if response.status_code != 200:
+            raise FeedRejected(f"HTTP {response.status_code}")
+        chunks: list[bytes] = []
+        size = 0
+        async for chunk in response.aiter_bytes():
+            size += len(chunk)
+            if size > MAX_FEED_BYTES:
+                raise FeedRejected("larger than the feed is allowed to be")
+            chunks.append(chunk)
+        return b"".join(chunks)
+
+
+async def refresh(db: AsyncSession) -> int | None:
+    """Fetch, verify and store. Returns how many apply here, or None if nothing changed.
+
+    Never raises for a network problem or a bad feed -- those are logged and the
+    last good list stays. A switched-off install fetches nothing.
+    """
+    if not await is_enabled(db):
+        return None
+    seen_before = int(await _get(db, SERIAL_KEY) or 0)
+    try:
+        content = await _download()
+        payload = verify_feed(content)
+        count = await apply_payload(db, payload, await install_facts(db))
+    except FeedRejected as exc:
+        await db.rollback()
+        logger.warning("Announcements feed ignored: %s", exc)
+        return None
+    except httpx.HTTPError as exc:
+        await db.rollback()
+        # Offline and air-gapped installs land here every time; not worth a warning.
+        logger.info("Announcements feed not fetched: %s", type(exc).__name__)
+        return None
+    await _set(db, LAST_FETCH_KEY, datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))
+    await db.commit()
+    logger.info("Announcements feed #%s: %d for this install", payload["serial"], count)
+    if payload["serial"] > seen_before:
+        await _tell_open_pages()
+    return count
+
+
+async def _tell_open_pages() -> None:
+    """Have every open Bambuddy page re-read the list, so a new message's dot and
+    banner appear without a reload. The event carries nothing: each page asks
+    GET /announcements, which answers by who is asking."""
+    from backend.app.core.websocket import ws_manager
+
+    try:
+        await ws_manager.broadcast({"type": "announcements_changed"})
+    except Exception:  # A page that misses it catches up on its own poll.
+        logger.debug("announcements_changed broadcast failed", exc_info=True)
+
+
+# ---- read state ----------------------------------------------------------------------
+
+
+async def list_for(db: AsyncSession, user_id: int | None) -> list[dict]:
+    """Every stored announcement, newest first, with this user's read state.
+
+    ``archived`` marks history: messages past their expiry, which the panel lists
+    under "Earlier" and which never count as unread or raise a banner. The feed
+    decides how much history there is; a message it drops is deleted here.
+    """
+    now = datetime.now(timezone.utc).replace(tzinfo=None)
+    rows = (
+        (await db.execute(select(Announcement).order_by(Announcement.published_at.desc(), Announcement.id.desc())))
+        .scalars()
+        .all()
+    )
+    reader = AnnouncementRead.user_id.is_(None) if user_id is None else AnnouncementRead.user_id == user_id
+    read_ids = set((await db.execute(select(AnnouncementRead.announcement_id).where(reader))).scalars().all())
+    result = []
+    for a in rows:
+        try:
+            texts = json.loads(a.texts)
+        except (TypeError, ValueError):
+            continue
+        result.append(
+            {
+                "id": a.public_id,
+                "level": a.level,
+                "texts": texts,
+                "link_url": a.link_url,
+                "published_at": a.published_at.isoformat() + "Z" if a.published_at else None,
+                "expires_at": a.expires_at.isoformat() + "Z" if a.expires_at else None,
+                "archived": a.expires_at is not None and a.expires_at <= now,
+                "read": a.id in read_ids,
+            }
+        )
+    return result
+
+
+async def mark_read(db: AsyncSession, public_id: str, user_id: int | None) -> bool:
+    """Record that this user read it. False if there is no such announcement."""
+    announcement = (
+        await db.execute(select(Announcement).where(Announcement.public_id == public_id))
+    ).scalar_one_or_none()
+    if announcement is None:
+        return False
+    reader = AnnouncementRead.user_id.is_(None) if user_id is None else AnnouncementRead.user_id == user_id
+    # Checked rather than left to the unique constraint: NULL user_ids never
+    # collide in one, so the auth-off row would be duplicated on every click.
+    already = (
+        await db.execute(select(AnnouncementRead.id).where(AnnouncementRead.announcement_id == announcement.id, reader))
+    ).first()
+    if already is None:
+        db.add(AnnouncementRead(announcement_id=announcement.id, user_id=user_id))
+    return True
+
+
+# ---- background loop -----------------------------------------------------------------
+
+_task: asyncio.Task | None = None
+
+
+async def _loop() -> None:
+    from backend.app.core.database import async_session
+
+    await asyncio.sleep(FIRST_FETCH_DELAY_SECONDS)
+    while True:
+        try:
+            async with async_session() as db:
+                await refresh(db)
+        except asyncio.CancelledError:
+            raise
+        except Exception:
+            logger.exception("Announcements refresh failed")
+        await asyncio.sleep(FETCH_INTERVAL_SECONDS + random.uniform(0, FETCH_JITTER_SECONDS))
+
+
+def start() -> None:
+    global _task
+    if _task is None:
+        _task = asyncio.create_task(_loop())
+
+
+def stop() -> None:
+    global _task
+    if _task is not None:
+        _task.cancel()
+        _task = None

+ 50 - 2
backend/app/services/archive.py

@@ -150,6 +150,54 @@ def swap_plate_suffix(name: str | None, target_plate: int) -> str | None:
     return f"{base}{separator}{target_plate}"
 
 
+_MESH_OPEN = b"<mesh"
+_MESH_CLOSE = b"</mesh>"
+_MODEL_READ_CHUNK = 1024 * 1024
+
+
+def read_3dmodel_without_meshes(zf: zipfile.ZipFile, model_path: str = "3D/3dmodel.model") -> str:
+    """Return the model XML with every ``<mesh>...</mesh>`` body left out.
+
+    The metadata scans only want ``<metadata>`` elements and the URLs inside
+    them, but a plain 3MF keeps its geometry inline in this same entry: a
+    combined plate's is hundreds of MB of vertex and triangle rows (#3162).
+    Streaming past the meshes keeps memory to the non-geometry part. The
+    entry can't simply be cut at ``<resources>``: BambuStudio writes its
+    MakerWorld metadata after ``</build>``. Mesh bodies hold only numbers, and
+    a ``<`` inside a metadata value is escaped, so nothing a scan looks for is
+    dropped.
+    """
+    out = bytearray()
+    pending = b""
+    in_mesh = False
+    with zf.open(model_path) as fh:
+        while chunk := fh.read(_MODEL_READ_CHUNK):
+            data = pending + chunk
+            pos = 0
+            while True:
+                if in_mesh:
+                    end = data.find(_MESH_CLOSE, pos)
+                    if end == -1:
+                        # Keep enough of the tail to match a tag split across chunks.
+                        pending = data[max(pos, len(data) - len(_MESH_CLOSE) + 1) :]
+                        break
+                    pos = end + len(_MESH_CLOSE)
+                    in_mesh = False
+                else:
+                    start = data.find(_MESH_OPEN, pos)
+                    if start == -1:
+                        keep = max(pos, len(data) - len(_MESH_OPEN) + 1)
+                        out += data[pos:keep]
+                        pending = data[keep:]
+                        break
+                    out += data[pos:start]
+                    pos = start + len(_MESH_OPEN)
+                    in_mesh = True
+    if not in_mesh:
+        out += pending
+    return out.decode("utf-8", errors="ignore")
+
+
 # How much of a plate's G-code to scan for header/config values. The header
 # block ends in the first kilobyte; the CONFIG_BLOCK that follows it carries
 # layer_height 14-25KB in (measured across the sliced 3MFs on hand, Bambu
@@ -550,7 +598,7 @@ class ThreeMFParser:
             if model_path not in zf.namelist():
                 return
 
-            content = zf.read(model_path).decode("utf-8", errors="ignore")
+            content = read_3dmodel_without_meshes(zf, model_path)
 
             # Parse XML metadata elements
             # MakerWorld adds metadata like: <metadata name="Designer">username</metadata>
@@ -799,7 +847,7 @@ class ProjectPageParser:
                 # Parse 3D/3dmodel.model for metadata
                 model_path = "3D/3dmodel.model"
                 if model_path in zf.namelist():
-                    content = zf.read(model_path).decode("utf-8", errors="ignore")
+                    content = read_3dmodel_without_meshes(zf, model_path)
 
                     # Extract metadata elements using regex
                     # Format: <metadata name="Key">Value</metadata> or <metadata name="Key" />

+ 8 - 1
backend/app/services/bambu_ftp.py

@@ -1275,7 +1275,14 @@ class BambuFTPClient:
             return False
         except (OSError, ftplib.Error) as e:
             logger.error("FTP upload failed for %s: %s (type: %s)", remote_path, e, type(e).__name__)
-            self.last_failure = FtpFailure(FtpFailureKind.NETWORK, str(e), _ftp_reply_code(e))
+            code = _ftp_reply_code(e)
+            # 452 is "insufficient storage" -- a 4xx, so ftplib raises it as
+            # error_temp rather than error_perm, but it is the printer talking
+            # about its card just as 552/553 are. As NETWORK it would read as a
+            # connection problem, and the queue would retry it forever (#3210).
+            is_storage = isinstance(e, ftplib.Error) and code == "452"
+            kind = FtpFailureKind.STORAGE if is_storage else FtpFailureKind.NETWORK
+            self.last_failure = FtpFailure(kind, str(e), code)
             return False
 
     def upload_bytes(self, data: bytes, remote_path: str) -> bool:

+ 209 - 33
backend/app/services/bambu_mqtt.py

@@ -22,8 +22,8 @@ from datetime import datetime, timezone
 import paho.mqtt.client as mqtt
 
 from backend.app.services.hms_actions import HMSAction, get_actions_for_error_code
-from backend.app.services.hms_errors import describe_fault
-from backend.app.utils.ams_drying import ACTIVE_DRY_STATUSES
+from backend.app.services.hms_errors import alert_level_from_print_error, describe_fault
+from backend.app.utils.ams_drying import ACTIVE_DRY_STATUSES, DRY_COUNTDOWN_STALL_SECONDS
 from backend.app.utils.ams_humidity import ams_humidity_percent
 from backend.app.utils.paho_teardown import retire_paho_client
 
@@ -179,6 +179,40 @@ def a2l_lite_wire_ids(ams_id: int, tray_id: int) -> tuple[int, int, int] | None:
     )
 
 
+def resolve_external_spools_in_mapping(ams_mapping: object, ams_mapping2: object, is_dual_nozzle: bool) -> object:
+    """Put the external spool back into a captured flat ``ams_mapping`` (#3166).
+
+    The firmware rejects 254/255 in the flat list, so BambuStudio and our own
+    dispatch both write an external spool there as -1 -- the same value as a
+    slot that isn't fed at all -- and carry the real target only in
+    ``ams_mapping2`` as ``{ams_id: 254|255, slot_id: 0}``. Capturing the flat
+    list alone turns "printed from the external spool" into "unmapped", and
+    usage then lands on whatever tray the fallback guesses.
+
+    Each -1 whose ``ams_mapping2`` entry names an external spool becomes the
+    global tray the rest of Bambuddy uses: the ams_id itself on dual-nozzle
+    printers (254 = left/deputy, 255 = right/main), 254 on single-nozzle
+    printers, which have one external spool that the wire always calls 255.
+    ``{255, 255}`` is the unmapped marker and stays -1. Every other entry is
+    left exactly as captured. Without a usable ``ams_mapping2`` the input is
+    returned unchanged.
+    """
+    if not isinstance(ams_mapping, list) or not isinstance(ams_mapping2, list):
+        return ams_mapping
+    if len(ams_mapping2) != len(ams_mapping):
+        return ams_mapping
+    resolved = list(ams_mapping)
+    for i, (flat, detail) in enumerate(zip(ams_mapping, ams_mapping2, strict=True)):
+        if flat != -1 or not isinstance(detail, dict):
+            continue
+        ams_id = detail.get("ams_id")
+        slot_id = detail.get("slot_id")
+        if ams_id not in (254, 255) or slot_id == 255:
+            continue
+        resolved[i] = ams_id if is_dual_nozzle else 254
+    return resolved
+
+
 def apply_tray_exist_bits(
     units: list,
     tray_exist_bits_str: str | int | None,
@@ -669,12 +703,14 @@ class HMSError:
     code: str
     attr: int  # Attribute value for constructing wiki URL
     module: int
-    severity: int  # 1=fatal, 2=serious, 3=common, 4=info
+    # Bambu's alert level: 1 error (task stopped), 2 warning (task paused),
+    # 3 notification, 0 invalid. From `code >> 16` for `hms[]` faults, and from
+    # the error's first hex digit for `print_error` (#2728).
+    severity: int
     # The bundled catalogue's sentence for this fault, resolved once here so
     # every surface that reports it — the status response, the WebSocket
     # broadcast, the completion payload, notifications — says the same thing.
-    # None when the catalogue does not cover the code; `describe_fault` documents
-    # the lookup and why the lossy `hms[]` collapse is kept as it was.
+    # None when Bambu publishes no text for the code; see `describe_fault`.
     # Replaces a `message` field that was never set or read anywhere.
     description: str | None = None
     # User-facing remediation actions from the bundled HMS catalog (e.g. "RESUME_PRINTING",
@@ -1277,6 +1313,15 @@ class BambuMQTTClient:
         # Per-AMS previous dry_time, used to detect the falling edge above.
         # Seeded lazily as we observe each AMS unit.
         self._previous_dry_times: dict[int, int] = {}
+        # Per-AMS monotonic stamp of the last time dry_time CHANGED value.
+        # A live cycle's countdown ticks once a minute; a command the firmware
+        # accepted but never actually started (observed on an H2D mid-print
+        # with two AMS-HT cycles already running: the third unit's timer sat
+        # frozen at its full duration) never ticks. dry_time > 0 with no tick
+        # for DRY_COUNTDOWN_STALL_SECONDS and no active dry_status phase is
+        # reported as dry_countdown_stalled so the UI can stop claiming an
+        # active cycle that is not running.
+        self._dry_time_changed_at: dict[int, float] = {}
         # Per-AMS active-cycle target params (filament + temp) we sent on the
         # last start. Bambu does not echo these back in the per-tick AMS push
         # — only the dry_time countdown — so we cache what we sent to drive
@@ -1387,6 +1432,9 @@ class BambuMQTTClient:
         # on both an X1C and an H2D (#2718). Filled by the MQTT thread, drained
         # by await_cali_ack.
         self._pending_cali_acks: dict[str, dict | None] = {}
+        # Acks for RFID re-reads (ams_get_rfid, and the M620 R gcode_line
+        # fallback), keyed the same way. Drained by ams_refresh_tray (#3206).
+        self._pending_rfid_acks: dict[str, dict | None] = {}
 
         # Identifies the one project_file *we* dispatched, so its echo on the
         # topic can be told apart from a slicer's. One-shot: consumed by the
@@ -1988,7 +2036,7 @@ class BambuMQTTClient:
                 self.state.current_project_url = url
                 self.state.last_project_url = url
             if "ams_mapping" in print_data:
-                self._captured_ams_mapping = print_data["ams_mapping"]
+                self._captured_ams_mapping = self._resolve_captured_mapping(print_data)
                 logger.info(
                     "[%s] Captured ams_mapping from print command: %s",
                     self.serial_number,
@@ -2068,13 +2116,24 @@ class BambuMQTTClient:
         # already captured this print's mapping that copy is the slicer's own,
         # and the echo can arrive without the field at all.
         if self._captured_ams_mapping is None and isinstance(print_data.get("ams_mapping"), list):
-            self._captured_ams_mapping = print_data["ams_mapping"]
+            self._captured_ams_mapping = self._resolve_captured_mapping(print_data)
             logger.info(
                 "[%s] Captured ams_mapping from print response: %s",
                 self.serial_number,
                 self._captured_ams_mapping,
             )
 
+    def _resolve_captured_mapping(self, print_data: dict) -> object:
+        """The ``ams_mapping`` of a project_file, with external spools resolved
+        from its ``ams_mapping2`` (#3166)."""
+        from backend.app.utils.printer_models import is_dual_nozzle_model
+
+        return resolve_external_spools_in_mapping(
+            print_data.get("ams_mapping"),
+            print_data.get("ams_mapping2"),
+            self._is_dual_nozzle or is_dual_nozzle_model(self.model),
+        )
+
     @staticmethod
     def _project_file_key(print_data: dict) -> str:
         """Identity of a project_file dispatch, for telling ours from a slicer's.
@@ -2393,6 +2452,25 @@ class BambuMQTTClient:
                 # INFO level so the body lands in support bundles by default.
                 elif cmd == "ams_filament_drying":
                     logger.info("[%s] ams_filament_drying response: %s", self.serial_number, print_data)
+                # RFID re-reads are user-initiated and rare, and a refusal was
+                # invisible until #3206 (X1Plus answering FAIL / ERROR STATE).
+                # gcode_line acks are only of interest when they answer our
+                # M620 R fallback, so those are matched by sequence_id alone.
+                ack_seq = str(print_data.get("sequence_id", ""))
+                if (
+                    cmd in ("ams_get_rfid", "gcode_line")
+                    and ack_seq in self._pending_rfid_acks
+                    and "result" in print_data
+                ):
+                    logger.info(
+                        "[%s] %s response: result=%s reason=%s seq=%s",
+                        self.serial_number,
+                        cmd,
+                        print_data.get("result"),
+                        print_data.get("reason", ""),
+                        ack_seq,
+                    )
+                    self._pending_rfid_acks[ack_seq] = print_data
                 # Check for developer mode probe response
                 if (
                     cmd == "ams_filament_setting"
@@ -3185,15 +3263,16 @@ class BambuMQTTClient:
                     # slot (typically 0) when the active feed is actually the
                     # external spool. X1C / P1S / A1 correctly report 254 in
                     # that case; H2S does not. When the slicer-captured
-                    # ams_mapping is all-external (every entry == -1), the
-                    # print can only be feeding from the external spool, so
-                    # promote tray_now to 254. Mixed (e.g. [5, -1]) and
+                    # ams_mapping is all-external (every entry is 254, or -1
+                    # when the command carried no ams_mapping2 to resolve it
+                    # from, #3166), the print can only be feeding from the
+                    # external spool, so promote tray_now to 254. Mixed (e.g. [5, 254]) and
                     # AMS-only mappings are NOT overridden — there's no
                     # evidence the firmware misreports in those cases. Prints
                     # started without a captured mapping (printer-screen start,
                     # or before Bambuddy connected) fall through unchanged.
                     captured = self._captured_ams_mapping
-                    if captured and all(s == -1 for s in captured):
+                    if captured and all(s in (-1, 254, 255) for s in captured):
                         if self.state.tray_now != 254:
                             logger.debug(
                                 f"[{self.serial_number}] tray_now external-spool override (#1822): "
@@ -3588,6 +3667,11 @@ class BambuMQTTClient:
                 continue
             if ams_id < 0:
                 continue
+            # The firmware phase outranks the stalled-countdown heuristic even
+            # on a transient-zero frame, which the completion guard below skips.
+            # Clear a previously raised flag before that early continue.
+            if ams_unit.get("dry_status") in ACTIVE_DRY_STATUSES:
+                ams_unit["dry_countdown_stalled"] = False
             # Only evaluate the edge when this update carries an explicit
             # dry_time. An absent / unparseable value is NOT zero — treating
             # it as 0 lets a tray-only partial fake a drying-complete edge
@@ -3623,6 +3707,18 @@ class BambuMQTTClient:
                 continue
             previous = self._previous_dry_times.get(ams_id, 0)
             self._previous_dry_times[ams_id] = current
+            # Stall detection: stamp value CHANGES only — a live countdown
+            # decrements once a minute, so repeats of the same value within
+            # the minute must not refresh the stamp, and a frame without a
+            # dry_time never reaches here (the absent-value skip above).
+            now_mono = time.monotonic()
+            if current != previous or ams_id not in self._dry_time_changed_at:
+                self._dry_time_changed_at[ams_id] = now_mono
+            ams_unit["dry_countdown_stalled"] = bool(
+                current > 0
+                and ams_unit.get("dry_status") not in ACTIVE_DRY_STATUSES
+                and now_mono - self._dry_time_changed_at[ams_id] > DRY_COUNTDOWN_STALL_SECONDS
+            )
             if previous > 0 and current == 0:
                 self._log_drying_cycle_end(ams_id, previous, ams_unit, self._drying_targets.pop(ams_id, None))
                 if self.on_drying_complete:
@@ -4785,13 +4881,20 @@ class BambuMQTTClient:
                             attr = int(attr.replace("0x", ""), 16) if attr else 0
                         if isinstance(code, str):
                             code = int(code.replace("0x", ""), 16) if code else 0
-                        # Severity is in attr byte 1 (bits 8-15)
-                        severity = (attr >> 8) & 0xF
+                        # attr is module, module no., part, part no. (a byte each);
+                        # code is the alert level (high 16 bits) then the error
+                        # (low 16). The level is 1 error (task stopped), 2 warning
+                        # (task paused), 3 notification; 0 is invalid. This is how
+                        # Bambu Studio decodes it (DevHMSItem::parse_hms_info). It
+                        # used to be read from attr bits 8-15, which is the part
+                        # byte (#2728).
+                        severity = (code >> 16) & 0xFFFF
                         # Module is in attr byte 3 (bits 24-31)
                         module = (attr >> 24) & 0xFF
-                        # Skip non-error status codes — all real HMS errors
-                        # have code >= 0x4000. Lower values are status/phase
-                        # indicators that some firmware sends during normal printing.
+                        # Skip non-error status codes. A real fault has a level of
+                        # at least 1 in the high half, so its code is >= 0x10000
+                        # and clears this; lower values are status/phase
+                        # indicators some firmware sends during normal printing.
                         if code < 0x4000:
                             continue
                         # Skip user-action echoes — the printer firmware emits these
@@ -4802,11 +4905,11 @@ class BambuMQTTClient:
                         short_code = f"{(attr >> 16) & 0xFFFF:04X}_{code & 0xFFFF:04X}"
                         if short_code in _HMS_USER_ACTION_CODES:
                             continue
-                        # Catalog has both 8-char keys (base class) and 16-char keys
-                        # (specific variants). The full 16-char identifier preserves
-                        # the 32 bits of `attr_low` + `code_high` that the short_code
-                        # discards — that's the firmware's matching key, so try it
-                        # first and fall back to the short form.
+                        # The full 16-char identifier is the firmware's matching key
+                        # and the HMS catalogue's key for this kind of fault; the
+                        # short code discards the part and alert-level groups. The
+                        # action table still has some short-form entries, so that
+                        # lookup falls back to it.
                         full_code = f"{attr:08X}{code:08X}"
                         if full_code == HMS_MQTT_VERIFY_FAILED:
                             verify_failed = True
@@ -4818,11 +4921,13 @@ class BambuMQTTClient:
                                 code=f"0x{code:x}" if code else "0x0",
                                 attr=attr,
                                 module=module,
-                                severity=severity if severity > 0 else 2,
+                                # No fallback: 0 is Bambu's "invalid" level, and
+                                # promoting it would render it as a fault.
+                                severity=severity,
                                 actions=actions,
                                 job_id=self.state.subtask_id,
                                 full_code=full_code,
-                                description=describe_fault(full_code),
+                                description=describe_fault(full_code, self.serial_number[:3]),
                             )
                         )
             self._apply_mqtt_verify_state(verify_failed)
@@ -4892,13 +4997,15 @@ class BambuMQTTClient:
                                     code=f"0x{error:x}",
                                     attr=print_error,  # Store full value for display
                                     module=module >> 8,  # High byte of module (e.g., 0x05)
-                                    severity=3,  # Warning level for print_error
+                                    # print_error has no level field; its error
+                                    # number's first digit carries it instead.
+                                    severity=alert_level_from_print_error(error),
                                     actions=actions,
                                     job_id=job_id,
                                     # print_error is already 32-bit — `f"{print_error:08X}"`
                                     # is the firmware's matching key with no truncation.
                                     full_code=f"{print_error:08X}",
-                                    description=describe_fault(f"{print_error:08X}"),
+                                    description=describe_fault(f"{print_error:08X}", self.serial_number[:3]),
                                 )
                             )
 
@@ -5483,6 +5590,9 @@ class BambuMQTTClient:
                         "attr": e.attr,
                         "module": e.module,
                         "severity": e.severity,
+                        # Lets the queue's failure reason label an `hms[]` fault
+                        # with its full code rather than a lossy short one (#2728).
+                        "full_code": e.full_code,
                         # Carried so the queue's failure reason quotes the same
                         # sentence the status response and the broadcast do,
                         # rather than resolving the code a fourth time (#2926).
@@ -7627,9 +7737,19 @@ class BambuMQTTClient:
         logger.info("[%s] AMS control: %s", self.serial_number, action)
         return True
 
-    def ams_refresh_tray(self, ams_id: int, tray_id: int) -> tuple[bool, str]:
+    async def ams_refresh_tray(self, ams_id: int, tray_id: int) -> tuple[bool, str]:
         """Trigger RFID re-read for a specific AMS tray.
 
+        Sends ``ams_get_rfid`` and waits for the printer's answer. Firmware
+        that refuses it (#3206: X1Plus on base 01.08.02.00 answers ``FAIL`` /
+        ``ERROR STATE``) gets the legacy ``M620 R<global tray>`` gcode instead,
+        which is what Bambu Studio sends to printers without the new protocol.
+        The fallback only follows an explicit refusal, so firmware that takes
+        ``ams_get_rfid`` never sees it.
+
+        Success means the printer accepted the request, not that the tag was
+        read: the slot updating in the next push is the only proof of that.
+
         Args:
             ams_id: AMS unit ID (0-3, or 128 for H2D external tray)
             tray_id: Tray ID within the AMS (0-3)
@@ -7662,15 +7782,71 @@ class BambuMQTTClient:
         if (_a2l := a2l_lite_wire_ids(ams_id, tray_id)) is not None:
             wire_ams_id, wire_slot_id, _ = _a2l
 
-        # Use ams_get_rfid command to trigger RFID re-read
-        # This command is used by Bambu Studio to re-read the RFID tag
-        command = {
-            "print": {"command": "ams_get_rfid", "ams_id": wire_ams_id, "slot_id": wire_slot_id, "sequence_id": "0"}
-        }
-        self._client.publish(self.topic_publish, json.dumps(command), qos=1)
         logger.info("[%s] Triggering RFID re-read: AMS %s, slot %s", self.serial_number, ams_id, tray_id)
+        refused = await self._send_rfid_command(
+            {"command": "ams_get_rfid", "ams_id": wire_ams_id, "slot_id": wire_slot_id}
+        )
+        if refused is None:
+            return True, f"Refreshing AMS {ams_id} tray {tray_id}"
+
+        # M620 R only for what Bambu Studio sends it to: a model older than the
+        # newer protocol, on one of the four regular AMS units (the only ones
+        # with a legacy global tray index). Never during a job either: a
+        # gcode_line would be executed inside the running print.
+        from backend.app.utils.printer_models import uses_legacy_rfid_refresh
+
+        if (
+            not 0 <= wire_ams_id <= 3
+            or not uses_legacy_rfid_refresh(self.model)
+            or self.state.state in _ACTIVE_PRINT_STATES
+        ):
+            return False, f"Printer refused the RFID refresh: {refused}"
+
+        logger.info(
+            "[%s] ams_get_rfid refused (%s), falling back to M620 R%s",
+            self.serial_number,
+            refused,
+            wire_ams_id * 4 + wire_slot_id,
+        )
+        legacy_refused = await self._send_rfid_command(
+            {"command": "gcode_line", "param": f"M620 R{wire_ams_id * 4 + wire_slot_id}\n"}
+        )
+        if legacy_refused is None:
+            return True, f"Refreshing AMS {ams_id} tray {tray_id} (legacy command)"
+        return False, f"Printer refused the RFID refresh: {legacy_refused}"
+
+    # The answer to ams_get_rfid was measured at 11ms in #3206's capture.
+    _rfid_ack_timeout: float = 3.0
+
+    async def _send_rfid_command(self, print_command: dict) -> str | None:
+        """Publish one RFID refresh command and wait for the printer's answer.
 
-        return True, f"Refreshing AMS {ams_id} tray {tray_id}"
+        Returns the printer's reason when it answered ``FAIL``, else None.
+        No answer within ``_rfid_ack_timeout`` counts as accepted, because
+        silence is not evidence of refusal and older firmware may not answer
+        at all.
+        """
+        timeout = self._rfid_ack_timeout
+        self._sequence_id += 1
+        seq_id = str(self._sequence_id)
+        command = {"print": {**print_command, "sequence_id": seq_id}}
+        self._pending_rfid_acks[seq_id] = None
+        try:
+            self._client.publish(self.topic_publish, json.dumps(command), qos=1)
+            deadline = time.monotonic() + timeout
+            while time.monotonic() < deadline:
+                ack = self._pending_rfid_acks.get(seq_id)
+                if ack is not None:
+                    if str(ack.get("result", "")).lower() == "fail":
+                        return str(ack.get("reason", "") or "") or "printer reported failure"
+                    return None
+                await asyncio.sleep(0.05)
+        finally:
+            self._pending_rfid_acks.pop(seq_id, None)
+        logger.info(
+            "[%s] No answer to %s seq=%s within %.1fs", self.serial_number, print_command["command"], seq_id, timeout
+        )
+        return None
 
     def ams_set_filament_setting(
         self,

+ 11 - 2
backend/app/services/camera_profiles.py

@@ -53,10 +53,19 @@ class CameraProfile:
     # Max consecutive ffmpeg respawns when the printer drops the RTSP
     # session mid-stream. Some firmwares cut the stream after a few
     # seconds (originally noted on P2S), so we transparently respawn
-    # to keep the MJPEG client alive.
+    # to keep the MJPEG client alive. Consecutive means failed sessions
+    # in a row: a session that delivered frames resets the count. A stock
+    # X1C ends every session after about a minute, so a lifetime count
+    # stopped a live view for good after half an hour.
     rtsp_reconnect_max: int = 30
-    # Seconds between ffmpeg respawn attempts.
+    # Seconds before respawning after a session that delivered frames --
+    # the routine drop, reconnected fast enough that viewers don't notice.
     rtsp_reconnect_delay: float = 0.2
+    # Each further failed attempt in a row doubles the delay up to this cap,
+    # so a printer that is refusing sessions for a while (another client on
+    # its camera, a busy camera service) gets minutes rather than 30 dials in
+    # nine seconds before the stream is given up.
+    rtsp_reconnect_backoff_max: float = 5.0
 
     # --- Extra ffmpeg input args ---------------------------------------------
     # Hook for future per-model knobs (e.g. `-fflags` overrides) without

+ 158 - 0
backend/app/services/color_catalog_lookup.py

@@ -0,0 +1,158 @@
+"""Resolve a Bambu Lab roll's colour from the colour catalogue, and recognise its product line.
+
+Extracted so the two inventory modes stop answering the same question in two
+places. ``spool_tag_matcher.create_spool_from_tray`` had this logic inline since
+#857, and three separate fixes landed there without crossing to the Spoolman
+side -- the sub-brand filter (#1227), the alpha guard (#1545) and the translucent
+handling. #2907 is the fourth. Both callers now go through here.
+"""
+
+from dataclasses import dataclass
+
+from sqlalchemy import func, select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.models.color_catalog import ColorCatalogEntry
+
+BAMBU_MANUFACTURER = "BAMBU LAB"
+
+
+@dataclass(frozen=True)
+class CatalogColor:
+    """What the catalogue row says about a roll's colour.
+
+    The name alone is not enough for the built-in path: a row can carry gradient
+    stops (``extra_colors``) and a rendering hint (``effect_type``), which is how
+    a roll the AMS identified draws the same as one picked by hand from that row.
+    """
+
+    name: str
+    extra_colors: str | None = None
+    effect_type: str | None = None
+
+
+async def resolve_bambu_color(db: AsyncSession, rgba: str | None, sub_brand: str | None) -> CatalogColor | None:
+    """The catalogue's colour for this roll, or None when it has no row for it.
+
+    Hex alone is not an identity: ``#FFFFFF`` is "Jade White" in PLA Basic,
+    "Ivory White" in PLA Matte and "White" in PLA Silk. The printer reports which
+    product line the roll belongs to as ``tray_sub_brands``, and the catalogue
+    stores the same string in its ``material`` column -- so the two compare
+    directly, with no need to rebuild one from a type plus a subtype.
+
+    None is a real answer and not a failure. The catalogue is seeded from Bambu's
+    published hex list and lags new colours, so a roll it has never heard of has
+    no name to give and the caller has to cope rather than pick something.
+
+    Alpha 00 short-circuits to "Clear" (#1545) before the catalogue is consulted.
+    The catalogue stores RGB only, so a clear roll's ``00000000`` would look up
+    ``#000000`` and come back "Black" -- the exact bug #1545 was filed for.
+
+    That short-circuit is also a limit worth stating. "Clear" is the answer for
+    every product line, so a PLA Matte clear roll and a PLA Basic clear roll get
+    the same name, and nothing downstream that keys on the name can tell them
+    apart. The Spoolman path's product-line criterion is inert for translucent
+    rolls for exactly this reason. Naming a genuinely translucent roll from the
+    catalogue would very likely beat "Clear", but that changes both modes and
+    what the built-in path has returned since #1545, so it is not something an
+    extraction should decide on its way past.
+    """
+    if not rgba:
+        return None
+    # #1545, and the same test the built-in path has always applied.
+    if len(rgba) == 8 and rgba[6:8].lower() == "00":
+        return CatalogColor("Clear")
+    if len(rgba) < 6:
+        return None
+    hex_prefix = f"#{rgba[:6].upper()}"
+    query = (
+        select(ColorCatalogEntry)
+        .where(func.upper(ColorCatalogEntry.hex_color) == hex_prefix)
+        .where(func.upper(ColorCatalogEntry.manufacturer) == BAMBU_MANUFACTURER)
+    )
+    if sub_brand:
+        query = query.where(func.upper(ColorCatalogEntry.material) == sub_brand.upper())
+    # Deterministic tiebreak for the case the sub-brand filter cannot settle --
+    # a roll reporting no sub-brand at all.
+    query = query.order_by(ColorCatalogEntry.id).limit(1)
+    entry = (await db.execute(query)).scalar_one_or_none()
+    if entry is None:
+        return None
+    return CatalogColor(entry.color_name, entry.extra_colors, entry.effect_type)
+
+
+def _normalise(name: str | None) -> str:
+    # "Tough+ Black" and "Silk+ Gold" are the library's spelling of lines the
+    # printer reports as "PLA Tough" and "PLA Silk". The plus is part of the
+    # product name, not the line, so it is dropped before comparing.
+    return " ".join((name or "").replace("+", " ").lower().split())
+
+
+def _line_word(sub_brand: str | None, material: str | None) -> str:
+    """The product line with the base material taken off: "PLA Matte" -> "matte"."""
+    line = _normalise(sub_brand)
+    base = _normalise(material)
+    if base and line.startswith(base + " "):
+        return line[len(base) + 1 :]
+    return ""
+
+
+def product_line_rank(
+    filament_name: str | None,
+    color_name: str | None,
+    sub_brand: str | None,
+    material: str | None,
+) -> int | None:
+    """How specifically a filament name identifies this roll's product line, or None if it does not.
+
+    Material and colour alone cannot answer this: PLA Basic Black and PLA Matte
+    Charcoal are both PLA at ``#000000``, which is why a Matte roll was linked to
+    the Basic filament (#2907). The name is the only field on a Spoolman filament
+    that carries the line.
+
+    Three spellings count, ranked by how much of the line they carry:
+
+    0. the sub-brand itself ("PLA Matte") -- what Bambuddy names the filaments
+       it creates, and has since before this fix;
+    1. the line folded into the colour ("Matte Charcoal", "Tough+ Black",
+       "Brown Galaxy") -- how SpoolmanDB names every Bambu Lab entry outside PLA
+       Basic, and so what a filament taken from the library used to be called;
+    2. the bare colour name ("Black") -- how SpoolmanDB names PLA Basic, and the
+       lines that have a base material of their own (ABS, ASA, PLA-CF ...).
+
+    Rank 2 is the weak one and callers must prefer anything better. "Black" is
+    the bare name for PLA Basic, PLA Tough, PETG HF and TPU 90A alike, so on its
+    own it cannot tell two lines of the same material apart -- it is accepted
+    because an existing instance is full of filaments called that and refusing
+    it would duplicate every one of them.
+    """
+    name = _normalise(filament_name)
+    if not name:
+        return None
+    if sub_brand and name == _normalise(sub_brand):
+        return 0
+    color = _normalise(color_name)
+    if not color:
+        return None
+    line = _line_word(sub_brand, material)
+    if line and name in (f"{line} {color}", f"{color} {line}"):
+        return 1
+    if name == color:
+        return 2
+    return None
+
+
+def best_product_line_match(
+    filaments: list[dict],
+    color_name: str | None,
+    sub_brand: str | None,
+    material: str | None,
+) -> dict | None:
+    """The filament whose name identifies this roll's line most specifically; first wins a tie."""
+    best: dict | None = None
+    best_rank: int | None = None
+    for filament in filaments:
+        rank = product_line_rank(filament.get("name"), color_name, sub_brand, material)
+        if rank is not None and (best_rank is None or rank < best_rank):
+            best, best_rank = filament, rank
+    return best

+ 19 - 3
backend/app/services/diagnostic_snapshot.py

@@ -22,6 +22,7 @@ from __future__ import annotations
 import asyncio
 import logging
 import re
+import time
 from typing import Any
 
 from sqlalchemy import select
@@ -32,8 +33,11 @@ logger = logging.getLogger(__name__)
 # Mirrors the IPv4 pattern in services.log_reader.sanitize_log_content. Kept as
 # a literal here (not imported) so a refactor of that module's internals can't
 # silently change snapshot sanitization. Skips firmware-version-shaped strings
-# (leading-zero octets like "01.09.01.00") via the [1-9]\d|\d alternations.
-_IPV4_RE = re.compile(r"\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\b")
+# (leading-zero octets like "01.09.01.00") via the [1-9]\d|\d alternations,
+# and four-number runs inside a longer dotted number such as an OID.
+_IPV4_RE = re.compile(
+    r"(?<!\d\.)\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\b(?!\.\d)"
+)
 
 # Per-diagnostic wall-clock cap. Each underlying probe carries its own (smaller)
 # TCP / HTTP timeouts; this is the outer guard so a hung interface or a wedged
@@ -54,6 +58,8 @@ async def _run_connection_for(printer) -> dict:
     from backend.app.services.printer_diagnostic import run_connection_diagnostic
 
     base = {"printer_id": printer.id, "printer_name": printer.name}
+    progress: dict[str, Any] = {}
+    started = time.monotonic()
     try:
         result = await asyncio.wait_for(
             run_connection_diagnostic(
@@ -61,12 +67,22 @@ async def _run_connection_for(printer) -> dict:
                 printer=printer,
                 serial_number=printer.serial_number,
                 access_code=printer.access_code,
+                progress=progress,
             ),
             timeout=_PER_DIAGNOSTIC_TIMEOUT_SECONDS,
         )
         return {**base, "result": _serialize(result)}
     except asyncio.TimeoutError:
-        return {**base, "error": "timed_out"}
+        # Name the step that hung and keep the checks that finished before it.
+        # Without them a bundle from a farm whose every printer overran said
+        # only "timed_out" fourteen times, and nothing about why (#3164).
+        return {
+            **base,
+            "error": "timed_out",
+            "stalled_in": progress.get("stage"),
+            "elapsed_s": round(time.monotonic() - started, 1),
+            "checks": [_serialize(c) for c in progress.get("checks", [])],
+        }
     except Exception as e:
         # Log with traceback so the bundle generation isn't silent about
         # a broken probe, but never propagate.

+ 10 - 0
backend/app/services/drying_preflight.py

@@ -10,6 +10,16 @@ from backend.app.services.printer_manager import drying_screen_only, supports_dr
 SCREEN_ONLY_DETAIL = "This printer only supports AMS drying from its own screen"
 UNSUPPORTED_DETAIL = "Drying not supported for this printer model or firmware version"
 
+# Codes stored on a failed scheduled drying (ScheduledDrying.error_code) so the
+# UI can show the reason translated. check_drying_supported() returns one of the
+# two details above; DETAIL_CODES maps it to its code.
+DID_NOT_START_DETAIL = "The printer accepted the drying command, but the AMS did not start drying"
+DETAIL_CODES = {
+    SCREEN_ONLY_DETAIL: "screen_only",
+    UNSUPPORTED_DETAIL: "unsupported",
+    DID_NOT_START_DETAIL: "did_not_start",
+}
+
 # Firmware dry_sf_reason codes, as surfaced by the AMS status payload.
 DRY_SF_REASON_MESSAGES = {
     0: "Printer is busy",

+ 60 - 10
backend/app/services/email_service.py

@@ -9,6 +9,7 @@ import secrets
 import smtplib
 import string
 from datetime import datetime, timezone
+from email.mime.image import MIMEImage
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 from typing import Any
@@ -179,6 +180,8 @@ def send_email(
     subject: str,
     body_text: str,
     body_html: str | None = None,
+    image_data: bytes | None = None,
+    image_cid: str = "bambuddy-inline-photo",
 ) -> None:
     """Send an email using SMTP.
 
@@ -188,21 +191,43 @@ def send_email(
         subject: Email subject
         body_text: Plain text body
         body_html: Optional HTML body
+        image_data: Optional JPEG bytes to embed inline. The caller is
+            responsible for putting a matching ``<img src="cid:{image_cid}">``
+            in body_html — this just attaches the bytes under that Content-ID.
+        image_cid: Content-ID the embedded image is referenced by in body_html.
 
     Raises:
         Exception: If email sending fails
     """
-    msg = MIMEMultipart("alternative")
-    msg["From"] = f"{smtp_settings.smtp_from_name} <{smtp_settings.smtp_from_email}>"
-    msg["To"] = to_email
-    msg["Subject"] = subject
+    if image_data and body_html:
+        # multipart/related wraps multipart/alternative so the inline image
+        # travels with the HTML part without becoming a visible attachment.
+        msg = MIMEMultipart("related")
+        msg["From"] = f"{smtp_settings.smtp_from_name} <{smtp_settings.smtp_from_email}>"
+        msg["To"] = to_email
+        msg["Subject"] = subject
+
+        alt = MIMEMultipart("alternative")
+        alt.attach(MIMEText(body_text, "plain"))
+        alt.attach(MIMEText(body_html, "html"))
+        msg.attach(alt)
+
+        img = MIMEImage(image_data, _subtype="jpeg")
+        img.add_header("Content-ID", f"<{image_cid}>")
+        img.add_header("Content-Disposition", "inline", filename="photo.jpg")
+        msg.attach(img)
+    else:
+        msg = MIMEMultipart("alternative")
+        msg["From"] = f"{smtp_settings.smtp_from_name} <{smtp_settings.smtp_from_email}>"
+        msg["To"] = to_email
+        msg["Subject"] = subject
 
-    # Attach plain text part
-    msg.attach(MIMEText(body_text, "plain"))
+        # Attach plain text part
+        msg.attach(MIMEText(body_text, "plain"))
 
-    # Attach HTML part if provided
-    if body_html:
-        msg.attach(MIMEText(body_html, "html"))
+        # Attach HTML part if provided
+        if body_html:
+            msg.attach(MIMEText(body_html, "html"))
 
     # Send email
     try:
@@ -580,6 +605,7 @@ async def send_user_print_notification(
     user_email: str,
     username: str,
     variables: dict,
+    image_data: bytes | None = None,
 ) -> None:
     """Send a print notification email to a user using Advanced Auth SMTP settings.
 
@@ -589,6 +615,10 @@ async def send_user_print_notification(
         user_email: Recipient email address
         username: Username of the recipient
         variables: Template variables (printer, filename, etc.)
+        image_data: Camera snapshot bytes, if one was captured for the event.
+            Inlined only when the template explicitly references
+            {finish_photo_url} — same opt-in as the provider-based email path
+            (#1792), so a user who never asked for a photo never gets one.
     """
     # Check that advanced auth is enabled (SMTP settings must be configured)
     smtp_settings = await get_smtp_settings(db)
@@ -613,8 +643,20 @@ async def send_user_print_notification(
     subject = render_template(template.title_template, all_variables)
     text_body = render_template(template.body_template, all_variables)
 
+    finish_photo_url = variables.get("finish_photo_url")
+    inline_photo = bool(image_data and finish_photo_url and finish_photo_url in text_body)
+
     # Build HTML body — content comes entirely from the database template
     escaped_text_body = html.escape(text_body).replace("\n", "<br>\n")
+    if inline_photo:
+        escaped_url = html.escape(finish_photo_url)
+        img_tag = (
+            '<img src="cid:bambuddy-user-print-photo" '
+            'alt="Printer camera snapshot" '
+            'style="max-width:100%;height:auto;border:1px solid #ddd;border-radius:4px;">'
+        )
+        escaped_text_body = escaped_text_body.replace(escaped_url, img_tag)
+
     html_body = f"""<!DOCTYPE html>
 <html>
 <head>
@@ -633,7 +675,15 @@ async def send_user_print_notification(
 """
 
     try:
-        send_email(smtp_settings, user_email, subject, text_body, html_body)
+        send_email(
+            smtp_settings,
+            user_email,
+            subject,
+            text_body,
+            html_body,
+            image_data=image_data if inline_photo else None,
+            image_cid="bambuddy-user-print-photo",
+        )
         logger.info("Sent %s notification email to %s", event_type, user_email)
     except Exception as e:
         logger.error("Failed to send %s notification to %s: %s", event_type, user_email, e)

+ 42 - 28
backend/app/services/external_camera.py

@@ -8,6 +8,7 @@ to ensure they are well-formed before use.
 """
 
 import asyncio
+import contextlib
 import functools
 import ipaddress
 import logging
@@ -921,42 +922,55 @@ async def generate_mjpeg_stream(
                 logger.exception("on_frame callback raised")
         return _format_mjpeg_frame(frame)
 
-    if camera_type == "mjpeg":
-        # Proxy MJPEG stream directly, with reconnect on timeout
-        max_retries = 3
-        for attempt in range(max_retries + 1):
+    async def _reconnecting(open_session, label: str):
+        """Yield frames across sessions, reconnecting after each that delivered.
+
+        A session that ends without a single frame stops the stream: the
+        source is down, and retrying is the viewer's call. One that delivered
+        frames and then ended is a routine drop and is always reconnected.
+        This used to be three reconnects for the life of the stream, so a
+        server that closes its sessions periodically ended the stream for good
+        on the fourth drop, however long each session had run -- the external
+        twin of the built-in RTSP path's lifetime reconnect budget.
+        """
+        drops = 0
+        while True:
             frame_yielded = False
-            async for frame in _stream_mjpeg(url):
-                frame_yielded = True
+            # aclosing: stop the session's ffmpeg the moment this generator is
+            # closed, rather than whenever the abandoned iterator is collected.
+            async with contextlib.aclosing(open_session()) as session:
+                async for frame in session:
+                    frame_yielded = True
+                    yield frame
+            if not frame_yielded or (stop_event is not None and stop_event.is_set()):
+                break
+            # The sessions swallow CancelledError and simply end, so a viewer
+            # whose task was cancelled mid-read looks like a routine drop here.
+            # Never redial for a task that is being cancelled (Task.cancelling
+            # is 3.11+; without it this falls back to the next await raising).
+            task = asyncio.current_task()
+            if task is not None and getattr(task, "cancelling", lambda: 0)():
+                break
+            drops += 1
+            logger.warning("External %s stream ended, reconnecting (drop %d)...", label, drops)
+            await asyncio.sleep(2)
+
+    if camera_type == "mjpeg":
+        # Proxy MJPEG stream directly, reconnecting after routine drops.
+        async with contextlib.aclosing(_reconnecting(lambda: _stream_mjpeg(url), "MJPEG")) as frames:
+            async for frame in frames:
                 current_time = asyncio.get_event_loop().time()
                 if current_time - last_frame_time >= frame_interval:
                     last_frame_time = current_time
                     yield _publish(frame)
-            if not frame_yielded or attempt == max_retries or (stop_event is not None and stop_event.is_set()):
-                break
-            logger.warning(
-                "External MJPEG stream ended, reconnecting (attempt %d/%d)...",
-                attempt + 1,
-                max_retries,
-            )
-            await asyncio.sleep(2)
 
     elif camera_type == "rtsp":
-        # Use ffmpeg to convert RTSP to MJPEG, with reconnect on timeout
-        max_retries = 3
-        for attempt in range(max_retries + 1):
-            frame_yielded = False
-            async for frame in _stream_rtsp(url, fps, on_process=on_process):
-                frame_yielded = True
+        # Use ffmpeg to convert RTSP to MJPEG, reconnecting after routine drops.
+        async with contextlib.aclosing(
+            _reconnecting(lambda: _stream_rtsp(url, fps, on_process=on_process), "RTSP")
+        ) as frames:
+            async for frame in frames:
                 yield _publish(frame)
-            if not frame_yielded or attempt == max_retries or (stop_event is not None and stop_event.is_set()):
-                break
-            logger.warning(
-                "External RTSP stream ended, reconnecting (attempt %d/%d)...",
-                attempt + 1,
-                max_retries,
-            )
-            await asyncio.sleep(2)
 
     elif camera_type == "usb":
         # Use ffmpeg to stream from USB camera

+ 26 - 0
backend/app/services/failure_analysis.py

@@ -103,6 +103,29 @@ class FailureAnalysisService:
         outcome_prints = successful_prints + failed_prints
         failure_rate = (failed_prints / outcome_prints * 100) if outcome_prints > 0 else 0
 
+        # Quality dimension (#1898): a completed print the user marked as
+        # reject is machine-success but scrap. Kept OUT of failure_rate — that
+        # number stays the machine's — and reported separately, with a yield
+        # rate that treats rejects as non-good output.
+        rejected_result = await self.db.execute(
+            select(func.count(PrintLogEntry.id)).where(
+                and_(*base_filter, PrintLogEntry.status == "completed", PrintLogEntry.user_verdict == "reject")
+            )
+        )
+        rejected_prints = rejected_result.scalar() or 0
+        yield_rate = ((successful_prints - rejected_prints) / outcome_prints * 100) if outcome_prints > 0 else 0
+
+        rejects_reason_result = await self.db.execute(
+            select(
+                PrintLogEntry.failure_reason,
+                func.count(PrintLogEntry.id).label("count"),
+            )
+            .where(and_(*base_filter, PrintLogEntry.status == "completed", PrintLogEntry.user_verdict == "reject"))
+            .group_by(PrintLogEntry.failure_reason)
+            .order_by(func.count(PrintLogEntry.id).desc())
+        )
+        rejects_by_reason = {(row[0] or "Unknown"): row[1] for row in rejects_reason_result.fetchall()}
+
         # Failures by reason
         reason_result = await self.db.execute(
             select(
@@ -257,6 +280,9 @@ class FailureAnalysisService:
             "total_prints": total_prints,
             "failed_prints": failed_prints,
             "failure_rate": round(failure_rate, 1),
+            "rejected_prints": rejected_prints,
+            "yield_rate": round(yield_rate, 1),
+            "rejects_by_reason": rejects_by_reason,
             "failures_by_reason": failures_by_reason,
             "failures_by_filament": failures_by_filament,
             "failures_by_printer": failures_by_printer,

+ 25 - 7
backend/app/services/filament_deficit.py

@@ -38,7 +38,7 @@ from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
-from backend.app.api.routes._spoolman_helpers import _map_spoolman_spool
+from backend.app.api.routes._spoolman_helpers import _map_spoolman_spool, spoolman_net_weight
 from backend.app.core.config import settings as app_settings
 from backend.app.models.print_queue import PrintQueueItem
 from backend.app.models.spool_assignment import SpoolAssignment
@@ -144,15 +144,15 @@ async def _spoolman_remaining_grams(spoolman_spool_id: int) -> float | None:
         return None
 
     # Spoolman exposes either an absolute remaining_weight, or used_weight +
-    # filament.weight. Either is sufficient — prefer remaining_weight when
-    # present (the user may have overridden it).
+    # the spool's net weight (initial_weight, else filament.weight; #3194).
+    # Either is sufficient — prefer remaining_weight when present (the user
+    # may have overridden it).
     remaining = spool.get("remaining_weight")
     if isinstance(remaining, (int, float)) and remaining >= 0:
         return float(remaining)
 
     used = spool.get("used_weight")
-    filament = spool.get("filament") or {}
-    total = filament.get("weight")
+    total = spoolman_net_weight(spool)
     if isinstance(used, (int, float)) and isinstance(total, (int, float)) and total > 0:
         return max(0.0, float(total) - float(used))
 
@@ -342,6 +342,15 @@ class SlotSpoolIdentity:
     subtype: str | None
     color_name: str | None
     rgba: str | None
+    # The rest of the spool's colour, for drawing it rather than naming it
+    # (#3159). A tray record carries one hex and nothing else, so a two-tone
+    # or glittery spool renders as its base colour unless the binding says
+    # otherwise -- and on a farm of third-party spools that is exactly the
+    # swatch somebody is comparing against the slicer's colour. Same shape the
+    # inventory row stores: ``extra_colors`` a comma-separated hex list,
+    # ``effect_type`` one of the client's FilamentEffect names.
+    extra_colors: str | None = None
+    effect_type: str | None = None
 
     def to_dict(self) -> dict:
         return {
@@ -350,6 +359,8 @@ class SlotSpoolIdentity:
             "subtype": self.subtype,
             "color_name": self.color_name,
             "rgba": self.rgba,
+            "extra_colors": self.extra_colors,
+            "effect_type": self.effect_type,
         }
 
 
@@ -367,6 +378,8 @@ def _identity_from_internal(spool) -> SlotSpoolIdentity:
         subtype=_clean(spool.subtype),
         color_name=_clean(spool.color_name),
         rgba=_clean(spool.rgba),
+        extra_colors=_clean(spool.extra_colors),
+        effect_type=_clean(spool.effect_type),
     )
 
 
@@ -408,6 +421,11 @@ def _identity_from_spoolman(spool_dict: dict) -> SlotSpoolIdentity | None:
         subtype=_clean(mapped.get("subtype")),
         color_name=color_name,
         rgba=_clean(mapped.get("rgba")),
+        # `_map_spoolman_spool` builds `extra_colors` from Spoolman's own
+        # multi-colour hexes and pins `effect_type` to None, because Spoolman
+        # has no field for it -- see the note there.
+        extra_colors=_clean(mapped.get("extra_colors")),
+        effect_type=_clean(mapped.get("effect_type")),
     )
 
 
@@ -507,7 +525,7 @@ async def build_slot_materials(db: AsyncSession, printer_id: int) -> list[SlotMa
                 remaining = float(rw)
             else:
                 used = spool_dict.get("used_weight")
-                total = (spool_dict.get("filament") or {}).get("weight")
+                total = spoolman_net_weight(spool_dict)
                 if isinstance(used, (int, float)) and isinstance(total, (int, float)) and total > 0:
                     remaining = max(0.0, float(total) - float(used))
             if remaining is None:
@@ -686,7 +704,7 @@ async def compute_deficit_for_queue_item(
                     remaining = float(rw)
                 else:
                     used = spool_dict.get("used_weight")
-                    total = (spool_dict.get("filament") or {}).get("weight")
+                    total = spoolman_net_weight(spool_dict)
                     if isinstance(used, (int, float)) and isinstance(total, (int, float)) and total > 0:
                         remaining = max(0.0, float(total) - float(used))
         else:

+ 1 - 1
backend/app/services/firmware_check.py

@@ -84,7 +84,7 @@ MODEL_TO_API_KEY = {
     "BL-P003": "x1e",
     "C11": "p1",
     "C12": "p1",
-    "C13": "p2s",
+    "C13": "x1e",
     "N2S": "a1",
     "N1": "a1-mini",
     "N6": "x2d",

+ 10 - 0
backend/app/services/github_backup.py

@@ -778,6 +778,16 @@ class GitHubBackupService:
                 "nozzle_temp_max": s.nozzle_temp_max,
                 "note": s.note,
                 "cost_per_kg": s.cost_per_kg,
+                # The user's own bookkeeping on the spool: purchasing number
+                # (#2870), category and low-stock override (#729), free-text
+                # storage. All four were missing from this whitelist, so a
+                # restore silently dropped them. `location_id` stays out —
+                # the locations table itself is not in the backup, so the ID
+                # would point at whatever happens to own it on the target.
+                "material_number": s.material_number,
+                "category": s.category,
+                "low_stock_threshold_pct": s.low_stock_threshold_pct,
+                "storage_location": s.storage_location,
                 "tag_uid": s.tag_uid,
                 "tray_uuid": s.tray_uuid,
                 "data_origin": s.data_origin,

+ 11 - 0
backend/app/services/github_restore.py

@@ -1313,6 +1313,17 @@ class GitHubRestoreService:
                 "archived_at": _parse_dt(entry.get("archived_at")),
             }
 
+            # The user's own bookkeeping on the spool: purchasing number
+            # (#2870), category and low-stock override (#729), free-text
+            # storage. Added to the backup format after the fields above, so
+            # they are applied only when the file actually carries them —
+            # an older backup must not wipe what the live row holds.
+            # `location_id` is deliberately absent: the locations table is
+            # not backed up, so the ID would point at whatever owns it here.
+            for late_field in ("material_number", "category", "low_stock_threshold_pct", "storage_location"):
+                if late_field in entry:
+                    fields[late_field] = entry[late_field]
+
             if existing is not None:
                 if old_id is not None:
                     spool_id_map[old_id] = existing.id

+ 76 - 904
backend/app/services/hms_errors.py

@@ -1,920 +1,92 @@
-"""HMS Error Code Descriptions.
+"""HMS fault descriptions.
 
-Auto-generated from frontend/src/components/HMSErrorModal.tsx
-Source: https://github.com/greghesp/ha-bambulab
-"""
+The texts come from Bambu Studio's own HMS files, generated into
+``backend/app/data/hms_catalog.json`` by ``scripts/generate_hms_catalog.py``
+(issue #2728). Do not edit the JSON by hand; rerun the script.
 
-# HMS error code to human-readable description mapping
-# Format: "XXXX_YYYY" where XXXX is module code, YYYY is error code
-HMS_ERROR_DESCRIPTIONS: dict[str, str] = {
-    "0300_4000": "Z axis homing failed; the task has been stopped.",
-    "0300_4001": "The printer timed out waiting for the nozzle to cool down before homing.",
-    "0300_4002": "Auto Bed Leveling failed; the task has been stopped.",
-    "0300_4005": "The hotend cooling fan speed is abnormal.",
-    "0300_4006": "The nozzle is clogged.",
-    "0300_4008": "The AMS failed to change filament.",
-    "0300_4009": "Homing XY axis failed.",
-    "0300_400A": "Mechanical resonance frequency identification failed.",
-    "0300_400B": "Internal communication exception",
-    "0300_400C": "The task was canceled.",
-    "0300_400D": "Resume failed after power loss.",
-    "0300_400E": "The motor self-check failed.",
-    "0300_400F": "The power supply voltage does not match the printer.",
-    "0300_4010": "Nozzle offset calibration failed.",
-    "0300_4011": "Flow Dynamics Calibration failed; please reinitiate printing or calibration.",
-    "0300_4013": "Printing cannot be initiated while AMS is drying.",
-    "0300_4014": "Homing Z axis failed: temperature control abnormality.",
-    "0300_4015": "Nozzle clumping detection calibration failed. Please go to 'Assistant' for troubleshooting.",
-    "0300_4016": "Nozzle cleaning failed. Please click the Assistant for troubleshooting.",
-    "0300_401F": "The hotend is not installed, and the toolhead cannot perform homing. Please install the hotend and then continue.",
-    "0300_4020": "The nozzle presence detection failed. Please check the Assistant for details.",
-    "0300_4021": "Nozzle offset calibration sensor signal abnormality detected. Please check the sensor and retry.",
-    "0300_4042": "The Laser Safety Window is not properly installed. The task has been stopped.",
-    "0300_4044": "The Flame Sensor is abnormal. The sensor may be short-circuited. Please troubleshoot the issue before starting a print job.",
-    "0300_404B": "Task aborted because the front door or top cover is open.",
-    "0300_404D": "The current temperature of the hotend, heatbed, or chamber is too high. Please wait for it to cool down to room temperature before restarting the task.",
-    "0300_4050": "Liveview Camera calibration timeout; please restart the printer.",
-    "0300_4052": "Blade Z-axis homing failed",
-    "0300_4057": "Z-axis step loss detected. The task has stopped. Please check if there are any obstructions beneath the heatbed.",
-    "0300_4066": "Calibration of motion precision failed.",
-    "0300_4067": "Calibration result is over the threshold.",
-    "0300_4068": "Step loss occurred during the motion accuracy enhancement process. Please try again.",
-    "0300_8000": "Printing was paused for unknown reason. You can select 'Resume' to resume the print job.",
-    "0300_8001": "Printing was paused by the user. You can select 'Resume' to continue printing.",
-    "0300_8002": "First layer defects were detected by the Micro Lidar. Please check the quality of the printed model before continuing your print.",
-    "0300_8003": "Spaghetti defects were detected by the AI Print Monitoring. Please check the quality of the printed model before continuing your print.",
-    "0300_8004": "Filament ran out. Please load new filament.",
-    "0300_8005": "Toolhead front cover fell off. Please remount the front cover and check to make sure your print is going okay.",
-    "0300_8006": "The build plate marker was not detected. Please confirm the build plate is correctly positioned on the heatbed with all four corners aligned, and the marker is visible.",
-    "0300_8007": "There was an unfinished print job when the printer lost power. If the model is still adhered to the build plate, you can try resuming the print job.",
-    "0300_8008": "Nozzle temperature malfunction",
-    "0300_8009": "Heatbed temperature malfunction",
-    "0300_800A": "A Filament pile-up was detected by AI Print Monitoring. Please clean filament from the waste chute.",
-    "0300_800B": "The cutter is stuck. Please make sure the cutter handle is out and check the filament sensor cable connection.",
-    "0300_800C": "Skipped step detected: auto-recover complete; please resume print and check if there are any layer shift problems.",
-    "0300_800D": "Detected that the extruder is not extruding normally. If the defects are acceptable, select 'Resume' to resume the print job.",
-    "0300_800E": "The print file is not available. Please check to see if the storage media has been removed.",
-    "0300_800F": "The door seems to be open, so printing was paused.",
-    "0300_8010": "The hotend cooling fan speed is abnormal.",
-    "0300_8011": "Detected build plate is not the same as the Gcode file. Please adjust slicer settings or use the correct plate.",
-    "0300_8013": "Printing paused due to the pause command added to the printing file.",
-    "0300_8014": "The nozzle is covered with filament, or the build plate is installed incorrectly. Please cancel this print and clean the nozzle or adjust the build plate according to the actual status. You can als...",
-    "0300_8015": "The filament on external spool has run out; please load new filament. If the filament is loaded, please select 'Resume'.",
-    "0300_8016": "The nozzle is clogged with filament. Please cancel this print and clean the nozzle or select 'Resume' to resume the print job.",
-    "0300_8017": "Foreign objects detected on heatbed. Please check and clean the heatbed. Then, select 'Resume' to resume the print job.",
-    "0300_8018": "Chamber temperature malfunction.",
-    "0300_8019": "No build plate is placed.",
-    "0300_801A": "Filament extrusion error; please check the assistant for troubleshooting. After resolving the issue, decide whether to cancel or resume the print job based on the actual print status.",
-    "0300_801B": "Nozzle temperature problem detected. Refer to Assistant to re-connect the hotend connector. POWER OFF the printer before this operation to avoid short circuits.",
-    "0300_801C": "The extrusion resistance is abnormal. The extruder may be clogged; please refer to the assistant. After trouble shooting, you can select 'Resume' to resume the print job.",
-    "0300_801D": "The extruder servo motor position sensor is malfunctioning. Please power off the printer first and check if the connection cable is loose.",
-    "0300_801E": "The extrusion motor is overloaded, please check the Assistant for details.",
-    "0300_8021": "The nozzle may not be installed or not properly installed. Please ensure the nozzle is correctly installed before proceeding.",
-    "0300_8022": "The heatbed may be obstructed while moving downward. Please clear any objects beneath the heatbed and check for any resistance or jamming during its movement.",
-    "0300_8028": "Nozzle offset calibration sensor error. If using a single hotend or the calibration function is disabled, you may ignore this and continue printing; otherwise, it is recommended to check the sensor...",
-    "0300_8041": "Platform detection timeout: please restart the printer.",
-    "0300_8042": "Task paused because the door is open.",
-    "0300_8043": "The laser module is abnormal.",
-    "0300_8044": "Fire was detected inside the chamber.",
-    "0300_8045": "Material detection timeout: please restart the printer.",
-    "0300_8046": "Foreign object detect timeout: please restart the printer.",
-    "0300_8047": "Quick-release lever detection time out: please restart the printer.",
-    "0300_8048": "Laser Module unlock has timed out, and the task cannot proceed. Please restart the printer and try again.",
-    "0300_8049": "The current plate is invalid.",
-    "0300_804A": "Emergency stop button improperly installed. Please reinstall according to the Wiki before proceeding.",
-    "0300_804B": "Task paused. The Laser Safety Window is open.",
-    "0300_804E": "This is a printing task. Please detach the Laser/Cutting Module from the Toolhead.",
-    "0300_804F": "The loading/unloading process is currently ongoing. Please stop the process or remove the laser/cutting module.",
-    "0300_8050": "This device does not support the 40W Laser Module. Please remove it or replace it with a 10W Laser Module.",
-    "0300_8051": "The cutting module has dropped or the cutting module cable is disconnected; please check the module.",
-    "0300_8053": "Laser module detected. Please install the right nozzle correctly to ensure proper Laser Module Mounting Calibration.",
-    "0300_8054": "Please place the paper required for Print Then Cut.",
-    "0300_8055": "The module mounted on the toolhead does not match the task. Please install the correct module.",
-    "0300_8057": "The rotary attachment is disconnected. Please ensure it is properly installed and the cable is securely plugged in.",
-    "0300_8058": "The rotary attachment is detected. Please remove it before continuing.",
-    "0300_8061": "The mode of Airflow System failed to activate; check the air door condition.",
-    "0300_8062": "The chamber temperature is too high. It may be due to high environmental temperature.",
-    "0300_8063": "The chamber temperature is too high. Please open the top cover and front door to cool down.",
-    "0300_8064": "The chamber temperature is too high. Please open the top cover and front door to cool down. (Open door detection for this print job will be set to 'Notification' level)",
-    "0300_8065": "The temperature of the MC module is too high. Please check the Wiki for possible explanations.",
-    "0300_8071": "The Toolhead Enhanced Cooling Fan module is malfunctioning.",
-    "0300_807D": "Fire Extinguisher not detected, the automatic extinguishing function will be unavailable.",
-    "0300_807E": "Fire Extinguisher not detected, the automatic extinguishing function will be unavailable.",
-    "0300_807F": "Fire Extinguisher is malfunctioning.",
-    "0300_8080": "Fire extinguisher motor reset failed.",
-    "0300_8081": "Fire extinguisher cylinder not installed. Please confirm on the extinguisher page.",
-    "0300_8082": "The Fire Extinguisher Gas Cylinder is empty.",
-    "0300_C012": "Please heat the nozzle to above 170°C.",
-    "0300_C056": "A minor fire was detected inside the chamber, and the Auto Fire Extinguishing process has been aborted.",
-    "0300_C070": "The fire extinguisher has been detected and is ready for use after the laser module is connected.",
-    "0500_4001": "Failed to connect to Bambu Cloud. Please check your network connection.",
-    "0500_4002": "Unsupported print file path or name. Please resend the print job.",
-    "0500_4003": "Printing stopped because the printer was unable to parse the file. Please resend your print job.",
-    "0500_4004": "Device is busy and cannot start new task. Please wait for current task to complete before sending new task.",
-    "0500_4005": "Print jobs are not allowed to be sent while updating firmware.",
-    "0500_4006": "There is not enough free storage space for the print job. Restoring to factory settings can free up available space.",
-    "0500_4007": "The device requires a repair upgrade, and printing is currently unavailable.",
-    "0500_4008": "Starting printing failed; please power cycle the printer and resend the print job.",
-    "0500_4009": "Print jobs are not allowed to be sent while updating logs.",
-    "0500_400A": "The file name is not supported. Please rename and restart the print job.",
-    "0500_400B": "There was a problem downloading a file. Please check your network connection and resend the print job.",
-    "0500_400C": "Please insert a MicroSD card and restart the print job.",
-    "0500_400D": "Please run a self-test and restart the print job.",
-    "0500_400E": "Printing was cancelled.",
-    "0500_400F": "AMS is initializing and cannot be upgraded at the moment. Please try again later.",
-    "0500_4010": "AMS is drying and cannot be upgraded at the moment. Please try again later.",
-    "0500_4011": "The printer is loading or unloading filament and cannot be upgraded at the moment. Please try again later.",
-    "0500_4012": "The device is printing and cannot be upgraded at the moment. Please try again later.",
-    "0500_4013": "AMS is in operation and cannot be upgraded at the moment. Please try again when it is idle.",
-    "0500_4014": "Slicing for the print job failed. Please check your settings and restart the print job.",
-    "0500_4015": "There is not enough free storage space for the print job. Please format or clear files from the MicroSD card to free up space.",
-    "0500_4016": "The MicroSD Card is write-protected. Please replace the MicroSD Card.",
-    "0500_4017": "Binding failed. Please retry or restart the printer and retry.",
-    "0500_4018": "Binding configuration information parsing failed; please try again.",
-    "0500_4019": "The printer has already been bound. Please unbind it and try again.",
-    "0500_401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the pri...",
-    "0500_401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
-    "0500_401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.",
-    "0500_4020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_4021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
-    "0500_4022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_4023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_4024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the pri...",
-    "0500_4025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_4026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_4027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
-    "0500_4028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_4029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0500_402A": "Failed to connect to the router, which may be caused by wireless interference or being too far away from the router. Please try again or move the printer closer to the router and try again.",
-    "0500_402B": "Router connection failed due to incorrect password. Please check the password and try again.",
-    "0500_402C": "Failed to obtain IP address, which may be caused by wireless interference resulting in data transmission failure or the DHCP address pool of the router being full. Please move the printer closer to...",
-    "0500_402D": "System exception",
-    "0500_402E": "System does not support the file system currently used by the USB flash drive. Please replace or format the USB flash drive to FAT32.",
-    "0500_402F": "The MicroSD card sector data is damaged. Please use the SD card repair tool to repair or format it. If it still cannot be identified, please replace the MicroSD card.",
-    "0500_4030": "The device is currently upgrading. Please try again when it is idle.",
-    "0500_4031": "The accessory firmware does not match the printer. Please update it on the 'Firmware' page.",
-    "0500_4033": "The AMS firmware does not match the printer. Please update it on the 'Firmware' page.",
-    "0500_4034": "The Laser Module firmware does not match the printer. Please update it on the 'Firmware' page.",
-    "0500_4035": "The BirdsEye Camera is malfunctioning. Please try restarting the device. If the issue persists after multiple restarts, check the camera connection status or contact customer support.",
-    "0500_4037": "Your sliced file is not compatible with current printer model. This file can't be printed on this printer.",
-    "0500_4038": "The nozzle diameter in sliced file is not consistent with the current nozzle setting. This file can't be printed.",
-    "0500_4039": "The current task does not allow the installation of the laser/cutting module, and the task has been halted.",
-    "0500_403A": "The current temperature is too low. In order to protect you and your printer, printing tasks, moving an axis and other operations are disabled. Please move the printer to an environment above 10 de...",
-    "0500_403B": "Laser/cutting tasks cannot be initiated on the machine at the moment. Please use the computer software to start the task.",
-    "0500_403C": "The current nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
-    "0500_403D": "The toolhead module is not set up. Please set it up before initiating the task.",
-    "0500_403E": "The current tool head does not support initialization.",
-    "0500_403F": "Failed to download print job; please check your network connection.",
-    "0500_4040": "The printer has reached its power limit. Please connect a dedicated power adapter to this AMS to enable drying.",
-    "0500_4041": "The AMS drying cannot be started during printing.",
-    "0500_4042": "Due to power limitations, starting AMS drying will pause current operations such as nozzle heating and fan running. Do you want to proceed with drying?",
-    "0500_4043": "Due to power limitations, only one AMS is allowed to use the device's power for drying.",
-    "0500_4044": "BirdsEye Camera malfunction: please contact customer support.",
-    "0500_4045": "Hotend check in progress. This operation is temporarily unavailable. Please wait.",
-    "0500_4050": "Error detected on the print board.",
-    "0500_4052": "Error detected on the hot end.",
-    "0500_4054": "Error detected on the mat.",
-    "0500_405D": "Laser module Serial Number error: unable to calibrate or make project.",
-    "0500_4065": "The task requires a Laser Platform, but the current one is a Cutting Platform. Please replace it, measure the material thickness in the software, and then restart the task.",
-    "0500_4070": "The laser or cutter module is connected, so the device cannot initiate a 3D printing task.",
-    "0500_4075": "No Laser Platform was detected, which may affect thickness measurement accuracy. Please place the laser platform correctly and ensure the rear markers are not blocked, then restart the thickness me...",
-    "0500_4076": "Please place the Laser Platform correctly and ensure the rear markers are not blocked, then restart the thickness measurement in the software before initiating the task.",
-    "0500_4097": "The device cannot detect the Laser Module. Please reconnect the module cable or restart the printer.",
-    "0500_4098": "The device cannot detect AMS A. Please reconnect the AMS cable or restart the printer.",
-    "0500_4099": "The firmware of Cutting Module does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0500_409A": "The firmware of the Air Pump does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0500_409B": "The firmware of the Laser Module does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0500_409D": "The firmware of AMS A does not match the printer; the device cannot continue working. Please upgrade it on the 'Firmware' page.",
-    "0500_409E": "The device cannot detect the Cutting Module. Please reconnect the module cable or restart the printer.",
-    "0500_409F": "The device cannot detect the Air Pump.  Please reconnect the module cable or restart the printer.",
-    "0500_40A0": "The Rotary Attachment module is not detected. Please reconnect the cable or restart the printer.",
-    "0500_40A1": "The Auto Fire Extinguishing System is not detected.  Please reconnect the module cable or restart the printer.",
-    "0500_40A3": "AMS(or AMS lite) A communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0500_40A4": "The current firmware only supports 1 AMS Lite. Please remove all AMS units before reconnecting the supported AMS Lite device.",
-    "0500_40A5": "The current firmware only supports AMS/AMS 2 Pro/AMS HT, with a maximum of 4 units. Please remove all AMS units before reconnecting the supported one.",
-    "0500_8013": "The print file is not available. Please check to see if the storage media has been removed.",
-    "0500_8036": "Your sliced file is not consistent with the current printer model. Continue?",
-    "0500_803C": "The current nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
-    "0500_8040": "Toolhead front cover is detached. Moving the toolhead may damage the printer. Do you want to continue?",
-    "0500_8041": "The filament in hotend is too cold. Extrusion may damage the extruder. Still feeding in/out the filament?",
-    "0500_8048": "The module on the toolhead is not calibrated. Please cancel the task to perform calibration or switch to a calibrated module.",
-    "0500_8051": "Detected build plate is not the same as the Gcode file. Please adjust slicer settings or use the correct plate.",
-    "0500_8053": "Nozzle mismatch was detected during printing. Please initiate the print after re-slicing, or continue printing after replacing with the correct nozzle. Caution: the hotend temperature is high.",
-    "0500_8055": "Laser module is installed, but a Cutting Platform is detected. Please place a Laser Platform and perform laser calibration.",
-    "0500_8056": "Cutting module is installed, but the laser platform is detected. Please place the cutting platform for calibration.",
-    "0500_8058": "Please place the light grip cutting mat correctly and ensure the marker is exposed.",
-    "0500_8059": "Cutting platform base is not correctly aligned. Please ensure that the four corners of the platform are aligned with the heatbed.",
-    "0500_805A": "Please place the cutting mat on cutting protection base.",
-    "0500_805B": "The cutting mat type is unknown; please replace it with the correct cutting mat.",
-    "0500_805C": "The grip cutting mat type does not match; please place a LightGrip cutting mat.",
-    "0500_805E": "Cutting module Serial Number error: unable to calibrate or make project.",
-    "0500_8060": "The current module on toolhead does not meet requirements. Please replace the module as per the on-screen instructions.",
-    "0500_8061": "No print plate detected. Please make sure it is placed correctly.",
-    "0500_8062": "The print plate marker was not detected. Please confirm the print plate is correctly positioned on the heatbed with all four corners aligned, and the marker is visible. If strong light is shining o...",
-    "0500_8063": "The platform is not detected during calibration; please make sure the Laser Platform is properly placed.",
-    "0500_8064": "Please place the Laser Platform correctly and ensure the rear markers are not blocked for laser calibration.",
-    "0500_8066": "The task requires a Cutting Platform, but the current one is a Laser Platform. Please replace it with a Cutting Platform (Cutting Protection Base + LightGrip cutting mat).",
-    "0500_8067": "Please place a LightGrip cutting mat on the cutting protection base.",
-    "0500_8068": "Please place the strong grip cutting mat correctly and ensure the marker is exposed.",
-    "0500_8069": "Unable to recognize the left and right hotends. They might be third party hotends, or the hotend marks may be dirty. Please manually set the hotend types.",
-    "0500_806A": "Unable to recognize the left and right hotends. They might be third party hotends, or the hotend marks may be dirty. Please set hotend types on printer screen before next print.",
-    "0500_806B": "Quick-release Lever is not locked. Please press down the external toolhead module to ensure it is properly seated, then push down the level to lock it in place.",
-    "0500_806C": "Please place the cutting platform correctly and ensure the marker is exposed.",
-    "0500_806D": "Material not detected. Please confirm placement and continue.",
-    "0500_806E": "Foreign objects detected on heatbed; please check and clean up the heatbed.",
-    "0500_806F": "The grip cutting mat type does not match; please place a StrongGrip cutting mat.",
-    "0500_8071": "No cutting platform was detected. Please confirm that it has been correctly placed.",
-    "0500_8072": "Live View camera is blocked",
-    "0500_8073": "Heatbed limit block is obstructed or contaminated. Please clean and ensure the limit block is visible, otherwise platform position offset detection may be inaccurate.",
-    "0500_8074": "The Laser Platform is offset. Please ensure that the four corners of the platform are aligned with the heatbed, and the marker is not obstructed.",
-    "0500_8077": "The visual marker was not detected. Please ensure the paper is properly placed.",
-    "0500_8078": "Current material does not match the sliced file settings. Please load the correct material and ensure the QR code on the material is not damaged or dirty.",
-    "0500_8079": "Please place the Laser Test Material (350g paperboard) and position support strips underneath to prevent material warping.",
-    "0500_807A": "The foreign object detection function is not working. You can continue the task or check the assistant for troubleshooting.",
-    "0500_807B": "Please place the cutting platform (cutting protection base + LightGrip cutting mat).",
-    "0500_807C": "Please place the cutting platform (cutting protection base + StrongGrip cutting mat).",
-    "0500_807D": "This task requires a Cutting Platform, but the current one is a Laser Platform. Please replace it with a Cutting Platform (Cutting Protection Base + StrongGrip Cutting Mat).",
-    "0500_807E": "Please place a StrongGrip cutting mat on the cutting protection base.",
-    "0500_8080": "The left and right hotends are not installed.",
-    "0500_8081": "The left and right hotends are not installed.",
-    "0500_8082": "Please remove the protective film on the Opaque Glossy Acrylic before processing",
-    "0500_8083": "Material is not allowed in Mounting Calibration. Please remove the material from the platform.",
-    "0500_8084": "The Live View Camera is dirty; please clean it and continue.",
-    "0500_8085": "Toolhead camera is obstructed",
-    "0500_8086": "Toolhead Camera is dirty, which affects the AI function; please clean the lens surface.",
-    "0500_8087": "BirdsEye camera is obstructed",
-    "0500_8088": "The Birdseye Camera is dirty",
-    "0500_8089": "Task paused due to Presence Check failed. Please check the printer to continue.",
-    "0500_808A": "The BirdsEye Camera is installed offset. Please refer to the assistant to reinstall it.",
-    "0500_808B": "The BirdsEye Camera setup failed. Please remove all objects and the mat on the heatbed to ensure the heatbed markers are visible. Meanwhile, please ensure the BirdsEye Camera is installed correctly...",
-    "0500_808C": "Detected build plate offset. Please align the build plate with the heatbed, and then continue.",
-    "0500_808D": "The Cutting Module offset calibration failed, which may result in inaccurate cuts. Please ensure the cutting material is properly positioned and check whether the cutting blade tip is worn.",
-    "0500_808E": "BirdsEye Camera initialization failed. The toolhead camera did not detect the Heatbed features. Please clean the Heatbed, remove all objects and pads, and ensure the bed markings are visible. Check...",
-    "0500_808F": "Nozzle camera lens is dirty, affecting AI monitoring. Clean the lens with a non-woven cloth and a small amount of alcohol. Beware of hotend heat; wait for it to cool before handling.",
-    "0500_8090": "Please attach the 80g White Printing Paper to the center area of the platform.",
-    "0500_8091": "The Cutting Module offset calibration failed, which may result in inaccurate cuts. Please ensure the 80g white printer paper(letter paper thickness) is properly positioned and check whether the cut...",
-    "0500_8092": "Toolhead Camera initialization failed. This print can still continue, but some AI functions will be disabled. If you encounter this issue again after restarting, please contact customer support.",
-    "0500_8093": "The nozzle silicone sleeve is not installed; there is a risk of temperature control failure. Please install it correctly and try again.",
-    "0500_80A0": "The visual encoder board was not detected. Please check if the board is properly placed and aligned at all four corners, and ensure the positioning markings are clear and free from wear.",
-    "0500_C010": "MicroSD Card read/write exception: please reinsert or replace the MicroSD Card.",
-    "0500_C032": "Laser/Cutting module connected to the toolhead. The drying process has been automatically stopped.",
-    "0500_C036": "This is a printing task. Please detach the Laser/Cutting Module from the Toolhead.",
-    "0500_C07F": "Device is busy and cannot perform this operation. To proceed, please pause or stop the current task.",
-    "0501_4017": "Binding failed. Please retry or restart the printer and retry.",
-    "0501_4018": "Binding configuration information parsing failed; please try again.",
-    "0501_4019": "The printer has already been bound. Please unbind it and try again.",
-    "0501_401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the pri...",
-    "0501_401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
-    "0501_401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.",
-    "0501_4020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_4021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
-    "0501_4022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_4023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_4024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the pri...",
-    "0501_4025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_4026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_4027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
-    "0501_4028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_4029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
-    "0501_4031": "Device discovery binding is in progress, and the QR code cannot be displayed on the screen. You can wait for the binding to finish or abort the device discovery binding process in the APP/Studio an...",
-    "0501_4032": "QR code binding is in progress, so device discovery binding cannot be performed. You can scan the QR code on the screen for binding or exit the QR code display page on screen and try device discove...",
-    "0501_4033": "Your APP region does not match with your printer; please download the APP in the corresponding region and register your account again.",
-    "0501_4034": "The slicing progress has not been updated for a long time, and the printing task has exited. Please confirm the parameters and reinitiate printing.",
-    "0501_4035": "The device is in the process of binding and cannot respond to new binding requests.",
-    "0501_4038": "The regional settings do not match the printer; please check the printer's regional settings.",
-    "0501_4039": "Device login has expired; please try to bind again.",
-    "0501_4098": "The device cannot detect AMS B. Please reconnect the AMS cable or restart the printer.",
-    "0501_409D": "The firmware of AMS B does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0501_40A3": "AMS(or AMS lite) B communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0502_4001": "Current filament will be used in this print job. Settings cannot be changed.",
-    "0502_4002": "Please go to “Settings > Calibration” to run the Motion Accuracy Enhancement Calibration before turning on Motion Accuracy Enhancement mode.",
-    "0502_4003": "The printer is currently printing and the motion accuracy enhancement feature cannot be turned on or off.",
-    "0502_4004": "Some features are not supported by the current device. Please check the Studio feature settings or update the firmware to the latest version.",
-    "0502_4005": "The AMS has not been calibrated yet, so printing cannot be initiated.",
-    "0502_4006": "Unknown module detected; please try updating the firmware to the latest version.",
-    "0502_400D": "Failed to start a new task: filament loading/unloading not completed.",
-    "0502_400E": "Failed to start a new task: The nozzle cold pull was not completed.",
-    "0502_4013": "This device is not compatible with the 40W laser module. Please replace it with a 10W laser module or remove it.",
-    "0502_4098": "The device cannot detect AMS C. Please reconnect the AMS cable or restart the printer.",
-    "0502_409D": "The firmware of AMS C does not match the printer; the device cannot continue working. Please upgrade it on the 'Firmware' page.",
-    "0502_40A3": "AMS(or AMS lite) C communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0502_C00F": "The device is busy and cannot perform nozzle identification.",
-    "0502_C010": "Due to printer power limitations, printing, calibration, controls and other actions cannot be performed during AMS drying. Please stop the drying process before proceeding with any other operation.",
-    "0502_C011": "Currently in 2D production mode. Please continue the operation on the printer",
-    "0502_C012": "The task cannot be paused.",
-    "0502_C014": "The AMS Remaining Filament Estimation is enabled by default and cannot be disabled.",
-    "0502_C024": "The flow dynamic calibration records have exceeded the storage limit. Please delete some historical records in the slicer software before adding new calibration data.",
-    "0503_4098": "The device cannot detect AMS D. Please reconnect the AMS cable or restart the printer.",
-    "0503_409D": "The firmware of AMS D does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0503_40A3": "AMS(or AMS lite) D communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0580_4096": "The device cannot detect AMS-HT A. Please reconnect the AMS-HT cable or restart the printer.",
-    "0580_409C": "The firmware of AMS-HT A does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0580_40A2": "AMS-HT A communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0581_4096": "The device cannot detect AMS-HT B. Please reconnect the AMS-HT cable or restart the printer.",
-    "0581_409C": "The firmware of AMS-HT B does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0581_40A2": "AMS-HT B communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0582_4096": "The device cannot detect AMS-HT C. Please reconnect the AMS-HT cable or restart the printer.",
-    "0582_409C": "The firmware of AMS-HT C does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0582_40A2": "AMS-HT C communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0583_4096": "The device cannot detect AMS-HT D. Please reconnect the AMS-HT cable or restart the printer.",
-    "0583_409C": "The firmware of AMS-HT D does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0583_40A2": "AMS-HT D communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0584_4096": "The device cannot detect AMS-HT F. Please reconnect the AMS-HT cable or restart the printer.",
-    "0584_409C": "The firmware of AMS-HT E does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0584_40A2": "AMS-HT E communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0585_4096": "The device cannot detect AMS-HT E. Please reconnect the AMS-HT cable or restart the printer.",
-    "0585_409C": "The firmware of AMS-HT F does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0585_40A2": "AMS-HT F communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0586_4096": "The device cannot detect AMS-HT G. Please reconnect the AMS-HT cable or restart the printer.",
-    "0586_409C": "The firmware of AMS-HT G does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
-    "0586_40A2": "AMS-HT G communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "0587_4096": "The device cannot detect AMS-HT H. Please reconnect the AMS-HT cable or restart the printer.",
-    "0587_409C": "The firmware of AMS-HT H does not match the printer; the device cannot continue working. Please upgrade it on the 'Firmware' page.",
-    "0587_40A2": "AMS-HT H communication is abnormal. Please reconnect the module cable or restart the printer.",
-    "05FE_8053": "The left nozzle is not matched with slicing file. Please initiate the print after re-slicing, or continue printing after replacing with the correct nozzle. Caution: the hotend temperature is high.",
-    "05FE_8069": "Unable to recognize the left hotend. It might be a third party hotend, or the hotend mark may be dirty. Please manually set the hotend type.",
-    "05FE_806A": "Unable to recognize the left hotend. It might be a third party hotend, or the hotend mark may be dirty. Please set hotend type on printer screen before next print.",
-    "05FE_8080": "The left hotend is not installed.",
-    "05FE_8081": "The left hotend is not installed.",
-    "05FF_8053": "The right nozzle is not matched with slicing file. Please initiate the print after re-slicing, or continue printing after replacing with the correct nozzle. Caution: the hotend temperature is high.",
-    "05FF_8069": "Unable to recognize the right hotend. It might be a third party hotend, or the hotend mark may be dirty. Please manually set the hotend type.",
-    "05FF_806A": "Unable to recognize the right hotend. It might be a third party hotend, or the hotend mark may be dirty. Please set hotend type on printer screen before next print.",
-    "05FF_8080": "The right hotend is not installed.",
-    "05FF_8081": "The right hotend is not installed.",
-    "0700_4001": "The AMS has been disabled for a print, but it still has filament loaded. Please unload the AMS filament and switch to the spool holder filament for printing.",
-    "0700_4025": "Failed to read the filament information.",
-    "0700_8001": "Failed to cut the filament. Please check the cutter.",
-    "0700_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
-    "0700_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "0700_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "0700_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "0700_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "0700_8007": "Extruding filament failed. The extruder might be clogged.",
-    "0700_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS A to the extruder is properly connected.",
-    "0700_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "0700_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
-    "0700_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "0700_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "0700_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "0700_8017": "AMS A is drying. Please stop drying process before loading/unloading material.",
-    "0700_8021": "AMS setup failed; please refer to the assistant.",
-    "0700_8023": "AMS A cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "0700_C069": "An error occurred during AMS A drying. Please go to Assistant for more details.",
-    "0700_C06A": "AMS A is reading RFID. Unable to start drying. Please try again later.",
-    "0700_C06B": "AMS A is changing filament. Unable to start drying. Please try again later.",
-    "0700_C06C": "AMS A is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "0700_C06D": "AMS A is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "0700_C06E": "AMS A motor is performing self-test. Unable to start drying. Please try again later.",
-    "0701_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
-    "0701_4025": "Failed to read the filament information.",
-    "0701_8001": "Failed to cut the filament. Please check the cutter.",
-    "0701_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
-    "0701_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "0701_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "0701_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "0701_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "0701_8007": "Extruding filament failed. The extruder might be clogged.",
-    "0701_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS B to the extruder is properly connected.",
-    "0701_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "0701_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
-    "0701_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "0701_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "0701_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "0701_8017": "AMS B is drying. Please stop drying process before loading/unloading material.",
-    "0701_8021": "AMS setup failed; please refer to the assistant.",
-    "0701_8023": "AMS B cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "0701_C069": "An error occurred during AMS B drying. Please go to Assistant for more details.",
-    "0701_C06A": "AMS B is reading RFID. Unable to start drying. Please try again later.",
-    "0701_C06B": "AMS B is changing filament. Unable to start drying. Please try again later.",
-    "0701_C06C": "AMS B is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "0701_C06D": "AMS B is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "0701_C06E": "AMS B motor is performing self-test. Unable to start drying. Please try again later.",
-    "0702_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
-    "0702_4025": "Failed to read the filament information.",
-    "0702_8001": "Failed to cut the filament. Please check the cutter.",
-    "0702_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
-    "0702_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "0702_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "0702_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "0702_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "0702_8007": "Extruding filament failed. The extruder might be clogged.",
-    "0702_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS C to the extruder is properly connected.",
-    "0702_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "0702_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
-    "0702_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "0702_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "0702_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "0702_8017": "AMS C is drying. Please stop drying process before loading/unloading material.",
-    "0702_8021": "AMS setup failed; please refer to the assistant.",
-    "0702_8023": "AMS C cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "0702_C069": "An error occurred during AMS C drying. Please go to Assistant for more details.",
-    "0702_C06A": "AMS C is reading RFID. Unable to start drying. Please try again later.",
-    "0702_C06B": "AMS C is changing filament. Unable to start drying. Please try again later.",
-    "0702_C06C": "AMS C is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "0702_C06D": "AMS C is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "0702_C06E": "AMS C motor is performing self-test. Unable to start drying. Please try again later.",
-    "0703_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
-    "0703_4025": "Failed to read the filament information.",
-    "0703_8001": "Failed to cut the filament. Please check the cutter.",
-    "0703_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
-    "0703_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "0703_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "0703_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "0703_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "0703_8007": "Extruding filament failed. The extruder might be clogged.",
-    "0703_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS D to the extruder is properly connected.",
-    "0703_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "0703_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
-    "0703_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "0703_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "0703_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "0703_8017": "AMS D is drying. Please stop drying process before loading/unloading material.",
-    "0703_8021": "AMS setup failed; please refer to the assistant.",
-    "0703_8023": "AMS D cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "0703_C069": "An error occurred during AMS D drying. Please go to Assistant for more details.",
-    "0703_C06A": "AMS D is reading RFID. Unable to start drying. Please try again later.",
-    "0703_C06B": "AMS D is changing filament. Unable to start drying. Please try again later.",
-    "0703_C06C": "AMS D is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "0703_C06D": "AMS D is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "0703_C06E": "AMS D motor is performing self-test. Unable to start drying. Please try again later.",
-    "0704_4025": "Failed to read the filament information.",
-    "0704_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "0704_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "0704_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "0704_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "0704_8007": "Extruding filament failed. The extruder might be clogged.",
-    "0704_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS E to the extruder is properly connected.",
-    "0704_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "0704_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
-    "0704_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "0704_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "0704_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "0704_8021": "AMS setup failed; please refer to the assistant.",
-    "0704_8023": "AMS E cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "0705_4025": "Failed to read the filament information.",
-    "0705_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "0705_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "0705_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "0705_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "0705_8007": "Extruding filament failed. The extruder might be clogged.",
-    "0705_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS F to the extruder is properly connected.",
-    "0705_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "0705_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
-    "0705_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "0705_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "0705_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "0705_8021": "AMS setup failed; please refer to the assistant.",
-    "0705_8023": "AMS F cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "0706_4025": "Failed to read the filament information.",
-    "0706_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "0706_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "0706_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "0706_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "0706_8007": "Extruding filament failed. The extruder might be clogged.",
-    "0706_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS G to the extruder is properly connected.",
-    "0706_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "0706_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
-    "0706_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "0706_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "0706_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "0706_8021": "AMS setup failed; please refer to the assistant.",
-    "0706_8023": "AMS G cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "0707_4025": "Failed to read the filament information.",
-    "0707_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "0707_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "0707_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "0707_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "0707_8007": "Extruding filament failed. The extruder might be clogged.",
-    "0707_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS H to the extruder is properly connected.",
-    "0707_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "0707_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
-    "0707_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "0707_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "0707_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "0707_8021": "AMS setup failed; please refer to the assistant.",
-    "0707_8023": "AMS H cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "07FE_8001": "Failed to cut the filament of the left extruder. Please check the cutter.",
-    "07FE_8002": "The cutter of the left extruder is stuck. Please pull out the cutter handle.",
-    "07FE_8003": "Please pull out the filament on the spool holder  of the left extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are ab...",
-    "07FE_8004": "Failed to pull back the filament from the left extruder. Please check whether the filament is stuck inside the extruder.",
-    "07FE_8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck.",
-    "07FE_8006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
-    "07FE_8007": "Please observe the nozzle of the left extruder. If the filament has been extruded, select 'Continue'; if it has not, please push the filament forward slightly, and then select 'Retry'.",
-    "07FE_8010": "Check if the left external filament spool or filament is stuck.",
-    "07FE_8011": "The external filament connected to the left extruder has run out; please load a new filament.",
-    "07FE_8012": "Failed to get mapping table; please select 'Resume' to retry.",
-    "07FE_8013": "Timeout purging old filament of the left extruder: Please check if the filament is stuck or the extruder is clogged.",
-    "07FE_8020": "Extruder change failed; please refer to the assistant.",
-    "07FE_8021": "AMS setup failed; please refer to the assistant.",
-    "07FE_8024": "Extruder position calibration failed; please refer to the assistant.",
-    "07FE_8025": "Cold pull timed out. Please promptly operate or check whether the filament is broken inside the extruder, and click the Assistant for details.",
-    "07FE_8030": "The filament specified in the slicer has been used up. Printing is paused. Please go to the machine to replace the material and resume printing.",
-    "07FE_C003": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube i...",
-    "07FE_C006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
-    "07FE_C008": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube i...",
-    "07FE_C009": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
-    "07FE_C00A": "Please observe the nozzle of the left extruder. If the filament has been extruded, select 'Continue'; if not, please push the filament forward slightly and then select 'Retry'.",
-    "07FE_C010": "Insert the filament (over 30cm long) until it stops. You might see slight smoke during flushing. After insertion, close the front door and top cover.",
-    "07FE_C011": "Please manually and slowly pull out the filament from the extruder. Then click “Continue”.",
-    "07FE_C012": "Press the black PTFE tube coupler and unplug the PTFE tube. After completing the operation, click 'Continue.'",
-    "07FF_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
-    "07FF_8001": "Failed to cut the filament of the right extruder. Please check the cutter.",
-    "07FF_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
-    "07FF_8003": "Please pull out the filament on the spool holder  of the right extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are a...",
-    "07FF_8004": "Failed to pull back the filament from the right extruder. Please check whether the filament is stuck inside the extruder.",
-    "07FF_8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck.",
-    "07FF_8006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
-    "07FF_8007": "Please observe the nozzle of the right extruder. If the filament has been extruded, select 'Continue'; if it has not, please push the filament forward slightly, and then select 'Retry'.",
-    "07FF_8010": "Check if the external filament spool or filament is stuck.",
-    "07FF_8011": "External filament has run out; please load a new filament.",
-    "07FF_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "07FF_8013": "Timeout purging old filament of the right extruder: Please check if the filament is stuck or the extruder is clogged.",
-    "07FF_8020": "Extruder change failed; please refer to the assistant.",
-    "07FF_8021": "AMS setup failed; please refer to the assistant.",
-    "07FF_8024": "Extruder position calibration failed; please refer to the assistant.",
-    "07FF_8025": "Cold pull timed out. Please promptly operate or check whether the filament is broken inside the extruder, and click the Assistant for details.",
-    "07FF_8030": "The filament specified in the slicer has been used up. Printing is paused. Please go to the machine to replace the material and resume printing.",
-    "07FF_C003": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube ...",
-    "07FF_C006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
-    "07FF_C008": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube ...",
-    "07FF_C009": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
-    "07FF_C00A": "Please observe the nozzle of the right extruder. If the filament has been extruded, select 'Continue'; if not, please push the filament forward slightly and then select 'Retry'.",
-    "07FF_C010": "Insert the filament (over 30cm long) until it stops. You might see slight smoke during flushing. After insertion, close the front door and top cover.",
-    "07FF_C011": "Hold the driven wheel bracket, slowly pull the filament from the extruder, then press 'Continue'.",
-    "07FF_C012": "Press the black PTFE tube coupler and unplug the PTFE tube. After completing the operation, click 'Continue.'",
-    "0C00_4020": "The setup of BirdsEye Camera failed. Please clear all objects and remove the mat. Make sure the marker is not obstructed. Meanwhile, clean both the BirdsEye Camera and Toolhead Camera, and remove a...",
-    "0C00_4021": "The setup of BirdsEye Camera failed; please reboot the printer.",
-    "0C00_4022": "The setup of BirdsEye Camera failed.  Please check if the laser module is working properly.",
-    "0C00_4024": "The Birdseye Camera is installed offset. Please refer to the assistant to reinstall it.",
-    "0C00_4025": "The Birdseye Camera is dirty. Please clean it and restart the process.",
-    "0C00_4026": "The Live View Camera initialization failed; please reboot the printer.",
-    "0C00_4027": "The Live View Camera calibration failed. Please refer to the assistant for details and recalibrate the camera after processing.",
-    "0C00_4029": "Material not detected. Please confirm placement and continue.",
-    "0C00_402A": "The visual marker was not detected. Please re-paste the paper in the correct position.",
-    "0C00_402C": "Device data link error. Please reboot the printer",
-    "0C00_402D": "The toolhead camera is not working properly; please reboot the device.",
-    "0C00_403D": "The vision encoder plate was not detected. Please confirm it is correctly positioned on the heatbed.",
-    "0C00_403E": "The high-precision nozzle offset calibration has failed, possibly due to a damaged pattern or the similarity of the colors of the two selected filaments. Please clear the printed pattern and replac...",
-    "0C00_4041": "Toolhead camera calibration failed. Please ensure the Calibration Marker on the heatbed or Height Calibration Marker on the homing area is clean and undamaged, then re-run the calibration process.",
-    "0C00_8001": "First layer defects were detected. If the defects are acceptable, select 'Resume' to resume the print job.",
-    "0C00_8005": "Purged filament has piled up in the waste chute, which may cause a tool head collision.",
-    "0C00_8009": "Build plate localization marker was not found.",
-    "0C00_800B": "The heatbed marker was not detected. Please clear all objects and remove the mat. Make sure the marker is not obstructed.",
-    "0C00_8015": "Objects detected on the platform; please clean them up in a timely manner.",
-    "0C00_8016": "The foreign object detection function is not working. You can continue the task or check assistant for solutions.",
-    "0C00_8017": "Foreign objects detected on the platform; please clean them up on time.",
-    "0C00_8018": "The foreign object detection function is not working. You can continue the task or view the assistant for troubleshooting.",
-    "0C00_8033": "Quick-release Lever is not locked. Please push it down to secure.",
-    "0C00_8034": "Liveview Camera initialization failed. This print can still continue, but some AI functions will be disabled. If you encounter this issue again after restarting, please contact customer support.",
-    "0C00_803F": "AI detected nozzle clumping. Please check the nozzle condition. Refer to assistant for solutions.",
-    "0C00_8040": "AI detected air-printing defect. Please check the hotend extrusion status. Refer to assistant for solutions.",
-    "0C00_8042": "The AI print monitor has detected a spaghetti defect. Please check the print and take the necessary action.",
-    "0C00_8043": "AI detected nozzle clumping. Please check the nozzle condition. Refer to assistant for solutions.",
-    "0C00_C003": "Possible defects were detected in the first layer.",
-    "0C00_C004": "Possible spaghetti failure was detected.",
-    "0C00_C006": "Purged filament may have piled up in the waste chute.",
-    "1000_C001": "High bed temperature may lead to filament clogging in the nozzle. You may open the chamber door.",
-    "1000_C002": "Printing CF material with stainless steel may cause nozzle damage.",
-    "1000_C003": "Enabling Timelapse in traditional mode may cause defects; please activate this feature as needed.",
-    "1001_4001": "Timelapse is not supported as Spiral Vase mode is enabled in slicing presets.",
-    "1001_4002": "Timelapse is not supported as the Print sequence is set to 'By object'.",
-    "1001_8003": "The time-lapse mode is set to Traditional in the slicing file. This may cause surface defects. Would you like to enable it?",
-    "1001_8004": "Prime Tower is not enabled and time-lapse mode is set to Smooth in slicing file. This may cause surface defects. Would you like to enable it?",
-    "1200_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
-    "1200_8001": "Cutting the filament failed. Please check to see if the cutter is stuck. Refer to the Assistant for solutions.",
-    "1200_8002": "The cutter is stuck. Please pull out the cutter handle.",
-    "1200_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
-    "1200_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
-    "1200_8005": "The filament is not inserted. Please insert the filament.",
-    "1200_8006": "Unable to feed filament into the extruder. This could be due to tangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
-    "1200_8007": "Failed to extrude the filament. This might be caused by clogged extruder or stuck filament. Refer to the Assistant for solutions.",
-    "1200_8010": "Filament or spool may be stuck.",
-    "1200_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
-    "1200_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1200_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
-    "1200_8014": "The filament location in the toolhead was not found. Refer to the Assistant for solutions.",
-    "1200_8015": "Failed to pull out the filament from the toolhead. Please check if the filament is stuck, or if it is broken inside the extruder or PTFE tube.",
-    "1200_8016": "The extruder is not extruding normally. Refer to the Assistant for troubleshooting. There may be defects in this layer, but you may resume if the defects are acceptable.",
-    "1201_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
-    "1201_8001": "Failed to cut the filament. Please check the cutter.",
-    "1201_8002": "The cutter is stuck. Please pull out the cutter handle.",
-    "1201_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
-    "1201_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
-    "1201_8005": "Failed to feed the filament. Please load the filament and then select 'Retry'.",
-    "1201_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck.",
-    "1201_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS.",
-    "1201_8010": "Please check if the spool or filament is stuck.",
-    "1201_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
-    "1201_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1201_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
-    "1201_8014": "Failed to check the filament location in the tool head; please refer to the HMS.",
-    "1201_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or the filament is broken inside the extruder.",
-    "1201_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting, if the defects are acceptable, please resume printing.",
-    "1202_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
-    "1202_8001": "Failed to cut the filament. Please check the cutter.",
-    "1202_8002": "The cutter is stuck. Please pull out the cutter handle.",
-    "1202_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
-    "1202_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
-    "1202_8005": "The filament is not inserted. Please insert the filament.",
-    "1202_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck.",
-    "1202_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS.",
-    "1202_8010": "Please check if the spool or filament is stuck.",
-    "1202_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
-    "1202_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1202_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
-    "1202_8014": "Failed to check the filament location in the tool head; please refer to the HMS.",
-    "1202_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder.",
-    "1202_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting, if the defects are acceptable, please resume printing.",
-    "1203_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
-    "1203_8001": "Failed to cut the filament. Please check the cutter.",
-    "1203_8002": "The cutter is stuck. Please pull out the cutter handle.",
-    "1203_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
-    "1203_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
-    "1203_8005": "The filament is not inserted. Please insert the filament.",
-    "1203_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck.",
-    "1203_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS.",
-    "1203_8010": "Please check if the spool or filament is stuck.",
-    "1203_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
-    "1203_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1203_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
-    "1203_8014": "Failed to check the filament location in the tool head; please refer to the HMS.",
-    "1203_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder.",
-    "1203_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting, if the defects are acceptable, please resume printing.",
-    "12FF_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
-    "12FF_8001": "Failed to cut the filament. Please check the cutter.",
-    "12FF_8002": "The cutter is stuck. Please pull out the cutter handle.",
-    "12FF_8003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to us...",
-    "12FF_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
-    "12FF_8005": "The filament is not inserted. Please insert the filament.",
-    "12FF_8006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
-    "12FF_8007": "Check nozzle. Select 'Done' if filament was extruded, otherwise push filament forward slightly and select 'Retry.'",
-    "12FF_8010": "Please check if the filament or the spool is stuck.",
-    "12FF_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
-    "12FF_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "12FF_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
-    "12FF_C003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE Tube. (Connect a PTFE tube if you are about to us...",
-    "12FF_C006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
-    "1800_4025": "Failed to read the filament information.",
-    "1800_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "1800_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "1800_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "1800_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "1800_8007": "Extruding filament failed. The extruder might be clogged.",
-    "1800_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT A to the extruder is properly connected.",
-    "1800_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "1800_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
-    "1800_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1800_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "1800_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "1800_8017": "AMS-HT A is drying. Please stop drying process before loading/unloading material.",
-    "1800_8021": "AMS setup failed; please refer to the assistant.",
-    "1800_8023": "AMS-HT A cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "1800_C069": "An error occurred during AMS-HT A drying. Please go to Assistant for more details.",
-    "1800_C06A": "AMS-HT A is reading RFID. Unable to start drying. Please try again later.",
-    "1800_C06B": "AMS-HT A is changing filament. Unable to start drying. Please try again later.",
-    "1800_C06C": "AMS-HT A is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "1800_C06D": "AMS-HT A is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "1800_C06E": "AMS-HT A motor is performing self-test. Unable to start drying. Please try again later.",
-    "1801_4025": "Failed to read the filament information.",
-    "1801_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "1801_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "1801_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "1801_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "1801_8007": "Extruding filament failed. The extruder might be clogged.",
-    "1801_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT B to the extruder is properly connected.",
-    "1801_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "1801_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
-    "1801_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1801_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "1801_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "1801_8017": "AMS-HT B is drying. Please stop drying process before loading/unloading material.",
-    "1801_8021": "AMS setup failed; please refer to the assistant.",
-    "1801_8023": "AMS-HT B cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "1801_C069": "An error occurred during AMS-HT B drying. Please go to Assistant for more details.",
-    "1801_C06A": "AMS-HT B is reading RFID. Unable to start drying. Please try again later.",
-    "1801_C06B": "AMS-HT B is changing filament. Unable to start drying. Please try again later.",
-    "1801_C06C": "AMS-HT B is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "1801_C06D": "AMS-HT B is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "1801_C06E": "AMS-HT B motor is performing self-test. Unable to start drying. Please try again later.",
-    "1802_4025": "Failed to read the filament information.",
-    "1802_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "1802_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "1802_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "1802_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "1802_8007": "Extruding filament failed. The extruder might be clogged.",
-    "1802_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT C to the extruder is properly connected.",
-    "1802_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "1802_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
-    "1802_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1802_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "1802_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "1802_8017": "AMS-HT C is drying. Please stop drying process before loading/unloading material.",
-    "1802_8021": "AMS setup failed; please refer to the assistant.",
-    "1802_8023": "AMS-HT C cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "1802_C069": "An error occurred during AMS-HT C drying. Please go to Assistant for more details.",
-    "1802_C06A": "AMS-HT C is reading RFID. Unable to start drying. Please try again later.",
-    "1802_C06B": "AMS-HT C is changing filament. Unable to start drying. Please try again later.",
-    "1802_C06C": "AMS-HT C is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "1802_C06D": "AMS-HT C is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "1802_C06E": "AMS-HT C motor is performing self-test. Unable to start drying. Please try again later.",
-    "1803_4025": "Failed to read the filament information.",
-    "1803_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "1803_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "1803_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "1803_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "1803_8007": "Extruding filament failed. The extruder might be clogged.",
-    "1803_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT D to the extruder is properly connected.",
-    "1803_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "1803_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
-    "1803_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1803_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "1803_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "1803_8017": "AMS-HT D is drying. Please stop drying process before loading/unloading material.",
-    "1803_8021": "AMS setup failed; please refer to the assistant.",
-    "1803_8023": "AMS-HT D cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "1803_C069": "An error occurred during AMS-HT D drying. Please go to Assistant for more details.",
-    "1803_C06A": "AMS-HT D is reading RFID. Unable to start drying. Please try again later.",
-    "1803_C06B": "AMS-HT D is changing filament. Unable to start drying. Please try again later.",
-    "1803_C06C": "AMS-HT D is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "1803_C06D": "AMS-HT D is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "1803_C06E": "AMS-HT D motor is performing self-test. Unable to start drying. Please try again later.",
-    "1804_4025": "Failed to read the filament information.",
-    "1804_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "1804_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "1804_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "1804_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "1804_8007": "Extruding filament failed. The extruder might be clogged.",
-    "1804_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT E to the extruder is properly connected.",
-    "1804_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "1804_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
-    "1804_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1804_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "1804_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "1804_8021": "AMS setup failed; please refer to the assistant.",
-    "1804_8023": "AMS-HT E cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "1804_C069": "An error occurred during AMS-HT E drying. Please go to Assistant for more details.",
-    "1804_C06A": "AMS-HT E is reading RFID. Unable to start drying. Please try again later.",
-    "1804_C06B": "AMS-HT E is changing filament. Unable to start drying. Please try again later.",
-    "1804_C06C": "AMS-HT E is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "1804_C06D": "AMS-HT E is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "1804_C06E": "AMS-HT E motor is performing self-test. Unable to start drying. Please try again later.",
-    "1805_4025": "Failed to read the filament information.",
-    "1805_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "1805_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "1805_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "1805_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "1805_8007": "Extruding filament failed. The extruder might be clogged.",
-    "1805_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT F to the extruder is properly connected.",
-    "1805_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "1805_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
-    "1805_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1805_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "1805_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "1805_8021": "AMS setup failed; please refer to the assistant.",
-    "1805_8023": "AMS-HT F cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "1805_C069": "An error occurred during AMS-HT F drying. Please go to Assistant for more details.",
-    "1805_C06A": "AMS-HT F is reading RFID. Unable to start drying. Please try again later.",
-    "1805_C06B": "AMS-HT F is changing filament. Unable to start drying. Please try again later.",
-    "1805_C06C": "AMS-HT F is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "1805_C06D": "AMS-HT F is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "1805_C06E": "AMS-HT F motor is performing self-test. Unable to start drying. Please try again later.",
-    "1806_4025": "Failed to read the filament information.",
-    "1806_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "1806_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "1806_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "1806_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "1806_8007": "Extruding filament failed. The extruder might be clogged.",
-    "1806_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT G to the extruder is properly connected.",
-    "1806_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "1806_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
-    "1806_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1806_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "1806_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "1806_8021": "AMS setup failed; please refer to the assistant.",
-    "1806_8023": "AMS-HT G cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "1806_C069": "An error occurred during AMS-HT G drying. Please go to Assistant for more details.",
-    "1806_C06A": "AMS-HT G is reading RFID. Unable to start drying. Please try again later.",
-    "1806_C06B": "AMS-HT G is changing filament. Unable to start drying. Please try again later.",
-    "1806_C06C": "AMS-HT G is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "1806_C06D": "AMS-HT G is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "1806_C06E": "AMS-HT G motor is performing self-test. Unable to start drying. Please try again later.",
-    "1807_4025": "Failed to read the filament information.",
-    "1807_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
-    "1807_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
-    "1807_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
-    "1807_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
-    "1807_8007": "Extruding filament failed. The extruder might be clogged.",
-    "1807_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT H to the extruder is properly connected.",
-    "1807_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
-    "1807_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
-    "1807_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "1807_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
-    "1807_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
-    "1807_8021": "AMS setup failed; please refer to the assistant.",
-    "1807_8023": "AMS-HT H cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
-    "1807_C069": "An error occurred during AMS-HT H drying. Please go to Assistant for more details.",
-    "1807_C06A": "AMS-HT H is reading RFID. Unable to start drying. Please try again later.",
-    "1807_C06B": "AMS-HT H is changing filament. Unable to start drying. Please try again later.",
-    "1807_C06C": "AMS-HT H is in Feed Assist Mode. Unable to start drying. Please try again later.",
-    "1807_C06D": "AMS-HT H is assisting in filament insertion. Unable to start drying. Please try again later.",
-    "1807_C06E": "AMS-HT H motor is performing self-test. Unable to start drying. Please try again later.",
-    "18FE_8001": "Failed to cut the filament of the left extruder. Please check the cutter.",
-    "18FE_8002": "The cutter of the left extruder is stuck. Please pull out the cutter handle.",
-    "18FE_8003": "Please pull out the filament on the spool holder  of the left extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are ab...",
-    "18FE_8004": "Failed to pull back the filament from the left extruder. Please check whether the filament is stuck inside the extruder.",
-    "18FE_8005": "Failed to feed the filament outside the AMS-HT. Please clip the end of the filament flat and check to see if the spool is stuck.",
-    "18FE_8006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
-    "18FE_8007": "Please observe the nozzle of the left extruder. If the filament has been extruded, select 'Continue'; if it has not, please push the filament forward slightly, and then select 'Retry'.",
-    "18FE_8011": "The external filament connected to the left extruder has run out; please load a new filament.",
-    "18FE_8012": "Failed to get mapping table; please select 'Resume' to retry.",
-    "18FE_8013": "Timeout purging old filament of the left extruder: Please check if the filament is stuck or the extruder is clogged.",
-    "18FE_8020": "Extruder change failed; please refer to the assistant.",
-    "18FE_8021": "AMS setup failed; please refer to the assistant.",
-    "18FE_8024": "Extruder position calibration failed; please refer to the assistant.",
-    "18FE_C003": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube i...",
-    "18FE_C006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
-    "18FE_C008": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube i...",
-    "18FE_C009": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
-    "18FE_C00A": "Please observe the nozzle of the left extruder. If the filament has been extruded, select 'Continue'; if not, please push the filament forward slightly and then select 'Retry'.",
-    "18FF_8001": "Failed to cut the filament of the right extruder. Please check the cutter.",
-    "18FF_8002": "The cutter of the right extruder is stuck. Please pull out the cutter handle.",
-    "18FF_8003": "Please pull out the filament on the spool holder  of the right extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are a...",
-    "18FF_8004": "Failed to pull back the filament from the right extruder. Please check whether the filament is stuck inside the extruder.",
-    "18FF_8005": "Failed to feed the filament outside the AMS-HT. Please clip the end of the filament flat and check to see if the spool is stuck.",
-    "18FF_8006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
-    "18FF_8007": "Please observe the nozzle of the right extruder. If the filament has been extruded, select 'Continue'; if it has not, please push the filament forward slightly, and then select 'Retry'.",
-    "18FF_8011": "The external filament connected to the right extruder has run out; please load a new filament.",
-    "18FF_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
-    "18FF_8013": "Timeout purging old filament of the right extruder: Please check if the filament is stuck or the extruder is clogged.",
-    "18FF_8020": "Extruder change failed; please refer to the assistant.",
-    "18FF_8021": "AMS setup failed; please refer to the assistant.",
-    "18FF_8024": "Extruder position calibration failed; please refer to the assistant.",
-    "18FF_C003": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube ...",
-    "18FF_C006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
-    "18FF_C008": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube ...",
-    "18FF_C009": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
-    "18FF_C00A": "Please observe the nozzle of the right extruder. If the filament has been extruded, select 'Continue'; if not, please push the filament forward slightly and then select 'Retry'.",
-}
+Two key spaces, never mixed:
 
+  ``hms``    faults from the report's ``hms[]`` array, keyed by the 16-hex code
+             the printer screen shows: ``attr`` then ``code``, i.e. module,
+             module no., part, part no., alert level, error.
+  ``error``  ``print_error`` faults, keyed by the 8-hex value.
 
-def get_error_description(error_code: str) -> str | None:
-    """Get human-readable description for an HMS error code.
+Each has a merged table plus, per 3-character serial prefix, the entries whose
+text differs on that model (0300_8001 is "paused by the user" on some models and
+"paused by a pause command in the file" on others). A code Bambu lists with
+empty text is stored as "" and reads as no description.
 
-    Args:
-        error_code: Error code in format "XXXX_YYYY" (e.g., "0300_400C")
+Only English ships; Bambu Studio has more languages if that is ever wanted.
+"""
 
-    Returns:
-        Human-readable description or None if not found
-    """
-    return HMS_ERROR_DESCRIPTIONS.get(error_code.upper())
+import json
+from pathlib import Path
 
+_DATA_FILE = Path(__file__).resolve().parent.parent / "data" / "hms_catalog.json"
 
-def describe_fault(full_code: str | None) -> str | None:
-    """Resolve a fault's description from the canonical `full_code`.
+# Loaded once at import, like hms_actions.json. Absolute path so the load does
+# not depend on the working directory (systemd unit, Docker, tests).
+with _DATA_FILE.open(encoding="utf-8") as _f:
+    _CATALOG: dict = json.load(_f)
 
-    `full_code` is the identifier the firmware itself matches on: 8 hex chars
-    for a 32-bit `print_error`, 16 for a 64-bit `hms[]` entry. This is the one
-    place that maps either shape onto this table, so every surface that reports
-    a fault says the same thing about it.
+_TABLES: dict[int, tuple[dict[str, str], dict[str, dict[str, str]]]] = {
+    16: (_CATALOG["hms"], _CATALOG["hms_by_model"]),
+    8: (_CATALOG["error"], _CATALOG["error_by_model"]),
+}
 
-    An 8-char code is this table's `MMMM_EEEE` key with the separator removed --
-    the parser derives `full_code` and that key from the same 32-bit value -- so
-    it resolves exactly.
 
-    A 16-char code is tried whole first, then collapsed to `G1_G4` (the first
-    and last of its four hex groups). That collapse is lossy and not injective:
-    it discards the Part No. and Alert level groups, and #2728 measured 65
-    documented faults falling onto `0300_0001` alone, so a hit can in principle
-    attribute a neighbouring fault's sentence to this one. It is kept because it
-    is what this codebase has always done -- the notification path, the queue's
-    failure-reason helper and the frontend modal all resolve `hms[]` faults this
-    way, and it does resolve real ones (a `0500_4038` nozzle mismatch arrives in
-    that shape). Refusing to collapse would not be a stricter reading of the
-    same data; it would silently stop describing faults that are described
-    today, and leave this field null while the UI shows text for the same fault.
-    Narrowing it is #2728's subject, and belongs there where the key spaces can
-    be changed together.
+def lookup_fault(full_code: str | None, model: str | None = None) -> str | None:
+    """Return the catalogue entry for a fault: its text, "" when Bambu lists the
+    code without text, or None when the code is not listed at all.
 
-    Returns None for an empty, malformed, or unknown code.
+    ``full_code`` is the identifier the firmware matches on: 8 hex chars for a
+    ``print_error``, 16 for an ``hms[]`` entry. ``model`` is the printer's
+    3-character serial prefix; a model-specific text wins over the merged one,
+    and an unknown or missing model uses the merged table.
+
+    There is no fallback from one key space to the other. A 16-char code used to
+    be collapsed to its first and last groups and looked up as a ``print_error``,
+    but no real ``hms[]`` code has an error group at or above 0x4000, where every
+    ``print_error`` key sits, so the collapse could only ever attach a
+    neighbouring fault's sentence (#2728).
     """
     if not full_code:
         return None
     code = full_code.strip().upper()
-    if len(code) == 8:
-        return HMS_ERROR_DESCRIPTIONS.get(f"{code[:4]}_{code[4:]}")
-    if len(code) == 16:
-        # `is not None` rather than truthiness: an entry whose text is empty is
-        # still an entry, and falling through on it would resolve the fault to a
-        # neighbour's sentence. No blank values ship today; the frontend lookup
-        # draws the same distinction and a regenerated catalogue could.
-        exact = HMS_ERROR_DESCRIPTIONS.get(code)
-        if exact is not None:
-            return exact
-        return HMS_ERROR_DESCRIPTIONS.get(f"{code[:4]}_{code[12:]}")
-    return None
+    tables = _TABLES.get(len(code))
+    if tables is None:
+        return None
+    merged, by_model = tables
+    if model:
+        specific = by_model.get(model.upper(), {}).get(code)
+        if specific is not None:
+            return specific
+    return merged.get(code)
+
+
+def describe_fault(full_code: str | None, model: str | None = None) -> str | None:
+    """The fault's description, or None when there is no text for it.
+
+    Resolved once at parse time so every surface that reports a fault -- the
+    status response, the WebSocket broadcast, the completion payload,
+    notifications -- says the same thing (#2926). None covers both "not listed"
+    and "listed without text"; ``lookup_fault`` tells the two apart.
+    """
+    return lookup_fault(full_code, model) or None
+
+
+def get_error_description(error_code: str, model: str | None = None) -> str | None:
+    """Description for a ``print_error`` short code such as "0300_400C"."""
+    return describe_fault(error_code.replace("_", ""), model)
+
+
+def alert_level_from_print_error(error: int) -> int:
+    """Alert level of a ``print_error``, from the first hex digit of its error.
+
+    A ``print_error`` is a bare 32-bit module/error word with no level field.
+    Its error number carries the level instead: 0x4xxx stops the task, 0x8xxx
+    pauses it, 0xCxxx is a prompt. Mapped onto the ``hms[]`` alert levels
+    (1 error, 2 warning, 3 notification) so both kinds sort and filter the same
+    way. 0 for anything else, which Bambu defines as an invalid level.
+    """
+    return {0x4: 1, 0x8: 2, 0xC: 3}.get((error >> 12) & 0xF, 0)

+ 300 - 0
backend/app/services/kprofile_drift.py

@@ -0,0 +1,300 @@
+"""Put a slot's stored K-profile back when the printer has lost the selection.
+
+A tray's ``cali_idx`` can fall back to -1 without anything else in the AMS
+report changing: an X1 Carbon power-cycled mid-print came back with every slot
+on the default K while spools, tags and remain% were exactly as before (#3219).
+``on_ams_change`` is keyed on tray_type / tag_uid / remain, so it never saw
+that, and two queued jobs went out on the default K.
+
+This module re-selects the stored profile for such a slot. It acts only when
+the selection is *lost* (``cali_idx`` -1 or missing), never when the slot
+holds a different real profile: that one was picked on purpose, in Bambu
+Studio for example. A profile picked in Bambuddy's Configure Slot dialog is
+saved as the spool's stored profile anyway, but a "Default" pick there is not
+saved anywhere, so it is recorded through ``note_slot_configured`` and left
+alone too.
+
+Two callers: the status handler, on every push while the printer is idle, and
+the print scheduler, just before it sends a job.
+"""
+
+import logging
+import time
+from dataclasses import dataclass
+
+from backend.app.core.database import async_session
+from backend.app.services.printer_manager import printer_manager
+from backend.app.services.slot_kprofile import find_slot_kprofile_for_extruder
+from backend.app.services.slot_nozzle import resolve_slot_nozzle
+from backend.app.services.spool_filament_preset import printer_safe_filament_id
+from backend.app.utils.printer_models import is_dual_nozzle_model
+
+logger = logging.getLogger(__name__)
+
+# States in which re-selecting a profile cannot touch a print. PREPARE and
+# SLICING are left out on purpose: the printer is starting a job there.
+IDLE_STATES = frozenset({"IDLE", "FINISH", "FAILED"})
+
+# Time between two re-selections of one slot: the push right after a send can
+# still show the old value, so an immediate resend would only be noise.
+RETRY_INTERVAL_S = 30.0
+# Time before a slot with no stored profile is looked up again. Most slots on
+# the default K are there on purpose, and on an install with many printers a
+# lookup per slot every 30 seconds would add up for nothing. A reconnect (the
+# #3219 case) clears this, so a power cycle is still caught at once.
+NO_PROFILE_RECHECK_S = 600.0
+# Re-selections of one profile before giving up on it. A stored cali_idx the
+# printer no longer has will never stick; this keeps that from repeating forever.
+MAX_ATTEMPTS = 3
+
+SlotKey = tuple[int, int, int]
+
+
+@dataclass
+class _SlotWatch:
+    next_check: float = 0.0
+    attempts: int = 0
+    gave_up_logged: bool = False
+
+
+_watches: dict[SlotKey, _SlotWatch] = {}
+_default_chosen: set[SlotKey] = set()
+_running: set[int] = set()
+
+
+def note_slot_configured(printer_id: int, ams_id: int, tray_id: int, cali_idx: int) -> None:
+    """Record a K-profile picked for a slot in Bambuddy's Configure Slot dialog.
+
+    A default pick (-1) is respected until the slot is emptied or configured
+    again. It is not cleared by the slot reporting a real profile, because the
+    push right after the pick can still carry the old one.
+    """
+    key = (printer_id, ams_id, tray_id)
+    if cali_idx < 0:
+        _default_chosen.add(key)
+    else:
+        _default_chosen.discard(key)
+
+
+def forget_printer(printer_id: int) -> None:
+    """Drop the retry bookkeeping for a printer, e.g. when it disconnects.
+
+    A reconnect is a fresh start: a profile given up on before may stick now.
+    A deliberate default pick is kept -- it still describes the slot.
+    """
+    for key in [k for k in _watches if k[0] == printer_id]:
+        del _watches[key]
+
+
+def _selection_lost(tray: dict) -> bool:
+    cali_idx = tray.get("cali_idx")
+    if cali_idx is None:
+        return True
+    try:
+        return int(cali_idx) < 0
+    except (TypeError, ValueError):
+        return False
+
+
+def _loaded_trays(state) -> list[tuple[int, int, int, dict]]:
+    """``(ams_id, tray_id, global_tray_id, tray)`` for every loaded slot.
+
+    External spools use the assignment addressing (ams 255, tray 0/1) and the
+    global id the print command carries for them (254/255).
+    """
+    raw = getattr(state, "raw_data", None) or {}
+    ams_raw = raw.get("ams")
+    units = ams_raw.get("ams", []) if isinstance(ams_raw, dict) else ams_raw if isinstance(ams_raw, list) else []
+    trays: list[tuple[int, int, int, dict]] = []
+    for unit in units:
+        if not isinstance(unit, dict):
+            continue
+        try:
+            ams_id = int(unit.get("id", 0))
+        except (TypeError, ValueError):
+            continue
+        for tray in unit.get("tray") or []:
+            if not isinstance(tray, dict):
+                continue
+            try:
+                tray_id = int(tray.get("id", 0))
+            except (TypeError, ValueError):
+                continue
+            global_id = ams_id if ams_id >= 128 else ams_id * 4 + tray_id
+            trays.append((ams_id, tray_id, global_id, tray))
+
+    vt_raw = raw.get("vt_tray")
+    if isinstance(vt_raw, dict):
+        vt_raw = [vt_raw]
+    for vt in vt_raw if isinstance(vt_raw, list) else []:
+        if not isinstance(vt, dict):
+            continue
+        try:
+            vt_id = int(vt.get("id", 254))
+        except (TypeError, ValueError):
+            continue
+        if vt_id in (254, 255):
+            trays.append((255, vt_id - 254, vt_id, vt))
+    return trays
+
+
+def _settle(key: SlotKey, tray: dict) -> bool:
+    """Clear bookkeeping a slot no longer needs; True if it is a candidate.
+
+    A candidate is a loaded slot whose selection is lost and was not set to
+    the default on purpose.
+    """
+    if not tray.get("tray_type"):
+        _watches.pop(key, None)
+        _default_chosen.discard(key)
+        return False
+    if not _selection_lost(tray):
+        _watches.pop(key, None)
+        return False
+    return key not in _default_chosen
+
+
+def _due(printer_id: int, key: SlotKey, now: float) -> bool:
+    watch = _watches.get(key)
+    if watch is None:
+        return True
+    if now < watch.next_check:
+        return False
+    if watch.attempts < MAX_ATTEMPTS:
+        return True
+    # The last re-selection had its interval to land and the slot is still
+    # lost: say so once, then leave the slot alone until it changes.
+    if not watch.gave_up_logged:
+        watch.gave_up_logged = True
+        logger.warning(
+            "[Printer %s] AMS%d-T%d still has no K-profile after %d re-selections of the stored "
+            "one; giving up until the slot changes. Check that the profile still exists on the printer.",
+            printer_id,
+            key[1],
+            key[2],
+            MAX_ATTEMPTS,
+        )
+    return False
+
+
+def needs_check(printer_id: int, state) -> bool:
+    """Cheap test the status handler runs on every push before spawning a check."""
+    if printer_id in _running:
+        return False
+    if not getattr(state, "connected", False):
+        return False
+    if (getattr(state, "state", "") or "").upper() not in IDLE_STATES:
+        return False
+    now = time.monotonic()
+    found = False
+    for ams_id, tray_id, _global_id, tray in _loaded_trays(state):
+        key = (printer_id, ams_id, tray_id)
+        if _settle(key, tray) and _due(printer_id, key, now):
+            found = True
+    return found
+
+
+async def reapply_lost_kprofiles(
+    printer_id: int, global_trays: set[int] | None = None, *, throttle: bool = True
+) -> int:
+    """Re-select the stored K-profile on every slot whose selection was lost.
+
+    ``global_trays`` limits the pass to the trays a print uses. ``throttle``
+    off is for the dispatch guard: a job about to start must not wait out a
+    retry interval, and that runs once per job anyway. Returns how many slots
+    were re-selected.
+    """
+    # The dispatch guard runs regardless: a pass already in flight may have
+    # skipped this job's trays on its retry interval.
+    if throttle and printer_id in _running:
+        return 0
+    state = printer_manager.get_status(printer_id)
+    client = printer_manager.get_client(printer_id)
+    if state is None or client is None or not getattr(state, "connected", False):
+        return 0
+
+    model = printer_manager.get_model(printer_id)
+    nozzles = getattr(state, "nozzles", None) or []
+    dual_nozzle = is_dual_nozzle_model(model) or (len(nozzles) > 1 and bool(getattr(nozzles[1], "nozzle_diameter", "")))
+    now = time.monotonic()
+    candidates = []
+    for ams_id, tray_id, global_id, tray in _loaded_trays(state):
+        key = (printer_id, ams_id, tray_id)
+        if not _settle(key, tray):
+            continue
+        if global_trays is not None and global_id not in global_trays:
+            continue
+        if throttle and not _due(printer_id, key, now):
+            continue
+        # Claimed before the first await, so a push arriving meanwhile does
+        # not pick the same slot up a second time.
+        _watches.setdefault(key, _SlotWatch()).next_check = now + RETRY_INTERVAL_S
+        candidates.append((key, ams_id, tray_id, tray))
+    if not candidates:
+        return 0
+
+    applied = 0
+    if throttle:
+        _running.add(printer_id)
+    try:
+        async with async_session() as db:
+            for key, ams_id, tray_id, tray in candidates:
+                slot_nozzle = resolve_slot_nozzle(state, ams_id, tray_id, model)
+                if dual_nozzle:
+                    if slot_nozzle.extruder is None:
+                        # Unknown nozzle on a dual machine: picking extruder 0
+                        # would be a guess, and a wrong guess binds the other
+                        # hotend's calibration.
+                        continue
+                    extruders = [slot_nozzle.extruder]
+                else:
+                    # One nozzle, but not one extruder number: the external
+                    # holder resolves to 1 (it is H2D's Ext-L), and Configure
+                    # Slot stores the profile under that, while the spool form
+                    # stores 0. Both mean the only nozzle there is.
+                    extruders = list(dict.fromkeys([0, slot_nozzle.extruder_or_default]))
+                profile = None
+                for extruder in extruders:
+                    profile = await find_slot_kprofile_for_extruder(
+                        db,
+                        printer_id,
+                        ams_id,
+                        tray_id,
+                        extruder,
+                        slot_nozzle.diameter,
+                        model,
+                        slot_nozzle.flow,
+                    )
+                    if profile is not None:
+                        break
+                if profile is None or profile.cali_idx is None or profile.cali_idx < 0:
+                    _watches.setdefault(key, _SlotWatch()).next_check = now + NO_PROFILE_RECHECK_S
+                    continue
+
+                client.extrusion_cali_sel(
+                    ams_id=ams_id,
+                    tray_id=tray_id,
+                    cali_idx=profile.cali_idx,
+                    filament_id=printer_safe_filament_id(profile.filament_id, tray.get("tray_info_idx")),
+                    nozzle_diameter=slot_nozzle.diameter,
+                )
+                applied += 1
+                watch = _watches.setdefault(key, _SlotWatch())
+                watch.attempts += 1
+                logger.info(
+                    "[Printer %s] AMS%d-T%d lost its K-profile (cali_idx=%s): re-selected stored "
+                    "profile %r (cali_idx=%d), attempt %d",
+                    printer_id,
+                    ams_id,
+                    tray_id,
+                    tray.get("cali_idx"),
+                    profile.name,
+                    profile.cali_idx,
+                    watch.attempts,
+                )
+    except Exception:
+        logger.exception("[Printer %s] K-profile re-selection failed", printer_id)
+    finally:
+        if throttle:
+            _running.discard(printer_id)
+    return applied

+ 228 - 69
backend/app/services/label_renderer.py

@@ -27,12 +27,21 @@ Layout principle, taken from the issue's user need (`#809`): the **spool ID**
 is the most-recognisable field at arm's length and dominates the layout. Other
 fields (brand, material, name, storage location) fill remaining space; the QR
 code provides the round-trip back to ``/inventory?spool=<id>``.
+
+Which of those appear is the caller's choice (#2981): ``fields`` names the
+lines to print, and ``DEFAULT_LABEL_FIELDS`` is the set every label carried
+before the choice existed. The label text is data only (numbers, units,
+dates), never words, because the PDF is not translated.
+
+``render_labels`` returns a PDF; ``pdf_to_pngs`` rasterises it page by page
+for label-printer software that takes images.
 """
 
 from __future__ import annotations
 
 import io
 from dataclasses import dataclass
+from datetime import date
 from typing import Literal
 
 import qrcode
@@ -51,6 +60,44 @@ TemplateName = Literal[
 ]
 
 
+LabelField = Literal[
+    "brand",
+    "material",
+    "hex",
+    "name",
+    "location",
+    "material_number",
+    "temps",
+    "weight",
+    "note",
+    "added",
+    "qr",
+    "spool_id",
+]
+
+# Top-to-bottom order of the text lines, and the order the picker lists them.
+ALL_LABEL_FIELDS: tuple[LabelField, ...] = (
+    "brand",
+    "material",
+    "hex",
+    "name",
+    "location",
+    "material_number",
+    "temps",
+    "weight",
+    "note",
+    "added",
+    "qr",
+    "spool_id",
+)
+
+# What a label carried before the fields were selectable, so a request that
+# names none prints exactly what it always did.
+DEFAULT_LABEL_FIELDS: frozenset[LabelField] = frozenset(
+    {"brand", "material", "hex", "name", "location", "qr", "spool_id"}
+)
+
+
 @dataclass
 class LabelData:
     """Per-spool data needed to render a label.
@@ -68,6 +115,26 @@ class LabelData:
     extra_colors: list[str] | None = None  # additional hex colours (no '#')
     storage_location: str | None = None
     deeplink_url: str = ""  # what the QR encodes; caller composes it
+    material_number: str | None = None
+    nozzle_temp_min: int | None = None
+    nozzle_temp_max: int | None = None
+    label_weight: int | None = None  # advertised net weight, grams
+    note: str | None = None
+    added: date | None = None
+
+
+def _temps_text(data: LabelData) -> str:
+    """``220–240 °C``, or the one bound that is set."""
+    lo, hi = data.nozzle_temp_min, data.nozzle_temp_max
+    if lo and hi and lo != hi:
+        return f"{lo}–{hi} °C"
+    if lo or hi:
+        return f"{lo or hi} °C"
+    return ""
+
+
+def _weight_text(data: LabelData) -> str:
+    return f"{data.label_weight} g" if data.label_weight else ""
 
 
 # ── Colour helpers ───────────────────────────────────────────────────────────
@@ -213,7 +280,14 @@ def _truncate_to_width(c: rl_canvas.Canvas, text: str, font: str, size: float, m
 
 
 def _draw_label(
-    c: rl_canvas.Canvas, x: float, y: float, w: float, h: float, data: LabelData, monochrome: bool = False
+    c: rl_canvas.Canvas,
+    x: float,
+    y: float,
+    w: float,
+    h: float,
+    data: LabelData,
+    monochrome: bool = False,
+    fields: frozenset[LabelField] = DEFAULT_LABEL_FIELDS,
 ) -> None:
     """Render one label inside the box (x, y, w, h). Origin is bottom-left.
 
@@ -244,9 +318,9 @@ def _draw_label(
     is_tight = h < 20 * mm
 
     if is_tight:
-        _draw_label_tight(c, x, y, w, h, inner_x, inner_y, inner_w, inner_h, pad, data, monochrome)
+        _draw_label_tight(c, x, y, w, h, inner_x, inner_y, inner_w, inner_h, pad, data, monochrome, fields)
     else:
-        _draw_label_roomy(c, x, y, w, h, inner_x, inner_y, inner_w, inner_h, pad, data, monochrome)
+        _draw_label_roomy(c, x, y, w, h, inner_x, inner_y, inner_w, inner_h, pad, data, monochrome, fields)
 
 
 def _draw_label_tight(
@@ -262,8 +336,12 @@ def _draw_label_tight(
     pad: float,
     data: LabelData,
     monochrome: bool = False,
+    fields: frozenset[LabelField] = DEFAULT_LABEL_FIELDS,
 ) -> None:
-    """Tight layout (h < 20 mm). Swatch + brand/material/hex/ID, no QR."""
+    """Tight layout (h < 20 mm). Swatch + brand/material/hex/ID, no QR.
+
+    Only those four lines fit, so they are the only ``fields`` it honours.
+    """
     # Monochrome: drop the colour swatch (see _draw_label_roomy) and give the
     # width to the text column (#1870).
     if monochrome:
@@ -283,14 +361,14 @@ def _draw_label_tight(
     # Top: brand — bumped to bold + larger per the #809 follow-up so it's the
     # easiest thing to read on a small AMS holder at arm's length.
     brand_size = 6.5
-    if data.brand:
+    if data.brand and "brand" in fields:
         c.setFont("Helvetica-Bold", brand_size)
         brand = _truncate_to_width(c, data.brand, "Helvetica-Bold", brand_size, text_w)
         c.drawString(text_x, y + h - pad - brand_size, brand)
 
     # Second line: material + subtype, small
     sub_size = 5
-    sub_line = " ".join(filter(None, [data.material, data.subtype]))
+    sub_line = " ".join(filter(None, [data.material, data.subtype])) if "material" in fields else ""
     sub_y_baseline = y + h - pad - brand_size - 0.6 - sub_size
     if sub_line:
         c.setFont("Helvetica", sub_size)
@@ -299,7 +377,7 @@ def _draw_label_tight(
 
     # Third line (when there's room): hex code, tiny — useful when the user
     # has multiple near-identical colours in the same material family.
-    hex_code = _hex_code_label(data.rgba)
+    hex_code = _hex_code_label(data.rgba) if "hex" in fields else ""
     if hex_code:
         hex_size = 4.5
         hex_y = sub_y_baseline - 0.4 - hex_size
@@ -309,6 +387,8 @@ def _draw_label_tight(
             c.drawString(text_x, hex_y, hex_code)
 
     # Bottom: BIG spool ID — the killer field at-a-glance.
+    if "spool_id" not in fields:
+        return
     id_size = 13
     c.setFont("Helvetica-Bold", id_size)
     id_text = _truncate_to_width(c, f"#{data.spool_id}", "Helvetica-Bold", id_size, text_w)
@@ -328,6 +408,7 @@ def _draw_label_roomy(
     pad: float,
     data: LabelData,
     monochrome: bool = False,
+    fields: frozenset[LabelField] = DEFAULT_LABEL_FIELDS,
 ) -> None:
     """Box-label / Avery layout. Swatch left, QR right, text middle."""
     # Swatch: full inner height, ~18% of inner width but capped so we never
@@ -342,73 +423,69 @@ def _draw_label_roomy(
         swatch_w = min(inner_w * 0.18, inner_h, 16 * mm)
         _draw_swatch(c, inner_x, inner_y, swatch_w, inner_h, data)
 
-    qr_size = _roomy_qr_size(inner_w, inner_h)
-    qr_x = x + w - pad - qr_size
-    qr_y = inner_y + (inner_h - qr_size) / 2
-    _draw_qr(c, qr_x, qr_y, qr_size, data.deeplink_url)
+    # Without the QR the text column runs to the right-hand padding.
+    if "qr" in fields:
+        qr_size = _roomy_qr_size(inner_w, inner_h)
+        qr_x = x + w - pad - qr_size
+        qr_y = inner_y + (inner_h - qr_size) / 2
+        _draw_qr(c, qr_x, qr_y, qr_size, data.deeplink_url)
+        text_right = qr_x - 1.5 * mm
+    else:
+        text_right = inner_x + inner_w
 
     text_x = inner_x + swatch_w + 1.5 * mm
-    text_w = qr_x - text_x - 1.5 * mm
+    text_w = text_right - text_x
     if text_w < 8 * mm:
         return
 
     c.setFillColor(black)
 
-    # Build the text rows we want to render, in top→bottom order.
-    line1 = data.brand or ""
-    line2 = " · ".join(filter(None, [data.material, data.subtype]))
+    # Spool ID — anchored at the bottom of the text column, big and bold. The
+    # lines above stop short of it rather than print over it.
+    id_size = 16
+    show_id = "spool_id" in fields
+    floor_y = inner_y + 0.5 + id_size if show_id else inner_y
+
     name = data.name or ""
-    hex_code = _hex_code_label(data.rgba)
+    # The name line is dropped when it only repeats the brand, or the subtype
+    # already printed beside the material (a Spoolman filament named after
+    # its colour yields both).
+    repeats = {data.brand or ""}
+    if "material" in fields and data.subtype:
+        repeats.add(data.subtype)
+    # (field, text, font, size, gap below). Sizes and gaps of the first five
+    # are what the label has always used; the rest share the location's.
+    rows: list[tuple[LabelField, str, str, float, float]] = [
+        # Brand — bumped to bold + larger per the #809 follow-up.
+        ("brand", data.brand or "", "Helvetica-Bold", 8, 1.2),
+        ("material", " · ".join(filter(None, [data.material, data.subtype])), "Helvetica", 7, 1.5),
+        # Hex colour code — useful for telling near-identical material+colour
+        # spools apart when the swatch is small or the user is colour-blind.
+        ("hex", _hex_code_label(data.rgba), "Helvetica", 6.5, 1.2),
+        ("name", name if name not in repeats else "", "Helvetica-Bold", 9, 1.2),
+        ("location", data.storage_location or "", "Helvetica-Oblique", 6.5, 1.2),
+        ("material_number", data.material_number or "", "Helvetica", 6.5, 1.2),
+        ("temps", _temps_text(data), "Helvetica", 6.5, 1.2),
+        ("weight", _weight_text(data), "Helvetica", 6.5, 1.2),
+        ("note", data.note or "", "Helvetica-Oblique", 6.5, 1.2),
+        ("added", data.added.isoformat() if data.added else "", "Helvetica", 6.5, 1.2),
+    ]
 
-    # Layout from the top of the text column.
     cursor_y = y + h - pad
-
-    # Brand — bumped to bold + larger per the #809 follow-up.
-    if line1:
-        size = 8
-        c.setFont("Helvetica-Bold", size)
-        text = _truncate_to_width(c, line1, "Helvetica-Bold", size, text_w)
-        cursor_y -= size
-        c.drawString(text_x, cursor_y, text)
-        cursor_y -= 1.2
-
-    if line2:
-        size = 7
-        c.setFont("Helvetica", size)
-        text = _truncate_to_width(c, line2, "Helvetica", size, text_w)
-        cursor_y -= size
-        c.drawString(text_x, cursor_y, text)
-        cursor_y -= 1.5
-
-    # Hex colour code — useful for telling near-identical material+colour
-    # spools apart when the swatch is small or the user is colour-blind.
-    if hex_code:
-        size = 6.5
-        c.setFont("Helvetica", size)
-        cursor_y -= size
-        c.drawString(text_x, cursor_y, hex_code)
-        cursor_y -= 1.2
-
-    if name and name != line1:
-        size = 9
-        c.setFont("Helvetica-Bold", size)
-        text = _truncate_to_width(c, name, "Helvetica-Bold", size, text_w)
-        cursor_y -= size
-        c.drawString(text_x, cursor_y, text)
-        cursor_y -= 1.2
-
-    if data.storage_location:
-        size = 6.5
-        c.setFont("Helvetica-Oblique", size)
-        text = _truncate_to_width(c, data.storage_location, "Helvetica-Oblique", size, text_w)
+    for field, text, font, size, gap in rows:
+        if field not in fields or not text:
+            continue
+        if cursor_y - size < floor_y:
+            break
+        c.setFont(font, size)
         cursor_y -= size
-        c.drawString(text_x, cursor_y, text)
+        c.drawString(text_x, cursor_y, _truncate_to_width(c, text, font, size, text_w))
+        cursor_y -= gap
 
-    # Spool ID — anchored at the bottom of the text column, big and bold.
-    id_size = 16
-    c.setFont("Helvetica-Bold", id_size)
-    id_text = _truncate_to_width(c, f"#{data.spool_id}", "Helvetica-Bold", id_size, text_w)
-    c.drawString(text_x, inner_y + 0.5, id_text)
+    if show_id:
+        c.setFont("Helvetica-Bold", id_size)
+        id_text = _truncate_to_width(c, f"#{data.spool_id}", "Helvetica-Bold", id_size, text_w)
+        c.drawString(text_x, inner_y + 0.5, id_text)
 
 
 # ── Template entry points ────────────────────────────────────────────────────
@@ -430,8 +507,13 @@ _SHEET_TEMPLATES: dict[str, tuple] = {
 }
 
 
-def _render_single_label_pdf(template: TemplateName, data_list: list[LabelData], monochrome: bool = False) -> bytes:
-    w_mm, h_mm = _SINGLE_LABEL_SIZES_MM[template]
+def _render_single_label_pdf(
+    template: TemplateName,
+    data_list: list[LabelData],
+    monochrome: bool = False,
+    fields: frozenset[LabelField] = DEFAULT_LABEL_FIELDS,
+) -> bytes:
+    w_mm, h_mm = label_size_mm(template)
     page_w, page_h = w_mm * mm, h_mm * mm
 
     buf = io.BytesIO()
@@ -439,13 +521,23 @@ def _render_single_label_pdf(template: TemplateName, data_list: list[LabelData],
     c.setTitle(f"Bambuddy spool labels ({template})")
 
     for data in data_list:
-        _draw_label(c, 0, 0, page_w, page_h, data, monochrome)
+        _draw_label(c, 0, 0, page_w, page_h, data, monochrome, fields)
         c.showPage()
 
     c.save()
     return buf.getvalue()
 
 
+def label_size_mm(template: TemplateName) -> tuple[float, float]:
+    """(width, height) of one label of ``template`` — a sheet's cell size."""
+    if template in _SINGLE_LABEL_SIZES_MM:
+        return _SINGLE_LABEL_SIZES_MM[template]
+    if template in _SHEET_TEMPLATES:
+        layout = _SHEET_TEMPLATES[template]
+        return layout[1], layout[2]
+    raise ValueError(f"Unknown label template: {template!r}")
+
+
 def get_sheet_capacity(template: TemplateName) -> int | None:
     """Return the number of slots on a sheet template, or ``None`` for roll labels."""
     layout = _SHEET_TEMPLATES.get(template)
@@ -459,6 +551,7 @@ def _render_sheet_pdf(
     data_list: list[LabelData],
     monochrome: bool,
     starting_position: int,
+    fields: frozenset[LabelField] = DEFAULT_LABEL_FIELDS,
 ) -> bytes:
     page_size, w_mm, h_mm, cols, rows, top_mm, left_mm, col_gap_mm, row_gap_mm = _SHEET_TEMPLATES[template]
     page_w, page_h = page_size
@@ -490,7 +583,7 @@ def _render_sheet_pdf(
             col = slot_index % cols
             x = left_margin + col * (label_w + col_gap)
             y = page_h - top_margin - (row + 1) * label_h - row * row_gap
-            _draw_label(c, x, y, label_w, label_h, data, monochrome)
+            _draw_label(c, x, y, label_w, label_h, data, monochrome, fields)
         c.showPage()
         data_index += len(chunk)
         page_number += 1
@@ -505,6 +598,7 @@ def render_labels(
     *,
     monochrome: bool = False,
     starting_position: int = 1,
+    fields: frozenset[LabelField] = DEFAULT_LABEL_FIELDS,
 ) -> bytes:
     """Render ``data_list`` to a PDF using the named template. Returns bytes.
 
@@ -517,16 +611,81 @@ def render_labels(
 
     ``starting_position`` is one-based and applies only to the first page of a
     sheet template. Later pages always begin at the first slot.
+
+    ``fields`` names the lines to print (#2981); see ``ALL_LABEL_FIELDS``.
     """
     if template in _SINGLE_LABEL_SIZES_MM:
         if starting_position != 1:
             raise ValueError("Starting position is only supported for sheet label templates")
-        return _render_single_label_pdf(template, data_list, monochrome)
+        return _render_single_label_pdf(template, data_list, monochrome, fields)
     if template in _SHEET_TEMPLATES:
-        return _render_sheet_pdf(template, data_list, monochrome, starting_position)
+        return _render_sheet_pdf(template, data_list, monochrome, starting_position, fields)
     raise ValueError(f"Unknown label template: {template!r}")
 
 
-__all__ = ["LabelData", "TemplateName", "get_sheet_capacity", "render_labels"]
+def render_label_preview_pdf(
+    template: TemplateName,
+    data: LabelData,
+    *,
+    monochrome: bool = False,
+    fields: frozenset[LabelField] = DEFAULT_LABEL_FIELDS,
+) -> bytes:
+    """One label of ``template`` on a page of its own size.
+
+    For a roll template that is the label as printed; for a sheet it is one
+    cell, which is all the preview needs to show.
+    """
+    w_mm, h_mm = label_size_mm(template)
+    page_w, page_h = w_mm * mm, h_mm * mm
+    buf = io.BytesIO()
+    c = rl_canvas.Canvas(buf, pagesize=(page_w, page_h))
+    _draw_label(c, 0, 0, page_w, page_h, data, monochrome, fields)
+    c.showPage()
+    c.save()
+    return buf.getvalue()
+
+
+def pdf_to_pngs(pdf: bytes, dpi: int) -> list[bytes]:
+    """Rasterise every page of ``pdf`` to a PNG at ``dpi``.
+
+    Images are drawn without smoothing: the QR is embedded as a bitmap, and
+    resampling it with interpolation greys the module edges, which is what
+    makes a small code unreadable on a 203 dpi thermal printer (#1870). The
+    PNG carries its dpi so label software prints it at the right size.
+    """
+    import pypdfium2 as pdfium
+
+    from backend.app.services.pdf_thumbnail import _PDFIUM_LOCK
+
+    pages: list[bytes] = []
+    with _PDFIUM_LOCK:
+        doc = pdfium.PdfDocument(pdf)
+        try:
+            for index in range(len(doc)):
+                page = doc[index]
+                try:
+                    image = page.render(scale=dpi / 72, no_smoothimage=True).to_pil()
+                finally:
+                    page.close()
+                out = io.BytesIO()
+                image.convert("RGB").save(out, "PNG", dpi=(dpi, dpi), optimize=True)
+                pages.append(out.getvalue())
+        finally:
+            doc.close()
+    return pages
+
+
+__all__ = [
+    "ALL_LABEL_FIELDS",
+    "DEFAULT_LABEL_FIELDS",
+    "LabelData",
+    "LabelField",
+    "TemplateName",
+    "get_sheet_capacity",
+    "label_size_mm",
+    "pdf_to_pngs",
+    "render_label_preview_pdf",
+    "render_labels",
+]
 # white re-exported for completeness; future templates may need a paper-tone variant.
 _ = white

+ 144 - 14
backend/app/services/ldap_service.py

@@ -14,7 +14,7 @@ import logging
 from dataclasses import dataclass
 
 from ldap3 import ALL, SUBTREE, Connection, Server, Tls
-from ldap3.core.exceptions import LDAPObjectClassError
+from ldap3.core.exceptions import LDAPException, LDAPObjectClassError, LDAPOperationResult
 
 logger = logging.getLogger(__name__)
 
@@ -102,6 +102,37 @@ def _create_server(config: LDAPConfig) -> Server:
     return Server(config.server_url, use_ssl=use_ssl, tls=tls, get_info=ALL, connect_timeout=10)
 
 
+class LDAPStartTLSRefusedError(Exception):
+    """The server answered the StartTLS request with an error result."""
+
+
+def _start_tls_if_configured(conn: Connection, config: LDAPConfig) -> None:
+    """Upgrade `conn` with StartTLS when the config asks for it.
+
+    A server that doesn't offer StartTLS (lldap, for one, only does LDAPS)
+    rejects the request with a bare "Unsupported extended operation" and the
+    StartTLS OID, which doesn't tell the admin what to change (#3197). Only a
+    rejection by the server is reworded; TLS handshake and certificate failures
+    keep their own messages.
+
+    ldap3 re-reads the server's info and schema straight after StartTLS by
+    default, still unauthenticated. A directory that refuses anonymous
+    searches (Active Directory, Samba AD) answers that read with
+    "operationsError", which failed every StartTLS connection to it and would
+    otherwise be reported as a refused StartTLS here. bind() reads the same
+    info once authenticated, so the early read is skipped.
+    """
+    if config.security != "starttls" or config.server_url.startswith("ldaps://"):
+        return
+    try:
+        conn.start_tls(read_server_info=False)
+    except LDAPOperationResult as e:
+        raise LDAPStartTLSRefusedError(
+            f"the server refused StartTLS ({e.description}). If it only offers LDAPS, "
+            "choose LDAPS and use its ldaps:// URL and port"
+        ) from e
+
+
 def _open_service_connection(config: LDAPConfig, server: Server, *, check_names: bool = True) -> Connection:
     """Open and bind a service-account LDAP connection. Raises on failure.
 
@@ -122,12 +153,107 @@ def _open_service_connection(config: LDAPConfig, server: Server, *, check_names:
         check_names=check_names,
     )
     conn.open()
-    if config.security == "starttls" and not config.server_url.startswith("ldaps://"):
-        conn.start_tls()
+    _start_tls_if_configured(conn, config)
     conn.bind()
     return conn
 
 
+# Group classes that list their members by DN, and the attribute each uses.
+_MEMBER_DN_GROUP_CLASSES = (("groupOfNames", "member"), ("groupOfUniqueNames", "uniqueMember"))
+
+
+def _schema_defines(names, name: str) -> bool | None:
+    """Whether a schema dict (attribute types or object classes) has `name`.
+
+    None when the server published no schema. ldap3 then skips its client-side
+    name checks, so the caller can request the name without it raising.
+    """
+    if not names:
+        return None
+    return name in names
+
+
+def _user_search_attributes(server: Server) -> list[str]:
+    """Attributes to request for a user entry: all user attributes plus memberOf.
+
+    `*` alone does not return memberOf on every directory. OpenLDAP's memberof
+    overlay makes it operational and lldap only returns it when asked by name
+    (#3197), so it has to be listed. But ldap3 rejects a requested name that the
+    server's schema doesn't define before anything is sent, even with
+    check_names off, so it is only listed when the schema has it (or publishes
+    no schema at all).
+    """
+    schema = server.schema
+    has_member_of = _schema_defines(schema.attribute_types if schema else None, "memberOf")
+    return ["*"] if has_member_of is False else ["*", "memberOf"]
+
+
+def _groups_base(server: Server, search_base: str) -> str:
+    """The naming context that contains `search_base`, else `search_base`.
+
+    Groups usually live beside the users rather than under them (ou=groups next
+    to ou=people), so a group search from the user search base finds nothing.
+    """
+    base_lower = search_base.lower()
+    contexts = server.info.naming_contexts if server.info and server.info.naming_contexts else []
+    for context in contexts:
+        context_lower = str(context).lower()
+        if base_lower == context_lower or base_lower.endswith("," + context_lower):
+            return str(context)
+    return search_base
+
+
+# Root DSE capability that Active Directory (and Samba AD) advertises.
+_ACTIVE_DIRECTORY_CAPABILITY = "1.2.840.113556.1.4.800"
+
+
+def _is_active_directory(server: Server) -> bool:
+    info = server.info
+    features = info.supported_features if info and info.supported_features else []
+    return any(feature[0] == _ACTIVE_DIRECTORY_CAPABILITY for feature in features)
+
+
+def _member_dn_groups(service_conn: Connection, config: LDAPConfig, user_dn: str) -> list[str]:
+    """Find groupOfNames / groupOfUniqueNames entries that list `user_dn` as a member.
+
+    memberOf alone is not enough outside Active Directory. Plain OpenLDAP has
+    none without the memberof overlay; with it, the overlay tracks only the
+    group class it was configured for (osixia's image: groupOfUniqueNames, so
+    groupOfNames groups are missing) and only groups changed after it was
+    loaded. The groups themselves are the authority, so they are asked too.
+
+    Active Directory is skipped: it keeps memberOf complete itself, and its
+    groups are objectClass=group, not either class searched here, so the
+    search would cost a subtree walk from the domain root and find nothing.
+
+    Only the classes the schema defines go into the filter, for the same
+    client-side check the POSIX lookup trips over.
+    """
+    schema = service_conn.server.schema
+    object_classes = schema.object_classes if schema else None
+    clauses = [
+        f"(&(objectClass={object_class})({attribute}={_ldap_escape(user_dn)}))"
+        for object_class, attribute in _MEMBER_DN_GROUP_CLASSES
+        if _schema_defines(object_classes, object_class) is not False
+    ]
+    if not clauses:
+        return []
+    search_filter = clauses[0] if len(clauses) == 1 else f"(|{''.join(clauses)})"
+    try:
+        service_conn.search(
+            search_base=_groups_base(service_conn.server, config.search_base),
+            search_filter=search_filter,
+            search_scope=SUBTREE,
+            attributes=["cn"],
+        )
+    except LDAPException as e:
+        # The exception text can carry the user's DN (PII, #2681), so only its
+        # type is logged. The user still logs in, with memberOf and POSIX groups.
+        logger.warning("LDAP group membership lookup failed (%s); mapping without it", type(e).__name__)
+        return []
+    return [str(entry.entry_dn) for entry in service_conn.entries]
+
+
 def _pick_canonical_username(entry, fallback: str) -> str:
     """Prefer sAMAccountName, then uid, then the supplied fallback."""
     if hasattr(entry, "sAMAccountName") and entry.sAMAccountName:
@@ -142,17 +268,23 @@ def _extract_user_info(
 ) -> LDAPUserInfo:
     """Build an LDAPUserInfo from an already-fetched directory entry.
 
-    Collects memberOf groups, POSIX memberUid groups, and the primary
-    gidNumber group; dedups DNs case-insensitively. Uses the supplied
-    service-bound connection to resolve POSIX groups.
+    Collects memberOf groups, the groupOfNames / groupOfUniqueNames entries
+    that list the user (except on Active Directory), POSIX
+    memberUid groups, and the primary gidNumber group; dedups DNs
+    case-insensitively. Uses the supplied service-bound connection for the
+    group searches. The entry must have been fetched with
+    `_user_search_attributes`, or memberOf may be missing from it.
     """
     email = str(user_entry.mail) if hasattr(user_entry, "mail") and user_entry.mail else None
     display_name = (
         str(user_entry.displayName) if hasattr(user_entry, "displayName") and user_entry.displayName else None
     )
 
-    # Collect groups from memberOf attribute (Active Directory / groupOfNames)
+    # Collect groups from the memberOf attribute (Active Directory, lldap,
+    # OpenLDAP with the memberof overlay, 389-DS), then ask the groups too.
     groups = [str(g) for g in user_entry.memberOf] if hasattr(user_entry, "memberOf") and user_entry.memberOf else []
+    if not _is_active_directory(service_conn.server):
+        groups.extend(_member_dn_groups(service_conn, config, str(user_entry.entry_dn)))
 
     canonical_username = _pick_canonical_username(user_entry, fallback_username)
 
@@ -202,7 +334,7 @@ def _extract_user_info(
         # error the operator can or should act on.
         logger.info(
             "Directory publishes no posixGroup object class; skipping POSIX group lookup "
-            "(memberOf groups are unaffected)"
+            "(memberOf and groupOfNames groups are unaffected)"
         )
 
     # Dedupe group DNs (user may be in a group via both memberUid and primary gidNumber).
@@ -250,7 +382,7 @@ def authenticate_ldap_user(config: LDAPConfig, username: str, password: str) ->
             search_base=config.search_base,
             search_filter=search_filter,
             search_scope=SUBTREE,
-            attributes=["*"],
+            attributes=_user_search_attributes(server),
         )
 
         if not service_conn.entries:
@@ -271,8 +403,7 @@ def authenticate_ldap_user(config: LDAPConfig, username: str, password: str) ->
                 read_only=True,
             )
             user_conn.open()
-            if config.security == "starttls" and not config.server_url.startswith("ldaps://"):
-                user_conn.start_tls()
+            _start_tls_if_configured(user_conn, config)
             user_conn.bind()
             user_conn.unbind()
         except Exception as e:
@@ -320,7 +451,7 @@ def lookup_ldap_user(config: LDAPConfig, username: str) -> LDAPUserInfo | None:
             search_base=config.search_base,
             search_filter=search_filter,
             search_scope=SUBTREE,
-            attributes=["*"],
+            attributes=_user_search_attributes(server),
         )
         if not service_conn.entries:
             logger.info("LDAP lookup: user not found: %s", username)
@@ -434,8 +565,7 @@ def test_ldap_connection(config: LDAPConfig) -> tuple[bool, str]:
             read_only=True,
         )
         conn.open()
-        if config.security == "starttls" and not config.server_url.startswith("ldaps://"):
-            conn.start_tls()
+        _start_tls_if_configured(conn, config)
         conn.bind()
 
         # Try a search to verify search base

+ 3 - 1
backend/app/services/library_trash.py

@@ -29,6 +29,7 @@ from backend.app.core.database import async_session
 from backend.app.models.library import LibraryFile
 from backend.app.models.print_queue import PrintQueueItem, PrintQueueVariant
 from backend.app.models.settings import Settings
+from backend.app.utils.library_paths import remove_library_photos_dir
 from backend.app.utils.local_time import utcnow_naive
 
 logger = logging.getLogger(__name__)
@@ -364,7 +365,7 @@ class LibraryTrashService:
 
     @staticmethod
     def _unlink_on_disk(row: LibraryFile) -> None:
-        """Best-effort cleanup of the file + thumbnail on disk."""
+        """Best-effort cleanup of the file, thumbnail and photos (#3077) on disk."""
         for rel in (row.file_path, row.thumbnail_path):
             abs_path = _to_absolute_path(rel)
             if abs_path is None:
@@ -374,6 +375,7 @@ class LibraryTrashService:
                     abs_path.unlink()
             except OSError as e:
                 logger.warning("Trash sweep: failed to unlink %s: %s", abs_path, e)
+        remove_library_photos_dir(row.id)
 
     # ---- User-facing trash ops ----------------------------------------
 

+ 4 - 2
backend/app/services/log_reader.py

@@ -184,9 +184,11 @@ def sanitize_log_content(content: str, sensitive_strings: dict[str, str] | None
     # Replace Bambu Lab printer serial numbers (format: 00M/01D/01S/01P/03W + alphanumeric, 12-16 chars total)
     content = re.sub(r"\b0[0-3][A-Z0-9][A-Z0-9]{9,13}\b", "[SERIAL]", content, flags=re.IGNORECASE)
 
-    # Replace IPv4 addresses (skip firmware versions like 01.09.01.00 which have leading zeros)
+    # Replace IPv4 addresses (skip firmware versions like 01.09.01.00 which have leading zeros,
+    # and four-number runs inside a longer dotted number, such as the LDAP StartTLS OID
+    # 1.3.6.1.4.1.1466.20037, whose masked "[IP].4.1.1466.20037" hid what failed, #3197)
     content = re.sub(
-        r"\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\b",
+        r"(?<!\d\.)\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\b(?!\.\d)",
         "[IP]",
         content,
     )

+ 75 - 0
backend/app/services/material_number.py

@@ -0,0 +1,75 @@
+"""Material-number inheritance for newly created spools (#2870).
+
+The material number is the internal purchasing/article identifier a business
+costs by (e.g. "15" = Bambu Lab PLA Basic). All spools of the same product
+share it, so a new spool of an already-numbered product should arrive with
+the number filled instead of blank — whether it is created manually, via the
+API, or by the RFID auto-add.
+"""
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.models.spool import Spool
+
+
+async def find_material_number_for_product(
+    db: AsyncSession,
+    *,
+    material: str | None,
+    subtype: str | None,
+    brand: str | None,
+    color_name: str | None,
+) -> str | None:
+    """Return the material number an existing spool of this product carries.
+
+    Product identity is the (material, subtype, brand, color_name) string
+    tuple — the same key FilamentSkuSettings groups by. The most recently
+    updated match wins, newest row on a tie. `updated_at` moves on every
+    write, usage included, so this is "most recently touched", not "most
+    recently numbered": when a product's spools disagree, bulk-edit them to
+    one number rather than relying on which one wins. Archived spools count:
+    a product being out of stock doesn't change its number.
+    """
+    if not material:
+        return None
+
+    def _same(column, value):
+        return column.is_(None) if value is None else column == value
+
+    result = await db.execute(
+        select(Spool.material_number)
+        .where(
+            # Normalised to NULL-or-non-empty by the schema validator, so
+            # NULL is the only "unset" state left to exclude.
+            Spool.material_number.is_not(None),
+            Spool.material == material,
+            _same(Spool.subtype, subtype),
+            _same(Spool.brand, brand),
+            _same(Spool.color_name, color_name),
+        )
+        .order_by(Spool.updated_at.desc(), Spool.id.desc())
+        .limit(1)
+    )
+    return result.scalars().first()
+
+
+async def apply_material_number_inheritance(db: AsyncSession, payload: dict) -> dict:
+    """Fill ``payload["material_number"]`` from a matching existing spool.
+
+    No-op when the caller already supplied a non-empty number. Only used on
+    the create paths — editing a spool never overwrites what the user set.
+    """
+    if payload.get("material_number"):
+        return payload
+    number = await find_material_number_for_product(
+        db,
+        material=payload.get("material"),
+        subtype=payload.get("subtype"),
+        brand=payload.get("brand"),
+        color_name=payload.get("color_name"),
+    )
+    if number:
+        payload = dict(payload)
+        payload["material_number"] = number
+    return payload

+ 361 - 0
backend/app/services/mesh_combine.py

@@ -0,0 +1,361 @@
+"""Combine several STL files into one multi-object 3MF.
+
+The slicer sidecar takes exactly one model file per slice, so slicing several
+separate STLs onto one plate means building that one file first. A plain 3MF
+(core spec, no Bambu/Orca ``Metadata/`` entries) is enough: both CLIs load it
+as a project with one object per build item, and ``--arrange`` lays them out
+on the target bed.
+
+Each source mesh is written once as a 3MF ``<object>`` and every copy of it is
+a ``<build><item>`` pointing at that object with its own transform, so ten
+copies of a 5 MB STL cost 5 MB, not 50. trimesh's own 3MF exporter duplicates
+the mesh per scene node, which is why this writes the XML directly, streamed
+into the zip entry so the model never exists as one string in memory.
+
+Copies are pre-placed on a simple shelf grid with a gap between footprints.
+The slicer re-arranges them anyway when arrange is on, but a grid means the
+file also opens sensibly in a desktop slicer and renders a readable
+thumbnail, rather than every object stacked on the origin.
+
+The preview is rendered here, from the meshes already in memory, and embedded
+as ``Metadata/thumbnail.png``. Loading the finished 3MF back to render it would
+expand every build item into its own copy of the mesh: 100 copies of a
+327k-face STL peaked at 13.6 GB that way. Here each source is simplified once,
+to a share of a fixed face budget, before its copies are placed.
+"""
+
+from __future__ import annotations
+
+import io
+import logging
+import math
+import re
+import zipfile
+from dataclasses import dataclass
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+# Hard cap on build items (sum of copies). Every item is a full object for the
+# slicer to arrange, support and slice; past this a single plate cannot hold
+# them anyway and the request is more likely a typo than a real print.
+MAX_COMBINE_INSTANCES = 100
+
+# Caps on the sources themselves, so a request of many large STLs can't hold
+# the whole lot in memory. Bytes are checked from the file sizes before
+# anything is loaded; faces after each load, so the request stops at the first
+# model that crosses the line. A typical printable STL is 1 to 20 MB; a binary
+# STL spends 50 bytes per face, so the two caps sit close to each other.
+MAX_COMBINE_SOURCE_BYTES = 300 * 1024 * 1024
+MAX_COMBINE_SOURCE_FACES = 5_000_000
+
+# Faces drawn in the embedded preview, shared across every copy on the plate.
+# Each source is simplified once to its per-copy share; the floor keeps a
+# 100-copy plate recognisable (100 x 1500 faces stays inside the budget).
+THUMBNAIL_FACE_BUDGET = 200_000
+THUMBNAIL_MIN_FACES_PER_COPY = 1_500
+THUMBNAIL_PATH = "Metadata/thumbnail.png"
+
+# Space between neighbouring footprints in the pre-placed grid, in mm.
+LAYOUT_GAP_MM = 5.0
+
+# Rows per chunk written to the model entry.
+_XML_CHUNK_ROWS = 20_000
+
+_CORE_NS = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
+
+_CONTENT_TYPES_XML = (
+    '<?xml version="1.0" encoding="UTF-8"?>\n'
+    '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
+    '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
+    '<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>'
+    '<Default Extension="png" ContentType="image/png"/>'
+    "</Types>"
+)
+
+_MODEL_REL = (
+    '<Relationship Target="/3D/3dmodel.model" Id="rel0" '
+    'Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>'
+)
+# The OPC package thumbnail relationship: how a 3MF names its preview image.
+_THUMBNAIL_REL = (
+    f'<Relationship Target="/{THUMBNAIL_PATH}" Id="rel1" '
+    'Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"/>'
+)
+
+
+def _rels_xml(with_thumbnail: bool) -> str:
+    return (
+        '<?xml version="1.0" encoding="UTF-8"?>\n'
+        '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
+        + _MODEL_REL
+        + (_THUMBNAIL_REL if with_thumbnail else "")
+        + "</Relationships>"
+    )
+
+
+class MeshCombineError(ValueError):
+    """A source could not be combined (unreadable, empty, or over the limits).
+
+    The message is user-facing: the route returns it as the 400 detail.
+    """
+
+
+@dataclass(frozen=True)
+class CombinePart:
+    """One source model and how many copies of it go on the plate."""
+
+    name: str
+    path: Path
+    copies: int = 1
+
+
+@dataclass(frozen=True)
+class _LoadedPart:
+    name: str
+    vertices: object  # numpy (N, 3) float array, footprint min at (0, 0), z min at 0
+    faces: object  # numpy (M, 3) int array
+    width: float
+    depth: float
+    copies: int
+
+
+def _object_name(filename: str) -> str:
+    """Object name shown in the slicer's object list: the filename sans extension."""
+    stem = Path(filename).stem or "object"
+    # Control characters are not valid in XML attribute values.
+    return re.sub(r"[\x00-\x1f]", "", stem)[:200] or "object"
+
+
+def _load_part(part: CombinePart) -> _LoadedPart:
+    import trimesh
+
+    try:
+        mesh = trimesh.load(str(part.path), force="mesh")
+    except Exception as exc:
+        raise MeshCombineError(f"Could not read {part.name}: {exc}") from exc
+    if mesh is None or not hasattr(mesh, "vertices") or len(mesh.vertices) == 0 or len(mesh.faces) == 0:
+        raise MeshCombineError(f"{part.name} contains no printable geometry")
+
+    vertices = mesh.vertices.copy()
+    lo = vertices.min(axis=0)
+    hi = vertices.max(axis=0)
+    # Normalise so the footprint starts at the origin and the model sits on
+    # the bed; the layout below then only has to translate in X/Y.
+    vertices -= lo
+    return _LoadedPart(
+        name=_object_name(part.name),
+        vertices=vertices,
+        faces=mesh.faces,
+        width=float(hi[0] - lo[0]),
+        depth=float(hi[1] - lo[1]),
+        copies=part.copies,
+    )
+
+
+def _check_source_bytes(parts: list[CombinePart]) -> None:
+    total = 0
+    for part in parts:
+        try:
+            total += part.path.stat().st_size
+        except OSError as exc:
+            raise MeshCombineError(f"Could not read {part.name}: {exc}") from exc
+    if total > MAX_COMBINE_SOURCE_BYTES:
+        raise MeshCombineError(
+            f"The selected models are too large to combine: {total / 1024**2:.0f} MB, "
+            f"at most {MAX_COMBINE_SOURCE_BYTES // 1024**2} MB in total"
+        )
+
+
+def _load_parts(parts: list[CombinePart]) -> list[_LoadedPart]:
+    loaded: list[_LoadedPart] = []
+    faces = 0
+    for part in parts:
+        item = _load_part(part)
+        faces += len(item.faces)
+        if faces > MAX_COMBINE_SOURCE_FACES:
+            raise MeshCombineError(
+                f"The selected models are too detailed to combine: more than "
+                f"{MAX_COMBINE_SOURCE_FACES:,} triangles in total"
+            )
+        loaded.append(item)
+    return loaded
+
+
+def layout_offsets(footprints: list[tuple[float, float]], gap: float = LAYOUT_GAP_MM) -> list[tuple[float, float]]:
+    """Shelf-pack footprints ``(width, depth)``; return each one's min-corner offset.
+
+    Rows are filled left to right up to a width that keeps the overall layout
+    roughly square, tallest footprints first so rows don't waste depth. The
+    result is centred on the origin. Offsets come back in input order.
+    """
+    if not footprints:
+        return []
+    area = sum((w + gap) * (d + gap) for w, d in footprints)
+    row_limit = max(max(w for w, _ in footprints), math.sqrt(area))
+
+    order = sorted(range(len(footprints)), key=lambda i: footprints[i][1], reverse=True)
+    offsets: list[tuple[float, float]] = [(0.0, 0.0)] * len(footprints)
+    x = y = row_depth = 0.0
+    total_w = 0.0
+    for i in order:
+        w, d = footprints[i]
+        if x > 0 and x + w > row_limit:
+            y += row_depth + gap
+            x = row_depth = 0.0
+        offsets[i] = (x, y)
+        total_w = max(total_w, x + w)
+        x += w + gap
+        row_depth = max(row_depth, d)
+    total_d = y + row_depth
+
+    cx, cy = total_w / 2, total_d / 2
+    return [(ox - cx, oy - cy) for ox, oy in offsets]
+
+
+def thumbnail_faces_per_copy(total_copies: int) -> int:
+    """Face budget for one copy in the preview."""
+    return max(THUMBNAIL_MIN_FACES_PER_COPY, THUMBNAIL_FACE_BUDGET // max(total_copies, 1))
+
+
+def _preview_mesh(loaded: list[_LoadedPart], owners: list[int], offsets: list[tuple[float, float]]):
+    """The plate as one mesh for the thumbnail, within the face budget.
+
+    Each source is simplified once, before its copies are placed, so the
+    work and the memory scale with the budget rather than with copies times
+    source faces.
+    """
+    import numpy as np
+    import trimesh
+
+    per_copy = thumbnail_faces_per_copy(len(owners))
+    reduced = []
+    for part in loaded:
+        mesh = trimesh.Trimesh(vertices=part.vertices, faces=part.faces, process=False)
+        if len(mesh.faces) > per_copy:
+            try:
+                mesh = mesh.simplify_quadric_decimation(face_count=per_copy)
+            except Exception as exc:
+                # Without simplification the budget can't be held, so no preview.
+                logger.warning("Thumbnail skipped: could not simplify %s: %s", part.name, exc)
+                return None
+        reduced.append(mesh)
+
+    vertex_blocks = []
+    face_blocks = []
+    base = 0
+    for owner, (ox, oy) in zip(owners, offsets, strict=True):
+        mesh = reduced[owner]
+        vertex_blocks.append(mesh.vertices + np.array([ox, oy, 0.0]))
+        face_blocks.append(mesh.faces + base)
+        base += len(mesh.vertices)
+    return trimesh.Trimesh(vertices=np.vstack(vertex_blocks), faces=np.vstack(face_blocks), process=False)
+
+
+def _render_thumbnail(loaded: list[_LoadedPart], owners: list[int], offsets: list[tuple[float, float]]) -> bytes | None:
+    """PNG preview of the plate, or None. Never fails the combine."""
+    from backend.app.services.stl_thumbnail import render_mesh_png
+
+    try:
+        mesh = _preview_mesh(loaded, owners, offsets)
+        if mesh is None:
+            return None
+        return render_mesh_png(mesh, label="combined 3MF")
+    except Exception as exc:
+        logger.warning("Could not render the combined 3MF thumbnail: %s", exc, exc_info=True)
+        return None
+
+
+def _fmt(value: float) -> str:
+    # 6 significant decimals is well below slicer resolution and keeps the
+    # XML compact; strip trailing zeros so "10.000000" becomes "10".
+    text = f"{value:.6f}".rstrip("0").rstrip(".")
+    return "0" if text in ("", "-0") else text
+
+
+def _attr(value: str) -> str:
+    return value.replace("&", "&amp;").replace('"', "&quot;").replace("<", "&lt;").replace(">", "&gt;")
+
+
+def _write_rows(out, rows, render) -> None:
+    """Write ``rows`` (a numpy array) through ``render`` in fixed-size chunks."""
+    for start in range(0, len(rows), _XML_CHUNK_ROWS):
+        chunk = rows[start : start + _XML_CHUNK_ROWS].tolist()
+        out.write("".join(render(*row) for row in chunk).encode("utf-8"))
+
+
+def _write_model(out, loaded: list[_LoadedPart], owners: list[int], offsets: list[tuple[float, float]]) -> None:
+    """Stream the 3MF model XML into the open zip entry ``out``."""
+    out.write(
+        (
+            '<?xml version="1.0" encoding="UTF-8"?>\n'
+            f'<model unit="millimeter" xml:lang="en-US" xmlns="{_CORE_NS}">'
+            '<metadata name="Application">Bambuddy</metadata>'
+            "<resources>"
+        ).encode()
+    )
+    for idx, part in enumerate(loaded):
+        out.write(f'<object id="{idx + 1}" name="{_attr(part.name)}" type="model"><mesh><vertices>'.encode())
+        _write_rows(out, part.vertices, lambda x, y, z: f'<vertex x="{_fmt(x)}" y="{_fmt(y)}" z="{_fmt(z)}"/>')
+        out.write(b"</vertices><triangles>")
+        _write_rows(out, part.faces, lambda a, b, c: f'<triangle v1="{a}" v2="{b}" v3="{c}"/>')
+        out.write(b"</triangles></mesh></object>")
+    out.write(b"</resources><build>")
+    out.write(
+        "".join(
+            f'<item objectid="{owner + 1}" transform="1 0 0 0 1 0 0 0 1 {_fmt(ox)} {_fmt(oy)} 0"/>'
+            for owner, (ox, oy) in zip(owners, offsets, strict=True)
+        ).encode("utf-8")
+    )
+    out.write(b"</build></model>")
+
+
+def combine_parts_to_3mf(parts: list[CombinePart]) -> bytes:
+    """Build a plain multi-object 3MF from ``parts``; return the zip bytes.
+
+    Blocking (mesh parsing, the preview render and XML writing are CPU-bound):
+    call it through ``asyncio.to_thread`` from request handlers.
+
+    Raises:
+        MeshCombineError: no parts, a copy count outside 1..MAX, too many
+            instances in total, sources over the size or face caps, or a
+            source that can't be read as a mesh.
+    """
+    if not parts:
+        raise MeshCombineError("Select at least one model to combine")
+    for part in parts:
+        if part.copies < 1:
+            raise MeshCombineError(f"{part.name}: copies must be at least 1")
+    total = sum(p.copies for p in parts)
+    if total > MAX_COMBINE_INSTANCES:
+        raise MeshCombineError(f"Too many objects: {total} requested, at most {MAX_COMBINE_INSTANCES} per plate")
+
+    _check_source_bytes(parts)
+    loaded = _load_parts(parts)
+
+    footprints: list[tuple[float, float]] = []
+    owners: list[int] = []
+    for idx, part in enumerate(loaded):
+        for _ in range(part.copies):
+            footprints.append((part.width, part.depth))
+            owners.append(idx)
+    offsets = layout_offsets(footprints)
+
+    thumbnail = _render_thumbnail(loaded, owners, offsets)
+
+    buf = io.BytesIO()
+    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
+        zf.writestr("[Content_Types].xml", _CONTENT_TYPES_XML)
+        zf.writestr("_rels/.rels", _rels_xml(with_thumbnail=thumbnail is not None))
+        with zf.open("3D/3dmodel.model", "w", force_zip64=True) as out:
+            _write_model(out, loaded, owners, offsets)
+        if thumbnail is not None:
+            zf.writestr(THUMBNAIL_PATH, thumbnail)
+    logger.info(
+        "Combined %d model(s) into %d object(s) on one plate (%d bytes, thumbnail %s)",
+        len(loaded),
+        total,
+        buf.tell(),
+        "embedded" if thumbnail is not None else "skipped",
+    )
+    return buf.getvalue()

+ 620 - 67
backend/app/services/notification_service.py

@@ -17,8 +17,16 @@ import httpx
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 
-from backend.app.models.notification import NotificationDigestQueue, NotificationLog, NotificationProvider
+from backend.app.models.archive import PrintArchive
+from backend.app.models.notification import (
+    NotificationDigestQueue,
+    NotificationLog,
+    NotificationProvider,
+    TelegramPendingVerdict,
+)
 from backend.app.models.notification_template import NotificationTemplate
+from backend.app.services.print_confirmation import one_tap_url
+from backend.app.utils.notification_photos import save_notification_photo
 
 logger = logging.getLogger(__name__)
 
@@ -29,6 +37,23 @@ logger = logging.getLogger(__name__)
 # bot signature for upstream WAFs.
 _USER_AGENT = "Bambuddy/1.0 (+https://github.com/maziggy/bambuddy)"
 
+# Appended to a Telegram outcome prompt in reaction mode (#3046); the message
+# itself carries no buttons there, so the user needs telling how to answer.
+TELEGRAM_REACTION_HINT = "React with \U0001f44d or \U0001f44e to record the outcome."
+
+
+def telegram_markdown_escape(message: str) -> str:
+    """Escape underscores in the message body so Telegram Markdown parsing
+    doesn't break on job names like "A1_plate_8" or error codes like
+    "0300_0001". The title is already wrapped in *bold* markers, so only
+    escape after the first newline. Shared with the reaction poller, which
+    re-sends the stored body when it edits the prompt (#3046)."""
+    if "\n" in message:
+        title_part, body_part = message.split("\n", 1)
+        body_part = body_part.replace("_", "\\_")
+        return f"{title_part}\n{body_part}"
+    return message
+
 
 def _looks_like_cloudflare_challenge(response: httpx.Response) -> bool:
     """Return True if ``response`` looks like a Cloudflare mitigation
@@ -113,6 +138,41 @@ def _opaque_http_failure(response: httpx.Response, *, label: str) -> str:
     return f"HTTP {response.status_code} from the configured {label} (see server logs at debug level for details)"
 
 
+_sample_notification_image: bytes | None = None
+_sample_notification_image_loaded = False
+
+
+def _load_sample_notification_image() -> bytes | None:
+    """Render the bundled app screenshot as a small JPEG.
+
+    Stands in for a real camera snapshot on Test Configuration / test-all,
+    since there's no live print to capture one from. Cached after the first
+    call so we're not re-reading and resizing the file on every click.
+    """
+    global _sample_notification_image, _sample_notification_image_loaded
+    if _sample_notification_image_loaded:
+        return _sample_notification_image
+    _sample_notification_image_loaded = True
+    try:
+        from io import BytesIO
+
+        from PIL import Image
+
+        from backend.app.core.config import settings
+
+        source_path = settings.static_dir / "img" / "screenshot-desktop.png"
+        with Image.open(source_path) as img:
+            resized = img.convert("RGB")
+            resized.thumbnail((640, 640))
+            buffer = BytesIO()
+            resized.save(buffer, format="JPEG", quality=85)
+            _sample_notification_image = buffer.getvalue()
+    except Exception as e:
+        logger.warning("Failed to build sample notification image: %s", e)
+        _sample_notification_image = None
+    return _sample_notification_image
+
+
 class NotificationService:
     """Service for sending notifications through various providers."""
 
@@ -264,34 +324,51 @@ class NotificationService:
         return title, body
 
     async def send_test_notification(
-        self, provider_type: str, config: dict[str, Any], db: AsyncSession | None = None
+        self,
+        provider_type: str,
+        config: dict[str, Any],
+        db: AsyncSession | None = None,
+        attach_photo: bool = True,
     ) -> tuple[bool, str]:
-        """Send a test notification to verify configuration."""
+        """Send a test notification to verify configuration.
+
+        attach_photo mirrors the provider's own toggle (see _send_to_provider)
+        — when set, a bundled sample image stands in for a real snapshot.
+        """
         if db:
             title, message = await self._build_message_from_template(db, "test", {})
         else:
             title = "Bambuddy Test"
             message = "This is a test notification. If you see this, notifications are working!"
 
+        image_data = await asyncio.to_thread(_load_sample_notification_image) if attach_photo else None
+
         try:
             if provider_type == "callmebot":
                 return await self._send_callmebot(config, f"{title}\n{message}")
             elif provider_type == "ntfy":
-                return await self._send_ntfy(config, title, message)
+                return await self._send_ntfy(config, title, message, image_data=image_data)
             elif provider_type == "pushover":
-                return await self._send_pushover(config, title, message)
+                return await self._send_pushover(config, title, message, image_data=image_data)
             elif provider_type == "telegram":
-                return await self._send_telegram(config, f"*{title}*\n{message}")
+                return await self._send_telegram(config, f"*{title}*\n{message}", image_data=image_data)
             elif provider_type == "email":
-                return await self._send_email(config, title, message)
+                return await self._send_email(config, title, message, image_data=image_data)
             elif provider_type == "discord":
-                return await self._send_discord(config, title, message)
+                return await self._send_discord(config, title, message, image_data=image_data)
             elif provider_type == "webhook":
-                return await self._send_webhook(config, title, message)
+                photo_url = None
+                if attach_photo and (config.get("payload_format") or "generic").strip() == "slack":
+                    photo_url = await self._get_or_build_photo_url(db, image_data, "test")
+                return await self._send_webhook(
+                    config, title, message, image_data=image_data, event_type="test", image_url=photo_url
+                )
             elif provider_type == "homeassistant":
-                return await self._send_homeassistant(config, title, message, db=db)
+                photo_url = await self._get_or_build_photo_url(db, image_data, "test") if attach_photo else None
+                return await self._send_homeassistant(config, title, message, db=db, image_url=photo_url)
             elif provider_type == "bark":
-                return await self._send_bark(config, title, message)
+                photo_url = await self._get_or_build_photo_url(db, image_data, "test") if attach_photo else None
+                return await self._send_bark(config, title, message, image_url=photo_url)
             else:
                 return False, f"Unknown provider type: {provider_type}"
         except Exception as e:
@@ -318,11 +395,15 @@ class NotificationService:
         else:
             return False, f"HTTP {response.status_code}: {response.text[:200]}"
 
-    async def _send_bark(self, config: dict, title: str, message: str) -> tuple[bool, str]:
+    async def _send_bark(
+        self, config: dict, title: str, message: str, url: str | None = None, image_url: str | None = None
+    ) -> tuple[bool, str]:
         """Send notification via Bark, the self-hostable iOS push service (#1495).
 
         POSTs JSON to {server}/push. Defaults to the official api.day.app
         relay; a self-hosted bark-server works by overriding the server URL.
+        ``url`` opens on tap — the outcome confirmation (#1898) deep-links
+        into the archive's confirmation dialog with it.
         """
         server = (config.get("server") or "https://api.day.app").strip().rstrip("/")
         device_key = (config.get("device_key") or "").strip()
@@ -348,6 +429,12 @@ class NotificationService:
         level = (config.get("level") or "").strip()
         if level in ("active", "timeSensitive", "critical", "passive"):
             payload["level"] = level
+        if url:
+            payload["url"] = url
+        if image_url:
+            # Closest thing Bark has to a photo attachment — shows as a
+            # round icon on iOS.
+            payload["icon"] = image_url
 
         client = await self._get_client()
         response = await client.post(f"{server}/push", json=payload)
@@ -375,8 +462,14 @@ class NotificationService:
         message: str,
         image_data: bytes | None = None,
         event_type: str | None = None,
+        actions: str | None = None,
     ) -> tuple[bool, str]:
-        """Send notification via ntfy."""
+        """Send notification via ntfy.
+
+        ``actions`` is a pre-built value for ntfy's Actions header (simple
+        format), used by the outcome-confirmation event (#1898) to put
+        one-tap Good/Reject buttons directly into the push notification.
+        """
         server = config.get("server", "https://ntfy.sh").rstrip("/")
         topic = config.get("topic", "").strip()
         auth_token = config.get("auth_token", "").strip()
@@ -420,6 +513,9 @@ class NotificationService:
         if auth_token:
             headers["Authorization"] = f"Bearer {auth_token}"
 
+        if actions:
+            headers["Actions"] = actions
+
         client = await self._get_client()
 
         if image_data:
@@ -452,7 +548,13 @@ class NotificationService:
         return False, _opaque_http_failure(response, label="ntfy server")
 
     async def _send_pushover(
-        self, config: dict, title: str, message: str, image_data: bytes | None = None
+        self,
+        config: dict,
+        title: str,
+        message: str,
+        image_data: bytes | None = None,
+        url: str | None = None,
+        url_title: str | None = None,
     ) -> tuple[bool, str]:
         """Send notification via Pushover.
 
@@ -461,6 +563,8 @@ class NotificationService:
             title: Notification title
             message: Notification body
             image_data: Optional JPEG image bytes to attach (max 2.5MB)
+            url: Optional supplementary URL shown under the message
+            url_title: Optional label for that URL
         """
         user_key = config.get("user_key", "").strip()
         app_token = config.get("app_token", "").strip()
@@ -472,7 +576,7 @@ class NotificationService:
         if not user_key or not app_token:
             return False, "User key and app token are required"
 
-        url = "https://api.pushover.net/1/messages.json"
+        api_url = "https://api.pushover.net/1/messages.json"
         data = {
             "token": app_token,
             "user": user_key,
@@ -480,6 +584,10 @@ class NotificationService:
             "message": message,
             "priority": priority,
         }
+        if url:
+            data["url"] = url
+            if url_title:
+                data["url_title"] = url_title
 
         # Emergency priority (2) keeps re-alerting until acknowledged, so
         # Pushover *requires* retry (how often, >= 30s) and expire (when to
@@ -502,9 +610,9 @@ class NotificationService:
         if image_data:
             # Pushover supports image attachments via multipart form-data
             files = {"attachment": ("photo.jpg", image_data, "image/jpeg")}
-            response = await client.post(url, data=data, files=files)
+            response = await client.post(api_url, data=data, files=files)
         else:
-            response = await client.post(url, data=data)
+            response = await client.post(api_url, data=data)
 
         if response.status_code == 200:
             return True, "Message sent successfully"
@@ -516,13 +624,45 @@ class NotificationService:
             except Exception:
                 return False, f"HTTP {response.status_code}: {response.text[:200]}"
 
-    async def _send_telegram(self, config: dict, message: str, image_data: bytes | None = None) -> tuple[bool, str]:
-        """Send notification via Telegram bot."""
+    async def _send_telegram(
+        self,
+        config: dict,
+        message: str,
+        image_data: bytes | None = None,
+        buttons: list[dict] | None = None,
+        link_preview: bool = True,
+    ) -> tuple[bool, str]:
+        """Send notification via Telegram bot.
+
+        ``buttons`` is one row of inline URL buttons (``{"text", "url"}``
+        entries), used by the outcome-confirmation event (#1898) to put
+        one-tap Good/Reject under the message. ``link_preview=False`` asks
+        Telegram not to fetch the first URL in the text for a preview card.
+        """
+        ok, status, _ = await self._send_telegram_message(
+            config, message, image_data=image_data, buttons=buttons, link_preview=link_preview
+        )
+        return ok, status
+
+    async def _send_telegram_message(
+        self,
+        config: dict,
+        message: str,
+        image_data: bytes | None = None,
+        buttons: list[dict] | None = None,
+        link_preview: bool = True,
+    ) -> tuple[bool, str, dict | None]:
+        """Send via Telegram and also hand back the Bot API ``result`` (the sent Message).
+
+        The outcome confirmation in reaction mode (#3046) needs the
+        ``message_id`` from it to recognise the reaction later; every other
+        caller goes through ``_send_telegram`` and ignores it.
+        """
         bot_token = config.get("bot_token", "").strip()
         chat_id = config.get("chat_id", "").strip()
 
         if not bot_token or not chat_id:
-            return False, "Bot token and chat ID are required"
+            return False, "Bot token and chat ID are required", None
 
         # Optional forum topic (#1518).  Telegram expects message_thread_id as an
         # integer in the JSON sendMessage body — a string 400s there even though
@@ -535,49 +675,139 @@ class NotificationService:
             try:
                 message_thread_id = int(thread_id_raw)
             except ValueError:
-                return False, f"Invalid message thread ID: {thread_id_raw!r} is not a number"
+                return False, f"Invalid message thread ID: {thread_id_raw!r} is not a number", None
 
-        # Escape underscores in the message body so Telegram Markdown
-        # parsing doesn't break on job names like "A1_plate_8" or error
-        # codes like "0300_0001".  The title is already wrapped in *bold*
-        # markers, so only escape after the first newline.
-        if "\n" in message:
-            title_part, body_part = message.split("\n", 1)
-            body_part = body_part.replace("_", "\\_")
-            message = f"{title_part}\n{body_part}"
+        message = telegram_markdown_escape(message)
 
         client = await self._get_client()
 
-        if image_data:
-            # Use sendPhoto to attach the thumbnail with the caption
-            url = f"https://api.telegram.org/bot{bot_token}/sendPhoto"
-            form: dict[str, Any] = {"chat_id": chat_id, "caption": message, "parse_mode": "Markdown"}
-            if message_thread_id is not None:
-                form["message_thread_id"] = message_thread_id
-            response = await client.post(
-                url,
-                data=form,
-                files={"photo": ("photo.jpg", image_data, "image/jpeg")},
-            )
-        else:
+        async def _post(with_buttons: bool):
+            if image_data:
+                # Use sendPhoto to attach the thumbnail with the caption
+                url = f"https://api.telegram.org/bot{bot_token}/sendPhoto"
+                form: dict[str, Any] = {"chat_id": chat_id, "caption": message, "parse_mode": "Markdown"}
+                if message_thread_id is not None:
+                    form["message_thread_id"] = message_thread_id
+                if with_buttons:
+                    # Multipart form fields are strings — reply_markup goes JSON-encoded.
+                    form["reply_markup"] = json.dumps({"inline_keyboard": [buttons]})
+                return await client.post(
+                    url,
+                    data=form,
+                    files={"photo": ("photo.jpg", image_data, "image/jpeg")},
+                )
+
             url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
-            data: dict[str, Any] = {
+            payload: dict[str, Any] = {
                 "chat_id": chat_id,
                 "text": message,
                 "parse_mode": "Markdown",
             }
+            if not link_preview:
+                payload["disable_web_page_preview"] = True
             if message_thread_id is not None:
-                data["message_thread_id"] = message_thread_id
-            response = await client.post(url, json=data)
-
-        if response.status_code == 200:
-            result = response.json()
+                payload["message_thread_id"] = message_thread_id
+            if with_buttons:
+                payload["reply_markup"] = {"inline_keyboard": [buttons]}
+            return await client.post(url, json=payload)
+
+        def _failure(resp) -> str | None:
+            """What Telegram objected to, or None when the send went through."""
+            if resp.status_code != 200:
+                return f"HTTP {resp.status_code}: {resp.text[:200]}"
+            result = resp.json()
             if result.get("ok"):
-                return True, "Message sent successfully"
-            else:
-                return False, f"Telegram error: {result.get('description', 'Unknown error')}"
-        else:
-            return False, f"HTTP {response.status_code}: {response.text[:200]}"
+                return None
+            return f"Telegram error: {result.get('description', 'Unknown error')}"
+
+        response = await _post(bool(buttons))
+        failure = _failure(response)
+        if failure and buttons:
+            # Telegram validates every inline-keyboard URL and refuses the whole
+            # send when one of them is not a URL it accepts — which is what an
+            # install without a public external_url produces for the #1898
+            # verdict links. Dropping the buttons is survivable; dropping the
+            # message the user is waiting for is not.
+            logger.warning("Telegram refused the message with inline buttons (%s); retrying without them", failure)
+            response = await _post(False)
+            failure = _failure(response)
+
+        if failure:
+            return False, failure, None
+        sent = response.json().get("result")
+        return True, "Message sent successfully", sent if isinstance(sent, dict) else None
+
+    async def _send_telegram_confirm_request(
+        self,
+        provider: NotificationProvider,
+        config: dict,
+        message: str,
+        db: AsyncSession | None,
+        image_data: bytes | None,
+        buttons: list[dict] | None,
+        archive_id: int | None,
+    ) -> tuple[bool, str]:
+        """Deliver the outcome prompt the way the provider's verdict mode asks (#3046).
+
+        "buttons" is the plain #1898 delivery. In "reactions" the inline
+        keyboard is dropped and the user answers with a thumbs-up/down on the
+        message itself; "both" keeps the keyboard as well. In either of those
+        the sent message is remembered in telegram_pending_verdicts, together
+        with the confirm token its links carry, so the reaction poller can map
+        the reaction back to the archive for as long as that token is live.
+        """
+        mode = provider.telegram_verdict_mode or "buttons"
+        # Telegram's servers GET the first URL in the text to build a preview
+        # card. An outcome prompt whose edited body still carries {good_url}
+        # would have that fetch answer the question before the operator saw
+        # it, so the preview is off for the prompt in every mode.
+        if mode == "buttons":
+            return await self._send_telegram(
+                config, message, image_data=image_data, buttons=buttons, link_preview=False
+            )
+
+        if mode == "reactions":
+            buttons = None
+        message = f"{message}\n\n{TELEGRAM_REACTION_HINT}"
+        ok, status, sent = await self._send_telegram_message(
+            config, message, image_data=image_data, buttons=buttons, link_preview=False
+        )
+        if not ok:
+            return ok, status
+
+        message_id = (sent or {}).get("message_id")
+        if not isinstance(message_id, int) or message_id <= 0:
+            logger.warning("Telegram did not return a message_id for the outcome prompt; reactions cannot be matched")
+            return ok, status
+        if db is None or archive_id is None:
+            return ok, status
+
+        try:
+            # dispatch_outcome_confirmation mints a live token right before
+            # sending. Without one the prompt's links are dead too, and a
+            # reaction must not outlive them, so there is nothing to remember.
+            archive = await db.get(PrintArchive, archive_id)
+            if archive is None or not archive.confirm_token or archive.confirm_token_used_at is not None:
+                logger.warning("Outcome prompt for archive %s went out without a live confirm token", archive_id)
+                return ok, status
+            db.add(
+                TelegramPendingVerdict(
+                    provider_id=provider.id,
+                    chat_id=str(((sent or {}).get("chat") or {}).get("id") or config.get("chat_id", "")).strip(),
+                    message_id=message_id,
+                    archive_id=archive_id,
+                    confirm_token=archive.confirm_token,
+                    has_caption=image_data is not None,
+                    message_text=message,
+                )
+            )
+            await db.commit()
+        except Exception as e:
+            # The prompt went out; losing the reaction mapping is a degraded
+            # outcome, not a failed notification.
+            logger.warning("Failed to record the Telegram outcome prompt for reactions: %s", e)
+            await db.rollback()
+        return ok, status
 
     async def _send_email(
         self,
@@ -755,6 +985,7 @@ class NotificationService:
         image_data: bytes | None = None,
         event_type: str | None = None,
         variables: dict | None = None,
+        image_url: str | None = None,
     ) -> tuple[bool, str]:
         """Send notification via generic webhook (POST JSON).
 
@@ -775,8 +1006,26 @@ class NotificationService:
 
         # Build payload based on format
         if payload_format == "slack":
-            # Slack/Mattermost format - just text field
+            # Incoming webhooks can't take a byte upload, so a photo has to
+            # be a URL Slack/Mattermost fetch themselves.
             data = {"text": f"*{title}*\n{message}"}
+            if event_type == "print_confirm_request":
+                # Slack and Mattermost fetch every URL in the text to build
+                # preview cards, and the outcome prompt (#1898) is the one
+                # message whose links are single-use capabilities — that fetch
+                # would be a machine answering the operator's question. Off
+                # here for the same reason Telegram's preview is.
+                #
+                # Only here: the slack payload never attaches image bytes (the
+                # base64 attach below is generic-format only), so unfurling is
+                # the only way a {finish_photo_url} in a print_complete body
+                # can render as a photo in the channel. Switching it off for
+                # every event would quietly take that away with no setting to
+                # get it back.
+                data["unfurl_links"] = False
+                data["unfurl_media"] = False
+            if image_url:
+                data["attachments"] = [{"fallback": title, "image_url": image_url}]
         else:
             # Generic format with custom field names
             custom_field_title = config.get("field_title", "title").strip() or "title"
@@ -823,7 +1072,12 @@ class NotificationService:
             return False, f"Webhook error: {str(e)}"
 
     async def _send_homeassistant(
-        self, config: dict, title: str, message: str, db: AsyncSession | None = None
+        self,
+        config: dict,
+        title: str,
+        message: str,
+        db: AsyncSession | None = None,
+        image_url: str | None = None,
     ) -> tuple[bool, str]:
         """Send notification via Home Assistant.
 
@@ -898,6 +1152,7 @@ class NotificationService:
         # reach the notify service. Only included when configured — the default
         # persistent_notification.create schema rejects unknown keys.
         raw_data = config.get("data")
+        data_payload: dict = {}
         if raw_data:
             if isinstance(raw_data, str):
                 try:
@@ -908,8 +1163,19 @@ class NotificationService:
                 parsed_data = raw_data
             if not isinstance(parsed_data, dict):
                 return False, 'The Data field must be a JSON object, e.g. {"priority": "high", "ttl": 0}'
-            if parsed_data:
-                payload["data"] = parsed_data
+            data_payload = parsed_data
+
+        if image_url and service:
+            # HA's notify.* services fetch data.image themselves. Gated on a
+            # custom service being set: the default persistent_notification
+            # .create has a strict schema and 400s on fields it doesn't
+            # recognize, so we'd break plain notifications for anyone who
+            # never asked for a photo. setdefault so a user's own "image"
+            # key still wins.
+            data_payload.setdefault("image", image_url)
+
+        if data_payload:
+            payload["data"] = data_payload
 
         client = await self._get_client()
         response = await client.post(url, json=payload, headers=headers)
@@ -925,6 +1191,54 @@ class NotificationService:
             # gets the same treatment.
             return False, _opaque_http_failure(response, label="Home Assistant endpoint")
 
+    async def _get_or_build_photo_url(
+        self,
+        db: AsyncSession | None,
+        image_data: bytes | None,
+        event_type: str | None,
+        photo_cache: dict | None = None,
+    ) -> str | None:
+        """Return a URL for a notification photo, for providers that fetch it themselves.
+
+        Writes *image_data* under an unguessable filename and builds the URL
+        from the ``external_url`` setting. The filename is the only credential
+        (see notification_photos.py), so the URL opens this one photo and
+        nothing else -- no camera token, which would open every printer's live
+        stream. Print Complete goes through here too rather than reusing the
+        archive's ``finish_photo_url``: that route needs a media token, so with
+        auth on HA/Bark/Slack would get a 401.
+
+        Returns None with no photo or no ``external_url`` set: HA and Bark
+        fetch this URL themselves, so a relative path does them no good.
+
+        *photo_cache* is shared by every provider in one send, so only the
+        first HA/Bark/Slack provider pays for the disk write. It's kept apart
+        from the template variables so the URL never lands in a generic
+        webhook's payload.
+        """
+        if photo_cache is not None and "url" in photo_cache:
+            return photo_cache["url"]
+
+        if not image_data or db is None:
+            return None
+
+        from backend.app.api.routes.settings import get_setting
+
+        external_url = (await get_setting(db, "external_url") or "").strip()
+        if not external_url:
+            return None
+
+        try:
+            filename = await asyncio.to_thread(save_notification_photo, image_data, event_type or "event")
+        except Exception as e:
+            logger.warning("Failed to persist notification photo: %s", e)
+            return None
+
+        url = f"{external_url.rstrip('/')}/api/v1/notifications/photos/{filename}"
+        if photo_cache is not None:
+            photo_cache["url"] = url
+        return url
+
     async def _send_to_provider(
         self,
         provider: NotificationProvider,
@@ -934,8 +1248,13 @@ class NotificationService:
         image_data: bytes | None = None,
         event_type: str | None = None,
         variables: dict | None = None,
+        photo_cache: dict | None = None,
     ) -> tuple[bool, str]:
-        """Send notification to a specific provider."""
+        """Send notification to a specific provider.
+
+        *photo_cache* lets the providers of one send share a single persisted
+        photo URL (see _get_or_build_photo_url).
+        """
         # Check quiet hours
         if self._is_in_quiet_hours(provider):
             logger.info("Skipping notification to %s - quiet hours active", provider.name)
@@ -943,14 +1262,94 @@ class NotificationService:
 
         config = json.loads(provider.config) if isinstance(provider.config, str) else provider.config
 
+        # attach_photo is a per-provider opt-out. Clearing image_data covers
+        # the byte-upload providers below; HA/Bark/Slack get their own check
+        # further down too, since they could otherwise get the URL an earlier
+        # provider in the same send left in photo_cache.
+        if not provider.attach_photo:
+            image_data = None
+
         try:
             if provider.provider_type == "callmebot":
                 return await self._send_callmebot(config, f"{title}\n{message}")
             elif provider.provider_type == "ntfy":
-                return await self._send_ntfy(config, title, message, image_data=image_data, event_type=event_type)
+                # Outcome confirmation (#1898): render the verdict capability
+                # links as one-tap buttons on the notification itself. http so
+                # no browser needs to open; POST because that is the method
+                # that records — a GET only opens the confirmation page, which
+                # is what keeps unfurlers from answering the prompt.
+                # clear=true dismisses the notification once a button was tapped.
+                ntfy_actions = None
+                good_url = (variables or {}).get("good_url")
+                reject_url = (variables or {}).get("reject_url")
+                # Buttons need absolute URLs; without a configured external_url
+                # the links are relative, and the body's deep link into the
+                # archive has to do.
+                if (
+                    event_type == "print_confirm_request"
+                    and good_url
+                    and reject_url
+                    and good_url.startswith("http")
+                    and reject_url.startswith("http")
+                ):
+                    ntfy_actions = (
+                        f"http, Good, {good_url}, method=POST, clear=true; "
+                        f"http, Reject, {reject_url}, method=POST, clear=true"
+                    )
+                return await self._send_ntfy(
+                    config, title, message, image_data=image_data, event_type=event_type, actions=ntfy_actions
+                )
             elif provider.provider_type == "pushover":
-                return await self._send_pushover(config, title, message, image_data=image_data)
+                # Outcome confirmation (#1898): Pushover has no arbitrary
+                # buttons, but supports one supplementary URL — deep-link into
+                # the archive's confirmation dialog.
+                supplement_url = None
+                supplement_url_title = None
+                _confirm_url = (variables or {}).get("confirm_url")
+                if event_type == "print_confirm_request" and _confirm_url and _confirm_url.startswith("http"):
+                    supplement_url = _confirm_url
+                    supplement_url_title = "Confirm print outcome"
+                return await self._send_pushover(
+                    config, title, message, image_data=image_data, url=supplement_url, url_title=supplement_url_title
+                )
             elif provider.provider_type == "telegram":
+                if event_type == "print_confirm_request":
+                    # Outcome confirmation (#1898): inline URL buttons under
+                    # the message — one tap records the verdict via the
+                    # capability link. Same absolute-URL requirement as the
+                    # ntfy actions. Telegram has no way to POST, so this is the
+                    # one affordance that opens a browser, and it is the one
+                    # that gets the one-tap marker: the page submits itself
+                    # only for a URL that came off a button. Telegram does not
+                    # fetch inline-keyboard URLs and nothing else can read
+                    # them, so the marker never reaches a scanner — which is
+                    # the difference between this and trusting the User-Agent.
+                    tg_buttons = None
+                    _tg_good = (variables or {}).get("good_url")
+                    _tg_reject = (variables or {}).get("reject_url")
+                    if _tg_good and _tg_reject and _tg_good.startswith("http") and _tg_reject.startswith("http"):
+                        tg_buttons = [
+                            {"text": "\U0001f44d Good", "url": one_tap_url(_tg_good)},
+                            {"text": "\U0001f44e Reject", "url": one_tap_url(_tg_reject)},
+                        ]
+                    # Telegram verdict mode (#3046): buttons, a reaction on
+                    # the message, or both. The link preview is off in all of
+                    # them (see _send_telegram_confirm_request).
+                    _archive_id = (variables or {}).get("archive_id")
+                    return await self._send_telegram_confirm_request(
+                        provider,
+                        config,
+                        f"*{title}*\n{message}",
+                        db,
+                        image_data,
+                        tg_buttons,
+                        _archive_id if isinstance(_archive_id, int) else None,
+                    )
+                # Every other event keeps Telegram's link preview, for the same
+                # reason as the Slack unfurl in _send_webhook: when the finish
+                # photo is too large to attach, the preview is how a
+                # {finish_photo_url} in a print_complete body still shows up as
+                # a photo in the chat.
                 return await self._send_telegram(config, f"*{title}*\n{message}", image_data=image_data)
             elif provider.provider_type == "email":
                 # finish_photo_url is pulled from the rendered template variables
@@ -963,13 +1362,40 @@ class NotificationService:
             elif provider.provider_type == "discord":
                 return await self._send_discord(config, title, message, image_data=image_data)
             elif provider.provider_type == "webhook":
+                # Only Slack format needs a URL — generic format gets the
+                # base64 image field instead.
+                photo_url = None
+                if provider.attach_photo and (config.get("payload_format") or "generic").strip() == "slack":
+                    photo_url = await self._get_or_build_photo_url(db, image_data, event_type, photo_cache)
                 return await self._send_webhook(
-                    config, title, message, image_data=image_data, event_type=event_type, variables=variables
+                    config,
+                    title,
+                    message,
+                    image_data=image_data,
+                    event_type=event_type,
+                    variables=variables,
+                    image_url=photo_url,
                 )
             elif provider.provider_type == "homeassistant":
-                return await self._send_homeassistant(config, title, message, db=db)
+                photo_url = (
+                    await self._get_or_build_photo_url(db, image_data, event_type, photo_cache)
+                    if provider.attach_photo
+                    else None
+                )
+                return await self._send_homeassistant(config, title, message, db=db, image_url=photo_url)
             elif provider.provider_type == "bark":
-                return await self._send_bark(config, title, message)
+                # Outcome confirmation (#1898): Bark opens one URL on tap —
+                # deep-link into the confirmation dialog, like Pushover.
+                bark_url = None
+                _bark_confirm = (variables or {}).get("confirm_url")
+                if event_type == "print_confirm_request" and _bark_confirm and _bark_confirm.startswith("http"):
+                    bark_url = _bark_confirm
+                photo_url = (
+                    await self._get_or_build_photo_url(db, image_data, event_type, photo_cache)
+                    if provider.attach_photo
+                    else None
+                )
+                return await self._send_bark(config, title, message, url=bark_url, image_url=photo_url)
             else:
                 return False, f"Unknown provider type: {provider.provider_type}"
         except Exception as e:
@@ -1072,11 +1498,19 @@ class NotificationService:
         All notifications are always sent immediately. If digest mode is enabled,
         the notification is ALSO queued for the daily digest summary.
         """
+        photo_cache: dict = {}
         for provider in providers:
             try:
                 # Always send notification immediately
                 success, error = await self._send_to_provider(
-                    provider, title, message, db, image_data=image_data, event_type=event_type, variables=variables
+                    provider,
+                    title,
+                    message,
+                    db,
+                    image_data=image_data,
+                    event_type=event_type,
+                    variables=variables,
+                    photo_cache=photo_cache,
                 )
 
                 # Also queue for digest if enabled (digest is a summary, not a queue)
@@ -1121,6 +1555,30 @@ class NotificationService:
                     printer_name=printer_name,
                 )
 
+    async def on_app_message(
+        self,
+        db: AsyncSession,
+        *,
+        sender: str,
+        title: str,
+        message: str,
+        url: str | None = None,
+    ) -> int:
+        """A message another application sends through Bambuddy.
+
+        Goes to every enabled channel with "Messages from connected apps" on,
+        through the same path as Bambuddy's own events: quiet hours, the daily
+        digest and the log (whose event type names the sender). The text is
+        the app's own; a link, when given, is appended so every channel type
+        carries it. Returns how many channels it was handed to.
+        """
+        providers = await self._get_providers_for_event(db, "on_app_message")
+        if not providers:
+            return 0
+        body = f"{message}\n{url}" if url else message
+        await self._send_to_providers(providers, title, body, db, event_type=f"app:{sender}"[:50])
+        return len(providers)
+
     async def on_print_start(
         self,
         printer_id: int,
@@ -1311,6 +1769,66 @@ class NotificationService:
             variables=variables,
         )
 
+    async def on_print_confirm_request(
+        self,
+        printer_id: int,
+        printer_name: str,
+        data: dict,
+        db: AsyncSession,
+        archive_data: dict | None = None,
+        good_url: str | None = None,
+        reject_url: str | None = None,
+        confirm_url: str | None = None,
+        archive_id: int | None = None,
+    ):
+        """Ask for a post-print outcome verdict (#1898).
+
+        Fires only for completed prints whose queue item opted in — the
+        provider-level toggle exists to mute a channel, not to enable the
+        feature. good_url / reject_url are the one-tap capability links
+        (rendered as ntfy action buttons), confirm_url deep-links into the
+        archive's confirmation dialog in the web UI. archive_id lets a Telegram
+        provider in reaction mode (#3046) tie the sent message to the archive.
+        """
+        providers = await self._get_providers_for_event(db, "on_print_confirm_request", printer_id)
+        if not providers:
+            return
+
+        subtask_name = data.get("subtask_name")
+        if subtask_name:
+            filename = subtask_name.replace("_", " ")
+        else:
+            filename = self._clean_filename(data.get("filename", "Unknown"))
+
+        variables = {"printer": printer_name, "filename": filename}
+        if good_url:
+            variables["good_url"] = good_url
+        if reject_url:
+            variables["reject_url"] = reject_url
+        if confirm_url:
+            variables["confirm_url"] = confirm_url
+        if archive_id is not None:
+            variables["archive_id"] = archive_id
+
+        image_data = None
+        if archive_data:
+            if archive_data.get("finish_photo_url"):
+                variables["finish_photo_url"] = archive_data["finish_photo_url"]
+            image_data = archive_data.get("image_data")
+
+        title, message = await self._build_message_from_template(db, "print_confirm_request", variables)
+        await self._send_to_providers(
+            providers,
+            title,
+            message,
+            db,
+            "print_confirm_request",
+            printer_id,
+            printer_name,
+            image_data=image_data,
+            variables=variables,
+        )
+
     async def on_print_progress(
         self,
         printer_id: int,
@@ -1515,6 +2033,7 @@ class NotificationService:
         printer_name: str,
         db: AsyncSession,
         difference_percent: float | None = None,
+        image_data: bytes | None = None,
     ):
         """Handle plate not empty event - objects detected on build plate before print."""
         providers = await self._get_providers_for_event(db, "on_plate_not_empty", printer_id)
@@ -1536,6 +2055,7 @@ class NotificationService:
             printer_id,
             printer_name,
             force_immediate=True,
+            image_data=image_data,
             variables=variables,
         )
 
@@ -1576,7 +2096,7 @@ class NotificationService:
         self,
         printer_id: int,
         printer_name: str,
-        slot: int,
+        slot: str,
         remaining_percent: int,
         db: AsyncSession,
         color: str | None = None,
@@ -1588,7 +2108,7 @@ class NotificationService:
 
         variables = {
             "printer": printer_name,
-            "slot": str(slot),
+            "slot": slot,
             "remaining_percent": str(remaining_percent),
             "color": color or "",
         }
@@ -1960,6 +2480,8 @@ class NotificationService:
         printer_name: str,
         filename: str,
         db: AsyncSession,
+        image_data: bytes | None = None,
+        finish_photo_url: str | None = None,
     ) -> None:
         """Send a print event email notification to the user who submitted the job.
 
@@ -1969,6 +2491,10 @@ class NotificationService:
             printer_name: Name of the printer
             filename: Raw filename or subtask name
             db: Database session
+            image_data: Camera snapshot bytes, if one was captured for the event.
+            finish_photo_url: The same snapshot's public URL — only used so the
+                template can reference {finish_photo_url}, which is what opts
+                the email into inlining the photo (see send_user_print_notification).
         """
         if created_by_id is None:
             logger.debug("[EMAIL] Skipping user print email (%s): no created_by_id", event_type)
@@ -2054,6 +2580,8 @@ class NotificationService:
                 "printer": printer_name,
                 "filename": self._clean_filename(filename),
             }
+            if finish_photo_url:
+                variables["finish_photo_url"] = finish_photo_url
 
             # Send the email
             await send_user_print_notification(
@@ -2062,6 +2590,7 @@ class NotificationService:
                 user_email=user.email,
                 username=user.username,
                 variables=variables,
+                image_data=image_data,
             )
             logger.info("[EMAIL] User print email sent: event=%s → %s", event_type, user.email)
         except Exception as e:
@@ -2240,15 +2769,31 @@ class NotificationService:
         rate_g_day: float,
         days_left: int,
         db: AsyncSession,
+        *,
+        subtype: str | None = None,
+        color: str | None = None,
+        skip_break_subscribers: bool = False,
     ):
-        """Fire when an inventory SKU reaches its reorder point."""
+        """Fire when an inventory SKU reaches its reorder point.
+
+        ``subtype`` and ``color`` are what tell apart the messages for two colours of one
+        product, which the forecast (grouped by colour as well) reports separately.
+
+        A SKU in stock break has also reached its reorder point. ``skip_break_subscribers``
+        leaves out the providers that have the break alert on, so the producer can send
+        the reorder event for a break SKU without telling those providers twice.
+        """
         providers = await self._get_providers_for_event(db, "on_stock_reorder_alert", None)
+        if skip_break_subscribers:
+            providers = [p for p in providers if not p.on_stock_break_alert]
         if not providers:
             return
 
         variables = {
             "material": material,
+            "subtype": subtype or "",
             "brand": brand or "",
+            "color": color or "",
             "stock_g": f"{stock_g:.0f}",
             "rate_g_day": f"{rate_g_day:.1f}",
             "days_left": str(days_left),
@@ -2266,15 +2811,23 @@ class NotificationService:
         days_left: int,
         lead_time_days: int,
         db: AsyncSession,
+        *,
+        subtype: str | None = None,
+        color: str | None = None,
     ):
-        """Fire when a stock break is detected (stock runs out before lead time)."""
+        """Fire when a stock break is detected (stock runs out before lead time).
+
+        ``subtype`` and ``color`` as for :meth:`on_stock_reorder_alert`.
+        """
         providers = await self._get_providers_for_event(db, "on_stock_break_alert", None)
         if not providers:
             return
 
         variables = {
             "material": material,
+            "subtype": subtype or "",
             "brand": brand or "",
+            "color": color or "",
             "stock_g": f"{stock_g:.0f}",
             "rate_g_day": f"{rate_g_day:.1f}",
             "days_left": str(days_left),

+ 10 - 3
backend/app/services/obico_actions.py

@@ -13,10 +13,14 @@ from backend.app.models.printer import Printer
 logger = logging.getLogger(__name__)
 
 
-async def execute_action(printer_id: int, action: str, task_name: str, score: float) -> None:
+async def execute_action(
+    printer_id: int, action: str, task_name: str, score: float, frame: bytes | None = None
+) -> None:
     """Run the configured action for a detected print failure.
 
     action: 'notify' | 'pause' | 'pause_and_off'
+    frame: the camera snapshot the ML model flagged, if any — attached to
+        the notification so the recipient can see what triggered it.
     """
     printer_name = await _get_printer_name(printer_id)
 
@@ -26,7 +30,7 @@ async def execute_action(printer_id: int, action: str, task_name: str, score: fl
     if action == "pause_and_off":
         await _turn_off_linked_plugs(printer_id)
 
-    await _notify(printer_id, printer_name, task_name, score, action)
+    await _notify(printer_id, printer_name, task_name, score, action, frame)
 
 
 async def _get_printer_name(printer_id: int) -> str:
@@ -63,7 +67,9 @@ async def _turn_off_linked_plugs(printer_id: int) -> None:
                 logger.error("Obico action: failed to turn off plug %s: %s", plug.name, e)
 
 
-async def _notify(printer_id: int, printer_name: str, task_name: str, score: float, action: str) -> None:
+async def _notify(
+    printer_id: int, printer_name: str, task_name: str, score: float, action: str, frame: bytes | None = None
+) -> None:
     """Fire the AI Failure Detection notification (#1794).
 
     Routed to its own event in 0.2.5b1; previously rode the multiplexed
@@ -81,6 +87,7 @@ async def _notify(printer_id: int, printer_name: str, task_name: str, score: flo
                 confidence=score,
                 action=action,
                 db=db,
+                image_data=frame,
             )
         except Exception as e:
             logger.error("Obico notify failed for printer %s: %s", printer_id, e)

+ 7 - 3
backend/app/services/obico_detection.py

@@ -369,9 +369,11 @@ class ObicoDetectionService:
 
         if verdict == "failure" and not self._action_fired.get(printer_id):
             self._action_fired[printer_id] = True
-            await self._dispatch_action(printer_id, settings["action"], task_name, score)
+            await self._dispatch_action(printer_id, settings["action"], task_name, score, frame)
 
-    async def _dispatch_action(self, printer_id: int, action: str, task_name: str, score: float):
+    async def _dispatch_action(
+        self, printer_id: int, action: str, task_name: str, score: float, frame: bytes | None = None
+    ):
         from backend.app.services.obico_actions import execute_action
 
         logger.warning(
@@ -382,7 +384,9 @@ class ObicoDetectionService:
             action,
         )
         try:
-            await execute_action(printer_id, action, task_name, score)
+            # Same frame the ML model flagged, not a fresh capture — the
+            # printer's moved on by the time this fires.
+            await execute_action(printer_id, action, task_name, score, frame)
         except Exception as e:
             self._last_error = f"Action dispatch failed: {e or type(e).__name__}"
             logger.error(self._last_error)

+ 153 - 0
backend/app/services/oidc_group_sync.py

@@ -0,0 +1,153 @@
+"""OIDC group sync (#3107).
+
+Mirrors the LDAP group sync semantics from api/routes/auth.py
+(`_sync_ldap_user`) for OIDC logins: the provider's ``group_mapping``
+configures which Bambuddy groups the IdP is allowed to manage, and every
+login replaces only that managed slice — manual assignments to any other
+group survive (#1292, same fix the LDAP path needed).
+
+Differences from LDAP worth stating:
+
+- LDAP reads group DNs from the directory entry. OIDC reads group values
+  from a JWT claim, and providers disagree on the shape: Keycloak ships a
+  JSON array, Authentik ships an array, Logto and some legacy setups ship a
+  space- or comma-separated string. ``extract_idp_groups`` accepts both.
+- LDAP has a ``default_group`` fallback when no mapped group matches. The
+  OIDC path already has ``default_group_id`` applied at account creation,
+  and re-asserting it on every login would fight manual upgrades: an admin
+  who promotes an auto-created user out of Viewers would see the promotion
+  reverted at the next SSO login. So the OIDC sync has no fallback — an
+  empty resolved set simply means "the IdP grants none of the mapped
+  groups", which removes exactly the mapped groups and nothing else.
+"""
+
+from __future__ import annotations
+
+import contextlib
+import logging
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.models.group import Group
+from backend.app.models.user import User
+
+logger = logging.getLogger(__name__)
+
+# Bound on the claim parsing below. A legitimate groups claim holds tens of
+# entries; anything past this is a malformed or hostile token, and iterating
+# it would just burn cycles before the mapping lookup ignores the extras.
+_MAX_CLAIM_ITEMS = 500
+
+
+def extract_idp_groups(claim_value: object) -> list[str]:
+    """Normalise a raw JWT claim value into a list of IdP group strings.
+
+    Accepts the shapes seen in the wild:
+    - list of strings (Keycloak, Authentik, most modern providers)
+    - single string, space- or comma-separated (Logto, some legacy setups)
+    - a single group name as a bare string
+
+    Non-string entries, empty fragments, and obvious non-group payloads
+    (dicts, numbers) are dropped rather than rejected: a provider adding an
+    unexpected claim shape must not lock users out of their mapped groups.
+    Duplicates are removed while preserving order (first occurrence wins).
+    The result is bounded by _MAX_CLAIM_ITEMS for both shapes — a string
+    claim splits into arbitrarily many fragments, so the slice applies after
+    splitting, not only on the list path.
+    """
+    if claim_value is None:
+        return []
+    if isinstance(claim_value, list):
+        raw_items = [item for item in claim_value[:_MAX_CLAIM_ITEMS] if isinstance(item, str)]
+    elif isinstance(claim_value, str):
+        # Space-separated is the OIDC convention (scope-style); commas are a
+        # pragmatic extra since some IdPs stringify arrays that way.
+        raw_items = claim_value.replace(",", " ").split(" ")[:_MAX_CLAIM_ITEMS]
+    else:
+        return []
+    seen: set[str] = set()
+    result: list[str] = []
+    for item in raw_items:
+        cleaned = item.strip()
+        if cleaned and cleaned not in seen:
+            seen.add(cleaned)
+            result.append(cleaned)
+    return result
+
+
+def resolve_oidc_group_mapping(idp_groups: list[str], group_mapping: dict[str, str]) -> list[str]:
+    """Map IdP group values to Bambuddy group names (case-insensitive on the key).
+
+    Same contract as ldap_service.resolve_group_mapping: returns the Bambuddy
+    group names the user should hold among the mapped set. Values are compared
+    case-insensitively because IdP group casing is not stable across providers
+    (Keycloak preserves case; some LDAP-backed OIDC deployments downcase),
+    and a case mismatch silently dropping a group is the failure mode an
+    admin can least diagnose from the UI.
+    """
+    if not group_mapping:
+        return []
+    mapping_lower = {k.lower(): v for k, v in group_mapping.items()}
+    result: list[str] = []
+    for idp_group in idp_groups:
+        mapped = mapping_lower.get(idp_group.lower())
+        if mapped and mapped not in result:
+            result.append(mapped)
+    return result
+
+
+async def sync_oidc_user_groups(
+    db: AsyncSession,
+    user: User,
+    *,
+    group_claim: str,
+    group_mapping: dict[str, str],
+    claims: dict,
+) -> None:
+    """Apply the provider's group mapping to ``user`` after a successful login.
+
+    Only Bambuddy groups named in ``group_mapping`` values are managed; every
+    other group on the user is a manual assignment and is preserved. Commits
+    only when something actually changed (the LDAP sync logs on change; same
+    here). Never raises: a group-sync failure must not abort the login the
+    token exchange already authenticated — the exception is logged and the
+    user keeps the groups they had.
+    """
+    if not group_mapping:
+        # No mapping configured: nothing is managed, so nothing may change.
+        # This is the default state and must remain a no-op for upgrades.
+        return
+
+    try:
+        mapped_names = resolve_oidc_group_mapping(extract_idp_groups(claims.get(group_claim)), group_mapping)
+
+        # Only groups that exist locally can be granted; a mapping entry
+        # pointing at a deleted group is skipped (the same dangling-FK
+        # tolerance default_group_id documents for SQLite).
+        if mapped_names:
+            groups_result = await db.execute(select(Group).where(Group.name.in_(mapped_names)))
+            target_groups = list(groups_result.scalars().all())
+        else:
+            target_groups = []
+
+        managed_names = set(group_mapping.values())
+        preserved = [g for g in user.groups if g.name not in managed_names]
+        new_groups = preserved + target_groups
+
+        current_ids = {g.id for g in user.groups}
+        new_ids = {g.id for g in new_groups}
+        if current_ids == new_ids:
+            return
+
+        user.groups = new_groups
+        await db.commit()
+        logger.info(
+            "OIDC group sync: user %s groups -> %s",
+            user.username,
+            sorted(g.name for g in new_groups),
+        )
+    except Exception:  # noqa: BLE001 -- login must survive a sync failure
+        logger.exception("OIDC group sync failed for user %s; groups left unchanged", user.username)
+        with contextlib.suppress(Exception):
+            await db.rollback()

+ 72 - 0
backend/app/services/pdf_thumbnail.py

@@ -0,0 +1,72 @@
+"""PDF Thumbnail Generation Service.
+
+Renders the first page of a PDF with PDFium (pypdfium2) so a library PDF gets
+its grid thumbnail at upload/scan time. Before this, PDF thumbnails only
+existed once somebody had opened the in-browser preview, which posted its
+first render back (#2976); that route stays as the fallback for a PDF this
+renderer cannot read.
+"""
+
+import logging
+import threading
+import uuid
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+# PDFium is not thread-safe, and callers run this through asyncio.to_thread,
+# so two uploads landing together would otherwise enter it concurrently.
+_PDFIUM_LOCK = threading.Lock()
+
+# Rendered at twice the output size and downsampled, so small print on the
+# page stays legible instead of breaking up into aliased specks.
+_SUPERSAMPLE = 2
+
+
+def generate_pdf_thumbnail(pdf_path: Path, thumbnails_dir: Path, size: int = 256) -> str | None:
+    """Render page one of a PDF into a square, white-backed PNG thumbnail.
+
+    Same shape as the thumbnail the browser preview posts back (a ``size``
+    square with the page centred on white), so a grid mixing both kinds
+    looks uniform. Returns the thumbnail path, or None if the file cannot be
+    rendered - an encrypted, empty or damaged PDF simply gets no thumbnail.
+    """
+    try:
+        import pypdfium2 as pdfium
+        from PIL import Image
+    except ImportError:
+        logger.warning("pypdfium2 not installed, skipping PDF thumbnail for %s", pdf_path.name)
+        return None
+
+    try:
+        with _PDFIUM_LOCK:
+            pdf = pdfium.PdfDocument(str(pdf_path))
+            try:
+                if len(pdf) == 0:
+                    return None
+                page = pdf[0]
+                try:
+                    width, height = page.get_size()
+                    if width <= 0 or height <= 0:
+                        return None
+                    # Scale from the page's own size, so the bitmap is bounded
+                    # by the output size whatever MediaBox the file declares.
+                    scale = (size * _SUPERSAMPLE) / max(width, height)
+                    rendered = page.render(scale=scale).to_pil()
+                finally:
+                    page.close()
+            finally:
+                pdf.close()
+
+        rendered = rendered.convert("RGB")
+        rendered.thumbnail((size, size), Image.Resampling.LANCZOS)
+        thumbnail = Image.new("RGB", (size, size), (255, 255, 255))
+        thumbnail.paste(rendered, ((size - rendered.width) // 2, (size - rendered.height) // 2))
+
+        thumb_filename = f"{uuid.uuid4().hex}.png"
+        thumb_path = thumbnails_dir / thumb_filename  # SEC-PATH-OK: thumb_filename = uuid.uuid4().hex + ".png"
+        thumbnail.save(thumb_path, "PNG", optimize=True)
+        return str(thumb_path)
+    except Exception as e:
+        logger.warning("Failed to create PDF thumbnail for %s: %s", pdf_path.name, e)
+        return None

+ 6 - 32
backend/app/services/print_batch.py

@@ -21,13 +21,14 @@ import logging
 from dataclasses import dataclass, field
 from datetime import datetime, timezone
 
-from sqlalchemy import func, select, text
+from sqlalchemy import func, select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
 from backend.app.models.print_batch import PrintBatch, PrintBatchPlate
 from backend.app.models.print_log import PrintLogEntry
 from backend.app.models.print_queue import PrintQueueItem, PrintQueueVariant
+from backend.app.services.queue_position import next_queue_position
 
 logger = logging.getLogger(__name__)
 
@@ -72,6 +73,7 @@ CLONED_SETTING_COLUMNS = (
     "timelapse",
     "use_ams",
     "nozzle_offset_cali",
+    "confirm_outcome",
     "preheat_override",
     "preheat_chamber_target_override",
     "skip_filament_check",
@@ -431,34 +433,6 @@ async def refresh_batch_status_for_item(db: AsyncSession, queue_item_id: int) ->
     await refresh_batch_status(db, batch)
 
 
-async def _next_position(db: AsyncSession, printer_id: int | None) -> int:
-    """Next free queue position in the scope a clone will land in.
-
-    Positions are per-queue, not global: one sequence per printer plus one
-    shared sequence for unassigned / model-based items, matching the scope the
-    add-to-queue route uses. Taking a global MAX here would drop every clone
-    at the end of whichever printer's queue happens to be longest and scramble
-    the order the user sees.
-    """
-    # Same advisory lock the add-to-queue route takes (#1625-followup): two
-    # concurrent inserts into an empty scope would otherwise both read
-    # MAX(position) as 0 and land on position 1. SQLite serialises writes
-    # implicitly and needs no equivalent.
-    bind = db.get_bind()
-    if bind.dialect.name == "postgresql":
-        await db.execute(
-            text("SELECT pg_advisory_xact_lock(1625, :k)"), {"k": printer_id if printer_id is not None else 0}
-        )
-
-    scope = PrintQueueItem.printer_id == printer_id if printer_id is not None else PrintQueueItem.printer_id.is_(None)
-    max_pos = (
-        await db.execute(
-            select(func.max(PrintQueueItem.position)).where(scope).where(PrintQueueItem.status == "pending")
-        )
-    ).scalar() or 0
-    return max_pos + 1
-
-
 def _clone_queue_item(source: PrintQueueItem, *, position: int, created_by_id: int | None) -> PrintQueueItem:
     """Copy *source*'s print configuration into a fresh pending item.
 
@@ -555,9 +529,9 @@ async def dispatch_remaining(
         if limit is not None:
             wanted = min(wanted, limit - len(created))
 
-        # One scope per source printer; clones for this plate all land in it,
-        # appended after whatever is already queued there.
-        position = await _next_position(db, source.printer_id)
+        # Clones for this plate are appended to the end of the queue (#3200),
+        # after whatever is already pending, in plate order.
+        position = await next_queue_position(db)
 
         for _ in range(wanted):
             clone = _clone_queue_item(source, position=position, created_by_id=created_by_id)

+ 229 - 0
backend/app/services/print_confirmation.py

@@ -0,0 +1,229 @@
+"""Post-print outcome confirmation helpers (#1898)."""
+
+import logging
+from collections.abc import Mapping
+from datetime import datetime, timezone
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.models.archive import PrintArchive
+from backend.app.models.print_log import PrintLogEntry
+
+logger = logging.getLogger(__name__)
+
+# How a verdict reached the archive. 'reaction' is written by the Telegram
+# reaction poller (#3046, services/telegram_reactions.py) through
+# apply_outcome_verdict below.
+VERDICT_SOURCES = ("dialog", "link", "plate_clear", "printer_card", "api", "reaction")
+
+# Link-preview unfurlers and mail-security scanners fetch every URL they find in
+# a message, unattended, within seconds of it being sent. Nothing they can do
+# with a GET records a verdict any more -- that is the POST route's job -- so
+# this list is the second layer: it decides whether the confirmation page
+# submits its own form, which is what keeps a human at one tap. A scanner that
+# runs JavaScript would otherwise press the button on the operator's behalf.
+# Matched as case-insensitive substrings of the User-Agent; the generic "bot"
+# token covers TelegramBot, Discordbot, Slackbot-LinkExpanding, Twitterbot and
+# LinkedInBot in one go.
+UNATTENDED_FETCH_AGENTS = (
+    "bot",
+    "crawler",
+    "spider",
+    "facebookexternalhit",
+    "whatsapp",
+    "skypeuripreview",
+    "bingpreview",
+    "safelinks",
+    "urldefense",
+    "proofpoint",
+    "mimecast",
+    "barracuda",
+    "forcepoint",
+)
+
+# Prefetch / preload hints. A finger on a notification button is never one.
+UNATTENDED_FETCH_HEADERS = {
+    "purpose": ("prefetch", "preview"),
+    "x-purpose": ("prefetch", "preview"),
+    "x-moz": ("prefetch",),
+    "sec-purpose": ("prefetch",),
+}
+
+
+def is_unattended_fetch(method: str, headers: Mapping[str, str]) -> bool:
+    """Whether a request for a one-tap verdict link came from a machine.
+
+    Decides whether the confirmation page submits itself. False positives are
+    deliberately cheap -- a browser mistaken for a bot gets the same page with
+    a button to press -- so the lists above err towards catching more.
+    """
+    if method.upper() != "GET":
+        # Anything that is not the page load is not a page load: only the GET
+        # route renders, and only a GET can be widened to HEAD by a future
+        # router change.
+        return True
+    for header, markers in UNATTENDED_FETCH_HEADERS.items():
+        value = (headers.get(header) or "").lower()
+        if value and any(marker in value for marker in markers):
+            return True
+    agent = (headers.get("user-agent") or "").lower()
+    return any(marker in agent for marker in UNATTENDED_FETCH_AGENTS)
+
+
+# The one-tap marker. The confirmation page submits its own form only when the
+# URL it was opened from carries this, and the marker is put on exactly one
+# thing: the Telegram inline keyboard's buttons -- an affordance no unfurler,
+# gateway or proxy reads, for the same reason the capability URLs themselves no
+# longer travel in message text.
+#
+# It is what closes the gap the User-Agent list above cannot: a mail-security
+# sandbox that renders HTML and runs JavaScript sends an ordinary Chrome string
+# (so does literal HeadlessChrome), and a verdict URL that reached it did so out
+# of the message BODY -- where the marker never appears. That fetch now gets the
+# page with a button on it and records nothing. The operator's tap on the
+# notification button still costs exactly one tap.
+ONE_TAP_PARAM = "tap"
+
+
+def one_tap_url(url: str) -> str:
+    """Mark a verdict URL as one a human is about to press.
+
+    Only for the affordances a person taps directly. A URL that goes into text
+    anybody's machine might follow is left unmarked on purpose.
+    """
+    return f"{url}{'&' if '?' in url else '?'}{ONE_TAP_PARAM}=1"
+
+
+def is_one_tap_request(query_params: Mapping[str, str]) -> bool:
+    """Whether this page load came from a button rather than from message text."""
+    return (query_params.get(ONE_TAP_PARAM) or "") == "1"
+
+
+def stamp_verdict(archive: PrintArchive, source: str) -> None:
+    """Record a verdict's provenance and the moment it landed (#1898).
+
+    Both fields move together on every verdict write, which is what keeps the
+    "already answered" page from pairing a new source with the timestamp of an
+    older decision. `retire_confirm_token` is separate on purpose: spending the
+    one-tap capability happens once, recording a verdict can happen again.
+    """
+    archive.user_verdict_source = source
+    archive.user_verdict_at = datetime.now(timezone.utc)
+
+
+def retire_confirm_token(archive: PrintArchive) -> None:
+    """Spend the one-tap capability token without destroying it.
+
+    The token is still single-use: once ``confirm_token_used_at`` is stamped,
+    no verdict path accepts it again. Keeping the VALUE is what lets the
+    one-tap route recognise a link belonging to an already-answered print and
+    say so, instead of 404ing as if the link had never been real (the live-farm
+    case: the plate-clear default answered the prompt, then the user tapped the
+    Telegram button and got "invalid or already used").
+    """
+    if archive.confirm_token and archive.confirm_token_used_at is None:
+        archive.confirm_token_used_at = datetime.now(timezone.utc)
+
+
+VERDICTS = ("good", "reject")
+
+
+async def apply_outcome_verdict(db: AsyncSession, archive: PrintArchive, verdict: str, *, source: str) -> bool:
+    """Record a verdict on an archive that has not been answered yet.
+
+    For verdict paths that arrive with nobody looking at the archive, like the
+    Telegram reaction poller (#3046). It writes what every verdict write in
+    this module writes: the archive's user_verdict with its provenance stamp,
+    the same value mirrored onto the latest PrintLogEntry (verdict-aware
+    statistics read the log, the #1444 mirror), and the one-tap capability
+    token spent.
+
+    First verdict wins. An archive that already carries one is left exactly
+    as it is and False is returned, so a late reaction cannot flip a decision
+    somebody made in the meantime. Deliberately does NOT commit.
+    """
+    if verdict not in VERDICTS:
+        raise ValueError(f"Verdict must be one of {VERDICTS}, got {verdict!r}")
+    if source not in VERDICT_SOURCES:
+        raise ValueError(f"Verdict source must be one of {VERDICT_SOURCES}, got {source!r}")
+    if archive.user_verdict is not None:
+        return False
+
+    archive.user_verdict = verdict
+    stamp_verdict(archive, source)
+    retire_confirm_token(archive)
+
+    latest_entry = await db.scalar(
+        select(PrintLogEntry).where(PrintLogEntry.archive_id == archive.id).order_by(PrintLogEntry.id.desc()).limit(1)
+    )
+    if latest_entry is not None:
+        latest_entry.user_verdict = verdict
+    return True
+
+
+async def resolve_pending_confirmation_as_good(db: AsyncSession, printer_id: int) -> int | None:
+    """Mark the printer's latest pending-confirmation archive as good.
+
+    Backs the opt-in ``confirm_default_good_on_plate_clear`` setting: releasing
+    the build plate is the moment the operator moves on to the next job, so an
+    unanswered outcome prompt can default to "good part" right there instead of
+    lingering as unconfirmed. Only the LATEST pending archive is resolved — the
+    plate release refers to the print that just came off the plate, not to
+    older unanswered prompts.
+
+    Mirrors the verdict onto the latest PrintLogEntry (the #1444 mirror) and
+    retires the one-tap capability token. Deliberately does NOT commit — both
+    callers (the clear-plate route and the queue dispatcher) manage their own
+    transaction.
+
+    Returns the resolved archive id, or None when nothing was pending.
+    """
+    archive = await db.scalar(
+        select(PrintArchive)
+        .where(
+            PrintArchive.printer_id == printer_id,
+            PrintArchive.status == "completed",
+            PrintArchive.confirm_requested.is_(True),
+            PrintArchive.user_verdict.is_(None),
+        )
+        .order_by(PrintArchive.id.desc())
+        .limit(1)
+    )
+    if archive is None:
+        return None
+
+    archive.user_verdict = "good"
+    stamp_verdict(archive, "plate_clear")
+    retire_confirm_token(archive)
+
+    latest_entry = await db.scalar(
+        select(PrintLogEntry).where(PrintLogEntry.archive_id == archive.id).order_by(PrintLogEntry.id.desc()).limit(1)
+    )
+    if latest_entry is not None:
+        latest_entry.user_verdict = "good"
+
+    logger.info("[#1898] Plate clear defaulted archive %s to 'good' (printer %s)", archive.id, printer_id)
+    return archive.id
+
+
+async def confirm_outcome_for_new_queue_item(db: AsyncSession, *, started_outside_bambuddy: bool = False) -> bool:
+    """The ask-for-outcome flag for a queue item created without the print dialog.
+
+    The dialog seeds its own per-job toggle from ``default_confirm_outcome``.
+    Every other queue-creation path -- the virtual printer, the library bulk
+    add, the webhook, a pipeline run -- has no toggle to seed and used to leave
+    the column at its ``False`` default, so "Ask for Outcome" only ever reached
+    jobs queued by hand.
+
+    ``started_outside_bambuddy`` additionally honours
+    ``confirm_outcome_external_prints``: a plate sent from Bambu Studio to a
+    virtual printer is one of the prints that setting's description names, but
+    it arrives with a queue item, so ``on_print_start`` never sees it as
+    external and the setting could not otherwise reach it.
+    """
+    from backend.app.api.routes.settings import get_setting, setting_is_true
+
+    if setting_is_true(await get_setting(db, "default_confirm_outcome")):
+        return True
+    return started_outside_bambuddy and setting_is_true(await get_setting(db, "confirm_outcome_external_prints"))

File diff suppressed because it is too large
+ 860 - 16
backend/app/services/print_scheduler.py


+ 18 - 0
backend/app/services/printer_diagnostic.py

@@ -413,6 +413,7 @@ async def run_connection_diagnostic(
     serial_number: str | None = None,
     access_code: str | None = None,
     wait_for_publish_seconds: float = 0.0,
+    progress: dict | None = None,
 ) -> PrinterDiagnosticResult:
     """Run connection checks for a printer.
 
@@ -422,10 +423,20 @@ async def run_connection_diagnostic(
     Each check carries a stable ``id`` and a ``status`` of
     pass / fail / warn / skip; the frontend renders the human-readable
     title and fix text (localized) keyed on that id + status.
+
+    ``progress``, when given, is kept current while the checks run: its
+    ``checks`` key is the list of finished checks and ``stage`` names the step
+    in flight. The support bundle cancels a run that overruns its budget, and
+    this is how it can still say which step hung and keep what finished --
+    a bare "timed out" for every printer is all #3164's bundle could show.
     """
     checks: list[DiagnosticCheck] = []
+    if progress is None:
+        progress = {}
+    progress["checks"] = checks
 
     # --- Port reachability (probed in parallel) ---
+    progress["stage"] = "ports"
     camera_port, camera_protocol = _camera_port_for_printer(printer)
     mqtt_ok, ftps_state, camera_ok = await asyncio.gather(
         _check_port(ip_address, PORT_MQTT),
@@ -452,6 +463,7 @@ async def run_connection_diagnostic(
     )
 
     # --- macOS Local Network permission ---
+    progress["stage"] = "macos_local_network"
     # Appended on macOS only. Everywhere else there is nothing to say, and a
     # permanently dimmed "skipped" row would be noise for the users who make
     # up nearly all of them.
@@ -485,6 +497,7 @@ async def run_connection_diagnostic(
                 checks.append(DiagnosticCheck(id="macos_local_network", status="warn", params={"reason": "permission"}))
 
     # --- Container network mode ---
+    progress["stage"] = "network_mode"
     # Not Docker-only: Podman runs Bambuddy in exactly the same two shapes and
     # its users were told "Not running in Docker", which reads as "you are on
     # bare metal" and sent them looking for the problem somewhere else (#3092).
@@ -514,6 +527,7 @@ async def run_connection_diagnostic(
             )
 
     # --- Subnet match ---
+    progress["stage"] = "subnet"
     # Skipped in bridge mode: the container IP is the bridge IP, not the host's,
     # so the comparison is meaningless and the network_mode check already covers it.
     if network_mode == "bridge":
@@ -534,6 +548,7 @@ async def run_connection_diagnostic(
             )
 
     # --- External storage (printer-side "Store sent files on external storage") ---
+    progress["stage"] = "external_storage"
     # Install step 4. The setting has two variants depending on
     # firmware/slicer combo: on newer firmware the toggle lives on the
     # printer (P2S 01.02 / BambuStudio 2.6+), on older versions it's
@@ -632,6 +647,7 @@ async def run_connection_diagnostic(
         checks.append(DiagnosticCheck(id="external_storage", status="skip"))
 
     # --- MQTT credentials / connection ---
+    progress["stage"] = "mqtt_auth"
     if not mqtt_ok:
         # Can't reach the broker at all — the port check already reported it.
         checks.append(DiagnosticCheck(id="mqtt_auth", status="skip"))
@@ -671,6 +687,7 @@ async def run_connection_diagnostic(
         checks.append(DiagnosticCheck(id="mqtt_auth", status="skip"))
 
     # --- LAN developer mode (only readable over a live MQTT connection) ---
+    progress["stage"] = "developer_mode"
     if state is not None and state.connected:
         if state.developer_mode is True:
             dev_status = "pass"
@@ -683,6 +700,7 @@ async def run_connection_diagnostic(
         checks.append(DiagnosticCheck(id="developer_mode", status="skip"))
 
     # --- Printer is actually publishing on its report topic ---
+    progress["stage"] = "printer_publishing"
     # The mqtt_auth check above only proves TCP + TLS + auth + SUBSCRIBE
     # succeed. A printer with a wrong-cased serial — or one that simply isn't
     # publishing for some other reason — still passes mqtt_auth because the

Some files were not shown because too many files changed in this diff