mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +02:00
Add real-time fan status monitoring to printer cards
- Controls section: New section before Filaments with fan status and printer controls - Part cooling fan (Fan icon, cyan) - Auxiliary fan (Wind icon, blue) - Chamber fan (AirVent icon, green) - Stop/Pause/Resume buttons moved from Temperature section - Real-time updates: Fan speeds now broadcast via WebSocket - Backend: Parse MQTT fan data (cooling_fan_speed, big_fan1_speed, big_fan2_speed) - Convert 0-15 scale to 0-100% - Always-visible badges with dynamic coloring based on fan state - Removed horizontal dividers before Controls and Filaments sections
This commit is contained in:
+11
-1
@@ -64,9 +64,19 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- Progress bar tracks items toward target count
|
||||
- Useful for batch printing (e.g., 10 copies in one print = 10 items)
|
||||
- Default quantity of 1 for backwards compatibility
|
||||
- **Fan status display** - Real-time fan speeds in new Controls section:
|
||||
- Part cooling fan, Auxiliary fan, Chamber fan status
|
||||
- Distinct icons for each fan type (Fan, Wind, AirVent)
|
||||
- Dynamic coloring: active fans show colored, off fans show gray
|
||||
- Percentage display (0-100%)
|
||||
- Real-time updates via WebSocket
|
||||
- Always visible on printer cards in expanded view
|
||||
|
||||
### Changed
|
||||
- **Temperature cards layout** - Refactored printer card layout with slimmer temperature displays to make room for control buttons
|
||||
- **Printer card layout** - Reorganized expanded view with new Controls section:
|
||||
- Temperature display is now standalone (no longer shares row with buttons)
|
||||
- New Controls section contains fan status (left) and print buttons (right)
|
||||
- Removed divider lines before Controls and Filaments sections for cleaner look
|
||||
- **Cover image availability** - Print cover image now shown in PAUSE/PAUSED states (not just RUNNING) for skip objects modal
|
||||
- **Spoolman info banner** - Updated settings UI with clearer sync documentation
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
### 📊 Monitoring & Control
|
||||
- Real-time printer status via WebSocket
|
||||
- Live camera streaming (MJPEG) & snapshots
|
||||
- Fan status monitoring (part cooling, auxiliary, chamber)
|
||||
- Printer control (stop, pause, resume)
|
||||
- Skip objects during print
|
||||
- AMS slot RFID re-read
|
||||
|
||||
+2
-1
@@ -237,7 +237,8 @@ async def on_printer_status_change(printer_id: int, state: PrinterState):
|
||||
status_key = (
|
||||
f"{state.connected}:{state.state}:{state.progress}:{state.layer_num}:"
|
||||
f"{nozzle_temp}:{bed_temp}:{nozzle_2_temp}:{chamber_temp}:"
|
||||
f"{state.stg_cur}:{bed_target}:{nozzle_target}"
|
||||
f"{state.stg_cur}:{bed_target}:{nozzle_target}:"
|
||||
f"{state.cooling_fan_speed}:{state.big_fan1_speed}:{state.big_fan2_speed}"
|
||||
)
|
||||
if _last_status_broadcast.get(printer_id) == status_key:
|
||||
return # No change, skip broadcast
|
||||
|
||||
@@ -153,3 +153,8 @@ class PrinterStatus(BaseModel):
|
||||
last_ams_update: float = 0.0
|
||||
# Number of printable objects in current print (for skip objects feature)
|
||||
printable_objects_count: int = 0
|
||||
# Fan speeds (0-100 percentage, None if not available for this model)
|
||||
cooling_fan_speed: int | None = None # Part cooling fan
|
||||
big_fan1_speed: int | None = None # Auxiliary fan
|
||||
big_fan2_speed: int | None = None # Chamber/exhaust fan
|
||||
heatbreak_fan_speed: int | None = None # Hotend heatbreak fan
|
||||
|
||||
@@ -150,6 +150,11 @@ class PrinterState:
|
||||
printable_objects: dict = field(default_factory=dict)
|
||||
# Objects that have been skipped during the current print
|
||||
skipped_objects: list = field(default_factory=list)
|
||||
# Fan speeds (0-100 percentage, None if not available for this model)
|
||||
cooling_fan_speed: int | None = None # Part cooling fan
|
||||
big_fan1_speed: int | None = None # Auxiliary fan
|
||||
big_fan2_speed: int | None = None # Chamber/exhaust fan
|
||||
heatbreak_fan_speed: int | None = None # Hotend heatbreak fan
|
||||
|
||||
|
||||
# Stage name mapping from BambuStudio DeviceManager.cpp
|
||||
@@ -912,6 +917,40 @@ class BambuMQTTClient:
|
||||
if "total_layer_num" in data:
|
||||
self.state.total_layers = int(data["total_layer_num"])
|
||||
|
||||
# Fan speeds (MQTT sends as string "0"-"15" representing speed levels, or percentage)
|
||||
# Convert to 0-100 percentage for display
|
||||
def parse_fan_speed(value: str | int | None) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
speed = int(value)
|
||||
# MQTT reports 0-15 speed levels, convert to percentage (0-100)
|
||||
# 15 = 100%, so multiply by 100/15 ≈ 6.67
|
||||
if speed <= 15:
|
||||
return round(speed * 100 / 15)
|
||||
# If already a percentage (0-255 scale from some printers), convert
|
||||
elif speed <= 255:
|
||||
return round(speed * 100 / 255)
|
||||
return speed
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
# Log fan fields once for debugging
|
||||
if not hasattr(self, "_fan_fields_logged"):
|
||||
fan_fields = {k: v for k, v in data.items() if "fan" in k.lower()}
|
||||
if fan_fields:
|
||||
logger.info(f"[{self.serial_number}] Fan fields in MQTT data: {fan_fields}")
|
||||
self._fan_fields_logged = True
|
||||
|
||||
if "cooling_fan_speed" in data:
|
||||
self.state.cooling_fan_speed = parse_fan_speed(data["cooling_fan_speed"])
|
||||
if "big_fan1_speed" in data:
|
||||
self.state.big_fan1_speed = parse_fan_speed(data["big_fan1_speed"])
|
||||
if "big_fan2_speed" in data:
|
||||
self.state.big_fan2_speed = parse_fan_speed(data["big_fan2_speed"])
|
||||
if "heatbreak_fan_speed" in data:
|
||||
self.state.heatbreak_fan_speed = parse_fan_speed(data["heatbreak_fan_speed"])
|
||||
|
||||
# Calibration stage tracking
|
||||
if "stg_cur" in data:
|
||||
new_stg = data["stg_cur"]
|
||||
|
||||
@@ -477,6 +477,11 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) ->
|
||||
"stg_cur_name": get_derived_status_name(state),
|
||||
# Printable objects count for skip objects feature
|
||||
"printable_objects_count": len(state.printable_objects),
|
||||
# Fan speeds (0-100 percentage, None if not available)
|
||||
"cooling_fan_speed": state.cooling_fan_speed,
|
||||
"big_fan1_speed": state.big_fan1_speed,
|
||||
"big_fan2_speed": state.big_fan2_speed,
|
||||
"heatbreak_fan_speed": state.heatbreak_fan_speed,
|
||||
}
|
||||
# Add cover URL if there's an active print and printer_id is provided
|
||||
# Include PAUSE/PAUSED states so skip objects modal can show cover
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 502 KiB |
@@ -165,6 +165,11 @@ export interface PrinterStatus {
|
||||
last_ams_update: number;
|
||||
// Number of printable objects in current print (for skip objects feature)
|
||||
printable_objects_count: number;
|
||||
// Fan speeds (0-100 percentage, null if not available for this model)
|
||||
cooling_fan_speed: number | null; // Part cooling fan
|
||||
big_fan1_speed: number | null; // Auxiliary fan
|
||||
big_fan2_speed: number | null; // Chamber/exhaust fan
|
||||
heatbreak_fan_speed: number | null; // Hotend heatbreak fan
|
||||
}
|
||||
|
||||
export interface PrinterCreate {
|
||||
|
||||
@@ -34,6 +34,9 @@ import {
|
||||
Play,
|
||||
X,
|
||||
Monitor,
|
||||
Fan,
|
||||
Wind,
|
||||
AirVent,
|
||||
} from 'lucide-react';
|
||||
|
||||
// Custom Skip Objects icon - arrow jumping over boxes
|
||||
@@ -1581,101 +1584,156 @@ function PrinterCard({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Temperatures + Print Controls */}
|
||||
{/* Temperatures */}
|
||||
{status.temperatures && viewMode === 'expanded' && (() => {
|
||||
// Use actual heater states from MQTT stream
|
||||
const nozzleHeating = status.temperatures.nozzle_heating || status.temperatures.nozzle_2_heating || false;
|
||||
const bedHeating = status.temperatures.bed_heating || false;
|
||||
const chamberHeating = status.temperatures.chamber_heating || false;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{/* Nozzle temp - combined for dual nozzle */}
|
||||
<div className="text-center p-2 bg-bambu-dark rounded-lg">
|
||||
<HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-orange-400" isHeating={nozzleHeating} />
|
||||
{status.temperatures.nozzle_2 !== undefined ? (
|
||||
<>
|
||||
<p className="text-[10px] text-bambu-gray">L / R</p>
|
||||
<p className="text-xs text-white">
|
||||
{Math.round(status.temperatures.nozzle || 0)}° / {Math.round(status.temperatures.nozzle_2 || 0)}°
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-[10px] text-bambu-gray">Nozzle</p>
|
||||
<p className="text-xs text-white">
|
||||
{Math.round(status.temperatures.nozzle || 0)}°C
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center p-2 bg-bambu-dark rounded-lg">
|
||||
<HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-blue-400" isHeating={bedHeating} />
|
||||
<p className="text-[10px] text-bambu-gray">Bed</p>
|
||||
<p className="text-xs text-white">
|
||||
{Math.round(status.temperatures.bed || 0)}°C
|
||||
</p>
|
||||
</div>
|
||||
{status.temperatures.chamber !== undefined ? (
|
||||
<div className="text-center p-2 bg-bambu-dark rounded-lg">
|
||||
<HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-green-400" isHeating={chamberHeating} />
|
||||
<p className="text-[10px] text-bambu-gray">Chamber</p>
|
||||
<p className="text-xs text-white">
|
||||
{Math.round(status.temperatures.chamber || 0)}°C
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div /> /* Empty placeholder to maintain grid */
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Controls - Fans + Print Buttons */}
|
||||
{viewMode === 'expanded' && (() => {
|
||||
// Determine print state for control buttons
|
||||
const isRunning = status.state === 'RUNNING';
|
||||
const isPaused = status.state === 'PAUSED' || status.state === 'PAUSE';
|
||||
const isPrinting = isRunning || isPaused;
|
||||
const isControlBusy = stopPrintMutation.isPending || pausePrintMutation.isPending || resumePrintMutation.isPending;
|
||||
|
||||
// Fan data
|
||||
const partFan = status.cooling_fan_speed;
|
||||
const auxFan = status.big_fan1_speed;
|
||||
const chamberFan = status.big_fan2_speed;
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
{/* Temperature cards */}
|
||||
<div className="flex-1 grid grid-cols-3 gap-2">
|
||||
{/* Nozzle temp - combined for dual nozzle */}
|
||||
<div className="text-center p-2 bg-bambu-dark rounded-lg">
|
||||
<HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-orange-400" isHeating={nozzleHeating} />
|
||||
{status.temperatures.nozzle_2 !== undefined ? (
|
||||
<>
|
||||
<p className="text-[10px] text-bambu-gray">L / R</p>
|
||||
<p className="text-xs text-white">
|
||||
{Math.round(status.temperatures.nozzle || 0)}° / {Math.round(status.temperatures.nozzle_2 || 0)}°
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-[10px] text-bambu-gray">Nozzle</p>
|
||||
<p className="text-xs text-white">
|
||||
{Math.round(status.temperatures.nozzle || 0)}°C
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center p-2 bg-bambu-dark rounded-lg">
|
||||
<HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-blue-400" isHeating={bedHeating} />
|
||||
<p className="text-[10px] text-bambu-gray">Bed</p>
|
||||
<p className="text-xs text-white">
|
||||
{Math.round(status.temperatures.bed || 0)}°C
|
||||
</p>
|
||||
</div>
|
||||
{status.temperatures.chamber !== undefined ? (
|
||||
<div className="text-center p-2 bg-bambu-dark rounded-lg">
|
||||
<HeaterThermometer className="w-4 h-4 mx-auto mb-1" color="text-green-400" isHeating={chamberHeating} />
|
||||
<p className="text-[10px] text-bambu-gray">Chamber</p>
|
||||
<p className="text-xs text-white">
|
||||
{Math.round(status.temperatures.chamber || 0)}°C
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div /> /* Empty placeholder to maintain grid */
|
||||
)}
|
||||
<div className="mt-3">
|
||||
{/* Section Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[10px] uppercase tracking-wider text-bambu-gray font-medium">
|
||||
Controls
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-bambu-dark-tertiary/30" />
|
||||
</div>
|
||||
|
||||
{/* Print control buttons */}
|
||||
<div className="flex flex-col justify-center gap-1.5 w-20">
|
||||
{/* Stop button - visible when printing/paused */}
|
||||
<button
|
||||
onClick={() => setShowStopConfirm(true)}
|
||||
disabled={!isPrinting || isControlBusy}
|
||||
className={`
|
||||
flex items-center justify-center gap-1 px-2 py-1.5 rounded-lg text-xs font-medium
|
||||
transition-colors
|
||||
${isPrinting
|
||||
? 'bg-red-500/20 text-red-400 hover:bg-red-500/30'
|
||||
: 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
|
||||
}
|
||||
`}
|
||||
title="Stop print"
|
||||
>
|
||||
<Square className="w-3 h-3" />
|
||||
Stop
|
||||
</button>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Left: Fan Status - always visible, dynamic coloring */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Part Cooling Fan */}
|
||||
<div
|
||||
className={`flex items-center gap-1 px-1.5 py-1 rounded ${partFan && partFan > 0 ? 'bg-cyan-500/10' : 'bg-bambu-dark'}`}
|
||||
title="Part Cooling Fan"
|
||||
>
|
||||
<Fan className={`w-3.5 h-3.5 ${partFan && partFan > 0 ? 'text-cyan-400' : 'text-bambu-gray/50'}`} />
|
||||
<span className={`text-[10px] ${partFan && partFan > 0 ? 'text-cyan-400' : 'text-bambu-gray/50'}`}>
|
||||
{partFan ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Pause/Resume button */}
|
||||
<button
|
||||
onClick={() => isPaused ? setShowResumeConfirm(true) : setShowPauseConfirm(true)}
|
||||
disabled={!isPrinting || isControlBusy}
|
||||
className={`
|
||||
flex items-center justify-center gap-1 px-2 py-1.5 rounded-lg text-xs font-medium
|
||||
transition-colors
|
||||
${isPrinting
|
||||
? isPaused
|
||||
? 'bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30'
|
||||
: 'bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30'
|
||||
: 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
|
||||
}
|
||||
`}
|
||||
title={isPaused ? 'Resume print' : 'Pause print'}
|
||||
>
|
||||
{isPaused ? <Play className="w-3 h-3" /> : <Pause className="w-3 h-3" />}
|
||||
{isPaused ? 'Resume' : 'Pause'}
|
||||
</button>
|
||||
{/* Auxiliary Fan */}
|
||||
<div
|
||||
className={`flex items-center gap-1 px-1.5 py-1 rounded ${auxFan && auxFan > 0 ? 'bg-blue-500/10' : 'bg-bambu-dark'}`}
|
||||
title="Auxiliary Fan"
|
||||
>
|
||||
<Wind className={`w-3.5 h-3.5 ${auxFan && auxFan > 0 ? 'text-blue-400' : 'text-bambu-gray/50'}`} />
|
||||
<span className={`text-[10px] ${auxFan && auxFan > 0 ? 'text-blue-400' : 'text-bambu-gray/50'}`}>
|
||||
{auxFan ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Chamber Fan */}
|
||||
<div
|
||||
className={`flex items-center gap-1 px-1.5 py-1 rounded ${chamberFan && chamberFan > 0 ? 'bg-green-500/10' : 'bg-bambu-dark'}`}
|
||||
title="Chamber Fan"
|
||||
>
|
||||
<AirVent className={`w-3.5 h-3.5 ${chamberFan && chamberFan > 0 ? 'text-green-400' : 'text-bambu-gray/50'}`} />
|
||||
<span className={`text-[10px] ${chamberFan && chamberFan > 0 ? 'text-green-400' : 'text-bambu-gray/50'}`}>
|
||||
{chamberFan ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Print Control Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Stop button */}
|
||||
<button
|
||||
onClick={() => setShowStopConfirm(true)}
|
||||
disabled={!isPrinting || isControlBusy}
|
||||
className={`
|
||||
flex items-center justify-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium
|
||||
transition-colors
|
||||
${isPrinting
|
||||
? 'bg-red-500/20 text-red-400 hover:bg-red-500/30'
|
||||
: 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
|
||||
}
|
||||
`}
|
||||
title="Stop print"
|
||||
>
|
||||
<Square className="w-3 h-3" />
|
||||
Stop
|
||||
</button>
|
||||
|
||||
{/* Pause/Resume button */}
|
||||
<button
|
||||
onClick={() => isPaused ? setShowResumeConfirm(true) : setShowPauseConfirm(true)}
|
||||
disabled={!isPrinting || isControlBusy}
|
||||
className={`
|
||||
flex items-center justify-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium
|
||||
transition-colors
|
||||
${isPrinting
|
||||
? isPaused
|
||||
? 'bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30'
|
||||
: 'bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30'
|
||||
: 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
|
||||
}
|
||||
`}
|
||||
title={isPaused ? 'Resume print' : 'Pause print'}
|
||||
>
|
||||
{isPaused ? <Play className="w-3 h-3" /> : <Pause className="w-3 h-3" />}
|
||||
{isPaused ? 'Resume' : 'Pause'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1689,9 +1747,9 @@ function PrinterCard({
|
||||
const isDualNozzle = printer.nozzle_count === 2 || status?.temperatures?.nozzle_2 !== undefined;
|
||||
|
||||
return (
|
||||
<div className="mt-4 pt-3 border-t border-bambu-dark-tertiary/50">
|
||||
<div className="mt-3">
|
||||
{/* Section Header */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[10px] uppercase tracking-wider text-bambu-gray font-medium">
|
||||
Filaments
|
||||
</span>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 502 KiB |
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-D9x3e2g2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bs58vo0R.css">
|
||||
<script type="module" crossorigin src="/assets/index-DvVnGkck.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-j3n1gAEX.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user