- 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".
This commit is contained in:
maziggy
2025-12-21 10:24:26 +01:00
parent 3ffe032635
commit 0200f69ba3
6 changed files with 475 additions and 391 deletions
+2 -3
View File
@@ -28,8 +28,7 @@ from backend.app.services.bambu_ftp import (
get_storage_info_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__)
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,
print_options=print_options,
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,
airduct_mode=state.airduct_mode,
speed_level=state.speed_level,
+6 -1
View File
@@ -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"
)
# 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 = (
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:
return # No change, skip broadcast
+53 -1
View File
@@ -5,7 +5,7 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
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:
@@ -280,6 +280,55 @@ class PrinterManager:
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:
"""Convert PrinterState to a JSON-serializable dict."""
# 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,
# WiFi signal strength
"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
if printer_id and state.state == "RUNNING" and state.gcode_file:
+32 -4
View File
@@ -504,6 +504,34 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
type SortOption = 'name' | 'status' | 'model' | 'location';
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({
printer,
hideIfDisconnected,
@@ -879,7 +907,7 @@ function PrinterCard({
<span className="text-xs text-white">{Math.round(status.progress || 0)}%</span>
</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>
) : (
@@ -897,7 +925,7 @@ function PrinterCard({
<div className="flex-1 min-w-0">
{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">
{status.subtask_name || status.current_print}
</p>
@@ -933,8 +961,8 @@ function PrinterCard({
) : (
<>
<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>
<div className="flex items-center justify-between text-sm">
<div className="flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3">
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
<!-- Splash screens for iOS -->
<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">
</head>
<body>