Nozzle-aware AMS mapping for dual-nozzle printers, BL spool detection fix, AMS startup fix, SQLite WAL (#318)

Dual-nozzle H2D/H2D Pro: filament matching now respects nozzle assignments
from the 3MF file. Each AMS unit feeds a specific nozzle (L/R), and the
scheduler/frontend constrain matching to only trays on the correct nozzle.
Falls back to unfiltered matching when no trays exist on the target nozzle.
L/R badges shown in the filament mapping UI. Translated in en/de/ja/it.

Fix AMS slot config overwritten on startup: on_ams_change unconditionally
unlinked BL spool assignments on every MQTT pushall, then re-assigned them
sending ams_filament_setting without setting_id — clearing the printer's
filament preset. Now compares spool RFID identifiers before unlinking.

Fix BL spool detection false positives: removed tray_info_idx from detection
logic in both backend is_bambu_lab_spool() and frontend isBambuLabSpool().
Third-party spools using Bambu generic presets had GF-prefixed tray_info_idx
values, causing misidentification. Now uses only tray_uuid and tag_uid.

SQLite WAL mode with 5s busy timeout reduces "database is locked" errors.
This commit is contained in:
maziggy
2026-02-13 11:48:32 +01:00
parent ee2af79f4a
commit f9b47282a1
26 changed files with 1105 additions and 515 deletions
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Circle, Check, AlertTriangle, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react';
import { api } from '../../api/client';
@@ -17,6 +18,7 @@ export function FilamentMapping({
onManualMappingChange,
defaultExpanded = false,
}: FilamentMappingProps & { defaultExpanded?: boolean }) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [isRefreshing, setIsRefreshing] = useState(false);
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
@@ -32,6 +34,7 @@ export function FilamentMapping({
useFilamentMapping(filamentReqs, printerStatus, manualMappings);
const hasFilamentReqs = filamentReqs?.filaments && filamentReqs.filaments.length > 0;
const isDualNozzle = filamentReqs?.filaments?.some((f) => f.nozzle_id != null) ?? false;
// Don't render if no filament requirements
if (!hasFilamentReqs) {
@@ -126,8 +129,16 @@ export function FilamentMapping({
<span title={`Required: ${item.type} - ${getColorName(item.color)}`}>
<Circle className="w-3 h-3" fill={item.color} stroke={item.color} />
</span>
{/* Required type + grams */}
<span className="text-white truncate">
{/* Required type + grams + nozzle badge */}
<span className="text-white truncate flex items-center gap-1">
{isDualNozzle && item.nozzle_id != null && (
<span
className="inline-flex items-center justify-center w-3.5 h-3.5 rounded text-[9px] font-bold leading-none bg-bambu-gray/20 text-bambu-gray shrink-0"
title={item.nozzle_id === 1 ? t('printModal.leftNozzleTooltip') : t('printModal.rightNozzleTooltip')}
>
{item.nozzle_id === 1 ? t('printModal.leftNozzle') : t('printModal.rightNozzle')}
</span>
)}
{item.type} <span className="text-bambu-gray">({item.used_grams}g)</span>
</span>
{/* Arrow */}
@@ -158,6 +158,7 @@ export interface FilamentReqsData {
color: string;
used_grams: number;
used_meters: number;
nozzle_id?: number;
}>;
}