|
|
@@ -1039,6 +1039,27 @@ function isBambuLabSpool(tray: {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+function toFixedHex(value: number, width: number): string {
|
|
|
+ const safe = Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
|
+ return safe.toString(16).toUpperCase().padStart(width, '0').slice(-width);
|
|
|
+}
|
|
|
+
|
|
|
+// 32-bit FNV-1a hash -> 8-char hex (stable for alphanumeric serials)
|
|
|
+function hashSerialToHex32(serial: string): string {
|
|
|
+ const input = (serial || '').trim().toUpperCase();
|
|
|
+ let hash = 0x811c9dc5;
|
|
|
+ for (let i = 0; i < input.length; i++) {
|
|
|
+ hash ^= input.charCodeAt(i);
|
|
|
+ hash = Math.imul(hash, 0x01000193);
|
|
|
+ }
|
|
|
+ return (hash >>> 0).toString(16).toUpperCase().padStart(8, '0');
|
|
|
+}
|
|
|
+
|
|
|
+function getFallbackSpoolTag(printerSerial: string, amsId: number, trayId: number): string {
|
|
|
+ // 16-char stable hex tag for slots without RFID identifiers
|
|
|
+ return `${hashSerialToHex32(printerSerial)}${toFixedHex(amsId, 4)}${toFixedHex(trayId, 4)}`;
|
|
|
+}
|
|
|
+
|
|
|
function CoverImage({ url, printName }: { url: string | null; printName?: string }) {
|
|
|
const { t } = useTranslation();
|
|
|
const [loaded, setLoaded] = useState(false);
|
|
|
@@ -1492,7 +1513,6 @@ function PrinterCard({
|
|
|
cardSize = 2,
|
|
|
amsThresholds,
|
|
|
spoolmanEnabled = false,
|
|
|
- hasUnlinkedSpools = false,
|
|
|
linkedSpools,
|
|
|
spoolmanUrl,
|
|
|
onGetAssignment,
|
|
|
@@ -1832,6 +1852,18 @@ function PrinterCard({
|
|
|
},
|
|
|
});
|
|
|
|
|
|
+ const unlinkSpoolMutation = useMutation({
|
|
|
+ mutationFn: (spoolId: number) => api.unlinkSpool(spoolId),
|
|
|
+ onSuccess: (result) => {
|
|
|
+ showToast(t('spoolman.unlinkSuccess') || result?.message, 'success');
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['linked-spools'] });
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['unlinked-spools'] });
|
|
|
+ },
|
|
|
+ onError: (error: Error) => {
|
|
|
+ showToast(error.message || t('spoolman.unlinkFailed'), 'error');
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
// Smart plug control mutations
|
|
|
const powerControlMutation = useMutation({
|
|
|
mutationFn: (action: 'on' | 'off') =>
|
|
|
@@ -3034,7 +3066,7 @@ function PrinterCard({
|
|
|
const slotPreset = slotPresets?.[globalTrayId];
|
|
|
|
|
|
// Fill level fallback chain: Spoolman → Inventory → AMS remain
|
|
|
- const trayTag = tray?.tray_uuid?.toUpperCase();
|
|
|
+ const trayTag = (tray?.tag_uid || tray?.tray_uuid || getFallbackSpoolTag(printer.serial_number, ams.id, slotIdx))?.toUpperCase();
|
|
|
const linkedSpool = trayTag ? linkedSpools?.[trayTag] : undefined;
|
|
|
const spoolmanFill = getSpoolmanFillLevel(linkedSpool);
|
|
|
const inventoryAssignment = onGetAssignment?.(printer.id, ams.id, slotIdx);
|
|
|
@@ -3153,18 +3185,21 @@ function PrinterCard({
|
|
|
data={filamentData}
|
|
|
spoolman={{
|
|
|
enabled: spoolmanEnabled,
|
|
|
- hasUnlinkedSpools,
|
|
|
- linkedSpoolId: filamentData.trayUuid ? linkedSpools?.[filamentData.trayUuid.toUpperCase()]?.id : undefined,
|
|
|
+ linkedSpoolId: trayTag
|
|
|
+ ? linkedSpools?.[trayTag]?.id
|
|
|
+ : undefined,
|
|
|
spoolmanUrl,
|
|
|
- onLinkSpool: spoolmanEnabled && filamentData.trayUuid ? (uuid) => {
|
|
|
+ onLinkSpool: spoolmanEnabled ? () => {
|
|
|
+ const linkTag = (filamentData.trayUuid || filamentData.tagUid || getFallbackSpoolTag(printer.serial_number, ams.id, slotIdx)).toUpperCase();
|
|
|
setLinkSpoolModal({
|
|
|
- tagUid: filamentData.tagUid || '',
|
|
|
- trayUuid: uuid,
|
|
|
+ tagUid: filamentData.tagUid || linkTag,
|
|
|
+ trayUuid: filamentData.trayUuid || '',
|
|
|
printerId: printer.id,
|
|
|
amsId: ams.id,
|
|
|
trayId: slotIdx,
|
|
|
});
|
|
|
} : undefined,
|
|
|
+ onUnlinkSpool: linkedSpool?.id ? () => unlinkSpoolMutation.mutate(linkedSpool.id) : undefined,
|
|
|
}}
|
|
|
inventory={spoolmanEnabled ? undefined : (() => {
|
|
|
const assignment = onGetAssignment?.(printer.id, ams.id, slotIdx);
|
|
|
@@ -3252,13 +3287,13 @@ function PrinterCard({
|
|
|
const cloudInfo = tray?.tray_info_idx ? filamentInfo?.[tray.tray_info_idx] : null;
|
|
|
// Get saved slot preset mapping (for user-configured slots)
|
|
|
const slotPreset = slotPresets?.[globalTrayId];
|
|
|
+ const htSlotId = tray?.id ?? 0;
|
|
|
|
|
|
// Fill level fallback chain: Spoolman → Inventory → AMS remain
|
|
|
- const htTrayTag = tray?.tray_uuid?.toUpperCase();
|
|
|
+ const htTrayTag = (tray?.tray_uuid || tray?.tag_uid || getFallbackSpoolTag(printer.serial_number, ams.id, htSlotId))?.toUpperCase();
|
|
|
const htLinkedSpool = htTrayTag ? linkedSpools?.[htTrayTag] : undefined;
|
|
|
const htSpoolmanFill = getSpoolmanFillLevel(htLinkedSpool);
|
|
|
- const htTraySlotId = tray?.id ?? 0;
|
|
|
- const htInventoryAssignment = onGetAssignment?.(printer.id, ams.id, htTraySlotId);
|
|
|
+ const htInventoryAssignment = onGetAssignment?.(printer.id, ams.id, htSlotId);
|
|
|
const htInventoryFill = (() => {
|
|
|
const sp = htInventoryAssignment?.spool;
|
|
|
if (sp && sp.label_weight > 0 && sp.weight_used != null) {
|
|
|
@@ -3285,7 +3320,6 @@ function PrinterCard({
|
|
|
fillSource: htFillSource,
|
|
|
} : null;
|
|
|
|
|
|
- const htSlotId = tray?.id ?? 0;
|
|
|
// Check if this specific slot is being refreshed
|
|
|
const isHtRefreshing = refreshingSlot?.amsId === ams.id &&
|
|
|
refreshingSlot?.slotId === htSlotId;
|
|
|
@@ -3397,18 +3431,21 @@ function PrinterCard({
|
|
|
data={filamentData}
|
|
|
spoolman={{
|
|
|
enabled: spoolmanEnabled,
|
|
|
- hasUnlinkedSpools,
|
|
|
- linkedSpoolId: filamentData.trayUuid ? linkedSpools?.[filamentData.trayUuid.toUpperCase()]?.id : undefined,
|
|
|
+ linkedSpoolId: htTrayTag
|
|
|
+ ? linkedSpools?.[htTrayTag]?.id
|
|
|
+ : undefined,
|
|
|
spoolmanUrl,
|
|
|
- onLinkSpool: spoolmanEnabled && filamentData.trayUuid ? (uuid) => {
|
|
|
+ onLinkSpool: spoolmanEnabled ? () => {
|
|
|
+ const linkTag = (filamentData.trayUuid || filamentData.tagUid || getFallbackSpoolTag(printer.serial_number, ams.id, htSlotId)).toUpperCase();
|
|
|
setLinkSpoolModal({
|
|
|
- tagUid: filamentData.tagUid || '',
|
|
|
- trayUuid: uuid,
|
|
|
+ tagUid: filamentData.tagUid || linkTag,
|
|
|
+ trayUuid: filamentData.trayUuid || '',
|
|
|
printerId: printer.id,
|
|
|
amsId: ams.id,
|
|
|
trayId: htSlotId,
|
|
|
});
|
|
|
} : undefined,
|
|
|
+ onUnlinkSpool: htLinkedSpool?.id ? () => unlinkSpoolMutation.mutate(htLinkedSpool.id) : undefined,
|
|
|
}}
|
|
|
inventory={spoolmanEnabled ? undefined : (() => {
|
|
|
const assignment = onGetAssignment?.(printer.id, ams.id, htSlotId);
|
|
|
@@ -3525,7 +3562,7 @@ function PrinterCard({
|
|
|
const extCloudInfo = extTray.tray_info_idx ? filamentInfo?.[extTray.tray_info_idx] : null;
|
|
|
const extSlotPreset = slotPresets?.[255 * 4 + slotTrayId];
|
|
|
|
|
|
- const extTrayTag = extTray.tray_uuid?.toUpperCase();
|
|
|
+ const extTrayTag = (extTray.tray_uuid || extTray.tag_uid || getFallbackSpoolTag(printer.serial_number, 255, slotTrayId))?.toUpperCase();
|
|
|
const extLinkedSpool = extTrayTag ? linkedSpools?.[extTrayTag] : undefined;
|
|
|
const extSpoolmanFill = getSpoolmanFillLevel(extLinkedSpool);
|
|
|
const extInventoryAssignment = onGetAssignment?.(printer.id, 255, slotTrayId);
|
|
|
@@ -3590,18 +3627,21 @@ function PrinterCard({
|
|
|
data={extFilamentData}
|
|
|
spoolman={{
|
|
|
enabled: spoolmanEnabled,
|
|
|
- hasUnlinkedSpools,
|
|
|
- linkedSpoolId: extFilamentData.trayUuid ? linkedSpools?.[extFilamentData.trayUuid.toUpperCase()]?.id : undefined,
|
|
|
+ linkedSpoolId: extTrayTag
|
|
|
+ ? linkedSpools?.[extTrayTag]?.id
|
|
|
+ : undefined,
|
|
|
spoolmanUrl,
|
|
|
- onLinkSpool: spoolmanEnabled && extFilamentData.trayUuid ? (uuid) => {
|
|
|
+ onLinkSpool: spoolmanEnabled ? () => {
|
|
|
+ const linkTag = (extFilamentData.trayUuid || extFilamentData.tagUid || getFallbackSpoolTag(printer.serial_number, 255, slotTrayId)).toUpperCase();
|
|
|
setLinkSpoolModal({
|
|
|
- tagUid: extFilamentData.tagUid || '',
|
|
|
- trayUuid: uuid,
|
|
|
+ tagUid: extFilamentData.tagUid || linkTag,
|
|
|
+ trayUuid: extFilamentData.trayUuid || '',
|
|
|
printerId: printer.id,
|
|
|
amsId: 255,
|
|
|
trayId: slotTrayId,
|
|
|
});
|
|
|
} : undefined,
|
|
|
+ onUnlinkSpool: extLinkedSpool?.id ? () => unlinkSpoolMutation.mutate(extLinkedSpool.id) : undefined,
|
|
|
}}
|
|
|
inventory={spoolmanEnabled ? undefined : (() => {
|
|
|
const assignment = onGetAssignment?.(printer.id, 255, slotTrayId);
|