mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
Add chamber light control to printer card
- Add light toggle button next to camera button at bottom of card
- Custom ChamberLight SVG icon with on/off states (yellow fill + rays when on)
- Backend endpoint: POST /printers/{id}/chamber-light?on=true|false
- Optimistic UI update for instant feedback
- Works on all Bambu Lab printers (X1, P1, A1, H2D)
- H2D dual chamber lights controlled together
Closes #67
This commit is contained in:
@@ -5,6 +5,11 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
## [0.1.6b9] - 2026-01-09
|
||||
|
||||
### Added
|
||||
- **Chamber light control** - Toggle the printer's chamber LED from the UI:
|
||||
- Light button next to camera button at bottom of printer card
|
||||
- Custom icon with on/off states (yellow when lit)
|
||||
- Works on all Bambu Lab printers (X1, P1, A1 series, H2D)
|
||||
- H2D dual chamber lights controlled together
|
||||
- **Archive list view improvements** - Full feature parity with card view:
|
||||
- Edit and delete buttons inline with each row
|
||||
- Three-dot menu button for context menu access
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
- Real-time printer status via WebSocket
|
||||
- Live camera streaming (MJPEG) & snapshots
|
||||
- Fan status monitoring (part cooling, auxiliary, chamber)
|
||||
- Printer control (stop, pause, resume)
|
||||
- Printer control (stop, pause, resume, chamber light)
|
||||
- Resizable printer cards (S/M/L/XL)
|
||||
- Skip objects during print
|
||||
- AMS slot RFID re-read
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -1154,6 +1154,29 @@ async def resume_print(printer_id: int, db: AsyncSession = Depends(get_db)):
|
||||
return {"success": True, "message": "Print resume command sent"}
|
||||
|
||||
|
||||
@router.post("/{printer_id}/chamber-light")
|
||||
async def set_chamber_light(
|
||||
printer_id: int,
|
||||
on: bool = Query(..., description="True to turn on, False to turn off"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Turn the chamber light on or off."""
|
||||
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
||||
printer = result.scalar_one_or_none()
|
||||
if not printer:
|
||||
raise HTTPException(404, "Printer not found")
|
||||
|
||||
client = printer_manager.get_client(printer_id)
|
||||
if not client:
|
||||
raise HTTPException(400, "Printer not connected")
|
||||
|
||||
success = client.set_chamber_light(on)
|
||||
if not success:
|
||||
raise HTTPException(500, "Failed to control chamber light")
|
||||
|
||||
return {"success": True, "message": f"Chamber light {'on' if on else 'off'}"}
|
||||
|
||||
|
||||
@router.get("/{printer_id}/print/objects")
|
||||
async def get_printable_objects(
|
||||
printer_id: int,
|
||||
|
||||
@@ -1333,6 +1333,12 @@ export const api = {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// Chamber Light Control
|
||||
setChamberLight: (printerId: number, on: boolean) =>
|
||||
request<{ success: boolean; message: string }>(`/printers/${printerId}/chamber-light?on=${on}`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// Skip Objects
|
||||
getPrintableObjects: (printerId: number) =>
|
||||
request<{
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
interface ChamberLightProps {
|
||||
on: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chamber light icon with on/off states.
|
||||
* Modern bulb design with radiating rays.
|
||||
* - On: Filled yellow bulb with visible rays
|
||||
* - Off: Outline only, muted color
|
||||
*/
|
||||
export function ChamberLight({ on, className = "w-5 h-5" }: ChamberLightProps) {
|
||||
const bulbFill = on ? "#facc15" : "none"; // yellow-400 when on
|
||||
const strokeColor = on ? "#78350f" : "currentColor"; // amber-900 when on
|
||||
const rayOpacity = on ? 1 : 0;
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
{/* Radiating rays */}
|
||||
<g stroke={strokeColor} opacity={rayOpacity}>
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="6.1" y1="6.1" x2="8.9" y2="8.9" />
|
||||
<line x1="25.9" y1="6.1" x2="23.1" y2="8.9" />
|
||||
<line x1="2" y1="16" x2="6" y2="16" />
|
||||
<line x1="30" y1="16" x2="26" y2="16" />
|
||||
</g>
|
||||
|
||||
{/* Bulb glass - smooth rounded shape */}
|
||||
<path
|
||||
d="M12 24v-2.3c0-.9-.4-1.7-1-2.3C9.2 17.6 8 15.4 8 13c0-4.4 3.6-8 8-8s8 3.6 8 8c0 2.4-1.2 4.6-3 6.4-.6.6-1 1.4-1 2.3V24"
|
||||
fill={bulbFill}
|
||||
stroke={strokeColor}
|
||||
/>
|
||||
|
||||
{/* Base rings */}
|
||||
<path d="M12 24h8" stroke={strokeColor} />
|
||||
<path d="M12 27h8" stroke={strokeColor} />
|
||||
<path d="M13 30h6" stroke={strokeColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -65,6 +65,7 @@ import { AMSHistoryModal } from '../components/AMSHistoryModal';
|
||||
import { FilamentHoverCard, EmptySlotHoverCard } from '../components/FilamentHoverCard';
|
||||
import { LinkSpoolModal } from '../components/LinkSpoolModal';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { ChamberLight } from '../components/icons/ChamberLight';
|
||||
|
||||
// Complete Bambu Lab filament color mapping by tray_id_name
|
||||
// Source: https://github.com/queengooborg/Bambu-Lab-RFID-Library
|
||||
@@ -1093,6 +1094,33 @@ function PrinterCard({
|
||||
onError: (error: Error) => showToast(error.message || 'Failed to resume print', 'error'),
|
||||
});
|
||||
|
||||
// Chamber light mutation with optimistic update
|
||||
const chamberLightMutation = useMutation({
|
||||
mutationFn: (on: boolean) => api.setChamberLight(printer.id, on),
|
||||
onMutate: async (on) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: ['printerStatus', printer.id] });
|
||||
// Snapshot the previous value
|
||||
const previousStatus = queryClient.getQueryData(['printerStatus', printer.id]);
|
||||
// Optimistically update
|
||||
queryClient.setQueryData(['printerStatus', printer.id], (old: typeof status) => ({
|
||||
...old,
|
||||
chamber_light: on,
|
||||
}));
|
||||
return { previousStatus };
|
||||
},
|
||||
onSuccess: (_, on) => {
|
||||
showToast(`Chamber light ${on ? 'on' : 'off'}`);
|
||||
},
|
||||
onError: (error: Error, _, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousStatus) {
|
||||
queryClient.setQueryData(['printerStatus', printer.id], context.previousStatus);
|
||||
}
|
||||
showToast(error.message || 'Failed to control chamber light', 'error');
|
||||
},
|
||||
});
|
||||
|
||||
// Query for printable objects (for skip functionality)
|
||||
// Fetch when printing with 2+ objects OR when modal is open
|
||||
const isPrintingWithObjects = (status?.state === 'RUNNING' || status?.state === 'PAUSE' || status?.state === 'PAUSED') && (status?.printable_objects_count ?? 0) >= 2;
|
||||
@@ -2360,6 +2388,18 @@ function PrinterCard({
|
||||
<p className="truncate">{printer.serial_number}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Chamber Light Toggle */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => chamberLightMutation.mutate(!status?.chamber_light)}
|
||||
disabled={!status?.connected || chamberLightMutation.isPending}
|
||||
title={status?.chamber_light ? 'Turn off chamber light' : 'Turn on chamber light'}
|
||||
className={status?.chamber_light ? 'bg-yellow-500/20 hover:bg-yellow-500/30 border-yellow-500/30' : ''}
|
||||
>
|
||||
<ChamberLight on={status?.chamber_light ?? false} className="w-4 h-4" />
|
||||
</Button>
|
||||
{/* Camera Button */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -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-CCWSzj3L.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-ClrkCtKg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BEtulymk.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user