Add a dedicated Control Page (/control) with full printer control capabilities, including:
bambu_mqtt.py# Print Control
async def pause_print(self) -> bool
async def resume_print(self) -> bool
# stop_print() already exists
# Temperature Control
async def set_bed_temperature(self, target: int) -> bool
async def set_nozzle_temperature(self, target: int, nozzle: int = 0) -> bool
# Speed Control
async def set_print_speed(self, mode: int) -> bool # 1=silent, 2=standard, 3=sport, 4=ludicrous
# Fan Control
async def set_part_fan(self, speed: int) -> bool # 0-255
async def set_aux_fan(self, speed: int) -> bool # 0-255
async def set_chamber_fan(self, speed: int) -> bool # 0-255
# Light Control
async def set_chamber_light(self, on: bool) -> bool
# Movement Control
async def home_axes(self, axes: str = "XYZ") -> bool
async def move_axis(self, axis: str, distance: float, speed: int = 3000) -> bool
async def disable_motors(self) -> bool
# AMS Control
async def ams_load_filament(self, tray_id: int) -> bool
async def ams_unload_filament(self) -> bool
# G-code
async def send_gcode(self, gcode: str) -> bool
| Command | JSON Payload |
|---|---|
| Pause | {"print": {"sequence_id": "0", "command": "pause"}} |
| Resume | {"print": {"sequence_id": "0", "command": "resume"}} |
| Bed Temp | {"print": {"sequence_id": "0", "command": "gcode_line", "param": "M140 S{temp}"}} |
| Nozzle Temp | {"print": {"sequence_id": "0", "command": "gcode_line", "param": "M104 S{temp}"}} |
| Print Speed | {"print": {"sequence_id": "0", "command": "print_speed", "param": "{1-4}"}} |
| Fan (P1=part, P2=aux, P3=chamber) | {"print": {"sequence_id": "0", "command": "gcode_line", "param": "M106 P{n} S{0-255}"}} |
| Light On | {"system": {"sequence_id": "0", "command": "ledctrl", "led_node": "chamber_light", "led_mode": "on", ...}} |
| Home | {"print": {"sequence_id": "0", "command": "gcode_line", "param": "G28 {axes}"}} |
| Move | {"print": {"sequence_id": "0", "command": "gcode_line", "param": "G91\nG0 {axis}{dist} F{speed}\nG90"}} |
| AMS Load | {"print": {"sequence_id": "0", "command": "ams_change_filament", "target": {tray_id}}} |
| AMS Unload | {"print": {"sequence_id": "0", "command": "ams_change_filament", "target": 255}} |
backend/app/api/routes/printer_control.py# Print Control
POST /api/v1/printers/{id}/control/pause
POST /api/v1/printers/{id}/control/resume
POST /api/v1/printers/{id}/control/stop
# Temperature
POST /api/v1/printers/{id}/control/temperature/bed
Body: {"target": 60}
POST /api/v1/printers/{id}/control/temperature/nozzle
Body: {"target": 200, "nozzle": 0}
# Speed
POST /api/v1/printers/{id}/control/speed
Body: {"mode": 2} # 1-4
# Fans
POST /api/v1/printers/{id}/control/fan/part
Body: {"speed": 255} # 0-255
POST /api/v1/printers/{id}/control/fan/aux
POST /api/v1/printers/{id}/control/fan/chamber
# Light
POST /api/v1/printers/{id}/control/light
Body: {"on": true}
# Movement
POST /api/v1/printers/{id}/control/home
Body: {"axes": "XYZ"} # optional, default all
POST /api/v1/printers/{id}/control/move
Body: {"axis": "Z", "distance": 10, "speed": 600}
POST /api/v1/printers/{id}/control/motors/disable
# AMS
POST /api/v1/printers/{id}/control/ams/load
Body: {"tray_id": 0}
POST /api/v1/printers/{id}/control/ams/unload
# G-code (advanced)
POST /api/v1/printers/{id}/control/gcode
Body: {"command": "G28"}
Commands that need confirmation token (generated and validated server-side):
stop - Aborts printhome while printing - Could cause issuesmove while printing - Dangerousmotors/disable - Causes position lossFlow:
{"requires_confirmation": true, "token": "abc123", "warning": "This will abort..."}{"confirm_token": "abc123"}Option A: MJPEG Stream (simpler)
/api/v1/printers/{id}/camera/stream<img src="..."> with streamingOption B: WebSocket Frames (more control)
Recommended: Option A (MJPEG) - Simpler, works in all browsers
# backend/app/api/routes/camera.py
@router.get("/printers/{printer_id}/camera/stream")
async def camera_stream(printer_id: int):
"""Stream camera as MJPEG"""
printer = get_printer(printer_id)
async def generate():
process = await asyncio.create_subprocess_exec(
'ffmpeg',
'-rtsp_transport', 'tcp',
'-i', f'rtsps://bblp:{printer.access_code}@{printer.ip_address}:{port}/streaming/live/1',
'-f', 'mjpeg',
'-q:v', '5',
'-r', '15', # 15 fps
'-',
stdout=asyncio.subprocess.PIPE
)
while True:
frame = await read_jpeg_frame(process.stdout)
if not frame:
break
yield (
b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n'
)
return StreamingResponse(
generate(),
media_type='multipart/x-mixed-replace; boundary=frame'
)
@router.get("/printers/{printer_id}/camera/snapshot")
async def camera_snapshot(printer_id: int):
"""Get single camera frame"""
# Use existing camera.py capture_frame logic
| Model | Port | Protocol |
|---|---|---|
| X1/X1C/H2D | 322 | RTSPS |
| P1/P1S/P1P | 6000 | RTSPS |
| A1/A1 Mini | 6000 | RTSPS |
/control
├── ControlPage.tsx # Main page with printer tabs
├── components/
│ ├── CameraFeed.tsx # Live video stream
│ ├── PrintControls.tsx # Pause/Resume/Stop + progress
│ ├── TemperaturePanel.tsx # Bed/Nozzle/Chamber controls
│ ├── SpeedControl.tsx # Speed mode selector
│ ├── FanControls.tsx # Part/Aux/Chamber fan sliders
│ ├── LightToggle.tsx # Chamber light on/off
│ ├── MovementControls.tsx # Home + XYZ jog buttons
│ ├── AMSPanel.tsx # AMS visualization + load/unload
│ └── ConfirmDialog.tsx # Safety confirmation modal
┌──────────────────────────────────────────────────────────────────┐
│ [Printer 1] [Printer 2] [Printer 3] tabs │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ ┌────────────────────────────────┐│
│ │ │ │ Print Status ││
│ │ Camera Feed │ │ ┌────────────────────────┐ ││
│ │ (16:9 aspect) │ │ │ State: RUNNING │ ││
│ │ │ │ │ File: benchy.3mf │ ││
│ │ │ │ │ Progress: ████████░░ 78%│ ││
│ │ │ │ │ Layer: 156/200 │ ││
│ │ │ │ │ Time: 45min remaining │ ││
│ │ │ │ └────────────────────────┘ ││
│ │ │ │ ││
│ │ [⏸ Pause] [■ Stop] │ │ [⏸ Pause] [▶ Resume] [■ Stop]││
│ └─────────────────────────┘ └────────────────────────────────┘│
│ │
│ ┌─────────────────────────┐ ┌────────────────────────────────┐│
│ │ Temperatures │ │ Speed & Fans ││
│ │ ┌───────────────────┐ │ │ Speed: [Silent][Std][Sport][!]││
│ │ │ 🛏️ Bed │ │ │ ││
│ │ │ 60°C → 60°C │ │ │ Part Fan: ████████░░ 80% ││
│ │ │ [-] [target] [+] │ │ │ Aux Fan: ░░░░░░░░░░ 0% ││
│ │ ├───────────────────┤ │ │ Chamber Fan: ████░░░░░░ 40% ││
│ │ │ 🔥 Nozzle │ │ └────────────────────────────────┘│
│ │ │ 205°C → 210°C │ │ │
│ │ │ [-] [target] [+] │ │ ┌────────────────────────────────┐│
│ │ ├───────────────────┤ │ │ Movement ││
│ │ │ 📦 Chamber: 35°C │ │ │ [Y+] ││
│ │ └───────────────────┘ │ │ [X-] [Home] [X+] ││
│ └─────────────────────────┘ │ [Y-] [Z+][Z-] ││
│ │ [Disable Motors] ││
│ ┌─────────────────────────┐ └────────────────────────────────┘│
│ │ 💡 Light [ON] / [OFF] │ │
│ └─────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ AMS ││
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ [Load] [Unload] ││
│ │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ ││
│ │ │ 🔴 │ │ 🔵 │ │ ⚪ │ │ ⬛ │ Selected: Slot 1 (PLA Red) ││
│ │ │80% │ │45% │ │100%│ │ -- │ ││
│ │ └────┘ └────┘ └────┘ └────┘ ││
│ └──────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────┘
Stacked vertically:
Use React Query for:
Control mutations with optimistic updates
// Example mutation
const pausePrint = useMutation({
mutationFn: (printerId: number) =>
api.post(`/printers/${printerId}/control/pause`),
onSuccess: () => {
// Optimistic: printer status will update via WebSocket
}
});
interface CameraFeedProps {
printerId: number;
enabled: boolean;
}
// Features:
// - MJPEG stream from /api/v1/printers/{id}/camera/stream
// - Fallback to static thumbnail if stream fails
// - Loading state with skeleton
// - Click to fullscreen
// - Optional: snapshot button
interface TemperaturePanelProps {
printerId: number;
bed: { current: number; target: number };
nozzle: { current: number; target: number };
nozzle2?: { current: number; target: number }; // H2D
chamber?: number;
}
// Features:
// - Visual temperature bars (current vs target)
// - Input field or +/- buttons for target
// - Presets: Off (0), PLA (60/200), PETG (70/230), ABS (90/250)
// - Debounced API calls (don't spam on rapid clicks)
// - Disable controls during print (optional setting)
// Speed modes as toggle buttons:
// [Silent] [Standard] [Sport] [Ludicrous]
// Visual feedback for current mode
// Warning tooltip for Ludicrous mode
// Sliders for each fan (0-100%)
// Convert to 0-255 for API
// Real-time value display
// Disable chamber fan if not available (check model)
// Grid layout:
// [Y+10] [Y+1]
// [X-10] [X-1] [Home] [X+1] [X+10]
// [Y-1] [Y-10]
// [Z+10] [Z+1] [Z-1] [Z-10]
//
// [Disable Motors] button with confirmation
// Warning: "Movement controls disabled during print" overlay
// Visual representation matching Bambu style:
// - 4 slots per AMS unit
// - Color-coded by filament
// - Percentage remaining
// - Active slot indicator (animated)
// - Click to select slot
// - [Load Selected] [Unload] buttons
// - Support for external spool indicator
Required for:
| Control | IDLE | RUNNING | PAUSE | FINISH |
|---|---|---|---|---|
| Pause | ❌ | ✅ | ❌ | ❌ |
| Resume | ❌ | ❌ | ✅ | ❌ |
| Stop | ❌ | ✅ | ✅ | ❌ |
| Temp Control | ✅ | ⚠️ | ✅ | ✅ |
| Speed | ❌ | ✅ | ❌ | ❌ |
| Fans | ✅ | ⚠️ | ✅ | ✅ |
| Movement | ✅ | ❌ | ⚠️ | ✅ |
| AMS Load | ✅ | ❌ | ❌ | ✅ |
⚠️ = Allowed with warning
Ensure these fields are included in printer status broadcasts:
interface PrinterStatus {
// Existing
state: string;
progress: number;
remaining_time: number;
temperatures: {...};
// Add for control page
print_speed_mode: number; // 1-4
fan_speeds: {
part: number; // 0-255
aux: number;
chamber: number;
};
light_state: boolean;
ams_status: {
units: [{
id: number;
trays: [{
id: number;
color: string; // hex
type: string; // PLA, PETG, etc
remaining: number; // percentage
active: boolean;
}];
}];
current_tray: number;
};
position?: {
x: number;
y: number;
z: number;
};
}
backend/app/api/routes/printer_control.py
backend/app/api/routes/camera.py
backend/app/schemas/control.py
frontend/src/pages/ControlPage.tsx
frontend/src/components/control/CameraFeed.tsx
frontend/src/components/control/PrintControls.tsx
frontend/src/components/control/TemperaturePanel.tsx
frontend/src/components/control/SpeedControl.tsx
frontend/src/components/control/FanControls.tsx
frontend/src/components/control/LightToggle.tsx
frontend/src/components/control/MovementControls.tsx
frontend/src/components/control/AMSPanel.tsx
frontend/src/components/control/ConfirmDialog.tsx
backend/app/services/bambu_mqtt.py # Add control methods
backend/app/api/routes/__init__.py # Register new routes
backend/app/main.py # Include new router
backend/app/schemas/printer.py # Extend status schema
frontend/src/App.tsx # Add route
frontend/src/components/Sidebar.tsx # Add nav item
frontend/src/api/client.ts # Add control API calls
Ready to begin implementation?