Просмотр исходного кода

fix(ams): show "?" for loaded-but-unconfigured AMS slots (#1694)

  Reporter on a 3-AMS P1S saw slots labelled "Empty" even though spools
  were physically loaded - OrcaSlicer's Device view showed the same
  slots as loaded.

  Root cause: the compact label below the AMS slot circle rendered
  tray.tray_type || t('ams.slotEmpty'), falling back to "Empty" whenever
  the printer firmware hadn't been told which material is in the slot.
  getEmptySlotKind already distinguishes 'physical' (firmware confirmed
  empty via state 9/10) from 'reset' (tray_type absent but firmware
  hasn't confirmed empty - spool loaded, just unassigned). The hover
  card and circle border already used that distinction; the compact
  label did not.

  Fix: label branches on emptyKind - 'physical' keeps "Empty", 'reset'
  shows "?" matching the slicer's own convention. External / VT tray
  label is unchanged (external trays have no "configured/unconfigured"
  distinction - they're either loaded or not). SpoolBuddy AmsUnitCard
  carried the same bug and got the same fix plus a tooltip.
maziggy 2 месяцев назад
Родитель
Сommit
529155413f

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 23 - 2
frontend/src/__tests__/components/spoolbuddy/AmsUnitCard.test.tsx

@@ -50,7 +50,9 @@ function makeUnit(overrides: Partial<AMSUnit> = {}): AMSUnit {
       makeTray({ id: 0, tray_color: 'FF0000FF', tray_type: 'PLA', remain: 80 }),
       makeTray({ id: 1, tray_color: '00FF00FF', tray_type: 'PETG', remain: 50 }),
       makeTray({ id: 2, tray_color: '0000FFFF', tray_type: 'ABS', remain: 10 }),
-      makeTray({ id: 3, tray_color: null, tray_type: '', remain: -1 }),
+      // state=9 = firmware-confirmed empty (#1694: vs state=null which would
+      // be "spool loaded but unconfigured", labelled "?" in the UI).
+      makeTray({ id: 3, tray_color: null, tray_type: '', remain: -1, state: 9 } as Partial<AMSTray> & { state: number }),
     ],
     serial_number: 'AMS001',
     sw_ver: '1.0.0',
@@ -88,11 +90,30 @@ describe('AmsUnitCard', () => {
     expect(screen.getByText('ABS')).toBeDefined();
   });
 
-  it('shows "Empty" for empty slot', () => {
+  it('shows "Empty" for firmware-confirmed empty slot (state 9/10)', () => {
     render(<AmsUnitCard unit={makeUnit()} activeSlot={null} />);
     expect(screen.getByText('Empty')).toBeDefined();
   });
 
+  it('shows "?" for loaded-but-unconfigured slot (#1694)', () => {
+    // No state reported by firmware + empty tray_type = spool loaded into the
+    // slot but no material assigned. Reporter on a 3-AMS P1S saw these slots
+    // mislabelled as "Empty" because the prior logic only checked tray_type.
+    const unit = makeUnit({
+      tray: [
+        makeTray({ id: 0, tray_type: 'PLA', remain: 80 }),
+        makeTray({ id: 1, tray_color: null, tray_type: '', remain: -1 } as Partial<AMSTray> & { state?: number }),
+        makeTray({ id: 2, tray_type: 'ABS', remain: 10 }),
+        makeTray({ id: 3, tray_color: null, tray_type: '', remain: -1, state: 9 } as Partial<AMSTray> & { state: number }),
+      ],
+    });
+    render(<AmsUnitCard unit={unit} activeSlot={null} />);
+    expect(screen.getByText('?')).toBeDefined();
+    // The firmware-empty slot still reads "Empty" — the two states are visually
+    // distinct, not collapsed.
+    expect(screen.getByText('Empty')).toBeDefined();
+  });
+
   it('renders fill level bars for slots with filament', () => {
     const { container } = render(
       <AmsUnitCard unit={makeUnit()} activeSlot={null} />

+ 15 - 2
frontend/src/components/spoolbuddy/AmsUnitCard.tsx

@@ -10,6 +10,15 @@ function isTrayEmpty(tray: AMSTray): boolean {
   return !tray.tray_type || tray.tray_type === '';
 }
 
+// Mirror of PrintersPage.getEmptySlotKind (#1694): 'physical' when firmware
+// confirms no spool (state 9/10), 'reset' when tray_type is absent but the
+// firmware hasn't confirmed empty (= spool loaded, slot just unconfigured).
+function getEmptySlotKind(tray: AMSTray): 'physical' | 'reset' | null {
+  if (tray.tray_type) return null;
+  const state = (tray as { state?: number | null }).state ?? null;
+  return state === 9 || state === 10 ? 'physical' : 'reset';
+}
+
 function getAmsName(id: number): string {
   if (id <= 3) return `AMS ${String.fromCharCode(65 + id)}`;
   if (id >= 128 && id <= 135) return `AMS HT ${String.fromCharCode(65 + id - 128)}`;
@@ -147,6 +156,7 @@ interface SpoolSlotProps {
 
 function SpoolSlot({ tray, slotIndex, isActive, fillOverride, spoolmanFill, onClick }: SpoolSlotProps) {
   const isEmpty = isTrayEmpty(tray);
+  const emptyKind = getEmptySlotKind(tray);
   const color = trayColorToCSS(tray.tray_color);
   const amsFill = tray.remain !== null && tray.remain !== undefined && tray.remain >= 0 ? tray.remain : null;
   // If inventory says 0% but AMS reports positive remain, prefer AMS (#676)
@@ -181,8 +191,11 @@ function SpoolSlot({ tray, slotIndex, isActive, fillOverride, spoolmanFill, onCl
       </div>
 
       {/* Material type */}
-      <span className="text-sm text-white/70 truncate max-w-full">
-        {isEmpty ? 'Empty' : tray.tray_type || 'Unknown'}
+      <span
+        className="text-sm text-white/70 truncate max-w-full"
+        title={emptyKind === 'reset' ? 'Spool loaded — slot not configured' : undefined}
+      >
+        {isEmpty ? (emptyKind === 'reset' ? '?' : 'Empty') : tray.tray_type || 'Unknown'}
       </span>
 
       {/* Fill level bar */}

+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -3972,6 +3972,7 @@ export default {
     empty: 'Leer',
     emptySlot: 'Leerer Slot',
     slotEmpty: 'Leer',
+    slotUnconfigured: '?',
     emptySlotReset: 'Keine Spule zugewiesen',
     unknown: 'Unbekannt',
     humidity: 'Luftfeuchtigkeit',

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -3984,6 +3984,7 @@ export default {
     empty: 'Empty',
     emptySlot: 'Empty slot',
     slotEmpty: 'Empty',
+    slotUnconfigured: '?',
     emptySlotReset: 'No filament assigned',
     unknown: 'Unknown',
     humidity: 'Humidity',

+ 1 - 0
frontend/src/i18n/locales/es.ts

@@ -3980,6 +3980,7 @@ export default {
     empty: 'Vacía',
     emptySlot: 'Ranura vacía',
     slotEmpty: 'Vacía',
+    slotUnconfigured: '?',
     emptySlotReset: 'No hay filamento asignado',
     unknown: 'Desconocido',
     humidity: 'Humedad',

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -3961,6 +3961,7 @@ export default {
     empty: 'Vide',
     emptySlot: 'Slot vide',
     slotEmpty: 'Vide',
+    slotUnconfigured: '?',
     emptySlotReset: 'Aucune bobine assignée',
     unknown: 'Inconnu',
     humidity: 'Humidité',

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -3960,6 +3960,7 @@ export default {
     empty: 'Vuoto',
     emptySlot: 'Slot vuoto',
     slotEmpty: 'Vuoto',
+    slotUnconfigured: '?',
     emptySlotReset: 'Nessuna bobina assegnata',
     unknown: 'Sconosciuto',
     humidity: 'Umidità',

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -3972,6 +3972,7 @@ export default {
     empty: '<空>',
     emptySlot: '空のスロット',
     slotEmpty: '空',
+    slotUnconfigured: '?',
     emptySlotReset: 'スプール未割当',
     unknown: '不明',
     humidity: '湿度',

+ 1 - 0
frontend/src/i18n/locales/ko.ts

@@ -3764,6 +3764,7 @@ export default {
     used: '사용됨',
     remainingUnit: '남음',
     slotEmpty: '비어 있음',
+    slotUnconfigured: '?',
     emptySlotReset: '필라멘트가 할당되지 않음'
   },
   printModal: {

+ 1 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -3960,6 +3960,7 @@ export default {
     empty: 'Vazio',
     emptySlot: 'Slot vazio',
     slotEmpty: 'Vazio',
+    slotUnconfigured: '?',
     emptySlotReset: 'Nenhum carretel atribuído',
     unknown: 'Desconhecido',
     humidity: 'Umidade',

+ 1 - 0
frontend/src/i18n/locales/tr.ts

@@ -3949,6 +3949,7 @@ export default {
     empty: 'Boş',
     emptySlot: 'Boş yuva',
     slotEmpty: 'Boş',
+    slotUnconfigured: '?',
     emptySlotReset: 'Atanmış filament yok',
     unknown: 'Bilinmiyor',
     humidity: 'Nem',

+ 1 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -3960,6 +3960,7 @@ export default {
     empty: '空',
     emptySlot: '空槽位',
     slotEmpty: '空',
+    slotUnconfigured: '?',
     emptySlotReset: '未分配料盘',
     unknown: '未知',
     humidity: '湿度',

+ 1 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -3960,6 +3960,7 @@ export default {
     empty: '空',
     emptySlot: '空槽位',
     slotEmpty: '空',
+    slotUnconfigured: '?',
     emptySlotReset: '未指派線材',
     unknown: '未知',
     humidity: '濕度',

+ 2 - 2
frontend/src/pages/PrintersPage.tsx

@@ -3649,7 +3649,7 @@ function PrinterCard({
                                       slotNumber={slotIdx + 1}
                                     />
                                     <div className="text-[9px] text-white font-bold truncate">
-                                      {tray?.tray_type || t('ams.slotEmpty')}
+                                      {tray?.tray_type || t(emptyKind === 'reset' ? 'ams.slotUnconfigured' : 'ams.slotEmpty')}
                                     </div>
                                     {/* Fill bar */}
                                     <div className="mt-1 h-1.5 bg-black/30 rounded-full overflow-hidden">
@@ -3977,7 +3977,7 @@ function PrinterCard({
                               slotNumber={1}
                             />
                             <div className="text-[9px] text-white font-bold truncate">
-                              {tray?.tray_type || t('ams.slotEmpty')}
+                              {tray?.tray_type || t(emptyKind === 'reset' ? 'ams.slotUnconfigured' : 'ams.slotEmpty')}
                             </div>
                             {/* Fill bar */}
                             <div className="mt-1 h-1.5 bg-black/30 rounded-full overflow-hidden">

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-BBXh9elm.js


+ 1 - 1
static/index.html

@@ -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-Ba6CVgCu.js"></script>
+    <script type="module" crossorigin src="/assets/index-BBXh9elm.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-BvmIMSUd.css">
   </head>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов