Browse Source

- Show detailed printer status stages on printer cards

  - Add getStatusDisplay() helper in frontend to format status nicely
  - Add get_derived_status_name() in backend to compute status from:
    - stg_cur calibration stages (bed leveling, vibration comp, etc.)
    - Temperature data when stg_cur unavailable (heating heatbed/nozzle)
  - Include stg_cur and stg_cur_name in WebSocket broadcasts
  - Update broadcast status key to detect heating phase changes

  Printer cards now show: "Heating heatbed", "Heating nozzle", "Preparing",
  "Auto bed leveling", "Cleaning nozzle tip", and 60+ other stage names
  instead of just "Printing" or "Idle".
maziggy 5 months ago
parent
commit
0200f69ba3

+ 2 - 3
backend/app/api/routes/printers.py

@@ -28,8 +28,7 @@ from backend.app.services.bambu_ftp import (
     get_storage_info_async,
     get_storage_info_async,
     list_files_async,
     list_files_async,
 )
 )
-from backend.app.services.bambu_mqtt import get_stage_name
-from backend.app.services.printer_manager import printer_manager
+from backend.app.services.printer_manager import get_derived_status_name, printer_manager
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 router = APIRouter(prefix="/printers", tags=["printers"])
 router = APIRouter(prefix="/printers", tags=["printers"])
@@ -300,7 +299,7 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
         nozzles=nozzles,
         nozzles=nozzles,
         print_options=print_options,
         print_options=print_options,
         stg_cur=state.stg_cur,
         stg_cur=state.stg_cur,
-        stg_cur_name=get_stage_name(state.stg_cur) if state.stg_cur >= 0 else None,
+        stg_cur_name=get_derived_status_name(state),
         stg=state.stg,
         stg=state.stg,
         airduct_mode=state.airduct_mode,
         airduct_mode=state.airduct_mode,
         speed_level=state.speed_level,
         speed_level=state.speed_level,

+ 6 - 1
backend/app/main.py

@@ -228,9 +228,14 @@ async def on_printer_status_change(printer_id: int, state: PrinterState):
                     f"Auto-detected dual-nozzle printer {printer_id}, updated nozzle_count=2"
                     f"Auto-detected dual-nozzle printer {printer_id}, updated nozzle_count=2"
                 )
                 )
 
 
+    # Include target temps for heating phase detection
+    bed_target = round(temps.get("bed_target", 0))
+    nozzle_target = round(temps.get("nozzle_target", 0))
+
     status_key = (
     status_key = (
         f"{state.connected}:{state.state}:{state.progress}:{state.layer_num}:"
         f"{state.connected}:{state.state}:{state.progress}:{state.layer_num}:"
-        f"{nozzle_temp}:{bed_temp}:{nozzle_2_temp}:{chamber_temp}"
+        f"{nozzle_temp}:{bed_temp}:{nozzle_2_temp}:{chamber_temp}:"
+        f"{state.stg_cur}:{bed_target}:{nozzle_target}"
     )
     )
     if _last_status_broadcast.get(printer_id) == status_key:
     if _last_status_broadcast.get(printer_id) == status_key:
         return  # No change, skip broadcast
         return  # No change, skip broadcast

+ 53 - 1
backend/app/services/printer_manager.py

@@ -5,7 +5,7 @@ from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 
 
 from backend.app.models.printer import Printer
 from backend.app.models.printer import Printer
-from backend.app.services.bambu_mqtt import BambuMQTTClient, MQTTLogEntry, PrinterState
+from backend.app.services.bambu_mqtt import BambuMQTTClient, MQTTLogEntry, PrinterState, get_stage_name
 
 
 
 
 class PrinterManager:
 class PrinterManager:
@@ -280,6 +280,55 @@ class PrinterManager:
         return result
         return result
 
 
 
 
+def get_derived_status_name(state: PrinterState) -> str | None:
+    """
+    Compute a human-readable status name based on printer state.
+
+    Uses stg_cur when available, otherwise derives status from temperature data
+    when the printer is heating before a print starts.
+    """
+    # If we have a valid calibration stage, use it
+    if state.stg_cur >= 0:
+        return get_stage_name(state.stg_cur)
+
+    # If not in RUNNING state, no derived status needed
+    if state.state != "RUNNING":
+        return None
+
+    # Check if we're in an early phase where temperatures are heating
+    temps = state.temperatures or {}
+    progress = state.progress or 0
+
+    # Only derive heating status when progress is very low (< 2%)
+    # This indicates we're in the preparation phase, not actually printing
+    if progress >= 2:
+        return None
+
+    # Check bed temperature - if target is set and current is significantly below
+    bed_temp = temps.get("bed", 0)
+    bed_target = temps.get("bed_target", 0)
+
+    # Check nozzle temperature
+    nozzle_temp = temps.get("nozzle", 0)
+    nozzle_target = temps.get("nozzle_target", 0)
+
+    # Temperature thresholds: consider "heating" if more than 10°C below target
+    TEMP_THRESHOLD = 10
+
+    # Determine what's heating (prioritize bed since it takes longer)
+    if bed_target > 30 and (bed_target - bed_temp) > TEMP_THRESHOLD:
+        return "Heating heatbed"
+    elif nozzle_target > 30 and (nozzle_target - nozzle_temp) > TEMP_THRESHOLD:
+        return "Heating nozzle"
+
+    # If targets are set but we're close to them, we might be in final prep
+    if bed_target > 30 or nozzle_target > 30:
+        if progress == 0 and state.layer_num == 0:
+            return "Preparing"
+
+    return None
+
+
 def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) -> dict:
 def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) -> dict:
     """Convert PrinterState to a JSON-serializable dict."""
     """Convert PrinterState to a JSON-serializable dict."""
     # Parse AMS data from raw_data
     # Parse AMS data from raw_data
