mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 04:31:28 +02:00
Replaced camera settings with wifi signal in top bar
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user