mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
Merge branch '0.2.0b' into main
This commit is contained in:
@@ -347,3 +347,182 @@ describe('computeAmsMapping', () => {
|
||||
expect(result).toEqual([254]); // External spool global ID
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildLoadedFilaments - nozzle awareness', () => {
|
||||
it('sets extruderId from ams_extruder_map', () => {
|
||||
const status = createPrinterStatus([
|
||||
{
|
||||
id: 0,
|
||||
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
tray: [{ id: 0, tray_type: 'PETG', tray_color: '00FF00' }],
|
||||
},
|
||||
]);
|
||||
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
|
||||
|
||||
const result = buildLoadedFilaments(status);
|
||||
|
||||
expect(result[0].extruderId).toBe(1); // AMS 0 → left nozzle
|
||||
expect(result[1].extruderId).toBe(0); // AMS 1 → right nozzle
|
||||
});
|
||||
|
||||
it('leaves extruderId undefined when no ams_extruder_map', () => {
|
||||
const status = createPrinterStatus([
|
||||
{
|
||||
id: 0,
|
||||
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = buildLoadedFilaments(status);
|
||||
|
||||
expect(result[0].extruderId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeAmsMapping - nozzle filtering', () => {
|
||||
it('filters candidates by nozzle_id when set', () => {
|
||||
// Filament requires left nozzle (extruder 1), only AMS 0 is on left
|
||||
const reqs = {
|
||||
filaments: [
|
||||
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 1 },
|
||||
],
|
||||
};
|
||||
const status = createPrinterStatus([
|
||||
{
|
||||
id: 0, // Left nozzle
|
||||
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
|
||||
},
|
||||
{
|
||||
id: 1, // Right nozzle
|
||||
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
|
||||
},
|
||||
]);
|
||||
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
|
||||
|
||||
const result = computeAmsMapping(reqs, status);
|
||||
|
||||
expect(result).toEqual([0]); // AMS 0, tray 0 (on left nozzle)
|
||||
});
|
||||
|
||||
it('filters to right nozzle when nozzle_id=0', () => {
|
||||
const reqs = {
|
||||
filaments: [
|
||||
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 0 },
|
||||
],
|
||||
};
|
||||
const status = createPrinterStatus([
|
||||
{
|
||||
id: 0, // Left nozzle
|
||||
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
|
||||
},
|
||||
{
|
||||
id: 1, // Right nozzle
|
||||
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
|
||||
},
|
||||
]);
|
||||
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
|
||||
|
||||
const result = computeAmsMapping(reqs, status);
|
||||
|
||||
expect(result).toEqual([4]); // AMS 1, tray 0 (global ID = 1*4+0 = 4, on right nozzle)
|
||||
});
|
||||
|
||||
it('falls back to all trays when target nozzle has no trays at all', () => {
|
||||
// Requires nozzle_id=1 (left), but no AMS units are on left nozzle
|
||||
const reqs = {
|
||||
filaments: [
|
||||
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 1 },
|
||||
],
|
||||
};
|
||||
const status = createPrinterStatus([
|
||||
{
|
||||
id: 0, // Right nozzle only
|
||||
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
|
||||
},
|
||||
]);
|
||||
(status as any).ams_extruder_map = { '0': 0 }; // AMS 0 → right nozzle, none on left
|
||||
|
||||
const result = computeAmsMapping(reqs, status);
|
||||
|
||||
expect(result).toEqual([0]); // Falls back to unfiltered (right nozzle PLA)
|
||||
});
|
||||
|
||||
it('stays restricted when target nozzle has trays but wrong type', () => {
|
||||
// Left nozzle has PETG, right has PLA — but requires PLA on left
|
||||
const reqs = {
|
||||
filaments: [
|
||||
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 1 },
|
||||
],
|
||||
};
|
||||
const status = createPrinterStatus([
|
||||
{
|
||||
id: 0, // Left nozzle - only PETG
|
||||
tray: [{ id: 0, tray_type: 'PETG', tray_color: '00FF00' }],
|
||||
},
|
||||
{
|
||||
id: 1, // Right nozzle - has PLA
|
||||
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
|
||||
},
|
||||
]);
|
||||
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
|
||||
|
||||
const result = computeAmsMapping(reqs, status);
|
||||
|
||||
expect(result).toEqual([-1]); // No PLA on left nozzle, stays restricted
|
||||
});
|
||||
|
||||
it('skips nozzle filtering when nozzle_id is undefined', () => {
|
||||
const reqs = {
|
||||
filaments: [
|
||||
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10 }, // No nozzle_id
|
||||
],
|
||||
};
|
||||
const status = createPrinterStatus([
|
||||
{
|
||||
id: 0,
|
||||
tray: [{ id: 0, tray_type: 'PETG', tray_color: '00FF00' }],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
|
||||
},
|
||||
]);
|
||||
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
|
||||
|
||||
const result = computeAmsMapping(reqs, status);
|
||||
|
||||
expect(result).toEqual([4]); // Picks best match regardless of nozzle
|
||||
});
|
||||
|
||||
it('handles dual-nozzle multi-slot mapping', () => {
|
||||
// Two filaments: one for left, one for right
|
||||
const reqs = {
|
||||
filaments: [
|
||||
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 1 }, // Left
|
||||
{ slot_id: 2, type: 'PETG', color: '#00FF00', used_grams: 10, nozzle_id: 0 }, // Right
|
||||
],
|
||||
};
|
||||
const status = createPrinterStatus([
|
||||
{
|
||||
id: 0, // Left nozzle
|
||||
tray: [
|
||||
{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1, // Right nozzle
|
||||
tray: [
|
||||
{ id: 0, tray_type: 'PETG', tray_color: '00FF00' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
|
||||
|
||||
const result = computeAmsMapping(reqs, status);
|
||||
|
||||
expect(result).toEqual([0, 4]); // Left gets AMS0-T0, Right gets AMS1-T0
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import type { PrinterStatus } from '../api/client';
|
||||
*/
|
||||
export function buildLoadedFilaments(printerStatus: PrinterStatus | undefined): LoadedFilament[] {
|
||||
const filaments: LoadedFilament[] = [];
|
||||
const amsExtruderMap = printerStatus?.ams_extruder_map;
|
||||
const hasDualNozzle = amsExtruderMap && Object.keys(amsExtruderMap).length > 0;
|
||||
|
||||
// Add filaments from all AMS units (regular and HT)
|
||||
printerStatus?.ams?.forEach((amsUnit) => {
|
||||
@@ -33,6 +35,7 @@ export function buildLoadedFilaments(printerStatus: PrinterStatus | undefined):
|
||||
label: formatSlotLabel(amsUnit.id, tray.id, isHt, false),
|
||||
globalTrayId: getGlobalTrayId(amsUnit.id, tray.id, false),
|
||||
trayInfoIdx: tray.tray_info_idx || '',
|
||||
extruderId: amsExtruderMap?.[String(amsUnit.id)],
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -52,6 +55,7 @@ export function buildLoadedFilaments(printerStatus: PrinterStatus | undefined):
|
||||
label: 'External',
|
||||
globalTrayId: 254,
|
||||
trayInfoIdx: printerStatus.vt_tray.tray_info_idx || '',
|
||||
extruderId: hasDualNozzle ? 0 : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,7 +94,15 @@ export function computeAmsMapping(
|
||||
const reqTrayInfoIdx = req.tray_info_idx || '';
|
||||
|
||||
// Get available trays (not already used)
|
||||
const available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
|
||||
let available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
|
||||
|
||||
// Nozzle-aware filtering: restrict to trays on the correct nozzle
|
||||
if (req.nozzle_id != null) {
|
||||
const nozzleFiltered = available.filter((f) => f.extruderId === req.nozzle_id);
|
||||
if (nozzleFiltered.length > 0) {
|
||||
available = nozzleFiltered;
|
||||
}
|
||||
}
|
||||
|
||||
let idxMatch: LoadedFilament | undefined;
|
||||
let exactMatch: LoadedFilament | undefined;
|
||||
@@ -191,6 +203,8 @@ export interface LoadedFilament {
|
||||
globalTrayId: number;
|
||||
/** Unique spool identifier (e.g., "GFA00", "P4d64437") */
|
||||
trayInfoIdx?: string;
|
||||
/** Extruder ID for dual-nozzle printers (0=right, 1=left) */
|
||||
extruderId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,6 +217,8 @@ export interface FilamentRequirement {
|
||||
used_grams: number;
|
||||
/** Unique spool identifier from slicing (e.g., "GFA00", "P4d64437") */
|
||||
tray_info_idx?: string;
|
||||
/** Target nozzle for dual-nozzle printers (0=right, 1=left) */
|
||||
nozzle_id?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,48 +263,7 @@ export function useLoadedFilaments(
|
||||
printerStatus: PrinterStatus | undefined
|
||||
): LoadedFilament[] {
|
||||
return useMemo(() => {
|
||||
const filaments: LoadedFilament[] = [];
|
||||
|
||||
// Add filaments from all AMS units (regular and HT)
|
||||
printerStatus?.ams?.forEach((amsUnit) => {
|
||||
const isHt = amsUnit.tray.length === 1; // AMS-HT has single tray
|
||||
amsUnit.tray.forEach((tray) => {
|
||||
if (tray.tray_type) {
|
||||
const color = normalizeColor(tray.tray_color);
|
||||
filaments.push({
|
||||
type: tray.tray_type,
|
||||
color,
|
||||
colorName: getColorName(color),
|
||||
amsId: amsUnit.id,
|
||||
trayId: tray.id,
|
||||
isHt,
|
||||
isExternal: false,
|
||||
label: formatSlotLabel(amsUnit.id, tray.id, isHt, false),
|
||||
globalTrayId: getGlobalTrayId(amsUnit.id, tray.id, false),
|
||||
trayInfoIdx: tray.tray_info_idx || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add external spool if loaded
|
||||
if (printerStatus?.vt_tray?.tray_type) {
|
||||
const color = normalizeColor(printerStatus.vt_tray.tray_color);
|
||||
filaments.push({
|
||||
type: printerStatus.vt_tray.tray_type,
|
||||
color,
|
||||
colorName: getColorName(color),
|
||||
amsId: -1,
|
||||
trayId: 0,
|
||||
isHt: false,
|
||||
isExternal: true,
|
||||
label: 'External',
|
||||
globalTrayId: 254,
|
||||
trayInfoIdx: printerStatus.vt_tray.tray_info_idx || '',
|
||||
});
|
||||
}
|
||||
|
||||
return filaments;
|
||||
return buildLoadedFilaments(printerStatus);
|
||||
}, [printerStatus]);
|
||||
}
|
||||
|
||||
@@ -355,7 +330,15 @@ export function useFilamentMapping(
|
||||
const reqTrayInfoIdx = req.tray_info_idx || '';
|
||||
|
||||
// Get available trays (not already used)
|
||||
const available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
|
||||
let available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
|
||||
|
||||
// Nozzle-aware filtering: restrict to trays on the correct nozzle
|
||||
if (req.nozzle_id != null) {
|
||||
const nozzleFiltered = available.filter((f) => f.extruderId === req.nozzle_id);
|
||||
if (nozzleFiltered.length > 0) {
|
||||
available = nozzleFiltered;
|
||||
}
|
||||
}
|
||||
|
||||
let idxMatch: LoadedFilament | undefined;
|
||||
let exactMatch: LoadedFilament | undefined;
|
||||
|
||||
@@ -124,26 +124,32 @@ function computeMatchDetails(
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-match
|
||||
const exactMatch = loadedFilaments.find(
|
||||
// Auto-match with nozzle-aware filtering
|
||||
let candidates = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
|
||||
if (req.nozzle_id != null) {
|
||||
const nozzleFiltered = candidates.filter((f) => f.extruderId === req.nozzle_id);
|
||||
if (nozzleFiltered.length > 0) {
|
||||
candidates = nozzleFiltered;
|
||||
}
|
||||
}
|
||||
|
||||
const exactMatch = candidates.find(
|
||||
(f) =>
|
||||
!usedTrayIds.has(f.globalTrayId) &&
|
||||
f.type?.toUpperCase() === req.type?.toUpperCase() &&
|
||||
normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
|
||||
);
|
||||
const similarMatch = exactMatch
|
||||
? undefined
|
||||
: loadedFilaments.find(
|
||||
: candidates.find(
|
||||
(f) =>
|
||||
!usedTrayIds.has(f.globalTrayId) &&
|
||||
f.type?.toUpperCase() === req.type?.toUpperCase() &&
|
||||
colorsAreSimilar(f.color, req.color)
|
||||
);
|
||||
const typeOnlyMatch =
|
||||
exactMatch || similarMatch
|
||||
? undefined
|
||||
: loadedFilaments.find(
|
||||
(f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase()
|
||||
: candidates.find(
|
||||
(f) => f.type?.toUpperCase() === req.type?.toUpperCase()
|
||||
);
|
||||
const loaded = exactMatch ?? similarMatch ?? typeOnlyMatch;
|
||||
|
||||
@@ -196,26 +202,32 @@ function computeMappingWithOverrides(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Auto-match
|
||||
const exactMatch = loadedFilaments.find(
|
||||
// Auto-match with nozzle-aware filtering
|
||||
let candidates = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
|
||||
if (req.nozzle_id != null) {
|
||||
const nozzleFiltered = candidates.filter((f) => f.extruderId === req.nozzle_id);
|
||||
if (nozzleFiltered.length > 0) {
|
||||
candidates = nozzleFiltered;
|
||||
}
|
||||
}
|
||||
|
||||
const exactMatch = candidates.find(
|
||||
(f) =>
|
||||
!usedTrayIds.has(f.globalTrayId) &&
|
||||
f.type?.toUpperCase() === req.type?.toUpperCase() &&
|
||||
normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
|
||||
);
|
||||
const similarMatch = exactMatch
|
||||
? undefined
|
||||
: loadedFilaments.find(
|
||||
: candidates.find(
|
||||
(f) =>
|
||||
!usedTrayIds.has(f.globalTrayId) &&
|
||||
f.type?.toUpperCase() === req.type?.toUpperCase() &&
|
||||
colorsAreSimilar(f.color, req.color)
|
||||
);
|
||||
const typeOnlyMatch =
|
||||
exactMatch || similarMatch
|
||||
? undefined
|
||||
: loadedFilaments.find(
|
||||
(f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase()
|
||||
: candidates.find(
|
||||
(f) => f.type?.toUpperCase() === req.type?.toUpperCase()
|
||||
);
|
||||
const loaded = exactMatch ?? similarMatch ?? typeOnlyMatch;
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ export default {
|
||||
nozzleCount: 'Düsenanzahl',
|
||||
autoArchive: 'Automatische Archivierung',
|
||||
status: {
|
||||
available: 'Verfügbar',
|
||||
idle: 'Bereit',
|
||||
printing: 'Druckt',
|
||||
paused: 'Pausiert',
|
||||
@@ -163,6 +164,7 @@ export default {
|
||||
},
|
||||
// Controls
|
||||
hideOffline: 'Offline ausblenden',
|
||||
nextAvailable: 'Nächster verfügbar',
|
||||
powerOn: 'Einschalten',
|
||||
offlinePrintersWithPlugs: 'Offline-Drucker mit Smart-Plugs',
|
||||
noPrintersConfigured: 'Noch keine Drucker konfiguriert',
|
||||
@@ -980,6 +982,7 @@ export default {
|
||||
removeFromPrinter: 'Von diesem Drucker entfernen',
|
||||
// Types
|
||||
types: {
|
||||
lubricateCarbonRods: 'Karbonstäbe schmieren',
|
||||
lubricateRails: 'Linearschienen schmieren',
|
||||
cleanNozzle: 'Düse/Hotend reinigen',
|
||||
checkBelts: 'Riemenspannung prüfen',
|
||||
@@ -988,6 +991,7 @@ export default {
|
||||
checkCooling: 'Kühlungslüfter prüfen',
|
||||
generalInspection: 'Allgemeine Inspektion',
|
||||
cleanCarbonRods: 'Kohlenstoffstangen reinigen',
|
||||
cleanLinearRails: 'Linearschienen reinigen',
|
||||
checkPtfeTube: 'PTFE-Schlauch prüfen',
|
||||
replaceHepaFilter: 'HEPA-Filter ersetzen',
|
||||
replaceCarbonFilter: 'Aktivkohlefilter ersetzen',
|
||||
@@ -2632,6 +2636,10 @@ export default {
|
||||
sameTypeDifferentColor: 'Gleicher Typ, andere Farbe',
|
||||
filamentTypeNotLoaded: 'Filamenttyp nicht geladen',
|
||||
openCalendar: 'Kalender öffnen',
|
||||
leftNozzle: 'L',
|
||||
rightNozzle: 'R',
|
||||
leftNozzleTooltip: 'Linke Düse',
|
||||
rightNozzleTooltip: 'Rechte Düse',
|
||||
},
|
||||
|
||||
// Backup
|
||||
@@ -2973,6 +2981,7 @@ export default {
|
||||
|
||||
// Maintenance type descriptions (built-in)
|
||||
maintenanceDescriptions: {
|
||||
lubricateCarbonRods: 'Schmiermittel auf Karbonstäbe für sanfte Bewegung auftragen',
|
||||
lubricateRails: 'Schmiermittel auf Linearschienen für sanfte Bewegung auftragen',
|
||||
cleanNozzle: 'Hotend und Düse reinigen, um Verstopfungen zu verhindern',
|
||||
checkBelts: 'Riemenspannung für präzise Drucke überprüfen',
|
||||
@@ -2981,6 +2990,7 @@ export default {
|
||||
checkCooling: 'Sicherstellen, dass Lüfter ordnungsgemäß funktionieren',
|
||||
generalInspection: 'Allgemeine Druckerinspektion',
|
||||
cleanCarbonRods: 'Karbonstäbe reinigen, um Reibung zu reduzieren',
|
||||
cleanLinearRails: 'Linearschienen abwischen, um Staub und Schmutz zu entfernen',
|
||||
checkPtfeTube: 'PTFE-Schlauch auf Verschleiß oder Beschädigung prüfen',
|
||||
replaceHepaFilter: 'HEPA-Filter für Luftqualität ersetzen',
|
||||
replaceCarbonFilter: 'Aktivkohlefilter ersetzen',
|
||||
|
||||
@@ -124,6 +124,7 @@ export default {
|
||||
nozzleCount: 'Nozzle Count',
|
||||
autoArchive: 'Auto Archive',
|
||||
status: {
|
||||
available: 'Available',
|
||||
idle: 'Idle',
|
||||
printing: 'Printing',
|
||||
paused: 'Paused',
|
||||
@@ -163,6 +164,7 @@ export default {
|
||||
},
|
||||
// Controls
|
||||
hideOffline: 'Hide offline',
|
||||
nextAvailable: 'Next available',
|
||||
powerOn: 'Power On',
|
||||
offlinePrintersWithPlugs: 'Offline printers with smart plugs',
|
||||
noPrintersConfigured: 'No printers configured yet',
|
||||
@@ -980,6 +982,7 @@ export default {
|
||||
removeFromPrinter: 'Remove from this printer',
|
||||
// Types
|
||||
types: {
|
||||
lubricateCarbonRods: 'Lubricate Carbon Rods',
|
||||
lubricateRails: 'Lubricate Linear Rails',
|
||||
cleanNozzle: 'Clean Nozzle/Hotend',
|
||||
checkBelts: 'Check Belt Tension',
|
||||
@@ -988,6 +991,7 @@ export default {
|
||||
checkCooling: 'Check Cooling Fans',
|
||||
generalInspection: 'General Inspection',
|
||||
cleanCarbonRods: 'Clean Carbon Rods',
|
||||
cleanLinearRails: 'Clean Linear Rails',
|
||||
checkPtfeTube: 'Check PTFE Tube',
|
||||
replaceHepaFilter: 'Replace HEPA Filter',
|
||||
replaceCarbonFilter: 'Replace Carbon Filter',
|
||||
@@ -2636,6 +2640,10 @@ export default {
|
||||
sameTypeDifferentColor: 'Same type, different color',
|
||||
filamentTypeNotLoaded: 'Filament type not loaded',
|
||||
openCalendar: 'Open calendar',
|
||||
leftNozzle: 'L',
|
||||
rightNozzle: 'R',
|
||||
leftNozzleTooltip: 'Left nozzle',
|
||||
rightNozzleTooltip: 'Right nozzle',
|
||||
},
|
||||
|
||||
// Backup
|
||||
@@ -2978,6 +2986,7 @@ export default {
|
||||
|
||||
// Maintenance type descriptions (built-in)
|
||||
maintenanceDescriptions: {
|
||||
lubricateCarbonRods: 'Apply lubricant to carbon rods for smooth motion',
|
||||
lubricateRails: 'Apply lubricant to linear rails for smooth motion',
|
||||
cleanNozzle: 'Clean hotend and nozzle to prevent clogs',
|
||||
checkBelts: 'Verify belt tension for accurate prints',
|
||||
@@ -2986,6 +2995,7 @@ export default {
|
||||
checkCooling: 'Ensure cooling fans are working properly',
|
||||
generalInspection: 'General printer inspection',
|
||||
cleanCarbonRods: 'Clean carbon rods to reduce friction',
|
||||
cleanLinearRails: 'Wipe linear rails to remove dust and debris',
|
||||
checkPtfeTube: 'Inspect PTFE tube for wear or damage',
|
||||
replaceHepaFilter: 'Replace HEPA filter for air quality',
|
||||
replaceCarbonFilter: 'Replace activated carbon filter',
|
||||
|
||||
@@ -121,6 +121,7 @@ export default {
|
||||
nozzleCount: 'Numero Ugelli',
|
||||
autoArchive: 'Auto Archiviazione',
|
||||
status: {
|
||||
available: 'Disponibile',
|
||||
idle: 'Inattiva',
|
||||
printing: 'In stampa',
|
||||
paused: 'In pausa',
|
||||
@@ -160,6 +161,7 @@ export default {
|
||||
},
|
||||
// Controls
|
||||
hideOffline: 'Nascondi offline',
|
||||
nextAvailable: 'Prossima disponibile',
|
||||
powerOn: 'Accendi',
|
||||
offlinePrintersWithPlugs: 'Stampanti offline con smart plug',
|
||||
noPrintersConfigured: 'Nessuna stampante configurata',
|
||||
@@ -967,6 +969,7 @@ export default {
|
||||
removeFromPrinter: 'Rimuovi da questa stampante',
|
||||
// Types
|
||||
types: {
|
||||
lubricateCarbonRods: 'Lubrifica aste in carbonio',
|
||||
lubricateRails: 'Lubrifica guide lineari',
|
||||
cleanNozzle: 'Pulisci ugello/Hotend',
|
||||
checkBelts: 'Controlla tensione cinghie',
|
||||
@@ -975,6 +978,7 @@ export default {
|
||||
checkCooling: 'Controlla ventole raffreddamento',
|
||||
generalInspection: 'Ispezione generale',
|
||||
cleanCarbonRods: 'Pulisci aste in carbonio',
|
||||
cleanLinearRails: 'Pulisci guide lineari',
|
||||
checkPtfeTube: 'Controlla tubo PTFE',
|
||||
replaceHepaFilter: 'Sostituisci filtro HEPA',
|
||||
replaceCarbonFilter: 'Sostituisci filtro carbone',
|
||||
@@ -2306,6 +2310,10 @@ export default {
|
||||
noPrintersAvailable: 'Nessuna stampante disponibile',
|
||||
printerBusy: 'Stampante occupata',
|
||||
printerOffline: 'Stampante offline',
|
||||
leftNozzle: 'L',
|
||||
rightNozzle: 'R',
|
||||
leftNozzleTooltip: 'Ugello sinistro',
|
||||
rightNozzleTooltip: 'Ugello destro',
|
||||
},
|
||||
|
||||
// Backup
|
||||
@@ -2647,6 +2655,7 @@ export default {
|
||||
|
||||
// Maintenance type descriptions (built-in)
|
||||
maintenanceDescriptions: {
|
||||
lubricateCarbonRods: 'Applica lubrificante alle aste in carbonio per un movimento fluido',
|
||||
lubricateRails: 'Applica lubrificante alle guide lineari per un movimento fluido',
|
||||
cleanNozzle: 'Pulisci hotend e ugello per prevenire intasamenti',
|
||||
checkBelts: 'Verifica tensione cinghie per stampe accurate',
|
||||
@@ -2655,6 +2664,7 @@ export default {
|
||||
checkCooling: 'Assicurati che le ventole di raffreddamento funzionino',
|
||||
generalInspection: 'Ispezione generale stampante',
|
||||
cleanCarbonRods: 'Pulisci le aste in carbonio per ridurre attrito',
|
||||
cleanLinearRails: 'Pulisci le guide lineari per rimuovere polvere e detriti',
|
||||
checkPtfeTube: 'Ispeziona il tubo PTFE per usura o danni',
|
||||
replaceHepaFilter: 'Sostituisci filtro HEPA per qualità aria',
|
||||
replaceCarbonFilter: 'Sostituisci filtro a carbone attivo',
|
||||
|
||||
@@ -133,6 +133,7 @@ export default {
|
||||
nozzleCount: 'ノズル数',
|
||||
autoArchive: '自動アーカイブ',
|
||||
status: {
|
||||
available: '利用可能',
|
||||
idle: '待機中',
|
||||
printing: '印刷中',
|
||||
paused: '一時停止',
|
||||
@@ -167,6 +168,7 @@ export default {
|
||||
extraLarge: '特大',
|
||||
},
|
||||
hideOffline: 'オフラインを非表示',
|
||||
nextAvailable: '次に完了',
|
||||
powerOn: '電源オン',
|
||||
noPrintersConfigured: 'プリンターが設定されていません',
|
||||
readyToPrint: '印刷可能',
|
||||
@@ -1015,6 +1017,7 @@ export default {
|
||||
noPrintersAssigned: 'プリンター未割り当て',
|
||||
removeFromPrinter: 'このプリンターから削除',
|
||||
types: {
|
||||
lubricateCarbonRods: 'カーボンロッドの潤滑',
|
||||
lubricateRails: 'リニアレールの潤滑',
|
||||
cleanNozzle: 'ノズル/ホットエンドの清掃',
|
||||
checkBelts: 'ベルト張力の確認',
|
||||
@@ -1023,6 +1026,7 @@ export default {
|
||||
checkCooling: '冷却ファンの確認',
|
||||
generalInspection: '総合点検',
|
||||
cleanCarbonRods: 'カーボンロッドの清掃',
|
||||
cleanLinearRails: 'リニアレールの清掃',
|
||||
checkPtfeTube: 'PTFEチューブの確認',
|
||||
replaceHepaFilter: 'HEPAフィルター交換',
|
||||
replaceCarbonFilter: 'カーボンフィルター交換',
|
||||
@@ -2552,6 +2556,10 @@ export default {
|
||||
printerBusy: 'プリンターは使用中です',
|
||||
printerOffline: 'プリンターはオフラインです',
|
||||
cancel: 'キャンセル',
|
||||
leftNozzle: 'L',
|
||||
rightNozzle: 'R',
|
||||
leftNozzleTooltip: '左ノズル',
|
||||
rightNozzleTooltip: '右ノズル',
|
||||
},
|
||||
backup: {
|
||||
restoreBackup: 'バックアップの復元',
|
||||
@@ -2856,6 +2864,7 @@ export default {
|
||||
},
|
||||
},
|
||||
maintenanceDescriptions: {
|
||||
lubricateCarbonRods: 'カーボンロッドに潤滑剤を塗布してスムーズな動きを確保',
|
||||
lubricateRails: 'リニアレールの潤滑',
|
||||
cleanNozzle: 'ノズル/ホットエンドの清掃',
|
||||
checkBelts: 'ベルト張力の確認',
|
||||
@@ -2864,6 +2873,7 @@ export default {
|
||||
checkCooling: '冷却ファンの確認',
|
||||
generalInspection: '総合点検',
|
||||
cleanCarbonRods: 'カーボンロッドの清掃',
|
||||
cleanLinearRails: 'リニアレールを拭いてほこりや汚れを除去',
|
||||
checkPtfeTube: 'PTFEチューブの確認',
|
||||
replaceHepaFilter: 'HEPAフィルター交換',
|
||||
replaceCarbonFilter: 'カーボンフィルター交換',
|
||||
|
||||
@@ -140,14 +140,19 @@ function getMaintenanceWikiUrl(typeName: string, printerModel: string | null): s
|
||||
const isP2S = model.includes('P2S');
|
||||
|
||||
switch (typeName) {
|
||||
case 'Lubricate Linear Rails':
|
||||
case 'Lubricate Carbon Rods':
|
||||
// X1, P1, P2S series have carbon rods
|
||||
if (isX1) return 'https://wiki.bambulab.com/en/x1/maintenance/basic-maintenance';
|
||||
if (isP1) return 'https://wiki.bambulab.com/en/p1/maintenance/p1p-maintenance';
|
||||
if (isP2S) return 'https://wiki.bambulab.com/en/p2s/maintenance/belt-tension';
|
||||
return null;
|
||||
|
||||
case 'Lubricate Linear Rails':
|
||||
// A1 and H2 series have linear rails
|
||||
if (isA1Mini) return 'https://wiki.bambulab.com/en/a1-mini/maintenance/lubricate-y-axis';
|
||||
if (isA1) return 'https://wiki.bambulab.com/en/a1/maintenance/lubricate-y-axis';
|
||||
if (isH2) return 'https://wiki.bambulab.com/en/h2/maintenance/x-axis-lubrication';
|
||||
if (isP2S) return 'https://wiki.bambulab.com/en/p2s/maintenance/belt-tension'; // P2S maintenance page
|
||||
return 'https://wiki.bambulab.com/en/general/lead-screws-lubrication';
|
||||
return null;
|
||||
|
||||
case 'Clean Nozzle/Hotend':
|
||||
if (isX1 || isP1) return 'https://wiki.bambulab.com/en/x1/troubleshooting/nozzle-clog';
|
||||
@@ -168,11 +173,16 @@ function getMaintenanceWikiUrl(typeName: string, printerModel: string | null): s
|
||||
return 'https://wiki.bambulab.com/en/x1/maintenance/belt-tension';
|
||||
|
||||
case 'Clean Carbon Rods':
|
||||
// Only X1 and P1 series have carbon rods
|
||||
if (isX1 || isP1) return 'https://wiki.bambulab.com/en/general/carbon-rods-clearance';
|
||||
// A1, H2, P2S don't have carbon rods - return null
|
||||
if (isA1Mini || isA1 || isH2 || isP2S) return null;
|
||||
return 'https://wiki.bambulab.com/en/general/carbon-rods-clearance';
|
||||
// X1, P1, P2S series have carbon rods
|
||||
if (isX1 || isP1 || isP2S) return 'https://wiki.bambulab.com/en/general/carbon-rods-clearance';
|
||||
return null;
|
||||
|
||||
case 'Clean Linear Rails':
|
||||
// A1 and H2 series have linear rails
|
||||
if (isA1Mini) return 'https://wiki.bambulab.com/en/a1-mini/maintenance/lubricate-y-axis';
|
||||
if (isA1) return 'https://wiki.bambulab.com/en/a1/maintenance/lubricate-y-axis';
|
||||
if (isH2) return 'https://wiki.bambulab.com/en/h2/maintenance/x-axis-lubrication';
|
||||
return null;
|
||||
|
||||
case 'Clean Build Plate':
|
||||
// Same for all printers
|
||||
@@ -381,7 +391,7 @@ function PrinterSection({
|
||||
hasPermission: (permission: Permission) => boolean;
|
||||
t: TFunction;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [editingHours, setEditingHours] = useState(false);
|
||||
const [hoursInput, setHoursInput] = useState(overview.total_print_hours.toFixed(1));
|
||||
|
||||
|
||||
@@ -1137,21 +1137,17 @@ function getWifiStrength(rssi: number): { labelKey: string; color: string; bars:
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tray contains a Bambu Lab spool.
|
||||
* Uses same logic as backend: tray_info_idx (GF*), tray_uuid, or tag_uid.
|
||||
* Check if a tray contains a Bambu Lab spool (RFID-tagged).
|
||||
* Only checks hardware identifiers (tray_uuid, tag_uid) — NOT tray_info_idx,
|
||||
* which is a filament profile/preset ID that third-party spools also get when
|
||||
* the user selects a generic Bambu preset (e.g. "GFA00" for Generic PLA).
|
||||
*/
|
||||
function isBambuLabSpool(tray: {
|
||||
tray_uuid?: string | null;
|
||||
tag_uid?: string | null;
|
||||
tray_info_idx?: string | null;
|
||||
} | null | undefined): boolean {
|
||||
if (!tray) return false;
|
||||
|
||||
// Check tray_info_idx first (most reliable - Bambu preset IDs start with "GF")
|
||||
if (tray.tray_info_idx && tray.tray_info_idx.startsWith('GF')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check tray_uuid (32 hex chars, non-zero)
|
||||
if (tray.tray_uuid && tray.tray_uuid !== '00000000000000000000000000000000') {
|
||||
return true;
|
||||
@@ -1240,39 +1236,16 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const counts = useMemo(() => {
|
||||
let printing = 0;
|
||||
let idle = 0;
|
||||
let offline = 0;
|
||||
let loading = 0;
|
||||
|
||||
printers?.forEach((printer) => {
|
||||
const status = queryClient.getQueryData<{ connected: boolean; state: string | null }>(['printerStatus', printer.id]);
|
||||
if (status === undefined) {
|
||||
// Status not yet loaded - don't count as offline yet
|
||||
loading++;
|
||||
} else if (!status.connected) {
|
||||
offline++;
|
||||
} else if (status.state === 'RUNNING') {
|
||||
printing++;
|
||||
} else {
|
||||
idle++;
|
||||
}
|
||||
});
|
||||
|
||||
return { printing, idle, offline, loading, total: (printers?.length || 0) };
|
||||
}, [printers, queryClient]);
|
||||
|
||||
// Subscribe to query cache changes to re-render when status updates
|
||||
// Throttled to prevent rapid re-renders from causing tab crashes
|
||||
const [, setTick] = useState(0);
|
||||
const [cacheTick, setCacheTick] = useState(0);
|
||||
useEffect(() => {
|
||||
let pending = false;
|
||||
const unsubscribe = queryClient.getQueryCache().subscribe(() => {
|
||||
if (!pending) {
|
||||
pending = true;
|
||||
requestAnimationFrame(() => {
|
||||
setTick(t => t + 1);
|
||||
setCacheTick(t => t + 1);
|
||||
pending = false;
|
||||
});
|
||||
}
|
||||
@@ -1280,10 +1253,53 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
|
||||
return () => unsubscribe();
|
||||
}, [queryClient]);
|
||||
|
||||
const { counts, nextFinish } = useMemo(() => {
|
||||
let printing = 0;
|
||||
let idle = 0;
|
||||
let offline = 0;
|
||||
let loading = 0;
|
||||
let nextPrinterName: string | null = null;
|
||||
let nextRemainingMin: number | null = null;
|
||||
let nextProgress: number = 0;
|
||||
|
||||
printers?.forEach((printer) => {
|
||||
const status = queryClient.getQueryData<{ connected: boolean; state: string | null; remaining_time: number | null; progress: number | null }>(['printerStatus', printer.id]);
|
||||
if (status === undefined) {
|
||||
// Status not yet loaded - don't count as offline yet
|
||||
loading++;
|
||||
} else if (!status.connected) {
|
||||
offline++;
|
||||
} else if (status.state === 'RUNNING') {
|
||||
printing++;
|
||||
if (status.remaining_time != null && status.remaining_time > 0) {
|
||||
if (nextRemainingMin === null || status.remaining_time < nextRemainingMin) {
|
||||
nextRemainingMin = status.remaining_time;
|
||||
nextPrinterName = printer.name;
|
||||
nextProgress = status.progress || 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
idle++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
counts: { printing, idle, offline, loading, total: (printers?.length || 0) },
|
||||
nextFinish: nextPrinterName && nextRemainingMin ? { name: nextPrinterName, remainingMin: nextRemainingMin, progress: nextProgress } : null,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [printers, queryClient, cacheTick]);
|
||||
|
||||
if (!printers?.length) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-2 h-2 rounded-full ${counts.idle > 0 ? 'bg-bambu-green' : 'bg-gray-500'}`} />
|
||||
<span className="text-bambu-gray">
|
||||
<span className="text-white font-medium">{counts.idle}</span> {t('printers.status.available').toLowerCase()}
|
||||
</span>
|
||||
</div>
|
||||
{counts.printing > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2 h-2 rounded-full bg-bambu-green animate-pulse" />
|
||||
@@ -1292,14 +1308,6 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{counts.idle > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-400" />
|
||||
<span className="text-bambu-gray">
|
||||
<span className="text-white font-medium">{counts.idle}</span> {t('printers.status.idle').toLowerCase()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{counts.offline > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2 h-2 rounded-full bg-gray-400" />
|
||||
@@ -1308,6 +1316,23 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{nextFinish && (
|
||||
<>
|
||||
<div className="w-px h-4 bg-bambu-dark-tertiary" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-bambu-green font-medium">{t('printers.nextAvailable')}:</span>
|
||||
<span className="text-white font-medium">{nextFinish.name}</span>
|
||||
<div className="w-16 bg-bambu-dark-tertiary rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-bambu-green h-1.5 rounded-full transition-all"
|
||||
style={{ width: `${nextFinish.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-white font-medium">{Math.round(nextFinish.progress)}%</span>
|
||||
<span className="text-bambu-gray">({formatTime(nextFinish.remainingMin * 60)})</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user