Replaced camera settings with wifi signal in top bar

This commit is contained in:
Martin Ziegler
2025-12-05 13:22:34 +01:00
parent f5e0395375
commit 065f22a0e3
11 changed files with 174 additions and 232 deletions
+1
View File
@@ -294,6 +294,7 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
store_to_sdcard=state.store_to_sdcard,
timelapse=state.timelapse,
ipcam=state.ipcam,
wifi_signal=state.wifi_signal,
nozzles=nozzles,
print_options=print_options,
stg_cur=state.stg_cur,
+1
View File
@@ -114,6 +114,7 @@ class PrinterStatus(BaseModel):
store_to_sdcard: bool = False # Store sent files on SD card
timelapse: bool = False # Timelapse recording active
ipcam: bool = False # Live view enabled
wifi_signal: int | None = None # WiFi signal strength in dBm
nozzles: list[NozzleInfoResponse] = [] # Nozzle hardware info (index 0=left/primary, 1=right)
print_options: PrintOptionsResponse | None = None # AI detection and print options
# Calibration stage tracking
+14
View File
@@ -97,6 +97,7 @@ class PrinterState:
store_to_sdcard: bool = False # Store sent files on SD card (home_flag bit 11)
timelapse: bool = False # Timelapse recording active
ipcam: bool = False # Live view / camera streaming enabled
wifi_signal: int | None = None # WiFi signal strength in dBm
# Nozzle hardware info (for dual nozzle printers, index 0 = left, 1 = right)
nozzles: list = field(default_factory=lambda: [NozzleInfo(), NozzleInfo()])
# AI detection and print options
@@ -1277,6 +1278,19 @@ class BambuMQTTClient:
else:
self.state.ipcam = ipcam_data is True
# Parse WiFi signal strength (dBm)
if "wifi_signal" in data:
wifi_signal = data["wifi_signal"]
if isinstance(wifi_signal, (int, float)):
# Convert string dBm to int (e.g., "-52dBm" -> -52)
self.state.wifi_signal = int(wifi_signal)
elif isinstance(wifi_signal, str):
# Handle string format like "-52dBm"
try:
self.state.wifi_signal = int(wifi_signal.replace("dBm", "").strip())
except ValueError:
pass
# Parse print speed level (1=silent, 2=standard, 3=sport, 4=ludicrous)
if "spd_lvl" in data:
new_speed = data["spd_lvl"]
+1
View File
@@ -117,6 +117,7 @@ export interface PrinterStatus {
store_to_sdcard: boolean; // Store sent files on SD card
timelapse: boolean; // Timelapse recording active
ipcam: boolean; // Live view enabled
wifi_signal: number | null; // WiFi signal strength in dBm
nozzles: NozzleInfo[]; // Nozzle hardware info (index 0=left/primary, 1=right)
print_options: PrintOptions | null; // AI detection and print options
// Calibration stage tracking
@@ -1,129 +0,0 @@
import { useEffect } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../../api/client';
import type { PrinterStatus } from '../../api/client';
import { X, Loader2 } from 'lucide-react';
import { Card, CardContent } from '../Card';
interface CameraSettingsModalProps {
printerId: number;
status: PrinterStatus | null | undefined;
onClose: () => void;
}
export function CameraSettingsModal({ printerId, status, onClose }: CameraSettingsModalProps) {
const queryClient = useQueryClient();
const isConnected = status?.connected ?? false;
// Close on Escape key
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
const timelapseMutation = useMutation({
mutationFn: (enable: boolean) => api.setTimelapse(printerId, enable),
onSuccess: (data) => {
console.log('Timelapse mutation success:', data);
queryClient.invalidateQueries({ queryKey: ['printerStatuses'] });
},
onError: (error) => {
console.error('Timelapse mutation error:', error);
},
});
const liveviewMutation = useMutation({
mutationFn: (enable: boolean) => api.setLiveview(printerId, enable),
onSuccess: (data) => {
console.log('Liveview mutation success:', data);
queryClient.invalidateQueries({ queryKey: ['printerStatuses'] });
},
onError: (error) => {
console.error('Liveview mutation error:', error);
},
});
console.log('CameraSettingsModal render - isConnected:', isConnected, 'status:', status);
const handleTimelapseToggle = () => {
console.log('Timelapse toggle clicked, current:', status?.timelapse, 'setting to:', !status?.timelapse);
timelapseMutation.mutate(!status?.timelapse);
};
const handleLiveviewToggle = () => {
console.log('Liveview toggle clicked, current:', status?.ipcam, 'setting to:', !status?.ipcam);
liveviewMutation.mutate(!status?.ipcam);
};
return (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
onClick={onClose}
>
<Card className="w-full max-w-sm" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<CardContent className="p-0">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-bambu-dark-tertiary">
<span className="text-sm font-medium text-white">Camera Settings</span>
<button
onClick={onClose}
className="p-1 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Settings */}
<div className="p-4 space-y-4">
{/* Auto-record Monitoring (ipcam_record - records to SD during print) */}
<div className="flex items-center justify-between">
<span className="text-sm text-white">Auto-record Monitoring</span>
<button
onClick={handleLiveviewToggle}
disabled={!isConnected || liveviewMutation.isPending}
className={`relative w-12 h-6 rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
status?.ipcam ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
}`}
>
{liveviewMutation.isPending ? (
<Loader2 className="absolute inset-0 m-auto w-4 h-4 animate-spin text-white" />
) : (
<span
className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform ${
status?.ipcam ? 'left-7' : 'left-1'
}`}
/>
)}
</button>
</div>
{/* Go Live (timelapse - live streaming) */}
<div className="flex items-center justify-between">
<span className="text-sm text-white">Go Live</span>
<button
onClick={handleTimelapseToggle}
disabled={!isConnected || timelapseMutation.isPending}
className={`relative w-12 h-6 rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
status?.timelapse ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
}`}
>
{timelapseMutation.isPending ? (
<Loader2 className="absolute inset-0 m-auto w-4 h-4 animate-spin text-white" />
) : (
<span
className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform ${
status?.timelapse ? 'left-7' : 'left-1'
}`}
/>
)}
</button>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,64 @@
interface WifiSignalProps {
signal: number | null | undefined; // dBm value
className?: string;
}
/**
* WiFi signal icon with 4 bars that fill based on signal strength.
* - 4 bars: >= -50 dBm (excellent)
* - 3 bars: >= -60 dBm (good)
* - 2 bars: >= -70 dBm (fair)
* - 1 bar: < -70 dBm (weak)
* - 0 bars: no signal data
*/
export function WifiSignal({ signal, className = "w-4 h-4" }: WifiSignalProps) {
let bars = 0;
if (signal != null) {
if (signal >= -50) bars = 4;
else if (signal >= -60) bars = 3;
else if (signal >= -70) bars = 2;
else bars = 1;
}
const activeColor = "#00ae42"; // bambu-green
const inactiveColor = "#4a4a4a"; // dark gray
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
{/* Dot at bottom */}
<circle
cx="12"
cy="20"
r="1"
fill={bars >= 1 ? activeColor : inactiveColor}
stroke={bars >= 1 ? activeColor : inactiveColor}
/>
{/* First arc (smallest) */}
<path
d="M8.5 16.5a5 5 0 0 1 7 0"
stroke={bars >= 2 ? activeColor : inactiveColor}
fill="none"
/>
{/* Second arc */}
<path
d="M5 13a10 10 0 0 1 14 0"
stroke={bars >= 3 ? activeColor : inactiveColor}
fill="none"
/>
{/* Third arc (largest) */}
<path
d="M1.5 9.5a15 15 0 0 1 21 0"
stroke={bars >= 4 ? activeColor : inactiveColor}
fill="none"
/>
</svg>
);
}
+7 -17
View File
@@ -10,16 +10,15 @@ import { JogPad } from '../components/control/JogPad';
import { BedControls } from '../components/control/BedControls';
import { ExtruderControls } from '../components/control/ExtruderControls';
import { AMSSectionDual } from '../components/control/AMSSectionDual';
import { CameraSettingsModal } from '../components/control/CameraSettingsModal';
import { PrinterPartsModal } from '../components/control/PrinterPartsModal';
import { PrintOptionsModal } from '../components/control/PrintOptionsModal';
import { CalibrationModal } from '../components/control/CalibrationModal';
import { Loader2, WifiOff, Video, Webcam, Settings } from 'lucide-react';
import { Loader2, WifiOff, Video, Webcam } from 'lucide-react';
import { WifiSignal } from '../components/icons/WifiSignal';
export function ControlPage() {
const [searchParams, setSearchParams] = useSearchParams();
const [selectedPrinterId, setSelectedPrinterId] = useState<number | null>(null);
const [showCameraSettings, setShowCameraSettings] = useState(false);
const [showPrinterParts, setShowPrinterParts] = useState(false);
const [showPrintOptions, setShowPrintOptions] = useState(false);
const [showCalibration, setShowCalibration] = useState(false);
@@ -148,12 +147,12 @@ export function ControlPage() {
<button className={`p-1.5 rounded hover:bg-bambu-dark-tertiary ${selectedStatus?.ipcam ? 'text-bambu-green' : 'text-bambu-gray hover:text-white'}`}>
<Webcam className="w-4 h-4" />
</button>
<button
onClick={() => setShowCameraSettings(true)}
className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white"
<div
className="p-1.5 rounded"
title={selectedStatus?.wifi_signal != null ? `WiFi: ${selectedStatus.wifi_signal} dBm` : 'WiFi signal unknown'}
>
<Settings className="w-4 h-4" />
</button>
<WifiSignal signal={selectedStatus?.wifi_signal} className="w-4 h-4" />
</div>
</div>
{/* Camera Feed - Embedded directly */}
@@ -264,15 +263,6 @@ export function ControlPage() {
</div>
)}
{/* Camera Settings Modal */}
{showCameraSettings && selectedPrinter && (
<CameraSettingsModal
printerId={selectedPrinter.id}
status={selectedStatus}
onClose={() => setShowCameraSettings(false)}
/>
)}
{/* Printer Parts Modal */}
{showPrinterParts && selectedPrinter && (
<PrinterPartsModal
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
+2 -2
View File
@@ -7,8 +7,8 @@
<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-1a-urQlt.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-gD5e-c1O.css">
<script type="module" crossorigin src="/assets/index-DX2Pky1o.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BpSfhfce.css">
</head>
<body>
<div id="root"></div>