@@ -383,6 +432,9 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) ->
         "ams_extruder_map": ams_extruder_map,
         "ams_extruder_map": ams_extruder_map,
         # WiFi signal strength
         # WiFi signal strength
         "wifi_signal": state.wifi_signal,
         "wifi_signal": state.wifi_signal,
+        # Calibration stage tracking
+        "stg_cur": state.stg_cur,
+        "stg_cur_name": get_derived_status_name(state),
     }
     }
     # Add cover URL if there's an active print and printer_id is provided
     # Add cover URL if there's an active print and printer_id is provided
     if printer_id and state.state == "RUNNING" and state.gcode_file:
     if printer_id and state.state == "RUNNING" and state.gcode_file:

+ 32 - 4
frontend/src/pages/PrintersPage.tsx

@@ -504,6 +504,34 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
 type SortOption = 'name' | 'status' | 'model' | 'location';
 type SortOption = 'name' | 'status' | 'model' | 'location';
 type ViewMode = 'expanded' | 'compact';
 type ViewMode = 'expanded' | 'compact';
 
 
+/**
+ * Get human-readable status display text for a printer.
+ * Uses stg_cur_name for detailed calibration/preparation stages,
+ * otherwise formats the gcode_state nicely.
+ */
+function getStatusDisplay(state: string | null | undefined, stg_cur_name: string | null | undefined): string {
+  // If we have a specific stage name (calibration, heating, etc.), use it
+  if (stg_cur_name) {
+    return stg_cur_name;
+  }
+
+  // Format the gcode_state nicely
+  switch (state) {
+    case 'RUNNING':
+      return 'Printing';
+    case 'PAUSE':
+      return 'Paused';
+    case 'FINISH':
+      return 'Finished';
+    case 'FAILED':
+      return 'Failed';
+    case 'IDLE':
+      return 'Idle';
+    default:
+      return state ? state.charAt(0) + state.slice(1).toLowerCase() : 'Idle';
+  }
+}
+
 function PrinterCard({
 function PrinterCard({
   printer,
   printer,
   hideIfDisconnected,
   hideIfDisconnected,
@@ -879,7 +907,7 @@ function PrinterCard({
                     <span className="text-xs text-white">{Math.round(status.progress || 0)}%</span>
                     <span className="text-xs text-white">{Math.round(status.progress || 0)}%</span>
                   </div>
                   </div>
                 ) : (
                 ) : (
-                  <p className="text-xs text-bambu-gray capitalize">{status.state?.toLowerCase() || 'Idle'}</p>
+                  <p className="text-xs text-bambu-gray">{getStatusDisplay(status.state, status.stg_cur_name)}</p>
                 )}
                 )}
               </div>
               </div>
             ) : (
             ) : (
@@ -897,7 +925,7 @@ function PrinterCard({
                     <div className="flex-1 min-w-0">
                     <div className="flex-1 min-w-0">
                       {status.current_print && status.state === 'RUNNING' ? (
                       {status.current_print && status.state === 'RUNNING' ? (
                         <>
                         <>
-                          <p className="text-sm text-bambu-gray mb-1">Printing</p>
+                          <p className="text-sm text-bambu-gray mb-1">{status.stg_cur_name || 'Printing'}</p>
                           <p className="text-white text-sm mb-2 truncate">
                           <p className="text-white text-sm mb-2 truncate">
                             {status.subtask_name || status.current_print}
                             {status.subtask_name || status.current_print}
                           </p>
                           </p>
@@ -933,8 +961,8 @@ function PrinterCard({
                       ) : (
                       ) : (
                         <>
                         <>
                           <p className="text-sm text-bambu-gray mb-1">Status</p>
                           <p className="text-sm text-bambu-gray mb-1">Status</p>
-                          <p className="text-white text-sm mb-2 capitalize">
-                            {status.state?.toLowerCase() || 'Idle'}
+                          <p className="text-white text-sm mb-2">
+                            {getStatusDisplay(status.state, status.stg_cur_name)}
                           </p>
                           </p>
                           <div className="flex items-center justify-between text-sm">
                           <div className="flex items-center justify-between text-sm">
                             <div className="flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3">
                             <div className="flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3">

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-B7Qp8Coa.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-Dszw0rVS.js"></script>
+    <script type="module" crossorigin src="/assets/index-B7Qp8Coa.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-CbCN6LSA.css">
     <link rel="stylesheet" crossorigin href="/assets/index-CbCN6LSA.css">
   </head>
   </head>
   <body>
   <body>

Some files were not shown because too many files changed in this diff