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

fix: anchor H2C nozzle rack slots to fixed base ID (#943)

  NozzleRackCard computed its rack base via min(present_ids), which breaks
  when the lowest-ID slot is the one currently mounted to a hotend — the
  firmware omits that ID from device.nozzle.info entirely, so min() picks
  the next slot up and every remaining nozzle renders one position too
  far left, with the "empty" placeholder pushed off the right end.

  Use the fixed H2C rack base of 16 (matching
  test_h2c_nozzle_rack_populated_with_8_entries in the backend) so the
  empty slot stays anchored to its physical position regardless of which
  nozzle is currently mounted.

  Adds a frontend regression test covering ID 16 missing.
maziggy 1 месяц назад
Родитель
Сommit
141eaa7711

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


+ 39 - 0
frontend/src/__tests__/pages/PrintersPage.test.tsx

@@ -236,6 +236,45 @@ describe('PrintersPage', () => {
       expect(dashes.length).toBeGreaterThanOrEqual(3); // 3 empty rack positions (IDs 19,20,21)
     });
 
+    it('keeps empty slot anchored to physical position when its nozzle is mounted (#943)', async () => {
+      // H2C with rack slot 16 picked up into the hotend — firmware omits ID 16
+      // entirely from nozzle.info. Each rack diameter is unique so we can assert
+      // the ordering by tooltip lookup.
+      const h2cSlot16Mounted = {
+        ...mockPrinterStatus,
+        nozzle_rack: [
+          { id: 0, nozzle_type: 'HS', nozzle_diameter: '0.4', wear: 5, stat: 1, max_temp: 300, serial_number: 'SN-L', filament_color: '', filament_id: '', filament_type: '' },
+          { id: 1, nozzle_type: 'HS', nozzle_diameter: '0.4', wear: 3, stat: 0, max_temp: 300, serial_number: 'SN-R', filament_color: '', filament_id: '', filament_type: '' },
+          // ID 16 missing — currently in hotend
+          { id: 17, nozzle_type: 'HS', nozzle_diameter: '0.2', wear: 0, stat: 0, max_temp: 300, serial_number: 'SN-17', filament_color: '', filament_id: '', filament_type: '' },
+          { id: 18, nozzle_type: 'HS', nozzle_diameter: '0.6', wear: 0, stat: 0, max_temp: 300, serial_number: 'SN-18', filament_color: '', filament_id: '', filament_type: '' },
+          { id: 19, nozzle_type: 'HS', nozzle_diameter: '0.8', wear: 0, stat: 0, max_temp: 300, serial_number: 'SN-19', filament_color: '', filament_id: '', filament_type: '' },
+          { id: 20, nozzle_type: 'HH01', nozzle_diameter: '1.0', wear: 0, stat: 0, max_temp: 300, serial_number: 'SN-20', filament_color: '', filament_id: '', filament_type: '' },
+          { id: 21, nozzle_type: 'HH01', nozzle_diameter: '1.2', wear: 0, stat: 0, max_temp: 300, serial_number: 'SN-21', filament_color: '', filament_id: '', filament_type: '' },
+        ],
+      };
+
+      server.use(
+        http.get('/api/v1/printers/:id/status', () => {
+          return HttpResponse.json(h2cSlot16Mounted);
+        })
+      );
+
+      render(<PrintersPage />);
+
+      await waitFor(() => {
+        expect(screen.getAllByText('Nozzle Rack').length).toBeGreaterThan(0);
+      });
+
+      // Slot 1 (leftmost, ID 16) should be the empty dash; slots 2..6 should
+      // hold the 5 remaining nozzles in order 17, 18, 19, 20, 21.
+      const rackLabel = screen.getAllByText('Nozzle Rack')[0];
+      const rackCard = rackLabel.parentElement!;
+      const slotRow = rackCard.querySelectorAll('div.flex')[0];
+      const slotTexts = Array.from(slotRow.querySelectorAll('span')).map(s => s.textContent);
+      expect(slotTexts).toEqual(['—', '0.2', '0.6', '0.8', '1.0', '1.2']);
+    });
+
     it('hides nozzle rack when only L/R nozzles present (H2D)', async () => {
       const h2dStatus = {
         ...mockPrinterStatus,

+ 7 - 4
frontend/src/pages/PrintersPage.tsx

@@ -487,14 +487,17 @@ function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, filamentInfo,
 // H2C Nozzle Rack Card — compact single row showing 6-position tool-changer dock
 function NozzleRackCard({ slots, filamentInfo }: { slots: import('../api/client').NozzleRackSlot[]; filamentInfo?: Record<string, { name: string; k: number | null }> }) {
   const { t } = useTranslation();
-  // Rack nozzles only (IDs >= 2) — excludes L/R hotend nozzles (IDs 0, 1)
-  // H2C rack IDs are 16-21 — map by actual ID so empty slots appear in the correct position
+  // Rack nozzles only (IDs >= 2) — excludes L/R hotend nozzles (IDs 0, 1).
+  // H2C rack slot IDs are fixed at 16..21. When a nozzle is picked up into the
+  // hotend the firmware omits that rack ID entirely, so we must map by the fixed
+  // base — computing it from min(present IDs) shifts everything left when slot 16
+  // is the one currently mounted (#943).
   const rackNozzles = slots.filter(s => s.id >= 2);
   const RACK_SIZE = 6;
-  const minRackId = rackNozzles.length > 0 ? Math.min(...rackNozzles.map(s => s.id)) : 16;
+  const RACK_BASE_ID = 16;
   const rackSlots: (import('../api/client').NozzleRackSlot)[] = Array.from(
     { length: RACK_SIZE },
-    (_, i) => rackNozzles.find(s => s.id === minRackId + i) ?? {
+    (_, i) => rackNozzles.find(s => s.id === RACK_BASE_ID + i) ?? {
       id: -(i + 1), nozzle_type: '', nozzle_diameter: '', wear: null, stat: null,
       max_temp: 0, serial_number: '', filament_color: '', filament_id: '', filament_type: '',
     },

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


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-ZFxtu4Z1.js"></script>
+    <script type="module" crossorigin src="/assets/index-DkQ9Ij-0.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-Czpqfgna.css">
   </head>
   <body>

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