Support non-0.4mm nozzles in AMS Slot config + guard dispatch (#1899)

The Configure AMS Slot picker was hardwired to 0.4mm (nozzleDiameter
prop never passed from PrintersPage / SpoolBuddyAmsPage), so a 0.6
machine could only set 0.4 profiles on its trays. Resolve the real
installed nozzle per-AMS (ams_extruder_map on dual-nozzle) and pass it
in. Separately, nothing validated the sliced nozzle against the
installed one, so a mismatch reached the printer as a cryptic HMS
_8012 "Failed to get AMS mapping table". Add a fail-safe pre-dispatch
guard in _start_print that fails the item with an actionable message
before upload; no slice diameter or no reported nozzles = no-op.
This commit is contained in:
maziggy
2026-07-08 08:57:56 +02:00
parent 55ea4b19c9
commit bb3e2a710e
9 changed files with 568 additions and 143 deletions
@@ -0,0 +1,63 @@
/**
* Tests for resolveSlotNozzleDiameter helper (#1899).
*
* The AMS Slot config picker must filter filament presets by the nozzle that
* actually feeds a given AMS, not the hardcoded 0.4mm default. This resolver
* reads the installed nozzle diameter from the printer status, honouring the
* per-AMS extruder binding on dual-nozzle printers. It returns undefined when
* the hardware hasn't been reported, so the caller keeps its own default.
*/
import { describe, it, expect } from 'vitest';
import { resolveSlotNozzleDiameter } from '../../utils/amsHelpers';
describe('resolveSlotNozzleDiameter', () => {
it('returns undefined when status is null or undefined', () => {
expect(resolveSlotNozzleDiameter(null, 0)).toBeUndefined();
expect(resolveSlotNozzleDiameter(undefined, 0)).toBeUndefined();
});
it('returns undefined when no nozzles are reported', () => {
expect(resolveSlotNozzleDiameter({ nozzles: [] }, 0)).toBeUndefined();
expect(resolveSlotNozzleDiameter({}, 0)).toBeUndefined();
});
it('returns undefined when the reported nozzle diameter is an empty default', () => {
expect(resolveSlotNozzleDiameter({ nozzles: [{ nozzle_diameter: '' }] }, 0)).toBeUndefined();
});
it('returns the single-nozzle diameter regardless of amsId (no extruder map)', () => {
const status = { nozzles: [{ nozzle_diameter: '0.6' }] };
expect(resolveSlotNozzleDiameter(status, 0)).toBe('0.6');
expect(resolveSlotNozzleDiameter(status, 3)).toBe('0.6');
});
it('resolves the per-AMS nozzle on a dual-nozzle printer via ams_extruder_map', () => {
// AMS 0 → left nozzle (0.4), AMS 1 → right nozzle (0.6)
const status = {
nozzles: [{ nozzle_diameter: '0.4' }, { nozzle_diameter: '0.6' }],
ams_extruder_map: { '0': 0, '1': 1 },
};
expect(resolveSlotNozzleDiameter(status, 0)).toBe('0.4');
expect(resolveSlotNozzleDiameter(status, 1)).toBe('0.6');
});
it('falls back to the primary nozzle when the AMS is not in the extruder map', () => {
const status = {
nozzles: [{ nozzle_diameter: '0.4' }, { nozzle_diameter: '0.6' }],
ams_extruder_map: { '0': 0 },
};
// AMS 5 has no mapping → index 0 (primary)
expect(resolveSlotNozzleDiameter(status, 5)).toBe('0.4');
});
it('falls back to the primary nozzle when the mapped nozzle has no diameter', () => {
// Dual-nozzle stub where the second entry is still an empty default.
const status = {
nozzles: [{ nozzle_diameter: '0.4' }, { nozzle_diameter: '' }],
ams_extruder_map: { '1': 1 },
};
expect(resolveSlotNozzleDiameter(status, 1)).toBe('0.4');
});
});
+2 -1
View File
@@ -115,7 +115,7 @@ import { SkipObjectsModal, SkipObjectsIcon } from '../components/SkipObjectsModa
import { FileUploadModal } from '../components/FileUploadModal';
import { PrintModal } from '../components/PrintModal';
import { PrinterInfoModal } from '../components/PrinterInfoModal';
import { getAmsLabel, getGlobalTrayId, getFillBarColor, getSpoolmanFillLevel, getFallbackSpoolTag, isBambuLabSpool } from '../utils/amsHelpers';
import { getAmsLabel, getGlobalTrayId, getFillBarColor, getSpoolmanFillLevel, getFallbackSpoolTag, isBambuLabSpool, resolveSlotNozzleDiameter } from '../utils/amsHelpers';
import { getPrinterImage, getWifiStrength, filterCompatibleQueueItems } from '../utils/printer';
import { FilamentSlotCircle } from '../components/FilamentSlotCircle';
import { Collapsible } from '../components/Collapsible';
@@ -6255,6 +6255,7 @@ function PrinterCard({
printerId={printer.id}
slotInfo={configureSlotModal}
printerModel={mapModelCode(printer.model) || undefined}
nozzleDiameter={resolveSlotNozzleDiameter(status, configureSlotModal.amsId)}
onSuccess={() => {
// Refresh slot presets to show updated profile name
queryClient.invalidateQueries({ queryKey: ['slotPresets', printer.id] });
@@ -6,7 +6,7 @@ import { Layers, Settings2, Package, Unlink, Link2, X } from 'lucide-react';
import type { SpoolBuddyOutletContext } from '../../components/spoolbuddy/SpoolBuddyLayout';
import { api } from '../../api/client';
import type { PrinterStatus, AMSTray, SpoolAssignment } from '../../api/client';
import { getGlobalTrayId, getFillBarColor, getSpoolmanFillLevel, getFallbackSpoolTag, formatSlotLabel, isBambuLabSpool } from '../../utils/amsHelpers';
import { getGlobalTrayId, getFillBarColor, getSpoolmanFillLevel, getFallbackSpoolTag, formatSlotLabel, isBambuLabSpool, resolveSlotNozzleDiameter } from '../../utils/amsHelpers';
import { getSwatchStyle } from '../../utils/colors';
import { AmsUnitCard, HumidityIndicator, TemperatureIndicator, NozzleBadge } from '../../components/spoolbuddy/AmsUnitCard';
import type { AmsThresholds } from '../../components/spoolbuddy/AmsUnitCard';
@@ -687,6 +687,7 @@ export function SpoolBuddyAmsPage() {
printerId={selectedPrinterId}
slotInfo={configureSlotModal}
printerModel={mapModelCode(printer?.model ?? null) || undefined}
nozzleDiameter={resolveSlotNozzleDiameter(status, configureSlotModal.amsId)}
fullScreen
onSuccess={() => {
queryClient.invalidateQueries({ queryKey: ['slotPresets', selectedPrinterId] });
+26
View File
@@ -343,6 +343,32 @@ export function filterFilamentsByNozzle<T extends { extruderId?: number }>(
);
}
/**
* Resolve the installed nozzle diameter feeding a given AMS unit, so the
* Configure-AMS-Slot picker filters filament presets by the nozzle actually on
* the machine instead of assuming 0.4mm (#1899).
*
* On dual-nozzle printers (H2D) each AMS is bound to one extruder via
* `ams_extruder_map` (amsId → extruder index, 0=left/primary, 1=right), so we
* read that nozzle's diameter. Single-nozzle printers have no map entry and
* fall back to the primary nozzle (index 0). Returns undefined when the printer
* hasn't reported nozzle hardware yet, letting the caller keep its own default.
* Diameter is the bare decimal string the status carries, e.g. "0.4" / "0.6".
*/
export function resolveSlotNozzleDiameter(
status: {
nozzles?: { nozzle_diameter?: string }[];
ams_extruder_map?: Record<string, number>;
} | null | undefined,
amsId: number,
): string | undefined {
const nozzles = status?.nozzles;
if (!nozzles || nozzles.length === 0) return undefined;
const extruderIdx = status?.ams_extruder_map?.[String(amsId)] ?? 0;
const diameter = nozzles[extruderIdx]?.nozzle_diameter || nozzles[0]?.nozzle_diameter;
return diameter || undefined;
}
/**
* Detect Bambu Lab RFID-tagged spool by tray_uuid (32 hex) or tag_uid (16 hex).
*