= {
@@ -917,7 +934,7 @@ function getHMSHomeUrl(): string {
return `https://wiki.bambulab.com/en/hms/home`;
}
-export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPermission }: HMSErrorModalProps) {
+export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPermission, runoutGuidance }: HMSErrorModalProps) {
const { t } = useTranslation();
const { showToast } = useToast();
const queryClient = useQueryClient();
@@ -1003,7 +1020,24 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
const { label, color, bgColor, Icon } = getSeverityInfo(error.severity);
const codeNum = parseInt(error.code.replace('0x', ''), 16) || 0;
const shortCode = getShortCode(error.attr, codeNum);
- const description = ERROR_DESCRIPTIONS[shortCode] ?? t('hmsErrors.unknownCode');
+ // Runout guidance (#2587): for an AMS per-slot runout on a paused
+ // print, name the slot the firmware now expects rather than the
+ // misleading generic "insert into the same slot" text.
+ let description = ERROR_DESCRIPTIONS[shortCode] ?? t('hmsErrors.unknownCode');
+ if (runoutGuidance && AMS_RUNOUT_SHORT_CODES.has(shortCode)) {
+ if (runoutGuidance.expectedSlotLabel && runoutGuidance.ranOutSlotLabel) {
+ description = t('hmsErrors.runoutExpectedSlot', {
+ expected: runoutGuidance.expectedSlotLabel,
+ ranOut: runoutGuidance.ranOutSlotLabel,
+ });
+ } else if (runoutGuidance.expectedSlotLabel) {
+ description = t('hmsErrors.runoutExpectedSlotOnly', {
+ expected: runoutGuidance.expectedSlotLabel,
+ });
+ } else {
+ description = t('hmsErrors.runoutSlotUnknown');
+ }
+ }
const hmsHomeUrl = getHMSHomeUrl();
const displayCode = shortCode.replace('_', '-');
diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts
index c82b83789..692574f07 100644
--- a/frontend/src/i18n/locales/de.ts
+++ b/frontend/src/i18n/locales/de.ts
@@ -604,6 +604,12 @@ export default {
title: 'Dieser Slot ist Filament {{n}} im aktiven Druck',
ariaLabel: 'Aktiver Druck-Slot {{n}}',
},
+ expectedSlot: {
+ title: 'Der Drucker wartet auf Filament in diesem Slot',
+ ariaLabel: 'Erwarteter Filament-Slot {{n}}',
+ label: '{{ams}} · Slot {{slot}}',
+ external: 'Externe Spule',
+ },
// Filaments section
filaments: 'Filamente',
// Camera
@@ -2805,6 +2811,9 @@ export default {
clearFailed: 'HMS-Fehler konnten nicht gelöscht werden',
actionSuccess: 'Aktion an Drucker gesendet',
actionFailed: 'Aktion konnte nicht gesendet werden',
+ runoutExpectedSlot: 'Das Filament in {{ranOut}} ist aufgebraucht. Der Drucker wartet jetzt auf kompatibles Filament in {{expected}}. Legen Sie eine Spule in {{expected}} ein und wählen Sie dann Wiederholen.',
+ runoutExpectedSlotOnly: 'Der Drucker wartet auf kompatibles Filament in {{expected}}. Legen Sie dort eine Spule ein und wählen Sie dann Wiederholen.',
+ runoutSlotUnknown: 'Das Filament ist aufgebraucht und der Druck ist pausiert. Bambuddy konnte nicht ermitteln, welchen Slot der Drucker jetzt erwartet — prüfen Sie am Druckerdisplay, welcher Slot angefordert wird.',
actions: {
RESUME_PRINTING: 'Druck fortsetzen',
RESUME_PRINTING_DEFECTS: 'Fortsetzen (Mängel akzeptabel)',
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index 09000df30..4195cae7f 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -608,6 +608,12 @@ export default {
title: 'This slot is filament {{n}} in the active print',
ariaLabel: 'Active print slot {{n}}',
},
+ expectedSlot: {
+ title: 'The printer is waiting for filament in this slot',
+ ariaLabel: 'Expected filament slot {{n}}',
+ label: '{{ams}} · Slot {{slot}}',
+ external: 'External spool',
+ },
// Filaments section
filaments: 'Filaments',
// Camera
@@ -2834,6 +2840,9 @@ export default {
clearFailed: 'Failed to clear HMS errors',
actionSuccess: 'Action sent to printer',
actionFailed: 'Failed to send action',
+ runoutExpectedSlot: 'Filament ran out in {{ranOut}}. The printer is now waiting for compatible filament in {{expected}}. Insert a spool into {{expected}}, then select Retry.',
+ runoutExpectedSlotOnly: 'The printer is waiting for compatible filament in {{expected}}. Insert a spool there, then select Retry.',
+ runoutSlotUnknown: 'Filament ran out and the print is paused. Bambuddy could not determine which slot the printer now expects — check the printer screen for the requested slot.',
actions: {
RESUME_PRINTING: "Resume Printing",
RESUME_PRINTING_DEFECTS: "Resume (defects acceptable)",
diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts
index a42b17ac9..21929f325 100644
--- a/frontend/src/i18n/locales/es.ts
+++ b/frontend/src/i18n/locales/es.ts
@@ -604,6 +604,12 @@ export default {
title: 'Este slot es el filamento {{n}} en la impresión activa',
ariaLabel: 'Slot de impresión activa {{n}}',
},
+ expectedSlot: {
+ title: 'La impresora está esperando filamento en este slot',
+ ariaLabel: 'Slot de filamento esperado {{n}}',
+ label: '{{ams}} · Slot {{slot}}',
+ external: 'Bobina externa',
+ },
// Filaments section
filaments: 'Filamentos',
// Camera
@@ -2808,6 +2814,9 @@ export default {
clearFailed: 'Error al borrar los errores HMS',
actionSuccess: 'Acción enviada a la impresora',
actionFailed: 'No se pudo enviar la acción',
+ runoutExpectedSlot: 'El filamento se agotó en {{ranOut}}. La impresora ahora espera filamento compatible en {{expected}}. Inserta una bobina en {{expected}} y luego selecciona Reintentar.',
+ runoutExpectedSlotOnly: 'La impresora está esperando filamento compatible en {{expected}}. Inserta una bobina ahí y luego selecciona Reintentar.',
+ runoutSlotUnknown: 'El filamento se agotó y la impresión está en pausa. Bambuddy no pudo determinar qué slot espera ahora la impresora — revisa la pantalla de la impresora para ver el slot solicitado.',
actions: {
RESUME_PRINTING: 'Reanudar impresión',
RESUME_PRINTING_DEFECTS: 'Reanudar (defectos aceptables)',
diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts
index 8b303d4d2..9b85a31af 100644
--- a/frontend/src/i18n/locales/fr.ts
+++ b/frontend/src/i18n/locales/fr.ts
@@ -604,6 +604,12 @@ export default {
title: 'Cet emplacement est le filament {{n}} dans l\'impression active',
ariaLabel: 'Emplacement d\'impression active {{n}}',
},
+ expectedSlot: {
+ title: 'L\'imprimante attend du filament dans cet emplacement',
+ ariaLabel: 'Emplacement de filament attendu {{n}}',
+ label: '{{ams}} · Emplacement {{slot}}',
+ external: 'Bobine externe',
+ },
// Filaments section
filaments: 'Filaments',
// Camera
@@ -2794,6 +2800,9 @@ export default {
clearFailed: 'Échec de l\'effacement des erreurs HMS',
actionSuccess: 'Action envoyée à l\'imprimante',
actionFailed: 'Échec de l\'envoi de l\'action',
+ runoutExpectedSlot: 'Le filament est épuisé dans {{ranOut}}. L\'imprimante attend maintenant du filament compatible dans {{expected}}. Insérez une bobine dans {{expected}}, puis sélectionnez Réessayer.',
+ runoutExpectedSlotOnly: 'L\'imprimante attend du filament compatible dans {{expected}}. Insérez-y une bobine, puis sélectionnez Réessayer.',
+ runoutSlotUnknown: 'Le filament est épuisé et l\'impression est en pause. Bambuddy n\'a pas pu déterminer quel emplacement l\'imprimante attend désormais — vérifiez l\'écran de l\'imprimante pour l\'emplacement demandé.',
actions: {
RESUME_PRINTING: 'Reprendre l\'impression',
RESUME_PRINTING_DEFECTS: 'Reprendre (défauts acceptables)',
diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts
index 9711379c8..ebf29a371 100644
--- a/frontend/src/i18n/locales/it.ts
+++ b/frontend/src/i18n/locales/it.ts
@@ -604,6 +604,12 @@ export default {
title: 'Questo slot è il filamento {{n}} nella stampa attiva',
ariaLabel: 'Slot stampa attiva {{n}}',
},
+ expectedSlot: {
+ title: 'La stampante è in attesa di filamento in questo slot',
+ ariaLabel: 'Slot filamento previsto {{n}}',
+ label: '{{ams}} · Slot {{slot}}',
+ external: 'Bobina esterna',
+ },
// Filaments section
filaments: 'Filamenti',
// Camera
@@ -2793,6 +2799,9 @@ export default {
clearFailed: 'Impossibile cancellare gli errori HMS',
actionSuccess: 'Azione inviata alla stampante',
actionFailed: 'Impossibile inviare l\'azione',
+ runoutExpectedSlot: 'Il filamento in {{ranOut}} è esaurito. La stampante ora attende filamento compatibile in {{expected}}. Inserisci una bobina in {{expected}}, quindi seleziona Riprova.',
+ runoutExpectedSlotOnly: 'La stampante attende filamento compatibile in {{expected}}. Inserisci lì una bobina, quindi seleziona Riprova.',
+ runoutSlotUnknown: 'Il filamento è esaurito e la stampa è in pausa. Bambuddy non è riuscito a determinare quale slot la stampante attende ora — controlla lo schermo della stampante per lo slot richiesto.',
actions: {
RESUME_PRINTING: 'Riprendi stampa',
RESUME_PRINTING_DEFECTS: 'Riprendi (difetti accettabili)',
diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts
index 3f432b96e..416e7f606 100644
--- a/frontend/src/i18n/locales/ja.ts
+++ b/frontend/src/i18n/locales/ja.ts
@@ -603,6 +603,12 @@ export default {
title: 'このスロットはアクティブな印刷のフィラメント {{n}} です',
ariaLabel: 'アクティブ印刷スロット {{n}}',
},
+ expectedSlot: {
+ title: 'プリンターはこのスロットのフィラメントを待っています',
+ ariaLabel: '要求スロット {{n}}',
+ label: '{{ams}} · スロット {{slot}}',
+ external: '外部スプール',
+ },
// Filaments section
filaments: 'フィラメント',
// Camera
@@ -2805,6 +2811,9 @@ export default {
clearFailed: 'HMSエラーのクリアに失敗しました',
actionSuccess: 'アクションをプリンターに送信しました',
actionFailed: 'アクションの送信に失敗しました',
+ runoutExpectedSlot: '{{ranOut}} のフィラメントが切れました。プリンターは現在 {{expected}} に対応するフィラメントを待っています。{{expected}} にスプールをセットして、「再試行」を選択してください。',
+ runoutExpectedSlotOnly: 'プリンターは {{expected}} に対応するフィラメントを待っています。そこにスプールをセットして、「再試行」を選択してください。',
+ runoutSlotUnknown: 'フィラメントが切れて印刷が一時停止しています。プリンターが現在要求しているスロットを Bambuddy が特定できませんでした。プリンターの画面で要求されているスロットを確認してください。',
actions: {
RESUME_PRINTING: '印刷を再開',
RESUME_PRINTING_DEFECTS: '再開(不具合を許容)',
diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts
index d637443e6..2dd7280f4 100644
--- a/frontend/src/i18n/locales/ko.ts
+++ b/frontend/src/i18n/locales/ko.ts
@@ -567,6 +567,12 @@ export default {
title: '이 슬롯은 활성 인쇄의 필라멘트 {{n}}입니다',
ariaLabel: '활성 인쇄 슬롯 {{n}}'
},
+ expectedSlot: {
+ title: '프린터가 이 슬롯의 필라멘트를 기다리고 있습니다',
+ ariaLabel: '요청된 필라멘트 슬롯 {{n}}',
+ label: '{{ams}} · 슬롯 {{slot}}',
+ external: '외부 스풀',
+ },
filaments: '필라멘트',
openCameraOverlay: '카메라 오버레이 열기',
openCameraWindow: '새 창에서 카메라 열기',
@@ -2655,6 +2661,9 @@ export default {
clearFailed: 'HMS 오류 지우기 실패',
actionSuccess: '프린터에 작업을 전송함',
actionFailed: '작업 전송 실패',
+ runoutExpectedSlot: '{{ranOut}}의 필라멘트가 소진되었습니다. 프린터가 이제 {{expected}}에 호환 필라멘트를 기다리고 있습니다. {{expected}}에 스풀을 넣은 다음 다시 시도를 선택하세요.',
+ runoutExpectedSlotOnly: '프린터가 {{expected}}에 호환 필라멘트를 기다리고 있습니다. 거기에 스풀을 넣은 다음 다시 시도를 선택하세요.',
+ runoutSlotUnknown: '필라멘트가 소진되어 인쇄가 일시정지되었습니다. Bambuddy가 프린터가 현재 요청하는 슬롯을 확인할 수 없습니다 — 프린터 화면에서 요청된 슬롯을 확인하세요.',
actions: {
RESUME_PRINTING: '인쇄 재개',
RESUME_PRINTING_DEFECTS: '재개 (결함 허용)',
diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts
index aebf4c774..57453aa99 100644
--- a/frontend/src/i18n/locales/pt-BR.ts
+++ b/frontend/src/i18n/locales/pt-BR.ts
@@ -604,6 +604,12 @@ export default {
title: 'Este slot é o filamento {{n}} na impressão ativa',
ariaLabel: 'Slot de impressão ativa {{n}}',
},
+ expectedSlot: {
+ title: 'A impressora está aguardando filamento neste slot',
+ ariaLabel: 'Slot de filamento esperado {{n}}',
+ label: '{{ams}} · Slot {{slot}}',
+ external: 'Bobina externa',
+ },
// Filaments section
filaments: 'Filamentos',
// Camera
@@ -2793,6 +2799,9 @@ export default {
clearFailed: 'Falha ao limpar erros HMS',
actionSuccess: 'Ação enviada à impressora',
actionFailed: 'Falha ao enviar ação',
+ runoutExpectedSlot: 'O filamento acabou em {{ranOut}}. A impressora agora aguarda filamento compatível em {{expected}}. Insira uma bobina em {{expected}} e selecione Tentar novamente.',
+ runoutExpectedSlotOnly: 'A impressora está aguardando filamento compatível em {{expected}}. Insira uma bobina ali e selecione Tentar novamente.',
+ runoutSlotUnknown: 'O filamento acabou e a impressão está pausada. O Bambuddy não conseguiu determinar qual slot a impressora agora espera — verifique a tela da impressora para o slot solicitado.',
actions: {
RESUME_PRINTING: 'Retomar impressão',
RESUME_PRINTING_DEFECTS: 'Retomar (defeitos aceitáveis)',
diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts
index 2bb58a408..becce60ef 100644
--- a/frontend/src/i18n/locales/tr.ts
+++ b/frontend/src/i18n/locales/tr.ts
@@ -604,6 +604,12 @@ export default {
title: 'Bu slot, aktif baskıdaki {{n}} numaralı filament',
ariaLabel: 'Aktif baskı slotu {{n}}',
},
+ expectedSlot: {
+ title: 'Yazıcı bu slotta filament bekliyor',
+ ariaLabel: 'Beklenen filament slotu {{n}}',
+ label: '{{ams}} · Slot {{slot}}',
+ external: 'Harici makara',
+ },
// Filamentler bölümü
filaments: 'Filamentler',
// Kamera
@@ -2809,6 +2815,9 @@ export default {
clearFailed: 'HMS hataları temizlenemedi',
actionSuccess: 'Eylem yazıcıya gönderildi',
actionFailed: 'Eylem gönderilemedi',
+ runoutExpectedSlot: '{{ranOut}} slotundaki filament bitti. Yazıcı şimdi {{expected}} slotunda uyumlu filament bekliyor. {{expected}} slotuna bir makara takın ve ardından Yeniden Dene\'yi seçin.',
+ runoutExpectedSlotOnly: 'Yazıcı {{expected}} slotunda uyumlu filament bekliyor. Oraya bir makara takın ve ardından Yeniden Dene\'yi seçin.',
+ runoutSlotUnknown: 'Filament bitti ve baskı duraklatıldı. Bambuddy yazıcının şu anda hangi slotu beklediğini belirleyemedi — istenen slot için yazıcının ekranını kontrol edin.',
actions: {
RESUME_PRINTING: 'Baskıyı sürdür',
RESUME_PRINTING_DEFECTS: 'Sürdür (kusurlar kabul edilebilir)',
diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts
index a126329ba..41218588f 100644
--- a/frontend/src/i18n/locales/zh-CN.ts
+++ b/frontend/src/i18n/locales/zh-CN.ts
@@ -604,6 +604,12 @@ export default {
title: '此料槽在当前打印中是耗材 {{n}}',
ariaLabel: '当前打印料槽 {{n}}',
},
+ expectedSlot: {
+ title: '打印机正在等待此料槽装入耗材',
+ ariaLabel: '需要装料的料槽 {{n}}',
+ label: '{{ams}} · 料槽 {{slot}}',
+ external: '外部料卷',
+ },
// Filaments section
filaments: '耗材',
// Camera
@@ -2793,6 +2799,9 @@ export default {
clearFailed: '清除 HMS 错误失败',
actionSuccess: '已向打印机发送操作',
actionFailed: '操作发送失败',
+ runoutExpectedSlot: '{{ranOut}} 的耗材已用尽。打印机现在正在等待 {{expected}} 装入兼容耗材。请将料卷装入 {{expected}},然后选择"重试"。',
+ runoutExpectedSlotOnly: '打印机正在等待 {{expected}} 装入兼容耗材。请在该料槽装入料卷,然后选择"重试"。',
+ runoutSlotUnknown: '耗材已用尽,打印已暂停。Bambuddy 无法确定打印机现在需要哪个料槽——请在打印机屏幕上查看所请求的料槽。',
actions: {
RESUME_PRINTING: '恢复打印',
RESUME_PRINTING_DEFECTS: '恢复 (缺陷可接受)',
diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts
index 35ef881ce..eb569aa67 100644
--- a/frontend/src/i18n/locales/zh-TW.ts
+++ b/frontend/src/i18n/locales/zh-TW.ts
@@ -604,6 +604,12 @@ export default {
title: '此料槽在目前列印中是耗材 {{n}}',
ariaLabel: '目前列印料槽 {{n}}',
},
+ expectedSlot: {
+ title: '印表機正在等待此料槽裝入耗材',
+ ariaLabel: '需要裝料的料槽 {{n}}',
+ label: '{{ams}} · 料槽 {{slot}}',
+ external: '外部料卷',
+ },
// Filaments section
filaments: '耗材',
// Camera
@@ -2793,6 +2799,9 @@ export default {
clearFailed: '清除 HMS 錯誤失敗',
actionSuccess: '已向印表機傳送動作',
actionFailed: '動作傳送失敗',
+ runoutExpectedSlot: '{{ranOut}} 的耗材已用盡。印表機現在正在等待 {{expected}} 裝入相容耗材。請將料卷裝入 {{expected}},然後選擇「重試」。',
+ runoutExpectedSlotOnly: '印表機正在等待 {{expected}} 裝入相容耗材。請在該料槽裝入料卷,然後選擇「重試」。',
+ runoutSlotUnknown: '耗材已用盡,列印已暫停。Bambuddy 無法確定印表機現在需要哪個料槽——請在印表機螢幕上查看所請求的料槽。',
actions: {
RESUME_PRINTING: '恢復列印',
RESUME_PRINTING_DEFECTS: '恢復 (瑕疵可接受)',
diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx
index d5be9a72b..ec45b1810 100644
--- a/frontend/src/pages/PrintersPage.tsx
+++ b/frontend/src/pages/PrintersPage.tsx
@@ -2105,6 +2105,34 @@ function PrinterCard({
? currentTrayNow
: cachedTrayNow.current;
+ // Runout / filament-replacement guidance (#2587). The backend fills these only
+ // while the print is PAUSED (global tray IDs, or null when idle/unresolvable):
+ // expectedTray = the slot the firmware now expects filament in
+ // previousTray = the slot that ran out
+ // With AMS Filament Backup the firmware advances to the next compatible slot,
+ // so these are often different — the graphic highlights the expected one.
+ const expectedTray = status?.expected_tray ?? null;
+ const previousTray = status?.previous_tray ?? null;
+ // Pre-format the runout slot labels (honoring user AMS friendly names) for the
+ // HMS modal (#2587). null when the slot can't be placed → honest fallback copy.
+ const formatRunoutSlotLabel = (globalId: number | null): string | null => {
+ if (globalId === null) return null;
+ if (globalId === 254) return t('printers.expectedSlot.external');
+ if (globalId >= 128 && globalId <= 135) {
+ return amsLabels?.[globalId] || getAmsLabel(globalId, 1);
+ }
+ const amsId = Math.floor(globalId / 4);
+ const slot = globalId % 4;
+ const amsName = amsLabels?.[amsId] || getAmsLabel(amsId, 4);
+ return t('printers.expectedSlot.label', { ams: amsName, slot: slot + 1 });
+ };
+ const runoutGuidance = status?.state === 'PAUSE'
+ ? {
+ expectedSlotLabel: formatRunoutSlotLabel(expectedTray),
+ ranOutSlotLabel: formatRunoutSlotLabel(previousTray),
+ }
+ : null;
+
// Fetch smart plug for this printer
const { data: smartPlug } = useQuery({
queryKey: ['smartPlugByPrinter', printer.id],
@@ -4633,6 +4661,10 @@ function PrinterCard({
// Global tray ID = ams.id * 4 + slot index (for standard AMS)
const globalTrayId = ams.id * 4 + slotIdx;
const isActive = effectiveTrayNow === globalTrayId;
+ // Runout guidance (#2587): the slot the paused print now
+ // expects filament in, and the slot that ran out.
+ const isExpectedSlot = expectedTray !== null && expectedTray === globalTrayId;
+ const isRanOutSlot = previousTray !== null && previousTray === globalTrayId;
// Get cloud preset info if available
const cloudInfo = tray?.tray_info_idx ? filamentInfo?.[tray.tray_info_idx] : null;
// Get saved slot preset mapping (for user-configured slots)
@@ -4704,8 +4736,25 @@ function PrinterCard({
// Slot visual content (goes inside hover card)
const slotVisual = (