mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
Wiring up AMS module
This commit is contained in:
@@ -133,6 +133,12 @@ class MoveRequest(ConfirmableRequest):
|
||||
|
||||
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 GcodeRequest(ConfirmableRequest):
|
||||
@@ -587,10 +593,11 @@ async def ams_load_filament(
|
||||
if client.state.state == "RUNNING":
|
||||
raise HTTPException(status_code=400, detail="Cannot change filament during print")
|
||||
|
||||
success = client.ams_load_filament(request.tray_id)
|
||||
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}" if success else "Failed to load filament"
|
||||
message=f"Loading filament from tray {request.tray_id}{extruder_info}" if success else "Failed to load filament"
|
||||
)
|
||||
|
||||
|
||||
@@ -614,6 +621,23 @@ async def ams_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 = client.ams_refresh_tray(request.ams_id, request.tray_id)
|
||||
return ControlResponse(
|
||||
success=success,
|
||||
message=f"Refreshing AMS {request.ams_id} tray {request.tray_id}" if success else "Failed to refresh tray"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Advanced: G-code Command
|
||||
# =============================================================================
|
||||
|
||||
@@ -13,6 +13,7 @@ from sqlalchemy import select
|
||||
from backend.app.core.database import get_db
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.models.printer import Printer
|
||||
from backend.app.models.slot_preset import SlotPresetMapping
|
||||
from backend.app.schemas.printer import (
|
||||
PrinterCreate,
|
||||
PrinterUpdate,
|
||||
@@ -156,21 +157,54 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
ams_exists = False
|
||||
raw_data = state.raw_data or {}
|
||||
|
||||
if "ams" in raw_data:
|
||||
if "ams" in raw_data and isinstance(raw_data["ams"], list):
|
||||
ams_exists = True
|
||||
for ams_data in raw_data["ams"]:
|
||||
# Skip if ams_data is not a dict (defensive check)
|
||||
if not isinstance(ams_data, dict):
|
||||
continue
|
||||
trays = []
|
||||
for tray_data in ams_data.get("tray", []):
|
||||
# Filter out empty/invalid tag values
|
||||
tag_uid = tray_data.get("tag_uid", "")
|
||||
if tag_uid in ("", "0000000000000000"):
|
||||
tag_uid = None
|
||||
tray_uuid = tray_data.get("tray_uuid", "")
|
||||
if tray_uuid in ("", "00000000000000000000000000000000"):
|
||||
tray_uuid = None
|
||||
trays.append(AMSTray(
|
||||
id=tray_data.get("id", 0),
|
||||
tray_color=tray_data.get("tray_color"),
|
||||
tray_type=tray_data.get("tray_type"),
|
||||
tray_sub_brands=tray_data.get("tray_sub_brands"),
|
||||
tray_id_name=tray_data.get("tray_id_name"),
|
||||
tray_info_idx=tray_data.get("tray_info_idx"),
|
||||
remain=tray_data.get("remain", 0),
|
||||
k=tray_data.get("k"),
|
||||
tag_uid=tag_uid,
|
||||
tray_uuid=tray_uuid,
|
||||
nozzle_temp_min=tray_data.get("nozzle_temp_min"),
|
||||
nozzle_temp_max=tray_data.get("nozzle_temp_max"),
|
||||
))
|
||||
# Prefer humidity_raw (percentage) over humidity (index 1-5)
|
||||
# humidity_raw is the actual percentage value from the sensor
|
||||
humidity_raw = ams_data.get("humidity_raw")
|
||||
humidity_idx = ams_data.get("humidity")
|
||||
# Use humidity_raw if available, otherwise fall back to humidity index
|
||||
humidity_value = None
|
||||
if humidity_raw is not None:
|
||||
try:
|
||||
humidity_value = int(humidity_raw)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if humidity_value is None and humidity_idx is not None:
|
||||
try:
|
||||
humidity_value = int(humidity_idx)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
ams_units.append(AMSUnit(
|
||||
id=ams_data.get("id", 0),
|
||||
humidity=ams_data.get("humidity"),
|
||||
humidity=humidity_value,
|
||||
temp=ams_data.get("temp"),
|
||||
tray=trays,
|
||||
))
|
||||
@@ -178,12 +212,24 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
# Virtual tray (external spool holder) - comes from vt_tray in raw_data
|
||||
if "vt_tray" in raw_data:
|
||||
vt_data = raw_data["vt_tray"]
|
||||
# Filter out empty/invalid tag values for vt_tray
|
||||
vt_tag_uid = vt_data.get("tag_uid", "")
|
||||
if vt_tag_uid in ("", "0000000000000000"):
|
||||
vt_tag_uid = None
|
||||
vt_tray_uuid = vt_data.get("tray_uuid", "")
|
||||
if vt_tray_uuid in ("", "00000000000000000000000000000000"):
|
||||
vt_tray_uuid = None
|
||||
vt_tray = AMSTray(
|
||||
id=254, # Virtual tray ID
|
||||
tray_color=vt_data.get("tray_color"),
|
||||
tray_type=vt_data.get("tray_type"),
|
||||
tray_sub_brands=vt_data.get("tray_sub_brands"),
|
||||
remain=vt_data.get("remain", 0),
|
||||
k=vt_data.get("k"),
|
||||
tag_uid=vt_tag_uid,
|
||||
tray_uuid=vt_tray_uuid,
|
||||
nozzle_temp_min=vt_data.get("nozzle_temp_min"),
|
||||
nozzle_temp_max=vt_data.get("nozzle_temp_max"),
|
||||
)
|
||||
|
||||
# Convert nozzle info to response format
|
||||
@@ -214,6 +260,18 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
filament_tangle_detect=state.print_options.filament_tangle_detect,
|
||||
)
|
||||
|
||||
# Get AMS mapping from raw_data (which AMS is connected to which nozzle)
|
||||
ams_mapping = raw_data.get("ams_mapping", [])
|
||||
# Get per-AMS extruder map: {ams_id: extruder_id} where 0=right, 1=left
|
||||
ams_extruder_map = raw_data.get("ams_extruder_map", {})
|
||||
logger.debug(f"API returning ams_mapping: {ams_mapping}, ams_extruder_map: {ams_extruder_map}")
|
||||
|
||||
# tray_now from MQTT is already a global tray ID: (ams_id * 4) + slot_id
|
||||
# Per OpenBambuAPI docs: 254 = external spool, 255 = no filament, otherwise global tray ID
|
||||
# No conversion needed - just use the raw value directly
|
||||
tray_now = state.tray_now
|
||||
logger.debug(f"Using tray_now directly as global ID: {tray_now}")
|
||||
|
||||
return PrinterStatus(
|
||||
id=printer_id,
|
||||
name=printer.name,
|
||||
@@ -245,6 +303,9 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
speed_level=state.speed_level,
|
||||
chamber_light=state.chamber_light,
|
||||
active_extruder=state.active_extruder,
|
||||
ams_mapping=ams_mapping,
|
||||
ams_extruder_map=ams_extruder_map,
|
||||
tray_now=tray_now,
|
||||
)
|
||||
|
||||
|
||||
@@ -701,3 +762,133 @@ async def start_calibration(
|
||||
"nozzle_offset": nozzle_offset,
|
||||
"high_temp_heatbed": high_temp_heatbed,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Slot Preset Mapping Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/{printer_id}/slot-presets")
|
||||
async def get_slot_presets(
|
||||
printer_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get all saved slot-to-preset mappings for a printer."""
|
||||
result = await db.execute(
|
||||
select(SlotPresetMapping).where(SlotPresetMapping.printer_id == printer_id)
|
||||
)
|
||||
mappings = result.scalars().all()
|
||||
|
||||
return {
|
||||
mapping.ams_id * 4 + mapping.tray_id: {
|
||||
"ams_id": mapping.ams_id,
|
||||
"tray_id": mapping.tray_id,
|
||||
"preset_id": mapping.preset_id,
|
||||
"preset_name": mapping.preset_name,
|
||||
}
|
||||
for mapping in mappings
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{printer_id}/slot-presets/{ams_id}/{tray_id}")
|
||||
async def get_slot_preset(
|
||||
printer_id: int,
|
||||
ams_id: int,
|
||||
tray_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get the saved preset for a specific slot."""
|
||||
result = await db.execute(
|
||||
select(SlotPresetMapping).where(
|
||||
SlotPresetMapping.printer_id == printer_id,
|
||||
SlotPresetMapping.ams_id == ams_id,
|
||||
SlotPresetMapping.tray_id == tray_id,
|
||||
)
|
||||
)
|
||||
mapping = result.scalar_one_or_none()
|
||||
|
||||
if not mapping:
|
||||
return None
|
||||
|
||||
return {
|
||||
"ams_id": mapping.ams_id,
|
||||
"tray_id": mapping.tray_id,
|
||||
"preset_id": mapping.preset_id,
|
||||
"preset_name": mapping.preset_name,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{printer_id}/slot-presets/{ams_id}/{tray_id}")
|
||||
async def save_slot_preset(
|
||||
printer_id: int,
|
||||
ams_id: int,
|
||||
tray_id: int,
|
||||
preset_id: str,
|
||||
preset_name: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Save a preset mapping for a specific slot."""
|
||||
# Check printer exists
|
||||
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
||||
if not result.scalar_one_or_none():
|
||||
raise HTTPException(404, "Printer not found")
|
||||
|
||||
# Check for existing mapping
|
||||
result = await db.execute(
|
||||
select(SlotPresetMapping).where(
|
||||
SlotPresetMapping.printer_id == printer_id,
|
||||
SlotPresetMapping.ams_id == ams_id,
|
||||
SlotPresetMapping.tray_id == tray_id,
|
||||
)
|
||||
)
|
||||
mapping = result.scalar_one_or_none()
|
||||
|
||||
if mapping:
|
||||
# Update existing
|
||||
mapping.preset_id = preset_id
|
||||
mapping.preset_name = preset_name
|
||||
else:
|
||||
# Create new
|
||||
mapping = SlotPresetMapping(
|
||||
printer_id=printer_id,
|
||||
ams_id=ams_id,
|
||||
tray_id=tray_id,
|
||||
preset_id=preset_id,
|
||||
preset_name=preset_name,
|
||||
)
|
||||
db.add(mapping)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(mapping)
|
||||
|
||||
return {
|
||||
"ams_id": mapping.ams_id,
|
||||
"tray_id": mapping.tray_id,
|
||||
"preset_id": mapping.preset_id,
|
||||
"preset_name": mapping.preset_name,
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{printer_id}/slot-presets/{ams_id}/{tray_id}")
|
||||
async def delete_slot_preset(
|
||||
printer_id: int,
|
||||
ams_id: int,
|
||||
tray_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Delete a saved preset mapping for a slot."""
|
||||
result = await db.execute(
|
||||
select(SlotPresetMapping).where(
|
||||
SlotPresetMapping.printer_id == printer_id,
|
||||
SlotPresetMapping.ams_id == ams_id,
|
||||
SlotPresetMapping.tray_id == tray_id,
|
||||
)
|
||||
)
|
||||
mapping = result.scalar_one_or_none()
|
||||
|
||||
if mapping:
|
||||
await db.delete(mapping)
|
||||
await db.commit()
|
||||
|
||||
return {"success": True}
|
||||
|
||||
Reference in New Issue
Block a user