mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Improvements AMS card
This commit is contained in:
@@ -141,6 +141,18 @@ class AMSRefreshTrayRequest(BaseModel):
|
||||
tray_id: int = Field(..., ge=0, le=3, description="Tray ID within the AMS (0-3)")
|
||||
|
||||
|
||||
class AMSFilamentSettingRequest(BaseModel):
|
||||
ams_id: int = Field(..., ge=0, le=128, description="AMS unit ID (0-3, or 128 for H2D external)")
|
||||
tray_id: int = Field(..., ge=0, le=3, description="Tray ID within the AMS (0-3)")
|
||||
tray_info_idx: str = Field(..., description="Filament preset ID (e.g., 'GFA00')")
|
||||
tray_type: str = Field(..., description="Filament type (e.g., 'PLA', 'PETG')")
|
||||
tray_sub_brands: str = Field(default="", description="Sub-brand name (e.g., 'PLA Basic')")
|
||||
tray_color: str = Field(..., description="Color in RRGGBBAA hex format")
|
||||
nozzle_temp_min: int = Field(..., ge=150, le=350, description="Minimum nozzle temperature")
|
||||
nozzle_temp_max: int = Field(..., ge=150, le=350, description="Maximum nozzle temperature")
|
||||
k: float = Field(..., ge=0, le=1, description="Pressure advance (K) value")
|
||||
|
||||
|
||||
class GcodeRequest(ConfirmableRequest):
|
||||
command: str = Field(..., min_length=1, max_length=500, description="G-code command(s)")
|
||||
|
||||
@@ -635,6 +647,33 @@ async def ams_refresh_tray(
|
||||
return ControlResponse(success=success, message=message)
|
||||
|
||||
|
||||
@router.post("/{printer_id}/control/ams/filament-setting", response_model=ControlResponse)
|
||||
async def ams_set_filament_setting(
|
||||
printer_id: int,
|
||||
request: AMSFilamentSettingRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Set filament settings for an AMS tray including K (pressure advance) value."""
|
||||
await get_printer_or_404(printer_id, db)
|
||||
client = get_mqtt_client_or_503(printer_id)
|
||||
|
||||
success = client.ams_set_filament_setting(
|
||||
ams_id=request.ams_id,
|
||||
tray_id=request.tray_id,
|
||||
tray_info_idx=request.tray_info_idx,
|
||||
tray_type=request.tray_type,
|
||||
tray_sub_brands=request.tray_sub_brands,
|
||||
tray_color=request.tray_color,
|
||||
nozzle_temp_min=request.nozzle_temp_min,
|
||||
nozzle_temp_max=request.nozzle_temp_max,
|
||||
k=request.k,
|
||||
)
|
||||
return ControlResponse(
|
||||
success=success,
|
||||
message=f"Updated AMS {request.ams_id} tray {request.tray_id} with K={request.k}" if success else "Failed to update filament setting"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Advanced: G-code Command
|
||||
# =============================================================================
|
||||
|
||||
@@ -2627,6 +2627,60 @@ class BambuMQTTClient:
|
||||
logger.info(f"[{self.serial_number}] Triggering RFID re-read: AMS {ams_id}, slot {tray_id}")
|
||||
return True, f"Refreshing AMS {ams_id} tray {tray_id}"
|
||||
|
||||
def ams_set_filament_setting(
|
||||
self,
|
||||
ams_id: int,
|
||||
tray_id: int,
|
||||
tray_info_idx: str,
|
||||
tray_type: str,
|
||||
tray_sub_brands: str,
|
||||
tray_color: str,
|
||||
nozzle_temp_min: int,
|
||||
nozzle_temp_max: int,
|
||||
k: float,
|
||||
) -> bool:
|
||||
"""Set AMS tray filament settings including K (pressure advance) value.
|
||||
|
||||
Args:
|
||||
ams_id: AMS unit ID (0-3)
|
||||
tray_id: Tray ID within the AMS (0-3)
|
||||
tray_info_idx: Filament preset ID (e.g., "GFA00")
|
||||
tray_type: Filament type (e.g., "PLA", "PETG")
|
||||
tray_sub_brands: Sub-brand name (e.g., "PLA Basic", "PETG HF")
|
||||
tray_color: Color in RRGGBBAA hex format (e.g., "FFFF00FF")
|
||||
nozzle_temp_min: Minimum nozzle temperature
|
||||
nozzle_temp_max: Maximum nozzle temperature
|
||||
k: Pressure advance (K) value (e.g., 0.020)
|
||||
|
||||
Returns:
|
||||
True if command was sent, False otherwise
|
||||
"""
|
||||
if not self._client or not self.state.connected:
|
||||
logger.warning(f"[{self.serial_number}] Cannot set AMS filament setting: not connected")
|
||||
return False
|
||||
|
||||
command = {
|
||||
"print": {
|
||||
"command": "ams_filament_setting",
|
||||
"ams_id": ams_id,
|
||||
"tray_id": tray_id,
|
||||
"tray_info_idx": tray_info_idx,
|
||||
"tray_type": tray_type,
|
||||
"tray_sub_brands": tray_sub_brands,
|
||||
"tray_color": tray_color,
|
||||
"nozzle_temp_min": nozzle_temp_min,
|
||||
"nozzle_temp_max": nozzle_temp_max,
|
||||
"k": k,
|
||||
"sequence_id": "0"
|
||||
}
|
||||
}
|
||||
|
||||
command_json = json.dumps(command)
|
||||
logger.info(f"[{self.serial_number}] Publishing ams_filament_setting: AMS {ams_id}, tray {tray_id}, k={k}")
|
||||
logger.debug(f"[{self.serial_number}] ams_filament_setting command: {command_json}")
|
||||
self._client.publish(self.topic_publish, command_json)
|
||||
return True
|
||||
|
||||
def set_timelapse(self, enable: bool) -> bool:
|
||||
"""Enable or disable timelapse recording.
|
||||
|
||||
|
||||
@@ -1281,6 +1281,21 @@ export const api = {
|
||||
}),
|
||||
amsUnloadFilament: (printerId: number) =>
|
||||
request<ControlResponse>(`/printers/${printerId}/control/ams/unload`, { method: 'POST' }),
|
||||
amsSetFilamentSetting: (printerId: number, data: {
|
||||
ams_id: number;
|
||||
tray_id: number;
|
||||
tray_info_idx: string;
|
||||
tray_type: string;
|
||||
tray_sub_brands: string;
|
||||
tray_color: string;
|
||||
nozzle_temp_min: number;
|
||||
nozzle_temp_max: number;
|
||||
k: number;
|
||||
}) =>
|
||||
request<ControlResponse>(`/printers/${printerId}/control/ams/filament-setting`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
sendGcode: (printerId: number, command: string, confirmToken?: string) =>
|
||||
request<ControlResult>(`/printers/${printerId}/control/gcode`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -74,6 +74,12 @@ function trayColorToHex(trayColor: string | null): string {
|
||||
return `#${cleanHex}`;
|
||||
}
|
||||
|
||||
function hexToTrayColor(hex: string): string {
|
||||
// Convert #RRGGBB to RRGGBBFF (with full opacity)
|
||||
const cleanHex = hex.replace('#', '').toUpperCase();
|
||||
return cleanHex.length === 6 ? `${cleanHex}FF` : `${cleanHex.substring(0, 6)}FF`;
|
||||
}
|
||||
|
||||
// Check if tray has valid Bambu Lab UUID (32-char hex)
|
||||
function isBambuLabSpool(trayUuid: string | null): boolean {
|
||||
if (!trayUuid) return false;
|
||||
@@ -210,16 +216,40 @@ export function AMSMaterialsModal({
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation for saving filament settings to printer (including K value)
|
||||
const saveFilamentSettingMutation = useMutation({
|
||||
mutationFn: (data: {
|
||||
ams_id: number;
|
||||
tray_id: number;
|
||||
tray_info_idx: string;
|
||||
tray_type: string;
|
||||
tray_sub_brands: string;
|
||||
tray_color: string;
|
||||
nozzle_temp_min: number;
|
||||
nozzle_temp_max: number;
|
||||
k: number;
|
||||
}) => api.amsSetFilamentSetting(printerId, data),
|
||||
onSuccess: (result) => {
|
||||
console.log('[AMSMaterialsModal] saveFilamentSettingMutation SUCCESS:', result);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('[AMSMaterialsModal] saveFilamentSettingMutation ERROR:', error);
|
||||
},
|
||||
});
|
||||
|
||||
// Get filament presets from cloud settings:
|
||||
// 1. Show ALL filament presets (like Bambu Studio does in cloud profiles)
|
||||
// 1. Filter out presets starting with "#" (experimental/special presets)
|
||||
// 2. Prioritize current printer model, then deduplicate by base name
|
||||
// 3. Sort alphabetically
|
||||
const filamentPresets = (() => {
|
||||
const allPresets = cloudSettings?.filament || [];
|
||||
|
||||
// Filter out presets starting with "#" (experimental/special presets)
|
||||
const filteredPresets = allPresets.filter(p => !p.name.startsWith('# '));
|
||||
|
||||
// Sort presets: current printer model first, then others
|
||||
const modelPattern = new RegExp(`@.*\\b${printerModel}\\b`, 'i');
|
||||
const sortedByModel = [...allPresets].sort((a, b) => {
|
||||
const sortedByModel = [...filteredPresets].sort((a, b) => {
|
||||
const aMatches = modelPattern.test(a.name);
|
||||
const bMatches = modelPattern.test(b.name);
|
||||
if (aMatches && !bMatches) return -1;
|
||||
@@ -323,6 +353,22 @@ export function AMSMaterialsModal({
|
||||
const bambuColorName = getColorNameFromTrayId(tray.tray_id_name);
|
||||
|
||||
useEffect(() => {
|
||||
// First, try to find a profile matching the tray's current K value (from printer)
|
||||
// This preserves the user's selection when reopening the modal
|
||||
const currentK = tray.k;
|
||||
if (currentK && currentK > 0) {
|
||||
const matchingKProfile = matchingProfiles.find(p => {
|
||||
const profileK = parseFloat(p.k_value);
|
||||
return Math.abs(profileK - currentK) < 0.0001; // Allow small floating point tolerance
|
||||
});
|
||||
if (matchingKProfile) {
|
||||
setSelectedKProfile(matchingKProfile.name);
|
||||
setKValue(currentK);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to name-based matching if no K-value match found
|
||||
// For Bambu spools, use tray_sub_brands (e.g., "PLA Basic") and color name
|
||||
// For non-Bambu, use clean name from preset (e.g., "Bambu PLA Basic" without #) for K-profile matching
|
||||
const subBrands = isBambuSpool
|
||||
@@ -340,7 +386,7 @@ export function AMSMaterialsModal({
|
||||
setSelectedKProfile('Default');
|
||||
setKValue(0);
|
||||
}
|
||||
}, [profiles, selectedFilamentType, selectedPresetName, tray.tray_type, tray.tray_sub_brands, tray.tray_id_name, isBambuSpool, bambuColorName]);
|
||||
}, [profiles, matchingProfiles, selectedFilamentType, selectedPresetName, tray.tray_type, tray.tray_sub_brands, tray.tray_id_name, tray.k, isBambuSpool, bambuColorName]);
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
@@ -360,14 +406,44 @@ export function AMSMaterialsModal({
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
console.log('[AMSMaterialsModal] handleConfirm called');
|
||||
console.log('[AMSMaterialsModal] amsId:', amsId, 'tray.id:', tray.id, 'kValue:', kValue);
|
||||
|
||||
// Save preset mapping for non-Bambu slots
|
||||
if (isEditable && selectedPresetId && selectedPresetName) {
|
||||
console.log('[AMSMaterialsModal] Saving preset mapping');
|
||||
savePresetMutation.mutate({
|
||||
presetId: selectedPresetId,
|
||||
presetName: selectedPresetName,
|
||||
});
|
||||
}
|
||||
|
||||
// Send filament settings to printer (including K value)
|
||||
// Use current tray data for Bambu spools, or edited values for non-Bambu
|
||||
const trayColor = isBambuSpool
|
||||
? (tray.tray_color || '808080FF')
|
||||
: hexToTrayColor(selectedColor);
|
||||
const trayType = isBambuSpool
|
||||
? (tray.tray_type || 'PLA')
|
||||
: (selectedFilamentType || 'PLA');
|
||||
const traySubBrands = isBambuSpool
|
||||
? (tray.tray_sub_brands || '')
|
||||
: (selectedPresetName ? getCleanFilamentName(selectedPresetName) : '');
|
||||
|
||||
const payload = {
|
||||
ams_id: amsId,
|
||||
tray_id: tray.id,
|
||||
tray_info_idx: tray.tray_info_idx || '',
|
||||
tray_type: trayType,
|
||||
tray_sub_brands: traySubBrands,
|
||||
tray_color: trayColor,
|
||||
nozzle_temp_min: nozzleTempMin,
|
||||
nozzle_temp_max: nozzleTempMax,
|
||||
k: kValue,
|
||||
};
|
||||
console.log('[AMSMaterialsModal] Calling saveFilamentSettingMutation with payload:', payload);
|
||||
saveFilamentSettingMutation.mutate(payload);
|
||||
|
||||
onConfirm?.({
|
||||
filamentType: selectedFilamentType,
|
||||
color: selectedColor,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../../api/client';
|
||||
import type { PrinterStatus, AMSUnit, AMSTray, KProfile } from '../../api/client';
|
||||
import { Loader2, ChevronDown, ChevronUp, RotateCw } from 'lucide-react';
|
||||
import { AMSHumidityModal } from './AMSHumidityModal';
|
||||
import { AMSMaterialsModal } from './AMSMaterialsModal';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { useAmsOperations } from './useAmsOperations';
|
||||
|
||||
|
||||
interface AMSSectionDualProps {
|
||||
@@ -519,7 +520,10 @@ function AMSPanelContent({
|
||||
return (
|
||||
<button
|
||||
key={tray.id}
|
||||
onClick={() => onSlotRefresh(selectedUnit.id, tray.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSlotRefresh(selectedUnit.id, tray.id);
|
||||
}}
|
||||
disabled={isRefreshing}
|
||||
className={`w-14 flex items-center justify-center gap-0.5 text-[10px] text-bambu-gray px-1.5 py-[3px] bg-bambu-dark rounded-full border border-bambu-dark-tertiary transition-colors ${
|
||||
isRefreshing ? 'opacity-70 cursor-wait' : 'hover:bg-bambu-dark-tertiary'
|
||||
@@ -927,20 +931,22 @@ export function AMSSectionDual({ printerId, printerModel, status, nozzleCount }:
|
||||
const [humidityModal, setHumidityModal] = useState<{ humidity: number; temp: number } | null>(null);
|
||||
const [materialsModal, setMaterialsModal] = useState<{ tray: AMSTray; slotLabel: string; amsId: number } | null>(null);
|
||||
|
||||
// Track refreshing slot - cleared when tray data updates from MQTT
|
||||
const [refreshingSlotState, setRefreshingSlotState] = useState<{ amsId: number; trayId: number; startTime: number } | null>(null);
|
||||
// Get ams_status values from printer status
|
||||
const amsStatusMain = status?.ams_status_main ?? 0;
|
||||
const trayNow = status?.tray_now ?? 255;
|
||||
|
||||
// Track user-initiated filament change operations (for showing progress card immediately)
|
||||
// Store both the operation type (load/unload) and the target tray ID for load operations
|
||||
const [userFilamentChange, setUserFilamentChange] = useState<{ isLoading: boolean; targetTrayId: number | null } | null>(null);
|
||||
// AMS Operations hook - manages state machine for refresh/load/unload
|
||||
const amsOps = useAmsOperations({
|
||||
printerId,
|
||||
amsUnits,
|
||||
amsStatusMain,
|
||||
trayNow,
|
||||
onToast: showToast,
|
||||
});
|
||||
|
||||
// Track if we've done initial sync from tray_now
|
||||
const initialSyncDone = useRef(false);
|
||||
|
||||
// Track intended operation type synchronously (refs update immediately, unlike state)
|
||||
// This prevents race conditions where MQTT updates arrive before React state updates
|
||||
const intendedOperationRef = useRef<'load' | 'unload' | null>(null);
|
||||
|
||||
// Sync selectedTray from status.tray_now on initial load
|
||||
// tray_now: 255 = no filament loaded, 0-253 = valid tray ID, 254 = external spool
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { api } from '../../api/client';
|
||||
import type { AMSUnit } from '../../api/client';
|
||||
|
||||
/**
|
||||
* AMS Operation State Machine
|
||||
*
|
||||
* States:
|
||||
* - IDLE: No operation in progress, all buttons enabled
|
||||
* - REFRESHING: RFID refresh in progress for a specific slot
|
||||
* - LOADING: Filament load in progress
|
||||
* - UNLOADING: Filament unload in progress
|
||||
*
|
||||
* Completion detection:
|
||||
* - REFRESH: AMS tray data changes (tag_uid, tray_uuid, etc.) OR timeout (15s)
|
||||
* - LOAD/UNLOAD: ams_status_main transitions from 1 (filament_change) to 0 (idle) OR timeout (60s)
|
||||
*
|
||||
* Rules:
|
||||
* - Only one operation at a time
|
||||
* - All operations have timeout fallback
|
||||
* - Operation can be cancelled/reset manually
|
||||
*/
|
||||
|
||||
export type OperationState = 'IDLE' | 'REFRESHING' | 'LOADING' | 'UNLOADING';
|
||||
|
||||
export interface RefreshTarget {
|
||||
amsId: number;
|
||||
trayId: number;
|
||||
}
|
||||
|
||||
export interface OperationContext {
|
||||
// For REFRESHING: which slot is being refreshed
|
||||
refreshTarget?: RefreshTarget;
|
||||
// For LOADING: target tray ID we're loading
|
||||
loadTargetTrayId?: number;
|
||||
// Timestamp when operation started
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
interface UseAmsOperationsProps {
|
||||
printerId: number;
|
||||
amsUnits: AMSUnit[];
|
||||
amsStatusMain: number;
|
||||
trayNow: number;
|
||||
onToast: (message: string, type: 'success' | 'error') => void;
|
||||
}
|
||||
|
||||
interface UseAmsOperationsReturn {
|
||||
// Current state
|
||||
state: OperationState;
|
||||
context: OperationContext | null;
|
||||
|
||||
// Operation triggers
|
||||
startRefresh: (amsId: number, trayId: number) => void;
|
||||
startLoad: (trayId: number, extruderId?: number) => void;
|
||||
startUnload: () => void;
|
||||
|
||||
// Manual reset (e.g., for retry)
|
||||
reset: () => void;
|
||||
|
||||
// Derived state helpers
|
||||
isOperationInProgress: boolean;
|
||||
isRefreshingSlot: (amsId: number, trayId: number) => boolean;
|
||||
|
||||
// For FilamentChangeCard - which type of operation
|
||||
isLoadOperation: boolean;
|
||||
loadTargetTrayId: number | null;
|
||||
|
||||
// Mutation error states (for UI feedback)
|
||||
loadError: Error | null;
|
||||
unloadError: Error | null;
|
||||
refreshError: Error | null;
|
||||
}
|
||||
|
||||
// Timeouts for different operations
|
||||
const REFRESH_TIMEOUT_MS = 15000; // 15 seconds for RFID refresh
|
||||
const FILAMENT_CHANGE_TIMEOUT_MS = 120000; // 2 minutes for load/unload (these can take a while with heating)
|
||||
|
||||
export function useAmsOperations({
|
||||
printerId,
|
||||
amsUnits,
|
||||
amsStatusMain,
|
||||
trayNow,
|
||||
onToast,
|
||||
}: UseAmsOperationsProps): UseAmsOperationsReturn {
|
||||
const [state, setState] = useState<OperationState>('IDLE');
|
||||
const [context, setContext] = useState<OperationContext | null>(null);
|
||||
|
||||
// Track previous values for transition detection
|
||||
const prevAmsStatusMainRef = useRef(amsStatusMain);
|
||||
const prevTrayDataRef = useRef<string>('');
|
||||
|
||||
// Timeout ref for cleanup
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Clear any pending timeout
|
||||
const clearOperationTimeout = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Reset to IDLE state
|
||||
const reset = useCallback(() => {
|
||||
clearOperationTimeout();
|
||||
setState('IDLE');
|
||||
setContext(null);
|
||||
prevTrayDataRef.current = '';
|
||||
}, [clearOperationTimeout]);
|
||||
|
||||
// Set up timeout for current operation
|
||||
const startOperationTimeout = useCallback((timeoutMs: number) => {
|
||||
clearOperationTimeout();
|
||||
const startTime = context?.startTime ?? Date.now();
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
console.log(`[useAmsOperations] Operation timed out after ${timeoutMs}ms`);
|
||||
reset();
|
||||
}, timeoutMs);
|
||||
}, [clearOperationTimeout, reset, context?.startTime]);
|
||||
|
||||
// === Mutations ===
|
||||
|
||||
const refreshMutation = useMutation({
|
||||
mutationFn: ({ amsId, trayId }: { amsId: number; trayId: number }) =>
|
||||
api.refreshAmsTray(printerId, amsId, trayId),
|
||||
onSuccess: (data) => {
|
||||
if (data.success) {
|
||||
onToast(data.message || 'RFID refresh started', 'success');
|
||||
} else {
|
||||
onToast(data.message || 'Failed to refresh tray', 'error');
|
||||
reset();
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('[useAmsOperations] Refresh error:', error);
|
||||
onToast('Failed to refresh tray', 'error');
|
||||
reset();
|
||||
},
|
||||
});
|
||||
|
||||
const loadMutation = useMutation({
|
||||
mutationFn: ({ trayId, extruderId }: { trayId: number; extruderId?: number }) =>
|
||||
api.amsLoadFilament(printerId, trayId, extruderId),
|
||||
onSuccess: (data) => {
|
||||
console.log('[useAmsOperations] Load request sent:', data);
|
||||
// Don't reset here - wait for ams_status_main transition
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('[useAmsOperations] Load error:', error);
|
||||
reset();
|
||||
},
|
||||
});
|
||||
|
||||
const unloadMutation = useMutation({
|
||||
mutationFn: () => api.amsUnloadFilament(printerId),
|
||||
onSuccess: (data) => {
|
||||
console.log('[useAmsOperations] Unload request sent:', data);
|
||||
// Don't reset here - wait for ams_status_main transition
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('[useAmsOperations] Unload error:', error);
|
||||
reset();
|
||||
},
|
||||
});
|
||||
|
||||
// === Operation Triggers ===
|
||||
|
||||
const startRefresh = useCallback((amsId: number, trayId: number) => {
|
||||
if (state !== 'IDLE') {
|
||||
console.log('[useAmsOperations] Cannot start refresh - operation in progress:', state);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[useAmsOperations] Starting refresh: AMS ${amsId}, Tray ${trayId}`);
|
||||
|
||||
// Capture current tray data signature for change detection
|
||||
const unit = amsUnits.find(u => u.id === amsId);
|
||||
const tray = unit?.tray?.find(t => t.id === trayId);
|
||||
if (tray) {
|
||||
prevTrayDataRef.current = JSON.stringify({
|
||||
tag_uid: tray.tag_uid,
|
||||
tray_uuid: tray.tray_uuid,
|
||||
tray_id_name: tray.tray_id_name,
|
||||
tray_type: tray.tray_type,
|
||||
tray_color: tray.tray_color,
|
||||
});
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
setState('REFRESHING');
|
||||
setContext({ refreshTarget: { amsId, trayId }, startTime });
|
||||
|
||||
// Set timeout
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
console.log(`[useAmsOperations] Refresh timeout for AMS ${amsId} tray ${trayId}`);
|
||||
reset();
|
||||
}, REFRESH_TIMEOUT_MS);
|
||||
|
||||
refreshMutation.mutate({ amsId, trayId });
|
||||
}, [state, amsUnits, reset, refreshMutation]);
|
||||
|
||||
const startLoad = useCallback((trayId: number, extruderId?: number) => {
|
||||
if (state !== 'IDLE') {
|
||||
console.log('[useAmsOperations] Cannot start load - operation in progress:', state);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[useAmsOperations] Starting load: tray ${trayId}, extruder ${extruderId}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
setState('LOADING');
|
||||
setContext({ loadTargetTrayId: trayId, startTime });
|
||||
|
||||
// Set timeout
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
console.log(`[useAmsOperations] Load timeout for tray ${trayId}`);
|
||||
reset();
|
||||
}, FILAMENT_CHANGE_TIMEOUT_MS);
|
||||
|
||||
loadMutation.mutate({ trayId, extruderId });
|
||||
}, [state, reset, loadMutation]);
|
||||
|
||||
const startUnload = useCallback(() => {
|
||||
if (state !== 'IDLE') {
|
||||
console.log('[useAmsOperations] Cannot start unload - operation in progress:', state);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[useAmsOperations] Starting unload');
|
||||
|
||||
const startTime = Date.now();
|
||||
setState('UNLOADING');
|
||||
setContext({ startTime });
|
||||
|
||||
// Set timeout
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
console.log('[useAmsOperations] Unload timeout');
|
||||
reset();
|
||||
}, FILAMENT_CHANGE_TIMEOUT_MS);
|
||||
|
||||
unloadMutation.mutate();
|
||||
}, [state, reset, unloadMutation]);
|
||||
|
||||
// === Completion Detection ===
|
||||
|
||||
// Detect REFRESH completion via tray data change
|
||||
useEffect(() => {
|
||||
if (state !== 'REFRESHING' || !context?.refreshTarget) return;
|
||||
|
||||
const { amsId, trayId } = context.refreshTarget;
|
||||
const unit = amsUnits.find(u => u.id === amsId);
|
||||
const tray = unit?.tray?.find(t => t.id === trayId);
|
||||
|
||||
if (!tray) return;
|
||||
|
||||
const currentSignature = JSON.stringify({
|
||||
tag_uid: tray.tag_uid,
|
||||
tray_uuid: tray.tray_uuid,
|
||||
tray_id_name: tray.tray_id_name,
|
||||
tray_type: tray.tray_type,
|
||||
tray_color: tray.tray_color,
|
||||
});
|
||||
|
||||
// Require minimum 500ms to avoid false positives from initial render
|
||||
const elapsed = Date.now() - context.startTime;
|
||||
if (prevTrayDataRef.current && prevTrayDataRef.current !== currentSignature && elapsed > 500) {
|
||||
console.log(`[useAmsOperations] Refresh complete: data changed for AMS ${amsId} tray ${trayId} (took ${elapsed}ms)`);
|
||||
reset();
|
||||
}
|
||||
}, [state, context, amsUnits, reset]);
|
||||
|
||||
// Detect LOAD/UNLOAD completion via ams_status_main transition 1 → 0
|
||||
useEffect(() => {
|
||||
if (state !== 'LOADING' && state !== 'UNLOADING') {
|
||||
prevAmsStatusMainRef.current = amsStatusMain;
|
||||
return;
|
||||
}
|
||||
|
||||
const wasActive = prevAmsStatusMainRef.current === 1;
|
||||
const isNowIdle = amsStatusMain === 0;
|
||||
|
||||
if (wasActive && isNowIdle) {
|
||||
console.log(`[useAmsOperations] ${state} complete: ams_status_main transitioned 1→0`);
|
||||
reset();
|
||||
}
|
||||
|
||||
prevAmsStatusMainRef.current = amsStatusMain;
|
||||
}, [state, amsStatusMain, reset]);
|
||||
|
||||
// Secondary completion detection for LOAD: tray_now matches target
|
||||
useEffect(() => {
|
||||
if (state !== 'LOADING' || !context?.loadTargetTrayId) return;
|
||||
|
||||
if (trayNow === context.loadTargetTrayId) {
|
||||
console.log(`[useAmsOperations] Load complete: tray_now=${trayNow} matches target`);
|
||||
reset();
|
||||
}
|
||||
}, [state, context, trayNow, reset]);
|
||||
|
||||
// Secondary completion detection for UNLOAD: tray_now becomes 255
|
||||
useEffect(() => {
|
||||
if (state !== 'UNLOADING') return;
|
||||
|
||||
// Only trigger if we're past the initial phase (give it 1s to start)
|
||||
const elapsed = context?.startTime ? Date.now() - context.startTime : 0;
|
||||
if (trayNow === 255 && elapsed > 1000) {
|
||||
console.log('[useAmsOperations] Unload complete: tray_now=255');
|
||||
reset();
|
||||
}
|
||||
}, [state, context, trayNow, reset]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearOperationTimeout();
|
||||
};
|
||||
}, [clearOperationTimeout]);
|
||||
|
||||
// === Derived State ===
|
||||
|
||||
const isOperationInProgress = state !== 'IDLE';
|
||||
|
||||
const isRefreshingSlot = useCallback((amsId: number, trayId: number) => {
|
||||
if (state !== 'REFRESHING' || !context?.refreshTarget) return false;
|
||||
return context.refreshTarget.amsId === amsId && context.refreshTarget.trayId === trayId;
|
||||
}, [state, context]);
|
||||
|
||||
const isLoadOperation = state === 'LOADING';
|
||||
const loadTargetTrayId = context?.loadTargetTrayId ?? null;
|
||||
|
||||
return {
|
||||
state,
|
||||
context,
|
||||
startRefresh,
|
||||
startLoad,
|
||||
startUnload,
|
||||
reset,
|
||||
isOperationInProgress,
|
||||
isRefreshingSlot,
|
||||
isLoadOperation,
|
||||
loadTargetTrayId,
|
||||
loadError: loadMutation.error,
|
||||
unloadError: unloadMutation.error,
|
||||
refreshError: refreshMutation.error,
|
||||
};
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -7,7 +7,7 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
|
||||
<script type="module" crossorigin src="/assets/index-CXerUj0x.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CLNJVLog.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BpSfhfce.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user