mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
771 lines
28 KiB
Python
771 lines
28 KiB
Python
"""Printer control API endpoints for full printer control."""
|
|
|
|
import logging
|
|
import secrets
|
|
import time
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
|
|
from backend.app.core.database import get_db
|
|
from backend.app.models.printer import Printer
|
|
from backend.app.services.printer_manager import printer_manager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/printers", tags=["printer-control"])
|
|
|
|
# Store confirmation tokens with expiry: {token: (printer_id, action, expiry_time)}
|
|
_confirmation_tokens: dict[str, tuple[int, str, float]] = {}
|
|
CONFIRMATION_TOKEN_EXPIRY = 60 # seconds
|
|
|
|
|
|
def _clean_expired_tokens():
|
|
"""Remove expired confirmation tokens."""
|
|
now = time.time()
|
|
expired = [t for t, (_, _, exp) in _confirmation_tokens.items() if now > exp]
|
|
for token in expired:
|
|
_confirmation_tokens.pop(token, None)
|
|
|
|
|
|
def _create_confirmation_token(printer_id: int, action: str) -> str:
|
|
"""Create a confirmation token for dangerous operations."""
|
|
_clean_expired_tokens()
|
|
token = secrets.token_urlsafe(16)
|
|
_confirmation_tokens[token] = (printer_id, action, time.time() + CONFIRMATION_TOKEN_EXPIRY)
|
|
return token
|
|
|
|
|
|
def _validate_confirmation_token(token: str, printer_id: int, action: str) -> bool:
|
|
"""Validate and consume a confirmation token."""
|
|
_clean_expired_tokens()
|
|
if token not in _confirmation_tokens:
|
|
return False
|
|
stored_printer_id, stored_action, expiry = _confirmation_tokens[token]
|
|
if stored_printer_id != printer_id or stored_action != action:
|
|
return False
|
|
if time.time() > expiry:
|
|
_confirmation_tokens.pop(token, None)
|
|
return False
|
|
# Consume the token
|
|
_confirmation_tokens.pop(token, None)
|
|
return True
|
|
|
|
|
|
async def get_printer_or_404(printer_id: int, db: AsyncSession) -> Printer:
|
|
"""Get printer by ID or raise 404."""
|
|
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
|
printer = result.scalar_one_or_none()
|
|
if not printer:
|
|
raise HTTPException(status_code=404, detail="Printer not found")
|
|
return printer
|
|
|
|
|
|
def get_mqtt_client_or_503(printer_id: int):
|
|
"""Get MQTT client for printer or raise 503."""
|
|
client = printer_manager.get_client(printer_id)
|
|
if not client:
|
|
raise HTTPException(status_code=503, detail="Printer not connected")
|
|
if not client.state.connected:
|
|
raise HTTPException(status_code=503, detail="Printer connection lost")
|
|
return client
|
|
|
|
|
|
# =============================================================================
|
|
# Request/Response Models
|
|
# =============================================================================
|
|
|
|
class ControlResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
|
|
|
|
class ConfirmationRequired(BaseModel):
|
|
requires_confirmation: bool = True
|
|
token: str
|
|
warning: str
|
|
expires_in: int = CONFIRMATION_TOKEN_EXPIRY
|
|
|
|
|
|
class ConfirmableRequest(BaseModel):
|
|
confirm_token: Optional[str] = None
|
|
|
|
|
|
class TemperatureRequest(ConfirmableRequest):
|
|
target: int = Field(..., ge=0, le=350, description="Target temperature in Celsius")
|
|
|
|
|
|
class NozzleTemperatureRequest(TemperatureRequest):
|
|
nozzle: int = Field(default=0, ge=0, le=1, description="Nozzle index (0 or 1 for dual nozzle)")
|
|
|
|
|
|
class SpeedRequest(BaseModel):
|
|
mode: int = Field(..., ge=1, le=4, description="Speed mode: 1=silent, 2=standard, 3=sport, 4=ludicrous")
|
|
|
|
|
|
class ExtruderRequest(BaseModel):
|
|
extruder: int = Field(..., ge=0, le=1, description="Extruder index (0=right, 1=left for H2D)")
|
|
|
|
|
|
class FanRequest(BaseModel):
|
|
speed: int = Field(..., ge=0, le=100, description="Fan speed percentage (0-100)")
|
|
|
|
|
|
class LightRequest(BaseModel):
|
|
on: bool = Field(..., description="Light state: true=on, false=off")
|
|
|
|
|
|
class CameraSettingRequest(BaseModel):
|
|
enable: bool = Field(..., description="Enable or disable the setting")
|
|
|
|
|
|
class HomeRequest(ConfirmableRequest):
|
|
axes: str = Field(default="XYZ", description="Axes to home (e.g., 'XYZ', 'X', 'XY', 'Z')")
|
|
|
|
|
|
class MoveRequest(ConfirmableRequest):
|
|
axis: str = Field(..., pattern="^[XYZxyz]$", description="Axis to move: X, Y, or Z")
|
|
distance: float = Field(..., ge=-100, le=100, description="Distance in mm (positive or negative)")
|
|
speed: int = Field(default=3000, ge=100, le=10000, description="Movement speed in mm/min")
|
|
|
|
|
|
class AMSLoadRequest(BaseModel):
|
|
tray_id: int = Field(..., ge=0, le=254, description="Tray ID (0-15 for AMS, 254 for external)")
|
|
extruder_id: int | None = Field(default=None, ge=0, le=1, description="Extruder ID for dual-nozzle printers (0=right, 1=left)")
|
|
|
|
|
|
class AMSRefreshTrayRequest(BaseModel):
|
|
ams_id: int = Field(..., ge=0, le=128, description="AMS unit ID (0-3, or 128 for H2D external)")
|
|
tray_id: int = Field(..., ge=0, le=3, description="Tray ID within the AMS (0-3)")
|
|
|
|
|
|
class AMSFilamentSettingRequest(BaseModel):
|
|
ams_id: int = Field(..., ge=0, le=128, description="AMS unit ID (0-3, or 128 for H2D external)")
|
|
tray_id: int = Field(..., ge=0, le=3, description="Tray ID within the AMS (0-3)")
|
|
tray_info_idx: str = Field(..., description="Filament preset ID (e.g., 'GFA00')")
|
|
tray_type: str = Field(..., description="Filament type (e.g., 'PLA', 'PETG')")
|
|
tray_sub_brands: str = Field(default="", description="Sub-brand name (e.g., 'PLA Basic')")
|
|
tray_color: str = Field(..., description="Color in RRGGBBAA hex format")
|
|
nozzle_temp_min: int = Field(..., ge=150, le=350, description="Minimum nozzle temperature")
|
|
nozzle_temp_max: int = Field(..., ge=150, le=350, description="Maximum nozzle temperature")
|
|
k: float = Field(..., ge=0, le=1, description="Pressure advance (K) value")
|
|
|
|
|
|
class GcodeRequest(ConfirmableRequest):
|
|
command: str = Field(..., min_length=1, max_length=500, description="G-code command(s)")
|
|
|
|
|
|
# =============================================================================
|
|
# Print Control Endpoints
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/pause", response_model=ControlResponse)
|
|
async def pause_print(
|
|
printer_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Pause the current print job."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Check if printer is actually printing
|
|
if client.state.state != "RUNNING":
|
|
raise HTTPException(status_code=400, detail="Printer is not currently printing")
|
|
|
|
success = client.pause_print()
|
|
return ControlResponse(
|
|
success=success,
|
|
message="Pause command sent" if success else "Failed to send pause command"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/resume", response_model=ControlResponse)
|
|
async def resume_print(
|
|
printer_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Resume a paused print job."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Check if printer is actually paused
|
|
if client.state.state != "PAUSE":
|
|
raise HTTPException(status_code=400, detail="Printer is not paused")
|
|
|
|
success = client.resume_print()
|
|
return ControlResponse(
|
|
success=success,
|
|
message="Resume command sent" if success else "Failed to send resume command"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/stop")
|
|
async def stop_print(
|
|
printer_id: int,
|
|
request: ConfirmableRequest = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Stop the current print job. Requires confirmation."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Check if printer is printing or paused
|
|
if client.state.state not in ("RUNNING", "PAUSE"):
|
|
raise HTTPException(status_code=400, detail="No active print to stop")
|
|
|
|
# Require confirmation for stop
|
|
if not request or not request.confirm_token:
|
|
token = _create_confirmation_token(printer_id, "stop")
|
|
return ConfirmationRequired(
|
|
token=token,
|
|
warning="This will abort the current print. The print cannot be resumed. Are you sure?"
|
|
)
|
|
|
|
if not _validate_confirmation_token(request.confirm_token, printer_id, "stop"):
|
|
raise HTTPException(status_code=400, detail="Invalid or expired confirmation token")
|
|
|
|
success = client.stop_print()
|
|
return ControlResponse(
|
|
success=success,
|
|
message="Stop command sent" if success else "Failed to send stop command"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Temperature Control Endpoints
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/temperature/bed", response_model=ControlResponse)
|
|
async def set_bed_temperature(
|
|
printer_id: int,
|
|
request: TemperatureRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Set the bed target temperature."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Warn for high temperatures
|
|
if request.target > 100 and not request.confirm_token:
|
|
token = _create_confirmation_token(printer_id, "bed_temp")
|
|
return ConfirmationRequired(
|
|
token=token,
|
|
warning=f"Setting bed to {request.target}°C is unusually high. Confirm?"
|
|
)
|
|
|
|
if request.target > 100:
|
|
if not _validate_confirmation_token(request.confirm_token, printer_id, "bed_temp"):
|
|
raise HTTPException(status_code=400, detail="Invalid or expired confirmation token")
|
|
|
|
success = client.set_bed_temperature(request.target)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Bed temperature set to {request.target}°C" if success else "Failed to set bed temperature"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/temperature/nozzle", response_model=ControlResponse)
|
|
async def set_nozzle_temperature(
|
|
printer_id: int,
|
|
request: NozzleTemperatureRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Set the nozzle target temperature."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Warn for high temperatures
|
|
if request.target > 280 and not request.confirm_token:
|
|
token = _create_confirmation_token(printer_id, "nozzle_temp")
|
|
return ConfirmationRequired(
|
|
token=token,
|
|
warning=f"Setting nozzle to {request.target}°C is very high. Confirm?"
|
|
)
|
|
|
|
if request.target > 280:
|
|
if not _validate_confirmation_token(request.confirm_token, printer_id, "nozzle_temp"):
|
|
raise HTTPException(status_code=400, detail="Invalid or expired confirmation token")
|
|
|
|
success = client.set_nozzle_temperature(request.target, request.nozzle)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Nozzle {request.nozzle} temperature set to {request.target}°C" if success else "Failed to set nozzle temperature"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/temperature/chamber", response_model=ControlResponse)
|
|
async def set_chamber_temperature(
|
|
printer_id: int,
|
|
request: TemperatureRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Set the chamber target temperature."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Warn for high temperatures (chamber typically maxes around 60°C)
|
|
if request.target > 60 and not request.confirm_token:
|
|
token = _create_confirmation_token(printer_id, "chamber_temp")
|
|
return ConfirmationRequired(
|
|
token=token,
|
|
warning=f"Setting chamber to {request.target}°C is very high. Confirm?"
|
|
)
|
|
|
|
if request.target > 60:
|
|
if not _validate_confirmation_token(request.confirm_token, printer_id, "chamber_temp"):
|
|
raise HTTPException(status_code=400, detail="Invalid or expired confirmation token")
|
|
|
|
success = client.set_chamber_temperature(request.target)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Chamber temperature set to {request.target}°C" if success else "Failed to set chamber temperature"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Speed Control Endpoint
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/speed", response_model=ControlResponse)
|
|
async def set_print_speed(
|
|
printer_id: int,
|
|
request: SpeedRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Set the print speed mode."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
speed_names = {1: "Silent", 2: "Standard", 3: "Sport", 4: "Ludicrous"}
|
|
success = client.set_print_speed(request.mode)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Speed set to {speed_names[request.mode]}" if success else "Failed to set speed"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Extruder Control Endpoint
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/extruder", response_model=ControlResponse)
|
|
async def select_extruder(
|
|
printer_id: int,
|
|
request: ExtruderRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Select the active extruder for dual-nozzle printers."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
extruder_names = {0: "Right", 1: "Left"}
|
|
success = client.select_extruder(request.extruder)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Selected {extruder_names[request.extruder]} extruder" if success else "Failed to select extruder"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Fan Control Endpoints
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/fan/part", response_model=ControlResponse)
|
|
async def set_part_fan(
|
|
printer_id: int,
|
|
request: FanRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Set part cooling fan speed (0-100%)."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Convert percentage to 0-255
|
|
speed_255 = int(request.speed * 255 / 100)
|
|
success = client.set_part_fan(speed_255)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Part fan set to {request.speed}%" if success else "Failed to set part fan"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/fan/aux", response_model=ControlResponse)
|
|
async def set_aux_fan(
|
|
printer_id: int,
|
|
request: FanRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Set auxiliary fan speed (0-100%)."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
speed_255 = int(request.speed * 255 / 100)
|
|
success = client.set_aux_fan(speed_255)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Aux fan set to {request.speed}%" if success else "Failed to set aux fan"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/fan/chamber", response_model=ControlResponse)
|
|
async def set_chamber_fan(
|
|
printer_id: int,
|
|
request: FanRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Set chamber fan speed (0-100%)."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
speed_255 = int(request.speed * 255 / 100)
|
|
success = client.set_chamber_fan(speed_255)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Chamber fan set to {request.speed}%" if success else "Failed to set chamber fan"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Air Conditioning Control Endpoint
|
|
# =============================================================================
|
|
|
|
class AirductModeRequest(BaseModel):
|
|
mode: str # "cooling" or "heating"
|
|
|
|
|
|
@router.post("/{printer_id}/control/airduct", response_model=ControlResponse)
|
|
async def set_airduct_mode(
|
|
printer_id: int,
|
|
request: AirductModeRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Set air conditioning mode (cooling or heating).
|
|
|
|
- Cooling: Suitable for PLA/PETG/TPU, filters and cools chamber air
|
|
- Heating: Suitable for ABS/ASA/PC/PA, circulates and heats chamber air, closes top exhaust flap
|
|
"""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
if request.mode not in ("cooling", "heating"):
|
|
raise HTTPException(status_code=400, detail="Mode must be 'cooling' or 'heating'")
|
|
|
|
success = client.set_airduct_mode(request.mode)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Air conditioning set to {request.mode}" if success else "Failed to set air conditioning mode"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Light Control Endpoint
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/light", response_model=ControlResponse)
|
|
async def set_chamber_light(
|
|
printer_id: int,
|
|
request: LightRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Turn chamber light on or off."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
success = client.set_chamber_light(request.on)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Light turned {'on' if request.on else 'off'}" if success else "Failed to control light"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Movement Control Endpoints
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/home")
|
|
async def home_axes(
|
|
printer_id: int,
|
|
request: HomeRequest = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Home the specified axes."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
axes = (request.axes if request else "XYZ").upper()
|
|
|
|
# Warn if homing during print
|
|
if client.state.state in ("RUNNING", "PAUSE"):
|
|
if not request or not request.confirm_token:
|
|
token = _create_confirmation_token(printer_id, "home")
|
|
return ConfirmationRequired(
|
|
token=token,
|
|
warning="Homing during an active print is not recommended. This may damage your print. Continue?"
|
|
)
|
|
if not _validate_confirmation_token(request.confirm_token, printer_id, "home"):
|
|
raise HTTPException(status_code=400, detail="Invalid or expired confirmation token")
|
|
|
|
success = client.home_axes(axes)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Homing {axes}" if success else "Failed to send home command"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/move")
|
|
async def move_axis(
|
|
printer_id: int,
|
|
request: MoveRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Move an axis by a relative distance."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Block movement during print unless confirmed
|
|
if client.state.state in ("RUNNING", "PAUSE"):
|
|
if not request.confirm_token:
|
|
token = _create_confirmation_token(printer_id, "move")
|
|
return ConfirmationRequired(
|
|
token=token,
|
|
warning="Manual movement during printing can damage your print. Are you sure?"
|
|
)
|
|
if not _validate_confirmation_token(request.confirm_token, printer_id, "move"):
|
|
raise HTTPException(status_code=400, detail="Invalid or expired confirmation token")
|
|
|
|
success = client.move_axis(request.axis.upper(), request.distance, request.speed)
|
|
direction = "+" if request.distance > 0 else ""
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Moving {request.axis.upper()} {direction}{request.distance}mm" if success else "Failed to send move command"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/motors/disable")
|
|
async def disable_motors(
|
|
printer_id: int,
|
|
request: ConfirmableRequest = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Disable stepper motors. Warning: This will lose position."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Always require confirmation
|
|
if not request or not request.confirm_token:
|
|
token = _create_confirmation_token(printer_id, "disable_motors")
|
|
return ConfirmationRequired(
|
|
token=token,
|
|
warning="Disabling motors will cause the printer to lose its position. You must home before printing. Continue?"
|
|
)
|
|
|
|
if not _validate_confirmation_token(request.confirm_token, printer_id, "disable_motors"):
|
|
raise HTTPException(status_code=400, detail="Invalid or expired confirmation token")
|
|
|
|
success = client.disable_motors()
|
|
return ControlResponse(
|
|
success=success,
|
|
message="Motors disabled" if success else "Failed to disable motors"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/motors/enable", response_model=ControlResponse)
|
|
async def enable_motors(
|
|
printer_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Enable stepper motors."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
success = client.enable_motors()
|
|
return ControlResponse(
|
|
success=success,
|
|
message="Motors enabled" if success else "Failed to enable motors"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# AMS Control Endpoints
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/ams/load", response_model=ControlResponse)
|
|
async def ams_load_filament(
|
|
printer_id: int,
|
|
request: AMSLoadRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Load filament from a specific AMS tray."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Don't allow during print
|
|
if client.state.state == "RUNNING":
|
|
raise HTTPException(status_code=400, detail="Cannot change filament during print")
|
|
|
|
success = client.ams_load_filament(request.tray_id, request.extruder_id)
|
|
extruder_info = f" to extruder {request.extruder_id}" if request.extruder_id is not None else ""
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Loading filament from tray {request.tray_id}{extruder_info}" if success else "Failed to load filament"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/ams/unload", response_model=ControlResponse)
|
|
async def ams_unload_filament(
|
|
printer_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Unload the currently loaded filament."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Don't allow during print
|
|
if client.state.state == "RUNNING":
|
|
raise HTTPException(status_code=400, detail="Cannot unload filament during print")
|
|
|
|
success = client.ams_unload_filament()
|
|
return ControlResponse(
|
|
success=success,
|
|
message="Unloading filament" if success else "Failed to unload filament"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/ams/refresh-tray", response_model=ControlResponse)
|
|
async def ams_refresh_tray(
|
|
printer_id: int,
|
|
request: AMSRefreshTrayRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Trigger RFID re-read for a specific AMS tray."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
success, message = client.ams_refresh_tray(request.ams_id, request.tray_id)
|
|
return ControlResponse(success=success, message=message)
|
|
|
|
|
|
@router.post("/{printer_id}/control/ams/filament-setting", response_model=ControlResponse)
|
|
async def ams_set_filament_setting(
|
|
printer_id: int,
|
|
request: AMSFilamentSettingRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Set filament settings for an AMS tray including K (pressure advance) value."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
success = client.ams_set_filament_setting(
|
|
ams_id=request.ams_id,
|
|
tray_id=request.tray_id,
|
|
tray_info_idx=request.tray_info_idx,
|
|
tray_type=request.tray_type,
|
|
tray_sub_brands=request.tray_sub_brands,
|
|
tray_color=request.tray_color,
|
|
nozzle_temp_min=request.nozzle_temp_min,
|
|
nozzle_temp_max=request.nozzle_temp_max,
|
|
k=request.k,
|
|
)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Updated AMS {request.ams_id} tray {request.tray_id} with K={request.k}" if success else "Failed to update filament setting"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Advanced: G-code Command
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/gcode")
|
|
async def send_gcode(
|
|
printer_id: int,
|
|
request: GcodeRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Send raw G-code command(s). Advanced users only."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
# Require confirmation for any G-code
|
|
if not request.confirm_token:
|
|
token = _create_confirmation_token(printer_id, "gcode")
|
|
return ConfirmationRequired(
|
|
token=token,
|
|
warning="Sending raw G-code can damage your printer if used incorrectly. Are you sure?"
|
|
)
|
|
|
|
if not _validate_confirmation_token(request.confirm_token, printer_id, "gcode"):
|
|
raise HTTPException(status_code=400, detail="Invalid or expired confirmation token")
|
|
|
|
success = client.send_gcode(request.command)
|
|
return ControlResponse(
|
|
success=success,
|
|
message="G-code sent" if success else "Failed to send G-code"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Camera Settings Endpoints
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/camera/timelapse", response_model=ControlResponse)
|
|
async def set_timelapse(
|
|
printer_id: int,
|
|
request: CameraSettingRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Enable or disable timelapse recording."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
success = client.set_timelapse(request.enable)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Timelapse {'enabled' if request.enable else 'disabled'}" if success else "Failed to set timelapse"
|
|
)
|
|
|
|
|
|
@router.post("/{printer_id}/control/camera/liveview", response_model=ControlResponse)
|
|
async def set_liveview(
|
|
printer_id: int,
|
|
request: CameraSettingRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Enable or disable live view / camera streaming."""
|
|
await get_printer_or_404(printer_id, db)
|
|
client = get_mqtt_client_or_503(printer_id)
|
|
|
|
success = client.set_liveview(request.enable)
|
|
return ControlResponse(
|
|
success=success,
|
|
message=f"Live view {'enabled' if request.enable else 'disabled'}" if success else "Failed to set live view"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Status Refresh Endpoint
|
|
# =============================================================================
|
|
|
|
@router.post("/{printer_id}/control/refresh", response_model=ControlResponse)
|
|
async def refresh_status(
|
|
printer_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Request a full status update from the printer.
|
|
|
|
This sends a 'pushall' command to get the latest data including nozzle info,
|
|
AMS status, and all other printer state.
|
|
"""
|
|
await get_printer_or_404(printer_id, db)
|
|
|
|
success = printer_manager.request_status_update(printer_id)
|
|
if not success:
|
|
raise HTTPException(status_code=503, detail="Printer not connected")
|
|
|
|
return ControlResponse(
|
|
success=success,
|
|
message="Status refresh requested"
|
|
)
|