fix(frontend/hms): surface uncataloged HMS faults that carry firmware actions (#1840)

filterKnownHMSErrors and the modal-local copy gated visibility on
    ERROR_DESCRIPTIONS membership. H2C 0500_809C carries IGNORE_RESUME /
    PROBLEM_SOLVED_RESUME but is missing from the bundled 853-entry catalog,
    so the entire error — pip, count, panel, action buttons — never rendered
    even though backend captured + dispatched it correctly.

    The gate isn't dead code: PrintersPageBucketing pins the post-cancel
    0C00_001B junk-echo regression to it. Widen the predicate to keep
    (cataloged) OR (actions.length > 0) so noise is still filtered out
    while user-actionable faults always surface.

    Replace the modal's inline filter with the shared helper so badge
    counts and modal contents agree by construction. Fall back to
    hmsErrors.unknownCode ("Unknown HMS code — see the Bambu Lab wiki
    for details.") when the catalog has no entry. New key translated in
    all 11 locales.

    New bucketing test pins PAUSE + uncataloged-with-actions = error;
    existing FAILED + uncataloged-without-actions = finished stays green.
This commit is contained in:
maziggy
2026-07-07 10:51:17 +02:00
17 changed files with 135 additions and 175 deletions
-73
View File
@@ -1,73 +0,0 @@
# Bambuddy Backers & Sponsors
Bambuddy is sustainable thanks to people who put their money where their use is. This page lists everyone who supports the project on [GitHub Sponsors](https://github.com/sponsors/maziggy) or [Ko-fi](https://ko-fi.com/maziggy).
If you'd like to support Bambuddy:
- **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
If you sponsor and your name isn't here within 48h, please write an email to martin@bambuddy.cool or open an Issue on the main repo.
---
## Corporate Sponsors ($300/mo+)
- [@northpole3dprinting](https://github.com/northpole3dprinting)
## Sustaining Sponsors ($150/mo+)
*None yet.*
## Patrons ($35/mo+)
- [@VREmma](https://github.com/VREmma)
- [@pwostran](https://github.com/pwostran)
- [@Praxeis](https://github.com/Praxeis)
- [@jmclaren7](https://github.com/jmclaren7)
- [@RoBoT24-web](https://github.com/RoBoT24-web)
- [@Rayvenhaus](https://github.com/Rayvenhaus)
## Supporters ($15/mo+)
- [@rewart01](https://github.com/rewart01)
- [@rstocks](https://github.com/rstocks)
- [@sixfootseven](https://github.com/sixfootseven)
- [@MethodicalMartian](https://github.com/MethodicalMartian)
- [@brianharwell](https://github.com/brianharwell)
## Backers ($5/mo+)
- [@aneopsy](https://github.com/aneopsy)
- [@flom89](https://github.com/flom89)
- [@grizz0blaw](https://github.com/grizz0blaw)
- [@NoahTingey](https://github.com/NoahTingey)
- [@sentinel-center](https://github.com/sentinel-center)
- [@brianehlert](https://github.com/brianehlert)
- [@siiruup](https://github.com/siiruup)
- [@agntcoopersea](https://github.com/agntcoopersea)
- [@PJMCL1618033](https://github.com/PJMCL1618033
- [@mgf99](https://github.com/mgf99)
- [@Geoff-S](https://github.com/Geoff-S)
- [@andyspinball](https://github.com/andyspinball
- [@avandeputte](https://github.com/avandeputte)
- [@joeferrante](https://github.com/joeferrante)
- [@GPop61](https://github.com)
- [@CooleyMcCoolson](https://github.com/CooleyMcCoolson)
- [@mikeloveridge](https://github.com/mikeloveridge)
- [@boernie](https://github.com/boernie)
- [@qoatzelcoat](https://github.com/qoatzelcoat)
- [@Sanaki](https://github.com/Sanaki)
- [@jlofshult](https://github.com/jlofshult)
- [@TriadX1](https://github.com/TriadX1)
- [@hazzardr](https://github.com/hazzardr)
---
## One-time and historical supporters
A general thank-you to everyone who's contributed via Ko-fi over the past months. Specific names get added on request — if you'd like to be listed, ping `maziggy`.
---
Thanks. — Martin
+2
View File
@@ -20,6 +20,8 @@ All notable changes to Bambuddy will be documented in this file.
## [0.2.5b2] - Unreleased
### Fixed
- **Uncataloged-but-actionable HMS faults now render in the UI (#1840, reporter @Boa-Thomas)** — H2C printers (firmware `01.02.00.00`) emit HMS faults whose short codes aren't in the bundled `ERROR_DESCRIPTIONS` map — e.g. `0500_809C`, which pauses the print and carries `IGNORE_RESUME` / `PROBLEM_SOLVED_RESUME` actions the user needs to dispatch. `filterKnownHMSErrors` in `frontend/src/components/HMSErrorModal.tsx:900` (and the inline modal-local copy at line 929) gated visibility purely on catalog membership (`ERROR_DESCRIPTIONS[shortCode] !== undefined`), so the entire error never rendered: no problem pip, no per-card count, no errors-panel entry, no action buttons. Backend correctly captured + dispatched the fault (verified via REST and WebSocket); frontend silently dropped it. The catalog gate isn't dead code — it's also a noise filter for transient post-cancel echoes like `0C00_001B` (see `PrintersPageBucketing.test.ts`) — so deleting it would re-introduce the FAILED-after-cancel "1 problem forever" regression. **Fix.** `filterKnownHMSErrors` now keeps an error if EITHER it's in `ERROR_DESCRIPTIONS` (existing behaviour, preserves bucketing for noise) OR it carries `actions.length > 0` (actionable fault from any source — surface so the buttons can render). The modal's inline filter is replaced with a call to the shared helper so badge counts and modal contents agree by construction. For uncataloged errors, the description falls back to `t('hmsErrors.unknownCode')` ("Unknown HMS code — see the Bambu Lab wiki for details."); the existing `[XXXX-YYYY]` short-code header, severity badge, action buttons, and wiki link all work without a catalog entry. The action-dispatch path is unchanged — `full_code` already flows through correctly from #1830, so `IGNORE_RESUME` etc. land on the firmware the moment the user clicks. The reporter's secondary observation about `severity === "error"` is a false positive — that comparison lives in `SystemHealthPanel.tsx` (log-health findings, string-typed severity), not the HMS path which correctly switches on the numeric 1–4 scale. Severity `6` falls into the default `Info` branch — acceptable for an unrecognized level and out of scope here. **Tests.** New case in `PrintersPageBucketing.test.ts`: `'classifies PAUSE + uncataloged HMS WITH actions as "error"'` pins the H2C scenario (`0500_809C` + `IGNORE_RESUME`/`PROBLEM_SOLVED_RESUME` → bucket `error`). Existing case `'classifies FAILED + only unknown HMS as "finished"'` (uncataloged WITHOUT actions = noise) stays green — the gate distinguishes the two by action presence. 18/18 frontend tests in `PrintersPageBucketing.test.ts` + `HMSErrorModal.test.tsx` green. **i18n.** One new key `hmsErrors.unknownCode`, real translation in all 11 locales (de/en/es/fr/it/ja/ko/pt-BR/tr/zh-CN/zh-TW), parity check clean. **Scope.** Frontend-only. No backend change. No DB migration. No new permission. The fix is data-shape agnostic — any future printer whose HMS dictionary diverges from the bundled catalog will now surface actionable faults without a Bambuddy release.
- **First-layer notification photo no longer shows pre-print calibration state (#1837, reporter @MartinNYHC)** — On P1S (and any Bambu printer with a long pre-print calibration sequence) the "First Layer Complete" notification fired during PREPARE, not after layer 1 was actually printed — the attached photo showed a lowered bed + parked toolhead + clean plate, because the firmware ticks `layer_num` during homing / auto-bed-leveling / bed-surface scan / nozzle clean *before* the first real extrusion. Reporter's log timeline made it explicit: print start at 13:54:27, notification fired at 14:10:13 with `[SNAPSHOT] Capturing fresh frame`, `gcode_state: RUNNING` not seen until 14:44:28 — i.e. the notification went out ~30 minutes before the print actually started. The trigger in `main.py:6044` only gated on `2 <= layer_num <= 5` with no check that the printer was actually printing. **Fix.** The trigger now requires `state.state == "RUNNING"` AND `state.mc_print_sub_stage in (None, 0)` — `0` is the "Printing" stage in the canonical Bambu `STAGE_NAMES` map (`bambu_mqtt.py:376`), so the non-zero pre-print sub-stages (`1` Auto bed leveling, `9` Scanning bed surface, `10` Inspecting first layer, `13` Homing toolhead, `14` Cleaning nozzle tip, …) all skip. `None` is preserved as a no-opinion fall-through for any firmware that doesn't push `mc_print_sub_stage` so unknown-firmware installs keep their existing behaviour. `_first_layer_notified` is only set once the gate passes, so calibration-phase `layer_num` ticks are non-consuming — the next on_layer_change edge after the printer enters real printing fires the notification. The trigger window widens from `[2, 5]` to `[2, 10]` so that if calibration consumes several `layer_num` slots before RUNNING, the deferred edge still falls inside. **Tests.** Manual verification via the issue reporter's installation; no new unit tests added (the on_layer_change closure is wired inside an event-handler factory and isn't a unit-testable pure function — would require a substantial fixture rewrite for a one-condition guard that's already covered by integration of the printer-state machine). **Scope.** Backend-only, single-file change. No DB migration. No new permission. No frontend change. No new i18n key. The window widening doesn't risk firing a stale notification on prints whose `layer_num` advances past 10 during PREPARE — the RUNNING + sub-stage gate ensures the notification only fires when the printer is actually printing, regardless of how many ticks PREPARE consumed.
- **Multi-nozzle prints no longer collapse all filaments onto one nozzle (#1825, reporter @needo37)** — The single-active-extruder shortcut added in #851 (for #827) at `threemf_tools.py:354` runs `before` the per-filament `group_id` mapping, and fires whenever `extruder_nozzle_stats` reports exactly one extruder as having a nozzle installed. On the H2D / H2D Pro / X2D (2-nozzle) and H2C (3+-nozzle tool-changer), this field is data-driven from the slicer profile's enumerated nozzle volume types — when an HT-AMS or High-Flow nozzle's type isn't enumerated in the slice's profile (common with asymmetric extruder setups, e.g. HT-AMS feeding the right nozzle on an H2D), the slicer emits e.g. `['Standard#1', 'Standard#0']` even though the print genuinely uses both extruders. `sum(active_extruders) == 1` triggered → every filament was force-assigned to `physical_extruder_map[active_idx]`, the authoritative per-filament `group_id` was discarded, and the Filament Mapping panel showed both filaments badged **L** with the auto-match hard filter (`print_scheduler.py` `_compute_ams_mapping_for_printer` ~line 1239) blocking the wrong-nozzle tray as "Type not found". Bug is **parser-side and model-agnostic** — triggers purely on 3MF data shape, not on the attached AMS hardware: regular dual-AMS H2D installs typically slice to `['Standard#1', 'Standard#1']` (sum==2) and never enter the buggy branch, which is why this bug was invisible on the most common dual-AMS setup. Physical nozzle routing was **not** affected — the actual extrude path comes from the sliced gcode + the verbatim `nozzle_mapping` from the project_file (#1780), not from this parse — so the bug surfaced as auto-match failure + wrong L/R badge, not wrong-nozzle extrusion. **Fix.** Gate the single-active shortcut on `len(distinct_group_ids) <= 1` from `slice_info.config`. The slice_info parse is hoisted above the shortcut check (and reused by Priority 1) so the gate adds zero extra I/O. When the slice contains ≥2 distinct group_ids, the shortcut skips and the existing `group_id`-based Priority 1 mapping runs. The gate only **narrows** the shortcut path — it can't widen the buggy collapse onto any previously-working slice. The same condition generalizes to H2C and any future N-nozzle printer for free (no nozzle-count branching). **Tests.** Two new cases in `TestExtractNozzleMappingFrom3MF`: `test_single_active_under_report_with_multi_group_falls_through` pins the #1825 regression (`['Standard#1','Standard#0']` + group_ids `{0,1}` → `{1:1, 2:0}` not `{1:1, 2:1}`); `test_single_active_with_single_group_still_uses_shortcut` preserves the #851 behaviour (same stats + only `group_id=0` → shortcut still fires → `{1:1, 2:1}`). Existing `test_single_active_extruder_maps_all_slots` and `test_two_active_extruders_falls_through` stay green. **Suites.** `pytest -n 30 backend/tests/unit/test_scheduler_ams_mapping.py backend/tests/unit/test_scheduler_filament_deficit.py backend/tests/unit/test_scheduler_filament_override.py backend/tests/unit/test_fallback_archive_mqtt_filament.py backend/tests/integration/test_archives_api.py backend/tests/integration/test_library_api.py` 272/272 green. `ruff check backend/` clean. **Scope.** Backend-only, parse layer. No DB migration. No new permission. No frontend change. The L/R-only badge limitation on 3+-nozzle printers (H2C tool-changer) called out in the report is a separate cosmetic follow-up and not part of this fix.
@@ -20,7 +20,7 @@ import { describe, it, expect } from 'vitest';
type Status = {
connected: boolean;
state: string | null;
hms_errors?: { code: string; attr: number; severity: number }[];
hms_errors?: { code: string; attr: number; severity: number; actions?: string[] }[];
};
type Bucket = 'printing' | 'paused' | 'finished' | 'idle' | 'offline' | 'error';
@@ -32,7 +32,8 @@ function filterKnownHMSErrors(errors: Status['hms_errors']): NonNullable<Status[
const codeNum = parseInt(e.code.replace('0x', ''), 16) || 0;
const module = ((e.attr >> 16) & 0xFFFF).toString(16).padStart(4, '0').toUpperCase();
const code = (codeNum & 0xFFFF).toString(16).padStart(4, '0').toUpperCase();
return KNOWN_HMS_CODES.has(`${module}_${code}`);
if (KNOWN_HMS_CODES.has(`${module}_${code}`)) return true;
return (e.actions?.length ?? 0) > 0;
});
}
@@ -77,6 +78,20 @@ describe('FAILED-without-HMS bucketing', () => {
expect(classifyPrinterStatus(cancelEcho)).toBe('finished');
});
it('classifies PAUSE + uncataloged HMS WITH actions as "error" (#1840: H2C 0500_809C carries actions but isnt in the bundled catalog)', () => {
const h2cActionableFault: Status = {
connected: true,
state: 'PAUSE',
hms_errors: [{
code: '0x809c',
attr: 0x0500_809C,
severity: 3,
actions: ['IGNORE_RESUME', 'PROBLEM_SOLVED_RESUME'],
}],
};
expect(classifyPrinterStatus(h2cActionableFault)).toBe('error');
});
it('classifies FINISH as "finished" (unchanged baseline)', () => {
const completedPrinter: Status = { connected: true, state: 'FINISH' };
expect(classifyPrinterStatus(completedPrinter)).toBe('finished');
+14 -9
View File
@@ -896,12 +896,20 @@ function getShortCode(attr: number, code: number): string {
return `${module.toString(16).padStart(4, '0').toUpperCase()}_${codeNum.toString(16).padStart(4, '0').toUpperCase()}`;
}
// Helper to filter only known HMS errors (exported for use in badge counts)
// Helper to filter HMS errors the UI should surface (exported for use in badge counts).
// Keeps an error if EITHER:
// - it's in the bundled ERROR_DESCRIPTIONS catalog (known, has a description), OR
// - it carries firmware actions (uncataloged but user-actionable — e.g. H2C 0500_809C
// with IGNORE_RESUME/PROBLEM_SOLVED_RESUME — must surface so the button can render).
// Drops uncataloged errors WITHOUT actions: those are transient junk like the post-cancel
// 0C00_001B echo that re-introduces the FAILED-after-cancel "1 problem forever"
// regression — see PrintersPageBucketing.test.ts.
export function filterKnownHMSErrors(errors: HMSError[]): HMSError[] {
return errors.filter((error) => {
const codeNum = parseInt(error.code.replace('0x', ''), 16) || 0;
const shortCode = getShortCode(error.attr, codeNum);
return ERROR_DESCRIPTIONS[shortCode] !== undefined;
if (ERROR_DESCRIPTIONS[shortCode] !== undefined) return true;
return (error.actions?.length ?? 0) > 0;
});
}
@@ -925,12 +933,9 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
},
});
// Filter to only show errors we have descriptions for (skip unknown codes)
const knownErrors = errors.filter((error) => {
const codeNum = parseInt(error.code.replace('0x', ''), 16) || 0;
const shortCode = getShortCode(error.attr, codeNum);
return ERROR_DESCRIPTIONS[shortCode] !== undefined;
});
// Surface cataloged errors and uncataloged-but-actionable errors. Mirrors
// filterKnownHMSErrors so the modal and the badge counts agree.
const knownErrors = filterKnownHMSErrors(errors);
// Close on Escape key
useEffect(() => {
@@ -998,7 +1003,7 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
const { label, color, bgColor, buttonHoverColor, Icon } = getSeverityInfo(error.severity);
const codeNum = parseInt(error.code.replace('0x', ''), 16) || 0;
const shortCode = getShortCode(error.attr, codeNum);
const description = ERROR_DESCRIPTIONS[shortCode];
const description = ERROR_DESCRIPTIONS[shortCode] ?? t('hmsErrors.unknownCode');
const hmsHomeUrl = getHMSHomeUrl();
const displayCode = shortCode.replace('_', '-');
+1
View File
@@ -2755,6 +2755,7 @@ export default {
title: 'Fehler - {{name}}',
noErrors: 'Keine Fehler',
viewOnWiki: 'Im Bambu Lab Wiki ansehen',
unknownCode: 'Unbekannter HMS-Code — Details siehe Bambu Lab Wiki.',
clearInstructions: 'Löschen Sie die Fehler am Drucker, um sie hier zu entfernen.',
clearErrors: 'Fehler löschen',
clearSuccess: 'HMS-Fehler gelöscht',
+1
View File
@@ -2784,6 +2784,7 @@ export default {
title: 'Errors - {{name}}',
noErrors: 'No errors',
viewOnWiki: 'View on Bambu Lab Wiki',
unknownCode: 'Unknown HMS code — see the Bambu Lab wiki for details.',
clearInstructions: 'Clear errors on the printer to dismiss them here.',
clearErrors: 'Clear Errors',
clearSuccess: 'HMS errors cleared',
+1
View File
@@ -2758,6 +2758,7 @@ export default {
title: 'Errores - {{name}}',
noErrors: 'No hay errores',
viewOnWiki: 'Ver en la wiki de Bambu Lab',
unknownCode: 'Código HMS desconocido — consulta la wiki de Bambu Lab para más detalles.',
clearInstructions: 'Borre los errores en la impresora para descartarlos aquí.',
clearErrors: 'Borrar errores',
clearSuccess: 'Errores HMS borrados',
+1
View File
@@ -2744,6 +2744,7 @@ export default {
title: 'Erreurs - {{name}}',
noErrors: 'Aucune erreur',
viewOnWiki: 'Voir sur le Wiki Bambu Lab',
unknownCode: 'Code HMS inconnu — consultez le wiki Bambu Lab pour plus de détails.',
clearInstructions: 'Effacez les erreurs sur l\'imprimante pour les retirer ici.',
clearErrors: 'Effacer les erreurs',
clearSuccess: 'Erreurs HMS effacées',
+1
View File
@@ -2743,6 +2743,7 @@ export default {
title: 'Errori - {{name}}',
noErrors: 'Nessun errore',
viewOnWiki: 'Vedi su Bambu Lab Wiki',
unknownCode: 'Codice HMS sconosciuto — consulta la wiki di Bambu Lab per i dettagli.',
clearInstructions: 'Cancella gli errori sulla stampante per rimuoverli qui.',
clearErrors: 'Cancella errori',
clearSuccess: 'Errori HMS cancellati',
+1
View File
@@ -2755,6 +2755,7 @@ export default {
title: 'エラー - {{name}}',
noErrors: 'エラーなし',
viewOnWiki: 'Bambu Lab Wikiで表示',
unknownCode: '不明なHMSコード — 詳細はBambu Lab Wikiを参照してください。',
clearInstructions: 'プリンターでエラーをクリアするとここからも消えます。',
clearErrors: 'エラーをクリア',
clearSuccess: 'HMSエラーをクリアしました',
+1
View File
@@ -2605,6 +2605,7 @@ export default {
title: '오류 - {{name}}',
noErrors: '오류 없음',
viewOnWiki: 'Bambu Lab 위키에서 보기',
unknownCode: '알 수 없는 HMS 코드 — 자세한 내용은 Bambu Lab 위키를 참조하세요.',
clearInstructions: '오류를 해제하려면 프린터에서 오류를 지우세요.',
clearErrors: '오류 지우기',
clearSuccess: 'HMS 오류가 지워졌습니다',
+1
View File
@@ -2743,6 +2743,7 @@ export default {
title: 'Erros - {{name}}',
noErrors: 'Nenhum erro',
viewOnWiki: 'Ver no Bambu Lab Wiki',
unknownCode: 'Código HMS desconhecido — consulte o wiki da Bambu Lab para mais detalhes.',
clearInstructions: 'Limpe os erros na impressora para descartá-los aqui.',
clearErrors: 'Limpar Erros',
clearSuccess: 'Erros HMS limpos',
+1
View File
@@ -2759,6 +2759,7 @@ export default {
title: 'Hatalar - {{name}}',
noErrors: 'Hata yok',
viewOnWiki: 'Bambu Lab Wiki\'de görüntüle',
unknownCode: 'Bilinmeyen HMS kodu — ayrıntılar için Bambu Lab wiki sayfasına bakın.',
clearInstructions: 'Buradan kapatmak için yazıcıdaki hataları temizleyin.',
clearErrors: 'Hataları Temizle',
clearSuccess: 'HMS hataları temizlendi',
+1
View File
@@ -2743,6 +2743,7 @@ export default {
title: '错误 - {{name}}',
noErrors: '无错误',
viewOnWiki: '在拓竹 Wiki 上查看',
unknownCode: '未知 HMS 代码 — 详情请参阅拓竹 Wiki。',
clearInstructions: '在打印机上清除错误以在此处消除它们。',
clearErrors: '清除错误',
clearSuccess: 'HMS 错误已清除',
+1
View File
@@ -2743,6 +2743,7 @@ export default {
title: '錯誤 - {{name}}',
noErrors: '無錯誤',
viewOnWiki: '在拓竹 Wiki 上檢視',
unknownCode: '未知 HMS 代碼 — 詳情請參閱拓竹 Wiki。',
clearInstructions: '在印表機上清除錯誤以在此處消除它們。',
clearErrors: '清除錯誤',
clearSuccess: 'HMS 錯誤已清除',
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-ChnivgH9.js"></script>
<script type="module" crossorigin src="/assets/index-CKTYjVC_.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BYzbe9TT.css">
</head>
<body>