Wired up printer parts button

This commit is contained in:
Martin Ziegler
2025-12-02 17:57:08 +01:00
parent 42458974d8
commit 50a8ed8ea9
10 changed files with 762 additions and 399 deletions
+26
View File
@@ -595,3 +595,29 @@ async def set_liveview(
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"
)
+11
View File
@@ -21,6 +21,7 @@ from backend.app.schemas.printer import (
HMSErrorResponse,
AMSUnit,
AMSTray,
NozzleInfoResponse,
)
from backend.app.services.printer_manager import printer_manager
from backend.app.services.bambu_ftp import (
@@ -183,6 +184,15 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
k=vt_data.get("k"),
)
# Convert nozzle info to response format
nozzles = [
NozzleInfoResponse(
nozzle_type=n.nozzle_type,
nozzle_diameter=n.nozzle_diameter,
)
for n in (state.nozzles or [])
]
return PrinterStatus(
id=printer_id,
name=printer.name,
@@ -204,6 +214,7 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
sdcard=state.sdcard,
timelapse=state.timelapse,
ipcam=state.ipcam,
nozzles=nozzles,
)
+6
View File
@@ -58,6 +58,11 @@ class AMSUnit(BaseModel):
tray: list[AMSTray] = []
class NozzleInfoResponse(BaseModel):
nozzle_type: str = "" # "stainless_steel" or "hardened_steel"
nozzle_diameter: str = "" # e.g., "0.4"
class PrinterStatus(BaseModel):
id: int
name: str
@@ -79,3 +84,4 @@ class PrinterStatus(BaseModel):
sdcard: bool = False # SD card inserted
timelapse: bool = False # Timelapse recording active
ipcam: bool = False # Live view enabled
nozzles: list[NozzleInfoResponse] = [] # Nozzle hardware info (index 0=left/primary, 1=right)
+95 -2
View File
@@ -47,6 +47,13 @@ class KProfile:
setting_id: str | None = None
@dataclass
class NozzleInfo:
"""Nozzle hardware configuration."""
nozzle_type: str = "" # "stainless_steel" or "hardened_steel"
nozzle_diameter: str = "" # e.g., "0.4"
@dataclass
class PrinterState:
connected: bool = False
@@ -66,6 +73,8 @@ class PrinterState:
sdcard: bool = False # SD card inserted
timelapse: bool = False # Timelapse recording active
ipcam: bool = False # Live view / camera streaming enabled
# Nozzle hardware info (for dual nozzle printers, index 0 = left, 1 = right)
nozzles: list = field(default_factory=lambda: [NozzleInfo(), NozzleInfo()])
class BambuMQTTClient:
@@ -120,8 +129,10 @@ class BambuMQTTClient:
if rc == 0:
self.state.connected = True
client.subscribe(self.topic_subscribe)
# Request full status update
# Request full status update (includes nozzle info in push_status response)
self._request_push_all()
# Note: get_accessories returns stale nozzle data on H2D, so we don't use it.
# The correct nozzle data comes from push_status.
# Prime K-profile request (Bambu printers often ignore first request)
self._prime_kprofile_request()
# Immediately broadcast connection state change
@@ -176,13 +187,19 @@ class BambuMQTTClient:
# Handle xcam data (camera settings) at top level
if "xcam" in payload:
xcam_data = payload["xcam"]
logger.info(f"[{self.serial_number}] Received xcam data: {xcam_data}")
logger.debug(f"[{self.serial_number}] Received xcam data: {xcam_data}")
if isinstance(xcam_data, dict):
if "ipcam_record" in xcam_data:
self.state.ipcam = xcam_data.get("ipcam_record") == "enable"
if "timelapse" in xcam_data:
self.state.timelapse = xcam_data.get("timelapse") == "enable"
# Handle system responses (accessories info, etc.)
if "system" in payload:
system_data = payload["system"]
logger.info(f"[{self.serial_number}] Received system data: {system_data}")
self._handle_system_response(system_data)
if "print" in payload:
print_data = payload["print"]
# Log when we see gcode_state changes
@@ -211,6 +228,22 @@ class BambuMQTTClient:
self._update_state(print_data)
def _handle_system_response(self, data: dict):
"""Handle system responses including accessories info.
Note: get_accessories returns stale/incorrect nozzle_type data on H2D.
The correct nozzle data comes from push_status, so we don't update
nozzle type/diameter from get_accessories. We just log the response
for debugging purposes.
"""
command = data.get("command")
if command == "get_accessories":
# Log response for debugging - but DON'T use it to update nozzle data
# because it returns stale values (e.g., 'stainless_steel' when the
# actual nozzle is 'HH01' hardened steel high-flow)
logger.info(f"[{self.serial_number}] Accessories response (not used for nozzle data): {data}")
def _handle_ams_data(self, ams_data):
"""Handle AMS data changes for Spoolman integration.
@@ -283,6 +316,12 @@ class BambuMQTTClient:
if temp_fields and not hasattr(self, '_temp_fields_logged'):
logger.info(f"[{self.serial_number}] Temperature fields in MQTT data: {temp_fields}")
self._temp_fields_logged = True
# Log nozzle hardware info fields (once)
nozzle_fields = {k: v for k, v in data.items() if 'nozzle' in k.lower() or 'hw' in k.lower() or 'extruder' in k.lower() or 'upgrade' in k.lower()}
if nozzle_fields and not hasattr(self, '_nozzle_fields_logged'):
logger.info(f"[{self.serial_number}] Nozzle/hardware fields in MQTT data: {nozzle_fields}")
self._nozzle_fields_logged = True
if "bed_temper" in data:
temps["bed"] = float(data["bed_temper"])
if "bed_target_temper" in data:
@@ -356,6 +395,30 @@ class BambuMQTTClient:
else:
self.state.ipcam = ipcam_data is True
# Parse nozzle hardware info (single nozzle printers)
if "nozzle_type" in data:
self.state.nozzles[0].nozzle_type = str(data["nozzle_type"])
if "nozzle_diameter" in data:
self.state.nozzles[0].nozzle_diameter = str(data["nozzle_diameter"])
# Parse nozzle hardware info (dual nozzle printers - H2D series)
# Left nozzle
if "left_nozzle_type" in data:
self.state.nozzles[0].nozzle_type = str(data["left_nozzle_type"])
if "left_nozzle_diameter" in data:
self.state.nozzles[0].nozzle_diameter = str(data["left_nozzle_diameter"])
# Right nozzle
if "right_nozzle_type" in data:
self.state.nozzles[1].nozzle_type = str(data["right_nozzle_type"])
if "right_nozzle_diameter" in data:
self.state.nozzles[1].nozzle_diameter = str(data["right_nozzle_diameter"])
# Alternative format for dual nozzle (nozzle_type_2, etc.)
if "nozzle_type_2" in data:
self.state.nozzles[1].nozzle_type = str(data["nozzle_type_2"])
if "nozzle_diameter_2" in data:
self.state.nozzles[1].nozzle_diameter = str(data["nozzle_diameter_2"])
# Preserve AMS and vt_tray data when updating raw_data
ams_data = self.state.raw_data.get("ams")
vt_tray_data = self.state.raw_data.get("vt_tray")
@@ -467,6 +530,36 @@ class BambuMQTTClient:
message = {"pushing": {"command": "pushall"}}
self._client.publish(self.topic_publish, json.dumps(message))
def request_status_update(self) -> bool:
"""Request a full status update from the printer (public API).
Sends both pushall and get_accessories commands to refresh all data
including nozzle hardware info.
Returns:
True if the request was sent, False if not connected.
"""
if not self._client or not self.state.connected:
return False
self._request_push_all()
# Note: get_accessories returns stale nozzle data on H2D.
# The correct nozzle data comes from push_status response.
return True
def _request_accessories(self):
"""Request accessories info (nozzle type, etc.) from printer."""
if self._client:
self._sequence_id += 1
message = {
"system": {
"sequence_id": str(self._sequence_id),
"command": "get_accessories",
"accessory_type": "none"
}
}
logger.debug(f"[{self.serial_number}] Requesting accessories info")
self._client.publish(self.topic_publish, json.dumps(message))
def _prime_kprofile_request(self):
"""Send a priming K-profile request on connect.
+9
View File
@@ -229,6 +229,15 @@ class PrinterManager:
return self._clients[printer_id].logging_enabled
return False
def request_status_update(self, printer_id: int) -> bool:
"""Request a full status update from the printer.
This sends a 'pushall' command to get the latest data including nozzle info.
"""
if printer_id in self._clients:
return self._clients[printer_id].request_status_update()
return False
async def test_connection(
self,
ip_address: str,