Wiring up print options: first try. needs mofre reverse engineering for sync process
@@ -42,3 +42,4 @@ archive/
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
*.log*
|
||||
|
||||
@@ -22,6 +22,7 @@ from backend.app.schemas.printer import (
|
||||
AMSUnit,
|
||||
AMSTray,
|
||||
NozzleInfoResponse,
|
||||
PrintOptionsResponse,
|
||||
)
|
||||
from backend.app.services.printer_manager import printer_manager
|
||||
from backend.app.services.bambu_ftp import (
|
||||
@@ -193,6 +194,25 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
for n in (state.nozzles or [])
|
||||
]
|
||||
|
||||
# Convert print options to response format
|
||||
print_options = PrintOptionsResponse(
|
||||
spaghetti_detector=state.print_options.spaghetti_detector,
|
||||
print_halt=state.print_options.print_halt,
|
||||
halt_print_sensitivity=state.print_options.halt_print_sensitivity,
|
||||
first_layer_inspector=state.print_options.first_layer_inspector,
|
||||
printing_monitor=state.print_options.printing_monitor,
|
||||
buildplate_marker_detector=state.print_options.buildplate_marker_detector,
|
||||
allow_skip_parts=state.print_options.allow_skip_parts,
|
||||
nozzle_clumping_detector=state.print_options.nozzle_clumping_detector,
|
||||
nozzle_clumping_sensitivity=state.print_options.nozzle_clumping_sensitivity,
|
||||
pileup_detector=state.print_options.pileup_detector,
|
||||
pileup_sensitivity=state.print_options.pileup_sensitivity,
|
||||
airprint_detector=state.print_options.airprint_detector,
|
||||
airprint_sensitivity=state.print_options.airprint_sensitivity,
|
||||
auto_recovery_step_loss=state.print_options.auto_recovery_step_loss,
|
||||
filament_tangle_detect=state.print_options.filament_tangle_detect,
|
||||
)
|
||||
|
||||
return PrinterStatus(
|
||||
id=printer_id,
|
||||
name=printer.name,
|
||||
@@ -212,9 +232,11 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
ams_exists=ams_exists,
|
||||
vt_tray=vt_tray,
|
||||
sdcard=state.sdcard,
|
||||
store_to_sdcard=state.store_to_sdcard,
|
||||
timelapse=state.timelapse,
|
||||
ipcam=state.ipcam,
|
||||
nozzles=nozzles,
|
||||
print_options=print_options,
|
||||
)
|
||||
|
||||
|
||||
@@ -542,3 +564,73 @@ async def clear_mqtt_logs(printer_id: int, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
printer_manager.clear_logs(printer_id)
|
||||
return {"status": "cleared"}
|
||||
|
||||
|
||||
# ============================================
|
||||
# Print Options (AI Detection) Endpoints
|
||||
# ============================================
|
||||
|
||||
@router.post("/{printer_id}/print-options")
|
||||
async def set_print_option(
|
||||
printer_id: int,
|
||||
module_name: str,
|
||||
enabled: bool,
|
||||
print_halt: bool = True,
|
||||
sensitivity: str = "medium",
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Set an AI detection / print option on the printer.
|
||||
|
||||
Valid module_name values:
|
||||
- spaghetti_detector: Spaghetti detection
|
||||
- first_layer_inspector: First layer inspection
|
||||
- printing_monitor: AI print quality monitoring
|
||||
- buildplate_marker_detector: Build plate marker detection
|
||||
- allow_skip_parts: Allow skipping failed parts
|
||||
"""
|
||||
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 or not client.state.connected:
|
||||
raise HTTPException(400, "Printer not connected")
|
||||
|
||||
# Validate module_name
|
||||
valid_modules = [
|
||||
"spaghetti_detector",
|
||||
"first_layer_inspector",
|
||||
"printing_monitor",
|
||||
"buildplate_marker_detector",
|
||||
"allow_skip_parts",
|
||||
"pileup_detector",
|
||||
"clump_detector",
|
||||
"airprint_detector",
|
||||
"auto_recovery_step_loss",
|
||||
]
|
||||
if module_name not in valid_modules:
|
||||
raise HTTPException(400, f"Invalid module_name. Must be one of: {valid_modules}")
|
||||
|
||||
# Validate sensitivity
|
||||
valid_sensitivities = ["low", "medium", "high", "never_halt"]
|
||||
if sensitivity not in valid_sensitivities:
|
||||
raise HTTPException(400, f"Invalid sensitivity. Must be one of: {valid_sensitivities}")
|
||||
|
||||
success = client.set_xcam_option(
|
||||
module_name=module_name,
|
||||
enabled=enabled,
|
||||
print_halt=print_halt,
|
||||
sensitivity=sensitivity,
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(500, "Failed to send command to printer")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"module_name": module_name,
|
||||
"enabled": enabled,
|
||||
"print_halt": print_halt,
|
||||
"sensitivity": sensitivity,
|
||||
}
|
||||
|
||||
@@ -63,6 +63,27 @@ class NozzleInfoResponse(BaseModel):
|
||||
nozzle_diameter: str = "" # e.g., "0.4"
|
||||
|
||||
|
||||
class PrintOptionsResponse(BaseModel):
|
||||
"""AI detection and print options from xcam data."""
|
||||
# Core AI detectors
|
||||
spaghetti_detector: bool = False
|
||||
print_halt: bool = False
|
||||
halt_print_sensitivity: str = "medium" # Spaghetti sensitivity
|
||||
first_layer_inspector: bool = False
|
||||
printing_monitor: bool = False
|
||||
buildplate_marker_detector: bool = False
|
||||
allow_skip_parts: bool = False
|
||||
# Additional AI detectors (decoded from cfg bitmask)
|
||||
nozzle_clumping_detector: bool = True
|
||||
nozzle_clumping_sensitivity: str = "medium"
|
||||
pileup_detector: bool = True
|
||||
pileup_sensitivity: str = "medium"
|
||||
airprint_detector: bool = True
|
||||
airprint_sensitivity: str = "medium"
|
||||
auto_recovery_step_loss: bool = True
|
||||
filament_tangle_detect: bool = False
|
||||
|
||||
|
||||
class PrinterStatus(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
@@ -82,6 +103,8 @@ class PrinterStatus(BaseModel):
|
||||
ams_exists: bool = False
|
||||
vt_tray: AMSTray | None = None # Virtual tray / external spool
|
||||
sdcard: bool = False # SD card inserted
|
||||
store_to_sdcard: bool = False # Store sent files on SD card
|
||||
timelapse: bool = False # Timelapse recording active
|
||||
ipcam: bool = False # Live view enabled
|
||||
nozzles: list[NozzleInfoResponse] = [] # Nozzle hardware info (index 0=left/primary, 1=right)
|
||||
print_options: PrintOptionsResponse | None = None # AI detection and print options
|
||||
|
||||
@@ -54,6 +54,28 @@ class NozzleInfo:
|
||||
nozzle_diameter: str = "" # e.g., "0.4"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrintOptions:
|
||||
"""AI detection and print options from xcam data."""
|
||||
# Core AI detectors
|
||||
spaghetti_detector: bool = False
|
||||
print_halt: bool = False
|
||||
halt_print_sensitivity: str = "medium" # Spaghetti sensitivity
|
||||
first_layer_inspector: bool = False
|
||||
printing_monitor: bool = False # AI print quality monitoring
|
||||
buildplate_marker_detector: bool = False
|
||||
allow_skip_parts: bool = False
|
||||
# Additional AI detectors - decoded from cfg bitmask
|
||||
nozzle_clumping_detector: bool = True
|
||||
nozzle_clumping_sensitivity: str = "medium"
|
||||
pileup_detector: bool = True
|
||||
pileup_sensitivity: str = "medium"
|
||||
airprint_detector: bool = True
|
||||
airprint_sensitivity: str = "medium"
|
||||
auto_recovery_step_loss: bool = True # Uses print.print_option command
|
||||
filament_tangle_detect: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrinterState:
|
||||
connected: bool = False
|
||||
@@ -71,10 +93,13 @@ class PrinterState:
|
||||
hms_errors: list = field(default_factory=list) # List of HMSError
|
||||
kprofiles: list = field(default_factory=list) # List of KProfile
|
||||
sdcard: bool = False # SD card inserted
|
||||
store_to_sdcard: bool = False # Store sent files on SD card (home_flag bit 11)
|
||||
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()])
|
||||
# AI detection and print options
|
||||
print_options: PrintOptions = field(default_factory=PrintOptions)
|
||||
|
||||
|
||||
class BambuMQTTClient:
|
||||
@@ -117,6 +142,11 @@ class BambuMQTTClient:
|
||||
self._pending_kprofile_response: asyncio.Event | None = None
|
||||
self._kprofile_response_data: list | None = None
|
||||
|
||||
# Xcam hold timers - OrcaSlicer pattern: ignore incoming data for 3 seconds after command
|
||||
# Key: module_name, Value: timestamp when command was sent
|
||||
self._xcam_hold_start: dict[str, float] = {}
|
||||
self._xcam_hold_time: float = 3.0 # Ignore incoming data for 3 seconds after command
|
||||
|
||||
@property
|
||||
def topic_subscribe(self) -> str:
|
||||
return f"device/{self.serial_number}/report"
|
||||
@@ -184,15 +214,14 @@ class BambuMQTTClient:
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.serial_number}] Error handling AMS data: {e}")
|
||||
|
||||
# Handle xcam data (camera settings) at top level
|
||||
# Handle xcam data (camera settings and AI detection) at top level
|
||||
if "xcam" in payload:
|
||||
xcam_data = payload["xcam"]
|
||||
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"
|
||||
logger.info(f"[{self.serial_number}] Received xcam data at top level: {xcam_data}")
|
||||
self._parse_xcam_data(xcam_data)
|
||||
# Fire state change callback for top-level xcam (not nested in "print")
|
||||
if "print" not in payload and self.on_state_change:
|
||||
self.on_state_change(self.state)
|
||||
|
||||
# Handle system responses (accessories info, etc.)
|
||||
if "system" in payload:
|
||||
@@ -202,6 +231,12 @@ class BambuMQTTClient:
|
||||
|
||||
if "print" in payload:
|
||||
print_data = payload["print"]
|
||||
|
||||
# Check if xcam is nested inside print data
|
||||
if "xcam" in print_data:
|
||||
logger.info(f"[{self.serial_number}] Found xcam inside print data: {print_data['xcam']}")
|
||||
self._parse_xcam_data(print_data["xcam"])
|
||||
|
||||
# Log when we see gcode_state changes
|
||||
if "gcode_state" in print_data:
|
||||
logger.info(
|
||||
@@ -244,6 +279,195 @@ class BambuMQTTClient:
|
||||
# actual nozzle is 'HH01' hardened steel high-flow)
|
||||
logger.info(f"[{self.serial_number}] Accessories response (not used for nozzle data): {data}")
|
||||
|
||||
def _parse_xcam_data(self, xcam_data):
|
||||
"""Parse xcam data for camera settings and AI detection options."""
|
||||
if not isinstance(xcam_data, dict):
|
||||
return
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
# Helper to check if we should accept incoming value for a module
|
||||
# OrcaSlicer pattern: simple hold timer, ignore ALL data for 3 seconds after command
|
||||
def should_accept_value(module_name: str, incoming_value: bool) -> bool:
|
||||
"""Check if we should accept an incoming xcam value.
|
||||
|
||||
OrcaSlicer pattern: After sending a command, ignore incoming data
|
||||
for 3 seconds. After that, accept whatever the printer sends.
|
||||
"""
|
||||
if module_name not in self._xcam_hold_start:
|
||||
return True # No hold timer, accept incoming
|
||||
|
||||
hold_start = self._xcam_hold_start[module_name]
|
||||
elapsed = current_time - hold_start
|
||||
|
||||
if elapsed > self._xcam_hold_time:
|
||||
# Hold timer expired - accept incoming and clear hold
|
||||
del self._xcam_hold_start[module_name]
|
||||
logger.debug(
|
||||
f"[{self.serial_number}] Hold expired for {module_name}, accepting {incoming_value}"
|
||||
)
|
||||
return True
|
||||
|
||||
# Within hold period - ignore incoming data
|
||||
logger.debug(
|
||||
f"[{self.serial_number}] Ignoring {module_name}={incoming_value} "
|
||||
f"(hold active, {elapsed:.1f}s < {self._xcam_hold_time}s)"
|
||||
)
|
||||
return False
|
||||
|
||||
# Log all xcam fields for debugging
|
||||
logger.debug(f"[{self.serial_number}] Parsing xcam data - all fields: {list(xcam_data.keys())}")
|
||||
|
||||
# The cfg bitmask contains the ACTUAL detector states - the individual boolean
|
||||
# fields (spaghetti_detector, etc.) are often stale/cached.
|
||||
# CFG bitmask structure (each detector uses 3 bits: [sens_low, sens_high, enabled]):
|
||||
# - Bits 5-7: spaghetti_detector (sens in 5-6, enabled in 7)
|
||||
# - Bits 8-10: pileup_detector (sens in 8-9, enabled in 10)
|
||||
# - Bits 11-13: clump_detector/nozzle_clumping (sens in 11-12, enabled in 13)
|
||||
# - Bits 14-16: airprint_detector (sens in 14-15, enabled in 16)
|
||||
# Sensitivity values: 0=low, 1=medium, 2=high
|
||||
if "cfg" in xcam_data:
|
||||
cfg = xcam_data["cfg"]
|
||||
logger.debug(f"[{self.serial_number}] xcam cfg bitmask: {cfg} (binary: {bin(cfg)})")
|
||||
|
||||
def decode_detector(start_bit):
|
||||
"""Decode a detector from cfg: returns (enabled, sensitivity_str)"""
|
||||
sens_bits = (cfg >> start_bit) & 0x3
|
||||
enabled = bool((cfg >> (start_bit + 2)) & 1)
|
||||
sensitivity = {0: "low", 1: "medium", 2: "high"}.get(sens_bits, "medium")
|
||||
return enabled, sensitivity
|
||||
|
||||
# Spaghetti detector (bits 5-7)
|
||||
cfg_spaghetti, cfg_sensitivity = decode_detector(5)
|
||||
if should_accept_value("spaghetti_detector", cfg_spaghetti):
|
||||
old_value = self.state.print_options.spaghetti_detector
|
||||
if cfg_spaghetti != old_value:
|
||||
logger.info(f"[{self.serial_number}] spaghetti_detector changed (from cfg): {old_value} -> {cfg_spaghetti}")
|
||||
self.state.print_options.spaghetti_detector = cfg_spaghetti
|
||||
|
||||
# Check hold timer for sensitivity before accepting
|
||||
if "halt_print_sensitivity" not in self._xcam_hold_start:
|
||||
if cfg_sensitivity != self.state.print_options.halt_print_sensitivity:
|
||||
logger.info(
|
||||
f"[{self.serial_number}] Sensitivity changed (from cfg): "
|
||||
f"{self.state.print_options.halt_print_sensitivity} -> {cfg_sensitivity}"
|
||||
)
|
||||
self.state.print_options.halt_print_sensitivity = cfg_sensitivity
|
||||
else:
|
||||
hold_start = self._xcam_hold_start["halt_print_sensitivity"]
|
||||
elapsed = current_time - hold_start
|
||||
if elapsed <= self._xcam_hold_time:
|
||||
logger.debug(
|
||||
f"[{self.serial_number}] Ignoring cfg sensitivity={cfg_sensitivity} "
|
||||
f"(hold active, {elapsed:.1f}s < {self._xcam_hold_time}s)"
|
||||
)
|
||||
else:
|
||||
# Hold expired - accept from cfg
|
||||
if cfg_sensitivity != self.state.print_options.halt_print_sensitivity:
|
||||
logger.info(
|
||||
f"[{self.serial_number}] Sensitivity synced (from cfg after hold): "
|
||||
f"{self.state.print_options.halt_print_sensitivity} -> {cfg_sensitivity}"
|
||||
)
|
||||
self.state.print_options.halt_print_sensitivity = cfg_sensitivity
|
||||
del self._xcam_hold_start["halt_print_sensitivity"]
|
||||
|
||||
# Pileup detector (bits 8-10)
|
||||
cfg_pileup, cfg_pileup_sens = decode_detector(8)
|
||||
if should_accept_value("pileup_detector", cfg_pileup):
|
||||
if cfg_pileup != self.state.print_options.pileup_detector:
|
||||
logger.info(f"[{self.serial_number}] pileup_detector changed (from cfg): {self.state.print_options.pileup_detector} -> {cfg_pileup}")
|
||||
self.state.print_options.pileup_detector = cfg_pileup
|
||||
# Pileup sensitivity with hold timer
|
||||
if "pileup_sensitivity" not in self._xcam_hold_start:
|
||||
if cfg_pileup_sens != self.state.print_options.pileup_sensitivity:
|
||||
logger.info(f"[{self.serial_number}] pileup_sensitivity changed (from cfg): {self.state.print_options.pileup_sensitivity} -> {cfg_pileup_sens}")
|
||||
self.state.print_options.pileup_sensitivity = cfg_pileup_sens
|
||||
else:
|
||||
hold_start = self._xcam_hold_start["pileup_sensitivity"]
|
||||
elapsed = current_time - hold_start
|
||||
if elapsed > self._xcam_hold_time:
|
||||
if cfg_pileup_sens != self.state.print_options.pileup_sensitivity:
|
||||
logger.info(f"[{self.serial_number}] pileup_sensitivity synced (from cfg after hold): {self.state.print_options.pileup_sensitivity} -> {cfg_pileup_sens}")
|
||||
self.state.print_options.pileup_sensitivity = cfg_pileup_sens
|
||||
del self._xcam_hold_start["pileup_sensitivity"]
|
||||
|
||||
# Clump/nozzle clumping detector (bits 11-13)
|
||||
cfg_clump, cfg_clump_sens = decode_detector(11)
|
||||
if should_accept_value("clump_detector", cfg_clump):
|
||||
if cfg_clump != self.state.print_options.nozzle_clumping_detector:
|
||||
logger.info(f"[{self.serial_number}] nozzle_clumping_detector changed (from cfg): {self.state.print_options.nozzle_clumping_detector} -> {cfg_clump}")
|
||||
self.state.print_options.nozzle_clumping_detector = cfg_clump
|
||||
# Clump sensitivity with hold timer
|
||||
if "nozzle_clumping_sensitivity" not in self._xcam_hold_start:
|
||||
if cfg_clump_sens != self.state.print_options.nozzle_clumping_sensitivity:
|
||||
logger.info(f"[{self.serial_number}] nozzle_clumping_sensitivity changed (from cfg): {self.state.print_options.nozzle_clumping_sensitivity} -> {cfg_clump_sens}")
|
||||
self.state.print_options.nozzle_clumping_sensitivity = cfg_clump_sens
|
||||
else:
|
||||
hold_start = self._xcam_hold_start["nozzle_clumping_sensitivity"]
|
||||
elapsed = current_time - hold_start
|
||||
if elapsed > self._xcam_hold_time:
|
||||
if cfg_clump_sens != self.state.print_options.nozzle_clumping_sensitivity:
|
||||
logger.info(f"[{self.serial_number}] nozzle_clumping_sensitivity synced (from cfg after hold): {self.state.print_options.nozzle_clumping_sensitivity} -> {cfg_clump_sens}")
|
||||
self.state.print_options.nozzle_clumping_sensitivity = cfg_clump_sens
|
||||
del self._xcam_hold_start["nozzle_clumping_sensitivity"]
|
||||
|
||||
# Airprint detector (bits 14-16)
|
||||
cfg_airprint, cfg_airprint_sens = decode_detector(14)
|
||||
if should_accept_value("airprint_detector", cfg_airprint):
|
||||
if cfg_airprint != self.state.print_options.airprint_detector:
|
||||
logger.info(f"[{self.serial_number}] airprint_detector changed (from cfg): {self.state.print_options.airprint_detector} -> {cfg_airprint}")
|
||||
self.state.print_options.airprint_detector = cfg_airprint
|
||||
# Airprint sensitivity with hold timer
|
||||
if "airprint_sensitivity" not in self._xcam_hold_start:
|
||||
if cfg_airprint_sens != self.state.print_options.airprint_sensitivity:
|
||||
logger.info(f"[{self.serial_number}] airprint_sensitivity changed (from cfg): {self.state.print_options.airprint_sensitivity} -> {cfg_airprint_sens}")
|
||||
self.state.print_options.airprint_sensitivity = cfg_airprint_sens
|
||||
else:
|
||||
hold_start = self._xcam_hold_start["airprint_sensitivity"]
|
||||
elapsed = current_time - hold_start
|
||||
if elapsed > self._xcam_hold_time:
|
||||
if cfg_airprint_sens != self.state.print_options.airprint_sensitivity:
|
||||
logger.info(f"[{self.serial_number}] airprint_sensitivity synced (from cfg after hold): {self.state.print_options.airprint_sensitivity} -> {cfg_airprint_sens}")
|
||||
self.state.print_options.airprint_sensitivity = cfg_airprint_sens
|
||||
del self._xcam_hold_start["airprint_sensitivity"]
|
||||
|
||||
# Camera settings
|
||||
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"
|
||||
|
||||
# Skip spaghetti_detector boolean field - we read from cfg bitmask above
|
||||
if "print_halt" in xcam_data:
|
||||
self.state.print_options.print_halt = bool(xcam_data.get("print_halt"))
|
||||
# Skip halt_print_sensitivity field - it's always stale ("medium")
|
||||
# We read the actual sensitivity from cfg bits 5-6 above
|
||||
if "first_layer_inspector" in xcam_data:
|
||||
new_value = bool(xcam_data.get("first_layer_inspector"))
|
||||
if should_accept_value("first_layer_inspector", new_value):
|
||||
self.state.print_options.first_layer_inspector = new_value
|
||||
if "printing_monitor" in xcam_data:
|
||||
new_value = bool(xcam_data.get("printing_monitor"))
|
||||
if should_accept_value("printing_monitor", new_value):
|
||||
self.state.print_options.printing_monitor = new_value
|
||||
if "buildplate_marker_detector" in xcam_data:
|
||||
new_value = bool(xcam_data.get("buildplate_marker_detector"))
|
||||
if should_accept_value("buildplate_marker_detector", new_value):
|
||||
self.state.print_options.buildplate_marker_detector = new_value
|
||||
if "allow_skip_parts" in xcam_data:
|
||||
new_value = bool(xcam_data.get("allow_skip_parts"))
|
||||
if should_accept_value("allow_skip_parts", new_value):
|
||||
self.state.print_options.allow_skip_parts = new_value
|
||||
|
||||
# Additional AI detectors - these are decoded from cfg bitmask above, not from
|
||||
# individual boolean fields (which are not sent by the printer)
|
||||
# pileup_detector, nozzle_clumping_detector, airprint_detector - from cfg
|
||||
# auto_recovery_step_loss and filament_tangle_detect - tracked locally only
|
||||
if "auto_recovery_step_loss" in xcam_data:
|
||||
self.state.print_options.auto_recovery_step_loss = bool(xcam_data.get("auto_recovery_step_loss"))
|
||||
if "filament_tangle_detect" in xcam_data:
|
||||
self.state.print_options.filament_tangle_detect = bool(xcam_data.get("filament_tangle_detect"))
|
||||
|
||||
def _handle_ams_data(self, ams_data):
|
||||
"""Handle AMS data changes for Spoolman integration.
|
||||
|
||||
@@ -380,6 +604,18 @@ class BambuMQTTClient:
|
||||
if "sdcard" in data:
|
||||
self.state.sdcard = data["sdcard"] is True
|
||||
|
||||
# Parse home_flag for "Store Sent Files on External Storage" setting (bit 11)
|
||||
if "home_flag" in data:
|
||||
home_flag = data["home_flag"]
|
||||
# Bit 11 controls "Store Sent Files on External Storage"
|
||||
# Convert to unsigned 32-bit if negative
|
||||
if home_flag < 0:
|
||||
home_flag = home_flag & 0xFFFFFFFF
|
||||
store_to_sdcard = bool((home_flag >> 11) & 1)
|
||||
if store_to_sdcard != self.state.store_to_sdcard:
|
||||
logger.info(f"[{self.serial_number}] store_to_sdcard changed: {self.state.store_to_sdcard} -> {store_to_sdcard}")
|
||||
self.state.store_to_sdcard = store_to_sdcard
|
||||
|
||||
# Parse timelapse status (recording active during print)
|
||||
if "timelapse" in data:
|
||||
logger.debug(f"[{self.serial_number}] timelapse field: {data['timelapse']}")
|
||||
@@ -651,6 +887,148 @@ class BambuMQTTClient:
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_xcam_option(
|
||||
self,
|
||||
module_name: str,
|
||||
enabled: bool,
|
||||
print_halt: bool = True,
|
||||
sensitivity: str = "medium"
|
||||
) -> bool:
|
||||
"""Set an xcam (AI detection) option on the printer.
|
||||
|
||||
Args:
|
||||
module_name: The xcam module to control (e.g., "spaghetti_detector",
|
||||
"first_layer_inspector", "printing_monitor", "buildplate_marker_detector")
|
||||
enabled: Whether to enable or disable the feature
|
||||
print_halt: Whether to halt print on detection (only applies to some detectors)
|
||||
sensitivity: Sensitivity level ("low", "medium", "high", or "never_halt")
|
||||
|
||||
Returns:
|
||||
True if command was sent, False if not connected
|
||||
"""
|
||||
if not self._client or not self.state.connected:
|
||||
return False
|
||||
|
||||
# auto_recovery_step_loss uses a different command format (print.print_option)
|
||||
if module_name == "auto_recovery_step_loss":
|
||||
return self._set_print_option("auto_recovery", enabled)
|
||||
|
||||
self._sequence_id += 1
|
||||
|
||||
# Build the xcam control command (exact OrcaSlicer format)
|
||||
# Key findings from OrcaSlicer source:
|
||||
# - Uses "xcam" wrapper (not "print")
|
||||
# - print_halt is ALWAYS true (legacy protocol requirement)
|
||||
# - Both "control" and "enable" are set to the same value
|
||||
# - halt_print_sensitivity controls actual halt behavior
|
||||
command = {
|
||||
"xcam": {
|
||||
"command": "xcam_control_set",
|
||||
"sequence_id": str(self._sequence_id),
|
||||
"module_name": module_name,
|
||||
"control": enabled,
|
||||
"enable": enabled, # old protocol compatibility
|
||||
"print_halt": True, # ALWAYS true per OrcaSlicer
|
||||
}
|
||||
}
|
||||
|
||||
# Only add sensitivity if not "never_halt"
|
||||
# OrcaSlicer uses halt_print_sensitivity for ALL detectors
|
||||
# The module_name field determines which detector's sensitivity is being set
|
||||
if sensitivity and sensitivity != "never_halt":
|
||||
command["xcam"]["halt_print_sensitivity"] = sensitivity
|
||||
|
||||
command_json = json.dumps(command)
|
||||
self._client.publish(self.topic_publish, command_json, qos=1)
|
||||
logger.info(f"[{self.serial_number}] Set xcam option: {module_name}={enabled}, sensitivity={sensitivity}")
|
||||
logger.debug(f"[{self.serial_number}] MQTT command sent: {command_json}")
|
||||
|
||||
# OrcaSlicer pattern: Set hold timer to ignore incoming data for 3 seconds
|
||||
# This prevents stale MQTT data from immediately overwriting our change
|
||||
self._xcam_hold_start[module_name] = time.time()
|
||||
|
||||
# Update local state immediately for responsive UI
|
||||
# NOTE: Spaghetti and Pileup sensitivities are linked in firmware
|
||||
# When spaghetti_detector sensitivity is changed, pileup also changes
|
||||
if module_name == "spaghetti_detector":
|
||||
self.state.print_options.spaghetti_detector = enabled
|
||||
self.state.print_options.print_halt = print_halt
|
||||
if sensitivity and sensitivity != "never_halt":
|
||||
# spaghetti_detector controls BOTH spaghetti and pileup sensitivities
|
||||
self.state.print_options.halt_print_sensitivity = sensitivity
|
||||
self.state.print_options.pileup_sensitivity = sensitivity
|
||||
self._xcam_hold_start["halt_print_sensitivity"] = time.time()
|
||||
self._xcam_hold_start["pileup_sensitivity"] = time.time()
|
||||
elif module_name == "first_layer_inspector":
|
||||
self.state.print_options.first_layer_inspector = enabled
|
||||
elif module_name == "printing_monitor":
|
||||
self.state.print_options.printing_monitor = enabled
|
||||
elif module_name == "buildplate_marker_detector":
|
||||
self.state.print_options.buildplate_marker_detector = enabled
|
||||
elif module_name == "allow_skip_parts":
|
||||
self.state.print_options.allow_skip_parts = enabled
|
||||
elif module_name == "pileup_detector":
|
||||
self.state.print_options.pileup_detector = enabled
|
||||
# Pileup sensitivity is linked to spaghetti - both are set via spaghetti_detector
|
||||
elif module_name == "clump_detector":
|
||||
self.state.print_options.nozzle_clumping_detector = enabled
|
||||
if sensitivity and sensitivity != "never_halt":
|
||||
self.state.print_options.nozzle_clumping_sensitivity = sensitivity
|
||||
self._xcam_hold_start["nozzle_clumping_sensitivity"] = time.time()
|
||||
elif module_name == "airprint_detector":
|
||||
self.state.print_options.airprint_detector = enabled
|
||||
if sensitivity and sensitivity != "never_halt":
|
||||
self.state.print_options.airprint_sensitivity = sensitivity
|
||||
self._xcam_hold_start["airprint_sensitivity"] = time.time()
|
||||
elif module_name == "auto_recovery_step_loss":
|
||||
self.state.print_options.auto_recovery_step_loss = enabled
|
||||
|
||||
return True
|
||||
|
||||
def _set_print_option(self, option_name: str, enabled: bool) -> bool:
|
||||
"""Set a print option using the print.print_option command.
|
||||
|
||||
This is different from xcam_control_set and is used for options like:
|
||||
- auto_recovery
|
||||
- air_print_detect
|
||||
- filament_tangle_detect
|
||||
- nozzle_blob_detect
|
||||
- sound_enable
|
||||
|
||||
Args:
|
||||
option_name: The option to control (e.g., "auto_recovery")
|
||||
enabled: Whether to enable or disable the option
|
||||
|
||||
Returns:
|
||||
True if command was sent, False if not connected
|
||||
"""
|
||||
if not self._client or not self.state.connected:
|
||||
return False
|
||||
|
||||
self._sequence_id += 1
|
||||
|
||||
command = {
|
||||
"print": {
|
||||
"command": "print_option",
|
||||
"sequence_id": str(self._sequence_id),
|
||||
option_name: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
command_json = json.dumps(command)
|
||||
self._client.publish(self.topic_publish, command_json, qos=1)
|
||||
logger.info(f"[{self.serial_number}] Set print option: {option_name}={enabled}")
|
||||
|
||||
# Set hold timer
|
||||
hold_key = f"print_option_{option_name}"
|
||||
self._xcam_hold_start[hold_key] = time.time()
|
||||
|
||||
# Update local state immediately
|
||||
if option_name == "auto_recovery":
|
||||
self.state.print_options.auto_recovery_step_loss = enabled
|
||||
|
||||
return True
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the printer."""
|
||||
if self._client:
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd" fill="#000" fill-rule="evenodd"><path d="m2.25 6c0-.41421.33579-.75.75-.75h3.5c.41421 0 .75.33579.75.75s-.33579.75-.75.75h-3.5c-.41421 0-.75-.33579-.75-.75zm8.5 0c0-.41421.3358-.75.75-.75h9.5c.4142 0 .75.33579.75.75s-.3358.75-.75.75h-9.5c-.4142 0-.75-.33579-.75-.75z"/><path d="m9 4.25c-.9665 0-1.75.7835-1.75 1.75s.7835 1.75 1.75 1.75 1.75-.7835 1.75-1.75-.7835-1.75-1.75-1.75zm-3.25 1.75c0-1.79493 1.45507-3.25 3.25-3.25 1.7949 0 3.25 1.45507 3.25 3.25s-1.4551 3.25-3.25 3.25c-1.79493 0-3.25-1.45507-3.25-3.25z"/><path d="m21.75 12c0-.4142-.3358-.75-.75-.75h-2c-.4142 0-.75.3358-.75.75s.3358.75.75.75h2c.4142 0 .75-.3358.75-.75zm-7 0c0-.4142-.3358-.75-.75-.75h-5c-.41421 0-.75.3358-.75.75s.33579.75.75.75h5c.4142 0 .75-.3358.75-.75zm-9 0c0-.4142-.33579-.75-.75-.75h-2c-.41421 0-.75.3358-.75.75s.33579.75.75.75h2c.41421 0 .75-.3358.75-.75z"/><path d="m16.5 10.25c.9665 0 1.75.7835 1.75 1.75s-.7835 1.75-1.75 1.75-1.75-.7835-1.75-1.75.7835-1.75 1.75-1.75zm3.25 1.75c0-1.7949-1.4551-3.25-3.25-3.25s-3.25 1.4551-3.25 3.25 1.4551 3.25 3.25 3.25 3.25-1.4551 3.25-3.25z"/><path d="m2.25 18c0-.4142.33579-.75.75-.75h5c.41421 0 .75.3358.75.75s-.33579.75-.75.75h-5c-.41421 0-.75-.3358-.75-.75zm10 0c0-.4142.3358-.75.75-.75h8c.4142 0 .75.3358.75.75s-.3358.75-.75.75h-8c-.4142 0-.75-.3358-.75-.75z"/><path d="m10.5 16.25c-.9665 0-1.75.7835-1.75 1.75s.7835 1.75 1.75 1.75 1.75-.7835 1.75-1.75-.7835-1.75-1.75-1.75zm-3.25 1.75c0-1.7949 1.45507-3.25 3.25-3.25 1.7949 0 3.25 1.4551 3.25 3.25s-1.4551 3.25-3.25 3.25c-1.79493 0-3.25-1.4551-3.25-3.25z"/></g></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512"><g id="_01_align_center" data-name="01 align center"><path d="M15,24H9V20.487a9,9,0,0,1-2.849-1.646L3.107,20.6l-3-5.2L3.15,13.645a9.1,9.1,0,0,1,0-3.29L.107,8.6l3-5.2L6.151,5.159A9,9,0,0,1,9,3.513V0h6V3.513a9,9,0,0,1,2.849,1.646L20.893,3.4l3,5.2L20.85,10.355a9.1,9.1,0,0,1,0,3.29L23.893,15.4l-3,5.2-3.044-1.758A9,9,0,0,1,15,20.487Zm-4-2h2V18.973l.751-.194A6.984,6.984,0,0,0,16.994,16.9l.543-.553,2.623,1.515,1-1.732-2.62-1.513.206-.746a7.048,7.048,0,0,0,0-3.75l-.206-.746,2.62-1.513-1-1.732L17.537,7.649,16.994,7.1a6.984,6.984,0,0,0-3.243-1.875L13,5.027V2H11V5.027l-.751.194A6.984,6.984,0,0,0,7.006,7.1l-.543.553L3.84,6.134l-1,1.732L5.46,9.379l-.206.746a7.048,7.048,0,0,0,0,3.75l.206.746L2.84,16.134l1,1.732,2.623-1.515.543.553a6.984,6.984,0,0,0,3.243,1.875l.751.194Zm1-6a4,4,0,1,1,4-4A4,4,0,0,1,12,16Zm0-6a2,2,0,1,0,2,2A2,2,0,0,0,12,10Z"/></g></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 974 B |
@@ -1 +1 @@
|
||||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m422.995 217.87c4.143 0 7.5-3.357 7.5-7.5v-192.87c0-9.649-7.851-17.5-17.5-17.5h-313.99c-9.649 0-17.5 7.851-17.5 17.5v477c0 9.649 7.851 17.5 17.5 17.5h313.99c9.649 0 17.5-7.851 17.5-17.5v-248.203c0-4.143-3.357-7.5-7.5-7.5s-7.5 3.357-7.5 7.5v52.811h-16.639v-117.615h16.639v28.877c0 4.143 3.358 7.5 7.5 7.5zm-294.851-144.77h33.028c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-33.028v-43.1h255.713v43.1h-186.759c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h186.759v22.158h-255.713zm-29.139-58.1h14.139v151.493h-16.639v-148.993c0-1.379 1.121-2.5 2.5-2.5zm-2.5 447v-15.278h16.639v15.278zm0-280.507h16.639v117.614h-16.639zm316.49 315.507h-313.99c-1.379 0-2.5-1.121-2.5-2.5v-17.5h318.99v17.5c0 1.379-1.121 2.5-2.5 2.5zm2.5-35h-16.639v-15.278h16.639zm0-147.893v117.614h-16.639v-117.614zm-31.639-15h-3.586c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h3.586v117.614h-3.586c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h3.586v15.279h-255.712v-15.278h3.586c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-3.586v-53.092c0-4.143-3.357-7.5-7.5-7.5s-7.5 3.357-7.5 7.5v53.092h-16.639v-117.615h16.639v28.596c0 4.143 3.357 7.5 7.5 7.5s7.5-3.357 7.5-7.5v-28.596h3.586c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-3.586v-117.614h3.586c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-3.586v-56.235h255.713v56.235h-3.586c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h3.586v117.614zm15-132.614v-151.493h14.139c1.379 0 2.5 1.121 2.5 2.5v148.993z"/><path d="m256 129.22c-17.53 0-31.793 14.263-31.793 31.794 0 17.53 14.263 31.793 31.793 31.793s31.793-14.263 31.793-31.793c0-17.532-14.263-31.794-31.793-31.794zm0 48.587c-9.26 0-16.793-7.533-16.793-16.793s7.533-16.794 16.793-16.794 16.793 7.534 16.793 16.794-7.533 16.793-16.793 16.793z"/><path d="m201.313 419.229c-1.407-1.406-3.314-2.196-5.304-2.196s-3.897.79-5.304 2.197l-10.465 10.466c-2.929 2.929-2.929 7.678.001 10.606 1.464 1.464 3.384 2.196 5.303 2.196s3.839-.732 5.304-2.197l5.161-5.162 5.162 5.163c2.93 2.928 7.678 2.928 10.607 0 2.929-2.93 2.929-7.678 0-10.607z"/><path d="m261.304 419.23c-1.406-1.407-3.314-2.197-5.304-2.197s-3.897.79-5.304 2.197l-10.465 10.466c-2.929 2.929-2.929 7.678.001 10.606 2.928 2.928 7.677 2.929 10.606-.001l5.161-5.161 5.161 5.161c1.465 1.465 3.384 2.197 5.304 2.197 1.919 0 3.839-.732 5.303-2.196 2.93-2.929 2.93-7.678.001-10.606z"/><path d="m321.295 419.23c-1.406-1.407-3.314-2.197-5.304-2.197s-3.896.79-5.304 2.196l-10.466 10.466c-2.929 2.93-2.929 7.678 0 10.607 2.93 2.928 7.678 2.928 10.607 0l5.162-5.163 5.161 5.162c1.465 1.465 3.384 2.197 5.304 2.197 1.919 0 3.839-.732 5.303-2.196 2.93-2.929 2.93-7.678.001-10.606z"/><path d="m213.252 44.05h85.496c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-85.496c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5z"/></g></g></svg>
|
||||
<svg id="Layer_1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m15 12h-6c-1.103 0-2 .897-2 2v4c0 1.103.897 2 2 2h6c1.103 0 2-.897 2-2v-4c0-1.103-.897-2-2-2zm1 6c0 .552-.448 1-1 1h-6c-.551 0-1-.448-1-1v-4c0-.552.449-1 1-1h6c.552 0 1 .448 1 1zm-2.5-2.5c0 .276-.224.5-.5.5h-2c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h2c.276 0 .5.224.5.5zm6-13.5h-1.5v-1.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5v1.5h-10v-1.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5v1.5h-1.5c-2.481 0-4.5 2.019-4.5 4.5v13c0 2.481 2.019 4.5 4.5 4.5h15c2.481 0 4.5-2.019 4.5-4.5v-13c0-2.481-2.019-4.5-4.5-4.5zm-15 1h15c1.93 0 3.5 1.57 3.5 3.5v1.5h-22v-1.5c0-1.93 1.57-3.5 3.5-3.5zm15 20h-15c-1.93 0-3.5-1.57-3.5-3.5v-10.5h22v10.5c0 1.93-1.57 3.5-3.5 3.5z"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 744 B |
@@ -1 +1,2 @@
|
||||
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><g id="Front_Heater" data-name="Front Heater"><path d="m33.14 29h2.86l-4-7-4 7h3l1.5 3.7a8.38 8.38 0 0 1 -.92 8l-.67.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .23-1.39 8.4 8.4 0 0 1 .21-9.32l.67-.82a10.32 10.32 0 0 0 1.22-10z"/><path d="m23.22 29h2.78l-4-7-4 7h3.06l1.5 3.7a8.35 8.35 0 0 1 -.92 8l-.67.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .24-1.39 8.4 8.4 0 0 1 .2-9.32l.67-.82a10.32 10.32 0 0 0 1.22-10z"/><path d="m57.88 20.09a3 3 0 0 0 -1.49-1.84l-7.73-4.1a35.48 35.48 0 0 0 -33.32 0l-7.72 4.1a3 3 0 0 0 -1.22 4.2l9.72 17a3 3 0 0 0 2.6 1.55 3 3 0 0 0 1.39-.34 1 1 0 0 0 .42-1.35 1 1 0 0 0 -1.34-.42 1 1 0 0 1 -1.33-.39l-9.72-17a1 1 0 0 1 -.1-.81 1 1 0 0 1 .52-.69l7.72-4.1a33.46 33.46 0 0 1 31.44 0l7.73 4.1a1 1 0 0 1 .51.63 1 1 0 0 1 -.1.81l-9.72 17a1 1 0 0 1 -1.33.39l-.2-.11a10.82 10.82 0 0 0 -.36-6.72l-1.19-3h2.94l-4-7-4 7h2.9l1.5 3.7a8.38 8.38 0 0 1 -.93 8l-.66.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .23-1.39 8.4 8.4 0 0 1 .21-9.32l.64-.78a8.27 8.27 0 0 0 .84-1.3 3 3 0 0 0 4-1.17l9.72-17a3 3 0 0 0 .32-2.44z"/></g></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" width="512" height="512" viewBox="0 0 24 24"><path d="M20.5,24c-.042,0-.086-.005-.129-.017-.267-.071-.426-.345-.354-.612,.114-.428,.243-.826,.369-1.218,.315-.98,.614-1.906,.614-3.16,0-2.766-1.029-4.801-1.927-6.265-.965-1.576-2.073-3.771-2.073-6.788,0-1.884,.772-4.482,1.294-5.646,.113-.252,.409-.365,.661-.251,.252,.113,.364,.409,.251,.661-.421,.938-1.206,3.436-1.206,5.236,0,2.766,1.029,4.801,1.927,6.265,.965,1.576,2.073,3.771,2.073,6.788,0,1.411-.337,2.456-.663,3.467-.121,.375-.244,.758-.354,1.168-.06,.224-.262,.371-.483,.371Zm-7,0c-.042,0-.086-.005-.129-.017-.267-.071-.426-.345-.354-.612,.114-.428,.243-.826,.369-1.218,.315-.98,.614-1.906,.614-3.16,0-2.766-1.029-4.801-1.927-6.265-.965-1.576-2.073-3.771-2.073-6.788,0-1.884,.772-4.482,1.294-5.646,.112-.252,.409-.365,.661-.251,.252,.113,.364,.409,.251,.661-.421,.938-1.206,3.436-1.206,5.236,0,2.766,1.029,4.801,1.927,6.265,.965,1.576,2.073,3.771,2.073,6.788,0,1.411-.337,2.456-.663,3.467-.121,.375-.244,.758-.354,1.168-.06,.224-.262,.371-.483,.371Zm-7,0c-.042,0-.086-.005-.129-.017-.267-.071-.426-.345-.354-.612,.114-.428,.243-.826,.369-1.218,.315-.98,.614-1.906,.614-3.16,0-2.766-1.029-4.801-1.927-6.265-.965-1.576-2.073-3.771-2.073-6.788,0-1.884,.772-4.482,1.294-5.646,.112-.252,.41-.365,.661-.251,.252,.113,.364,.409,.251,.661-.421,.938-1.206,3.436-1.206,5.236,0,2.766,1.029,4.801,1.927,6.265,.965,1.576,2.073,3.771,2.073,6.788,0,1.411-.337,2.456-.663,3.467-.121,.375-.244,.758-.354,1.168-.06,.224-.262,.371-.483,.371Z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1 +1,4 @@
|
||||
<svg id="Ecommerce" enable-background="new 0 0 48 48" height="512" viewBox="0 0 48 48" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m25 7.03c0-.552-.447-1-1-1-5.514 0-10 4.486-10 10 0 .552.447 1 1 1s1-.448 1-1c0-4.411 3.589-8 8-8 .553 0 1-.448 1-1z"/><path d="m22.246 45.79h1.754 1.754c1.652 0 2.999-1.345 3-3v-1.099c1.032-.475 1.755-1.512 1.755-2.721v-3.72c.176-.382.281-.802.281-1.25 0-1.016.226-2 .671-2.927.441-.919 1.086-1.752 1.864-2.409 3.746-3.165 5.709-7.989 5.25-12.909-.7-7.375-6.813-13.189-14.265-13.525l-.31-.01-.354.011c-7.408.335-13.521 6.149-14.222 13.526-.458 4.917 1.505 9.742 5.251 12.906.778.658 1.423 1.491 1.864 2.41.445.927.671 1.911.671 2.927 0 .447.105.868.281 1.25v3.721c0 1.209.722 2.247 1.755 2.721v1.1c.001 1.653 1.348 2.998 3 2.998zm4.508-3c0 .552-.449 1-1 1h-1.754-1.754c-.551 0-1-.449-1-1v-.82h2.754 2.754zm1.755-3.819c0 .551-.448 1-1 1h-3.509-3.509c-.552 0-1-.449-1-1v-2.068c.232.058.47.097.719.097h3.79 3.79c.249 0 .487-.039.719-.097zm-9.299-4.971c0-1.318-.292-2.595-.868-3.793-.563-1.171-1.385-2.233-2.376-3.071-3.247-2.742-4.948-6.926-4.551-11.191.607-6.389 5.903-11.426 12.275-11.715l.31-.01.265.009c6.417.29 11.713 5.327 12.319 11.714.398 4.267-1.303 8.451-4.55 11.193-.991.838-1.813 1.9-2.376 3.071-.576 1.198-.868 2.475-.868 3.793 0 .551-.448 1-1 1h-3.79-3.79c-.552 0-1-.449-1-1z"/></g></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
|
||||
<path d="m6.443,4.08L4.304.567,5.157.048l2.14,3.513-.854.52Zm13.557,7.92c0,2.323-1.01,4.528-2.771,6.051-.781.674-1.229,1.641-1.229,2.653v3.296h-8v-3.295c0-1.007-.456-1.982-1.252-2.675-2.062-1.796-3.058-4.497-2.661-7.227.512-3.521,3.457-6.36,7.003-6.753,2.307-.256,4.527.45,6.245,1.987,1.693,1.517,2.665,3.689,2.665,5.962Zm-5,8.704c0-.239.04-.471.077-.704h-6.156c.038.233.078.467.078.705v2.295h6v-2.296Zm4-8.704c0-1.988-.85-3.89-2.332-5.217-1.502-1.344-3.438-1.964-5.469-1.738-3.1.343-5.675,2.825-6.122,5.903-.348,2.391.522,4.757,2.327,6.328.553.481.963,1.076,1.234,1.724h2.861v-5.551c-1.14-.232-2-1.242-2-2.449h1c0,.827.673,1.5,1.5,1.5s1.5-.673,1.5-1.5h1c0,1.208-.86,2.217-2,2.449v5.551h2.856c.268-.645.672-1.234,1.218-1.706,1.542-1.332,2.426-3.262,2.426-5.294Zm.696-11.433l-.854-.52-2.14,3.513.854.52,2.14-3.513Zm3.86,4.342l-3.536,1.597.412.912,3.536-1.597-.412-.912ZM.031,5.821l3.536,1.597.412-.912L.443,4.909l-.412.912Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1 +1,4 @@
|
||||
<svg id="Layer_1" height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m36 39.47v-32.47a7 7 0 1 0 -14 0v32.47a13.26 13.26 0 1 0 14 0zm-7 22.59a11.32 11.32 0 0 1 -5.53-21.19 1 1 0 0 0 .53-.87v-33a5 5 0 1 1 10 0v33a1 1 0 0 0 .49.84 11.32 11.32 0 0 1 -5.49 21.22z"/><path d="m30 44.89v-30.25a1 1 0 1 0 -1.94 0v30.25a5.94 5.94 0 1 0 1.94 0zm-1 9.85a4 4 0 1 1 4-4 4 4 0 0 1 -4 4z"/><path d="m40.32 9.64h7a1 1 0 0 0 0-1.94h-7a1 1 0 1 0 0 1.94z"/><path d="m40.32 16.06h4.06a1 1 0 0 0 0-1.94h-4.06a1 1 0 0 0 0 1.94z"/><path d="m47.29 20.55h-7a1 1 0 1 0 0 1.93h7a1 1 0 1 0 0-1.93z"/><path d="m40.32 28.91h4.06a1 1 0 0 0 0-1.94h-4.06a1 1 0 0 0 0 1.94z"/><path d="m47.29 33.39h-7a1 1 0 0 0 0 1.94h7a1 1 0 1 0 0-1.94z"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
|
||||
<path d="m12.5,15.051V5h-1v10.051c-1.14.232-2,1.242-2,2.449,0,1.379,1.121,2.5,2.5,2.5s2.5-1.121,2.5-2.5c0-1.208-.86-2.217-2-2.449Zm-.5,3.949c-.827,0-1.5-.673-1.5-1.5s.673-1.5,1.5-1.5,1.5.673,1.5,1.5-.673,1.5-1.5,1.5Zm4.5-6.181V4.5c0-2.481-2.019-4.5-4.5-4.5s-4.5,2.019-4.5,4.5v8.319c-1.627,1.561-2.32,3.805-1.859,6.049.508,2.472,2.506,4.476,4.972,4.987.459.096.92.143,1.376.143,1.495,0,2.942-.503,4.111-1.454,1.525-1.241,2.4-3.08,2.4-5.044,0-1.763-.727-3.456-2-4.681Zm-1.031,8.949c-1.292,1.05-2.989,1.454-4.653,1.108-2.081-.432-3.767-2.124-4.194-4.21-.405-1.968.235-3.933,1.713-5.258l.166-.148V4.5c0-1.93,1.57-3.5,3.5-3.5s3.5,1.57,3.5,3.5v8.761l.166.148c1.166,1.046,1.834,2.537,1.834,4.091,0,1.662-.74,3.218-2.031,4.269Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 770 B After Width: | Height: | Size: 865 B |
@@ -1 +1,4 @@
|
||||
<svg id="layer_1" enable-background="new 0 0 200 200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><g fill="rgb(0,0,0)"><path d="m61.3366 198.7365c-3.8893 0-7.7945-1.0847-11.3086-3.2781-32.6418-20.3714-44.9485-46.703-49.5247-65.2053-6.1066-30.8373 44.9333-36.4535 67.8505-35.056 1.761-6.9634 6.1301-13.08 12.1249-17.0214-9.0714-12.1387-21.2458-19.7867-36.2543-22.7586-19.3391-3.2847-23.4263-31.1087-5.8545-39.8477 33.3392-19.0024 97.1952-22.5083 96.3704 10.811-.6835 19.2868-6.9715 37.8308-18.7038 55.1859 5.1612 4.9953 8.292 11.7803 8.6884 19.0147 15.0429-1.7888 27.7494-8.5077 37.8247-20.0165 6.0052-6.8593 15.3868-9.1156 23.9022-5.7478 8.5361 3.3773 13.8491 11.4631 13.535 20.6005-1.3216 38.4536-17.9721 62.2774-31.708 75.4917-23.6412 20.7064-54.0319-20.679-64.2755-41.2153-6.816 1.9769-14.557 1.2532-20.8202-1.9875-5.969 13.9198-6.5027 28.2804-1.574 42.7574 5.0949 13.3396-6.2694 28.6219-20.2725 28.273zm-1.6188-94.5681c-14.1176.1632-54.2272 5.0317-50.2069 23.856 4.1646 16.8367 15.4261 40.8368 45.43 59.562 5.3197 3.3207 10.6838 1.7539 13.9293-.8201 3.2305-2.5623 5.9658-7.4048 3.9546-13.3126-6.3202-18.5652-4.873-36.9425 4.3011-54.6216.6379-1.2292 1.7915-2.1091 3.1458-2.3995 1.3551-.2882 2.7674.0404 3.8526.8993 3.4512 2.733 7.6019 4.1775 12.0037 4.1775 4.9261.8516 11.3607-5.6527 14.5071.4457 6.0281 15.2396 35.1102 56.5264 51.2108 42.2675 12.4987-12.025 27.6526-33.7782 28.8673-69.1245.0159-11.6249-13.2308-16.8886-21.1812-8.4221-12.9177 14.7553-29.5546 22.6907-49.4492 23.5865-1.3715.0602-2.7195-.4957-3.6482-1.5215-1.8205-1.8024-.9363-4.3611-.9455-6.5928-.0001-6.3459-3.1223-12.2987-8.3528-15.9233-1.0257-.7109-1.7221-1.8051-1.931-3.0357-.2093-1.2306.0861-2.4938.8196-3.5037 10.2153-12.9246 31.3669-58.5212 11.0023-65.4878-16.6655-4.8123-43.08-7.0587-74.2968 9.5628-10.0753 5.7972-8.0115 19.9044 3.2961 22.5543 19.2372 3.809 34.4279 14.2491 45.1507 31.0306 1.5812 2.3482.4574 5.827-2.1953 6.8094-6.7913 2.7009-11.5582 9.1434-12.144 16.4131-.1006 1.2455-.6991 2.3982-1.6611 3.1966-4.5407 2.4665-10.5485-.3007-15.459.4039z"/><path d="m96.1279 112.2302c-7.607 1.0321-15.4151-13.051-5.4426-14.7224 2.5623.0007 4.6397 2.0781 4.6397 4.6403 0 .4354.3675.8028.8029.8028 6.0654.0249 6.0675 9.2545 0 9.2793z"/><path d="m9.2329 99.228c-12.7712-.764 4.8333-40.6245 9.5289-46.5697 3.292-5.0803 11.0691-.1194 7.8098 5.0118-7.1818 11.1897-11.5808 24.0937-12.7217 37.3166-.2085 2.4172-2.2352 4.2413-4.617 4.2413z"/><path d="m39.2577 89.6348c-11.0543-1.6919 1.3997-18.9153 4.3572-24.1282 3.4934-4.9529 11.064.3241 7.6063 5.3167-3.325 4.7567-5.8564 9.9749-7.5229 15.5084-.6044 2.0077-2.4471 3.3031-4.4406 3.3031z"/><path d="m96.2071 193.9939c-3.6525.2417-8.8695-.1084-8.6734-4.9527.12-2.5591 2.2921-4.5182 4.8525-4.4162 13.2881.6302 26.6579-2.0181 38.6783-7.6414 5.4914-2.5456 9.4266 5.808 3.9316 8.4049-12.0948 5.6586-25.4224 8.6054-38.789 8.6054z"/><path d="m96.194 166.1857c-3.9048.076-10.1254.1951-9.7645-5.2634.2215-2.5524 2.4715-4.439 5.0229-4.2215 5.7823.502 11.5673.0847 17.1924-1.2392 2.4947-.584 4.9917.9597 5.5789 3.4539.8588 8.4162-12.5193 6.4771-18.0297 7.2702z"/><path d="m173.5515 66.6619c-1.6806 0-3.3021-.9157-4.1263-2.512-6.0995-11.8142-15.0751-22.0758-25.9565-29.6754-4.9499-3.4817.3164-11.067 5.3132-7.6072 6.2137 3.1209 41.191 37.1803 24.7696 39.7946z"/><path d="m149.9162 81.597c-1.7462 0-3.4186-.99-4.2065-2.6759-2.4571-5.2576-5.7103-10.0585-9.6698-14.27-4.1842-4.3187 2.6992-10.8071 6.7603-6.3556 3.7145 4.588 19.1197 21.1144 7.116 23.3015z"/></g></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
|
||||
<path d="m22.98,10.346c-.721-.804-1.714-1.278-2.798-1.336-1.661-.081-4.16.444-5.969.983-.179-.197-.378-.371-.603-.515.164-.177.324-.352.494-.533,1.375-1.456,2.797-2.96,2.89-4.691C17.113,2.032,15.42.126,13.219.006c-1.067-.062-2.08.302-2.872,1.013-.804.721-1.278,1.715-1.336,2.798-.09,1.676.443,4.161.982,5.969-.197.179-.371.378-.515.603-.178-.164-.354-.325-.534-.495-1.455-1.374-2.96-2.795-4.69-2.888C2.054,6.892.127,8.581.006,10.781c-.059,1.06.302,2.08,1.014,2.873.721.804,1.714,1.278,2.798,1.336.125.006.255.01.389.01,1.655,0,3.907-.494,5.58-.993.178.197.378.371.603.515-.164.177-.324.352-.494.533-1.375,1.456-2.796,2.96-2.889,4.691-.119,2.222,1.574,4.127,3.775,4.247.074.004.147.006.221.006.979,0,1.914-.358,2.651-1.02.804-.721,1.278-1.714,1.336-2.798.09-1.676-.443-4.161-.982-5.969.197-.179.371-.378.515-.603.178.164.354.325.534.495,1.455,1.374,2.96,2.795,4.69,2.888.073.004.146.006.22.006,2.125,0,3.91-1.654,4.027-3.781.059-1.06-.302-2.08-1.014-2.873Zm-10.98,3.654c-1.103,0-2-.897-2-2s.897-2,2-2,2,.897,2,2-.897,2-2,2Zm-1.991-10.129c.044-.817.401-1.565,1.006-2.108.595-.533,1.36-.801,2.149-.759,1.651.09,2.921,1.524,2.832,3.195-.073,1.365-1.367,2.734-2.618,4.059-.258.274-.51.543-.751.81-.202-.043-.412-.068-.627-.068-.4,0-.78.082-1.129.224-.468-1.64-.941-3.886-.863-5.353ZM3.87,13.991c-.816-.043-1.564-.4-2.106-1.005-.533-.594-.803-1.358-.76-2.15.091-1.651,1.554-2.924,3.196-2.831,1.364.073,2.732,1.365,4.057,2.616.274.259.544.511.811.753-.043.203-.068.412-.068.627,0,.4.082.78.224,1.129-1.64.469-3.883.943-5.354.863Zm10.121,6.138c-.044.816-.401,1.565-1.006,2.107-.593.533-1.359.801-2.149.759-1.651-.09-2.921-1.524-2.832-3.195.073-1.365,1.367-2.734,2.618-4.058.258-.275.51-.544.751-.81.202.043.412.068.627.068.4,0,.78-.082,1.129-.224.468,1.64.941,3.886.863,5.353Zm5.809-4.134c-1.364-.073-2.732-1.365-4.057-2.616-.274-.259-.544-.511-.811-.753.043-.202.068-.412.068-.627,0-.4-.082-.78-.224-1.129,1.64-.469,3.886-.938,5.354-.862.816.043,1.564.4,2.106,1.005.533.594.803,1.358.76,2.15-.091,1.651-1.53,2.932-3.196,2.831Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -61,6 +61,26 @@ export interface NozzleInfo {
|
||||
nozzle_diameter: string; // e.g., "0.4"
|
||||
}
|
||||
|
||||
export interface PrintOptions {
|
||||
// Core AI detectors
|
||||
spaghetti_detector: boolean;
|
||||
print_halt: boolean;
|
||||
halt_print_sensitivity: string; // "low", "medium", "high" - spaghetti sensitivity
|
||||
first_layer_inspector: boolean;
|
||||
printing_monitor: boolean;
|
||||
buildplate_marker_detector: boolean;
|
||||
allow_skip_parts: boolean;
|
||||
// Additional AI detectors (decoded from cfg bitmask)
|
||||
nozzle_clumping_detector: boolean;
|
||||
nozzle_clumping_sensitivity: string; // "low", "medium", "high"
|
||||
pileup_detector: boolean;
|
||||
pileup_sensitivity: string; // "low", "medium", "high"
|
||||
airprint_detector: boolean;
|
||||
airprint_sensitivity: string; // "low", "medium", "high"
|
||||
auto_recovery_step_loss: boolean;
|
||||
filament_tangle_detect: boolean;
|
||||
}
|
||||
|
||||
export interface PrinterStatus {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -86,9 +106,11 @@ export interface PrinterStatus {
|
||||
ams_exists: boolean;
|
||||
vt_tray: AMSTray | null; // Virtual tray / external spool
|
||||
sdcard: boolean; // SD card inserted
|
||||
store_to_sdcard: boolean; // Store sent files on SD card
|
||||
timelapse: boolean; // Timelapse recording active
|
||||
ipcam: boolean; // Live view enabled
|
||||
nozzles: NozzleInfo[]; // Nozzle hardware info (index 0=left/primary, 1=right)
|
||||
print_options: PrintOptions | null; // AI detection and print options
|
||||
}
|
||||
|
||||
export interface PrinterCreate {
|
||||
@@ -1188,4 +1210,29 @@ export const api = {
|
||||
request<ControlResponse>(`/printers/${printerId}/control/refresh`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// Print Options (AI Detection)
|
||||
setPrintOption: (
|
||||
printerId: number,
|
||||
moduleName: string,
|
||||
enabled: boolean,
|
||||
printHalt = true,
|
||||
sensitivity = 'medium'
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
module_name: moduleName,
|
||||
enabled: String(enabled),
|
||||
print_halt: String(printHalt),
|
||||
sensitivity,
|
||||
});
|
||||
return request<{
|
||||
success: boolean;
|
||||
module_name: string;
|
||||
enabled: boolean;
|
||||
print_halt: boolean;
|
||||
sensitivity: string;
|
||||
}>(`/printers/${printerId}/print-options?${params}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '../../api/client';
|
||||
import type { Printer, PrinterStatus } from '../../api/client';
|
||||
import { X, Loader2, AlertTriangle, ChevronDown } from 'lucide-react';
|
||||
import { Card, CardContent } from '../Card';
|
||||
|
||||
interface PrintOptionsModalProps {
|
||||
printer: Printer;
|
||||
status: PrinterStatus | null | undefined;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Checkbox component matching Bambu Studio style
|
||||
function Checkbox({
|
||||
checked,
|
||||
onChange,
|
||||
disabled,
|
||||
loading,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => !disabled && !loading && onChange(!checked)}
|
||||
disabled={disabled || loading}
|
||||
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
|
||||
checked
|
||||
? 'bg-bambu-green border-bambu-green'
|
||||
: 'bg-transparent border-bambu-gray'
|
||||
} ${disabled || loading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin text-white" />
|
||||
) : checked ? (
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Sensitivity dropdown component
|
||||
function SensitivityDropdown({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
loading,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const options = ['Low', 'Medium', 'High'];
|
||||
|
||||
const displayValue = value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => !isDisabled && setIsOpen(!isOpen)}
|
||||
disabled={isDisabled}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-sm text-white min-w-[100px] justify-between ${
|
||||
isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:border-bambu-gray'
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin text-bambu-green" />
|
||||
) : (
|
||||
<span>{displayValue}</span>
|
||||
)}
|
||||
<ChevronDown className="w-4 h-4 text-bambu-gray" />
|
||||
</button>
|
||||
{isOpen && !isDisabled && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
|
||||
<div className="absolute top-full left-0 mt-1 w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded shadow-lg z-20">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
onClick={() => {
|
||||
onChange(option.toLowerCase());
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
|
||||
value.toLowerCase() === option.toLowerCase() ? 'text-bambu-green' : 'text-white'
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PrintOptionsModal({ printer, status, onClose }: PrintOptionsModalProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const printOptions = status?.print_options;
|
||||
const isConnected = status?.connected ?? false;
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
// Set print option mutation - track both UI and API module names
|
||||
const setOptionMutation = useMutation({
|
||||
mutationFn: ({
|
||||
apiModuleName,
|
||||
enabled,
|
||||
printHalt,
|
||||
sensitivity,
|
||||
}: {
|
||||
uiModuleName: string; // The module name from the UI (for loading state) - accessed via variables
|
||||
apiModuleName: string; // The module name sent to API (may differ due to firmware quirk)
|
||||
enabled: boolean;
|
||||
printHalt?: boolean;
|
||||
sensitivity?: string;
|
||||
}) => api.setPrintOption(printer.id, apiModuleName, enabled, printHalt, sensitivity),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['printerStatuses'] });
|
||||
},
|
||||
});
|
||||
|
||||
const handleToggle = (moduleName: string, enabled: boolean, sensitivity?: string) => {
|
||||
const printHalt = enabled && sensitivity !== undefined && sensitivity !== 'never_halt';
|
||||
setOptionMutation.mutate({
|
||||
uiModuleName: moduleName,
|
||||
apiModuleName: moduleName,
|
||||
enabled,
|
||||
printHalt,
|
||||
sensitivity: sensitivity ?? 'medium'
|
||||
});
|
||||
};
|
||||
|
||||
const handleSensitivityChange = (moduleName: string, sensitivity: string) => {
|
||||
// Spaghetti and Pileup sensitivities are linked in firmware
|
||||
// For pileup, we send spaghetti_detector to set the shared sensitivity
|
||||
// All other detectors use their own module name
|
||||
let apiModuleName = moduleName;
|
||||
if (moduleName === 'pileup_detector') {
|
||||
// Pileup sensitivity is linked to spaghetti - use spaghetti_detector
|
||||
apiModuleName = 'spaghetti_detector';
|
||||
}
|
||||
|
||||
setOptionMutation.mutate({
|
||||
uiModuleName: moduleName,
|
||||
apiModuleName,
|
||||
enabled: true,
|
||||
printHalt: true,
|
||||
sensitivity
|
||||
});
|
||||
};
|
||||
|
||||
// Check loading state using the UI module name (not API module name)
|
||||
const isToggling = (moduleName: string) =>
|
||||
setOptionMutation.isPending && setOptionMutation.variables?.uiModuleName === moduleName;
|
||||
|
||||
// Check if sensitivity is being changed for a specific detector
|
||||
// This also returns true for linked detectors (e.g., spaghetti loading shows for pileup too)
|
||||
const isSensitivityLoading = (moduleName: string) => {
|
||||
if (!setOptionMutation.isPending || !setOptionMutation.variables?.sensitivity) return false;
|
||||
const uiModule = setOptionMutation.variables.uiModuleName;
|
||||
// Direct match
|
||||
if (uiModule === moduleName) return true;
|
||||
// Linked: spaghetti and pileup share sensitivity
|
||||
if ((uiModule === 'spaghetti_detector' && moduleName === 'pileup_detector') ||
|
||||
(uiModule === 'pileup_detector' && moduleName === 'spaghetti_detector')) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Card className="w-full max-w-xl max-h-[90vh] flex flex-col" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<CardContent className="p-0 flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-bambu-dark-tertiary">
|
||||
<span className="text-sm font-medium text-white">Print Options</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content - Scrollable */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{!isConnected && (
|
||||
<div className="flex items-center gap-2 p-3 bg-red-500/20 border border-red-500/50 rounded text-red-400">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span className="text-sm">Printer not connected. Options cannot be changed.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!printOptions ? (
|
||||
<div className="flex items-center gap-2 text-bambu-gray">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span className="text-sm">Print options not available. Try refreshing.</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* AI Detections Section */}
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-white mb-1">AI Detections</h3>
|
||||
<p className="text-xs text-bambu-gray mb-4">
|
||||
Printer will send assistant message or pause printing if any of the following problem is detected.
|
||||
</p>
|
||||
<div className="space-y-5">
|
||||
{/* Spaghetti Detection */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={printOptions.spaghetti_detector}
|
||||
onChange={(checked) => handleToggle('spaghetti_detector', checked, printOptions.halt_print_sensitivity)}
|
||||
disabled={!isConnected}
|
||||
loading={isToggling('spaghetti_detector')}
|
||||
/>
|
||||
<span className="text-sm font-medium text-white">Spaghetti Detection</span>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray ml-8">
|
||||
Detects spaghetti failure (scattered loose filament).
|
||||
</p>
|
||||
<div className="flex items-center gap-3 ml-8">
|
||||
<span className="text-xs text-bambu-gray">Pausing Sensitivity:</span>
|
||||
<SensitivityDropdown
|
||||
value={printOptions.halt_print_sensitivity || 'medium'}
|
||||
onChange={(value) => handleSensitivityChange('spaghetti_detector', value)}
|
||||
disabled={!isConnected || !printOptions.spaghetti_detector}
|
||||
loading={isSensitivityLoading('spaghetti_detector')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Purge Chute Pile-Up Detection */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={printOptions.pileup_detector}
|
||||
onChange={(checked) => handleToggle('pileup_detector', checked, printOptions.pileup_sensitivity)}
|
||||
disabled={!isConnected}
|
||||
loading={isToggling('pileup_detector')}
|
||||
/>
|
||||
<span className="text-sm font-medium text-white">Purge Chute Pile-Up Detection</span>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray ml-8">
|
||||
Monitors if the waste is piled up in the purge chute.
|
||||
</p>
|
||||
<div className="flex items-center gap-3 ml-8">
|
||||
<span className="text-xs text-bambu-gray">Pausing Sensitivity:</span>
|
||||
<SensitivityDropdown
|
||||
value={printOptions.pileup_sensitivity || 'medium'}
|
||||
onChange={(value) => handleSensitivityChange('pileup_detector', value)}
|
||||
disabled={!isConnected || !printOptions.pileup_detector}
|
||||
loading={isSensitivityLoading('pileup_detector')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nozzle Clumping Detection */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={printOptions.nozzle_clumping_detector}
|
||||
onChange={(checked) => handleToggle('clump_detector', checked, printOptions.nozzle_clumping_sensitivity)}
|
||||
disabled={!isConnected}
|
||||
loading={isToggling('clump_detector')}
|
||||
/>
|
||||
<span className="text-sm font-medium text-white">Nozzle Clumping Detection</span>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray ml-8">
|
||||
Checks if the nozzle is clumping by filaments or other foreign objects.
|
||||
</p>
|
||||
<div className="flex items-center gap-3 ml-8">
|
||||
<span className="text-xs text-bambu-gray">Pausing Sensitivity:</span>
|
||||
<SensitivityDropdown
|
||||
value={printOptions.nozzle_clumping_sensitivity || 'medium'}
|
||||
onChange={(value) => handleSensitivityChange('clump_detector', value)}
|
||||
disabled={!isConnected || !printOptions.nozzle_clumping_detector}
|
||||
loading={isSensitivityLoading('clump_detector')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Air Printing Detection */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={printOptions.airprint_detector}
|
||||
onChange={(checked) => handleToggle('airprint_detector', checked, printOptions.airprint_sensitivity)}
|
||||
disabled={!isConnected}
|
||||
loading={isToggling('airprint_detector')}
|
||||
/>
|
||||
<span className="text-sm font-medium text-white">Air Printing Detection</span>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray ml-8">
|
||||
Detects air printing caused by nozzle clogging or filament grinding.
|
||||
</p>
|
||||
<div className="flex items-center gap-3 ml-8">
|
||||
<span className="text-xs text-bambu-gray">Pausing Sensitivity:</span>
|
||||
<SensitivityDropdown
|
||||
value={printOptions.airprint_sensitivity || 'medium'}
|
||||
onChange={(value) => handleSensitivityChange('airprint_detector', value)}
|
||||
disabled={!isConnected || !printOptions.airprint_detector}
|
||||
loading={isSensitivityLoading('airprint_detector')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-bambu-dark-tertiary" />
|
||||
|
||||
{/* Build Plate Detection Section */}
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-white mb-1">Build Plate Detection</h3>
|
||||
<p className="text-xs text-bambu-gray mb-4">
|
||||
Ensures the build plate type and placement are correct.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={printOptions.buildplate_marker_detector}
|
||||
onChange={(checked) => handleToggle('buildplate_marker_detector', checked)}
|
||||
disabled={!isConnected}
|
||||
loading={isToggling('buildplate_marker_detector')}
|
||||
/>
|
||||
<span className="text-sm font-medium text-white">Type Detection</span>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray ml-8">
|
||||
Pauses printing when the detected build plate type does not match the selected one.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-bambu-dark-tertiary" />
|
||||
|
||||
{/* Other Options */}
|
||||
<div className="space-y-5">
|
||||
{/* Auto-recover from step loss */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={printOptions.auto_recovery_step_loss}
|
||||
onChange={(checked) => handleToggle('auto_recovery_step_loss', checked)}
|
||||
disabled={!isConnected}
|
||||
loading={isToggling('auto_recovery_step_loss')}
|
||||
/>
|
||||
<span className="text-sm font-medium text-white">Auto-recover from step loss</span>
|
||||
</div>
|
||||
|
||||
{/* Store Sent Files on External Storage - Read-only (no MQTT command available) */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={status?.store_to_sdcard ?? false}
|
||||
onChange={() => {}}
|
||||
disabled={true}
|
||||
/>
|
||||
<span className="text-sm font-medium text-white">Store Sent Files on External Storage</span>
|
||||
<span className="text-xs text-bambu-gray italic">(read-only)</span>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray ml-8">
|
||||
Save the printing files initiated from Bambu Studio, Bambu Handy and MakerWorld on External Storage.
|
||||
This option can only be changed on the printer's touchscreen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { ExtruderControls } from '../components/control/ExtruderControls';
|
||||
import { AMSSectionDual } from '../components/control/AMSSectionDual';
|
||||
import { CameraSettingsModal } from '../components/control/CameraSettingsModal';
|
||||
import { PrinterPartsModal } from '../components/control/PrinterPartsModal';
|
||||
import { PrintOptionsModal } from '../components/control/PrintOptionsModal';
|
||||
import { Loader2, WifiOff, Video, Webcam, Settings } from 'lucide-react';
|
||||
|
||||
export function ControlPage() {
|
||||
@@ -19,6 +20,7 @@ export function ControlPage() {
|
||||
const [selectedPrinterId, setSelectedPrinterId] = useState<number | null>(null);
|
||||
const [showCameraSettings, setShowCameraSettings] = useState(false);
|
||||
const [showPrinterParts, setShowPrinterParts] = useState(false);
|
||||
const [showPrintOptions, setShowPrintOptions] = useState(false);
|
||||
|
||||
// Fetch all printers
|
||||
const { data: printers, isLoading: loadingPrinters } = useQuery({
|
||||
@@ -178,7 +180,10 @@ export function ControlPage() {
|
||||
>
|
||||
Printer Parts
|
||||
</button>
|
||||
<button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
|
||||
<button
|
||||
onClick={() => setShowPrintOptions(true)}
|
||||
className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark"
|
||||
>
|
||||
Print Options
|
||||
</button>
|
||||
<button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
|
||||
@@ -261,6 +266,15 @@ export function ControlPage() {
|
||||
onClose={() => setShowPrinterParts(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Print Options Modal */}
|
||||
{showPrintOptions && selectedPrinter && (
|
||||
<PrintOptionsModal
|
||||
printer={selectedPrinter}
|
||||
status={selectedStatus}
|
||||
onClose={() => setShowPrintOptions(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd" fill="#000" fill-rule="evenodd"><path d="m2.25 6c0-.41421.33579-.75.75-.75h3.5c.41421 0 .75.33579.75.75s-.33579.75-.75.75h-3.5c-.41421 0-.75-.33579-.75-.75zm8.5 0c0-.41421.3358-.75.75-.75h9.5c.4142 0 .75.33579.75.75s-.3358.75-.75.75h-9.5c-.4142 0-.75-.33579-.75-.75z"/><path d="m9 4.25c-.9665 0-1.75.7835-1.75 1.75s.7835 1.75 1.75 1.75 1.75-.7835 1.75-1.75-.7835-1.75-1.75-1.75zm-3.25 1.75c0-1.79493 1.45507-3.25 3.25-3.25 1.7949 0 3.25 1.45507 3.25 3.25s-1.4551 3.25-3.25 3.25c-1.79493 0-3.25-1.45507-3.25-3.25z"/><path d="m21.75 12c0-.4142-.3358-.75-.75-.75h-2c-.4142 0-.75.3358-.75.75s.3358.75.75.75h2c.4142 0 .75-.3358.75-.75zm-7 0c0-.4142-.3358-.75-.75-.75h-5c-.41421 0-.75.3358-.75.75s.33579.75.75.75h5c.4142 0 .75-.3358.75-.75zm-9 0c0-.4142-.33579-.75-.75-.75h-2c-.41421 0-.75.3358-.75.75s.33579.75.75.75h2c.41421 0 .75-.3358.75-.75z"/><path d="m16.5 10.25c.9665 0 1.75.7835 1.75 1.75s-.7835 1.75-1.75 1.75-1.75-.7835-1.75-1.75.7835-1.75 1.75-1.75zm3.25 1.75c0-1.7949-1.4551-3.25-3.25-3.25s-3.25 1.4551-3.25 3.25 1.4551 3.25 3.25 3.25 3.25-1.4551 3.25-3.25z"/><path d="m2.25 18c0-.4142.33579-.75.75-.75h5c.41421 0 .75.3358.75.75s-.33579.75-.75.75h-5c-.41421 0-.75-.3358-.75-.75zm10 0c0-.4142.3358-.75.75-.75h8c.4142 0 .75.3358.75.75s-.3358.75-.75.75h-8c-.4142 0-.75-.3358-.75-.75z"/><path d="m10.5 16.25c-.9665 0-1.75.7835-1.75 1.75s.7835 1.75 1.75 1.75 1.75-.7835 1.75-1.75-.7835-1.75-1.75-1.75zm-3.25 1.75c0-1.7949 1.45507-3.25 3.25-3.25 1.7949 0 3.25 1.4551 3.25 3.25s-1.4551 3.25-3.25 3.25c-1.79493 0-3.25-1.4551-3.25-3.25z"/></g></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512"><g id="_01_align_center" data-name="01 align center"><path d="M15,24H9V20.487a9,9,0,0,1-2.849-1.646L3.107,20.6l-3-5.2L3.15,13.645a9.1,9.1,0,0,1,0-3.29L.107,8.6l3-5.2L6.151,5.159A9,9,0,0,1,9,3.513V0h6V3.513a9,9,0,0,1,2.849,1.646L20.893,3.4l3,5.2L20.85,10.355a9.1,9.1,0,0,1,0,3.29L23.893,15.4l-3,5.2-3.044-1.758A9,9,0,0,1,15,20.487Zm-4-2h2V18.973l.751-.194A6.984,6.984,0,0,0,16.994,16.9l.543-.553,2.623,1.515,1-1.732-2.62-1.513.206-.746a7.048,7.048,0,0,0,0-3.75l-.206-.746,2.62-1.513-1-1.732L17.537,7.649,16.994,7.1a6.984,6.984,0,0,0-3.243-1.875L13,5.027V2H11V5.027l-.751.194A6.984,6.984,0,0,0,7.006,7.1l-.543.553L3.84,6.134l-1,1.732L5.46,9.379l-.206.746a7.048,7.048,0,0,0,0,3.75l.206.746L2.84,16.134l1,1.732,2.623-1.515.543.553a6.984,6.984,0,0,0,3.243,1.875l.751.194Zm1-6a4,4,0,1,1,4-4A4,4,0,0,1,12,16Zm0-6a2,2,0,1,0,2,2A2,2,0,0,0,12,10Z"/></g></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 974 B |
@@ -1 +1 @@
|
||||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m422.995 217.87c4.143 0 7.5-3.357 7.5-7.5v-192.87c0-9.649-7.851-17.5-17.5-17.5h-313.99c-9.649 0-17.5 7.851-17.5 17.5v477c0 9.649 7.851 17.5 17.5 17.5h313.99c9.649 0 17.5-7.851 17.5-17.5v-248.203c0-4.143-3.357-7.5-7.5-7.5s-7.5 3.357-7.5 7.5v52.811h-16.639v-117.615h16.639v28.877c0 4.143 3.358 7.5 7.5 7.5zm-294.851-144.77h33.028c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-33.028v-43.1h255.713v43.1h-186.759c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h186.759v22.158h-255.713zm-29.139-58.1h14.139v151.493h-16.639v-148.993c0-1.379 1.121-2.5 2.5-2.5zm-2.5 447v-15.278h16.639v15.278zm0-280.507h16.639v117.614h-16.639zm316.49 315.507h-313.99c-1.379 0-2.5-1.121-2.5-2.5v-17.5h318.99v17.5c0 1.379-1.121 2.5-2.5 2.5zm2.5-35h-16.639v-15.278h16.639zm0-147.893v117.614h-16.639v-117.614zm-31.639-15h-3.586c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h3.586v117.614h-3.586c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h3.586v15.279h-255.712v-15.278h3.586c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-3.586v-53.092c0-4.143-3.357-7.5-7.5-7.5s-7.5 3.357-7.5 7.5v53.092h-16.639v-117.615h16.639v28.596c0 4.143 3.357 7.5 7.5 7.5s7.5-3.357 7.5-7.5v-28.596h3.586c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-3.586v-117.614h3.586c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-3.586v-56.235h255.713v56.235h-3.586c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h3.586v117.614zm15-132.614v-151.493h14.139c1.379 0 2.5 1.121 2.5 2.5v148.993z"/><path d="m256 129.22c-17.53 0-31.793 14.263-31.793 31.794 0 17.53 14.263 31.793 31.793 31.793s31.793-14.263 31.793-31.793c0-17.532-14.263-31.794-31.793-31.794zm0 48.587c-9.26 0-16.793-7.533-16.793-16.793s7.533-16.794 16.793-16.794 16.793 7.534 16.793 16.794-7.533 16.793-16.793 16.793z"/><path d="m201.313 419.229c-1.407-1.406-3.314-2.196-5.304-2.196s-3.897.79-5.304 2.197l-10.465 10.466c-2.929 2.929-2.929 7.678.001 10.606 1.464 1.464 3.384 2.196 5.303 2.196s3.839-.732 5.304-2.197l5.161-5.162 5.162 5.163c2.93 2.928 7.678 2.928 10.607 0 2.929-2.93 2.929-7.678 0-10.607z"/><path d="m261.304 419.23c-1.406-1.407-3.314-2.197-5.304-2.197s-3.897.79-5.304 2.197l-10.465 10.466c-2.929 2.929-2.929 7.678.001 10.606 2.928 2.928 7.677 2.929 10.606-.001l5.161-5.161 5.161 5.161c1.465 1.465 3.384 2.197 5.304 2.197 1.919 0 3.839-.732 5.303-2.196 2.93-2.929 2.93-7.678.001-10.606z"/><path d="m321.295 419.23c-1.406-1.407-3.314-2.197-5.304-2.197s-3.896.79-5.304 2.196l-10.466 10.466c-2.929 2.93-2.929 7.678 0 10.607 2.93 2.928 7.678 2.928 10.607 0l5.162-5.163 5.161 5.162c1.465 1.465 3.384 2.197 5.304 2.197 1.919 0 3.839-.732 5.303-2.196 2.93-2.929 2.93-7.678.001-10.606z"/><path d="m213.252 44.05h85.496c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-85.496c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5z"/></g></g></svg>
|
||||
<svg id="Layer_1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m15 12h-6c-1.103 0-2 .897-2 2v4c0 1.103.897 2 2 2h6c1.103 0 2-.897 2-2v-4c0-1.103-.897-2-2-2zm1 6c0 .552-.448 1-1 1h-6c-.551 0-1-.448-1-1v-4c0-.552.449-1 1-1h6c.552 0 1 .448 1 1zm-2.5-2.5c0 .276-.224.5-.5.5h-2c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h2c.276 0 .5.224.5.5zm6-13.5h-1.5v-1.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5v1.5h-10v-1.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5v1.5h-1.5c-2.481 0-4.5 2.019-4.5 4.5v13c0 2.481 2.019 4.5 4.5 4.5h15c2.481 0 4.5-2.019 4.5-4.5v-13c0-2.481-2.019-4.5-4.5-4.5zm-15 1h15c1.93 0 3.5 1.57 3.5 3.5v1.5h-22v-1.5c0-1.93 1.57-3.5 3.5-3.5zm15 20h-15c-1.93 0-3.5-1.57-3.5-3.5v-10.5h22v10.5c0 1.93-1.57 3.5-3.5 3.5z"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 744 B |
@@ -1 +1,2 @@
|
||||
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><g id="Front_Heater" data-name="Front Heater"><path d="m33.14 29h2.86l-4-7-4 7h3l1.5 3.7a8.38 8.38 0 0 1 -.92 8l-.67.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .23-1.39 8.4 8.4 0 0 1 .21-9.32l.67-.82a10.32 10.32 0 0 0 1.22-10z"/><path d="m23.22 29h2.78l-4-7-4 7h3.06l1.5 3.7a8.35 8.35 0 0 1 -.92 8l-.67.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .24-1.39 8.4 8.4 0 0 1 .2-9.32l.67-.82a10.32 10.32 0 0 0 1.22-10z"/><path d="m57.88 20.09a3 3 0 0 0 -1.49-1.84l-7.73-4.1a35.48 35.48 0 0 0 -33.32 0l-7.72 4.1a3 3 0 0 0 -1.22 4.2l9.72 17a3 3 0 0 0 2.6 1.55 3 3 0 0 0 1.39-.34 1 1 0 0 0 .42-1.35 1 1 0 0 0 -1.34-.42 1 1 0 0 1 -1.33-.39l-9.72-17a1 1 0 0 1 -.1-.81 1 1 0 0 1 .52-.69l7.72-4.1a33.46 33.46 0 0 1 31.44 0l7.73 4.1a1 1 0 0 1 .51.63 1 1 0 0 1 -.1.81l-9.72 17a1 1 0 0 1 -1.33.39l-.2-.11a10.82 10.82 0 0 0 -.36-6.72l-1.19-3h2.94l-4-7-4 7h2.9l1.5 3.7a8.38 8.38 0 0 1 -.93 8l-.66.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .23-1.39 8.4 8.4 0 0 1 .21-9.32l.64-.78a8.27 8.27 0 0 0 .84-1.3 3 3 0 0 0 4-1.17l9.72-17a3 3 0 0 0 .32-2.44z"/></g></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" width="512" height="512" viewBox="0 0 24 24"><path d="M20.5,24c-.042,0-.086-.005-.129-.017-.267-.071-.426-.345-.354-.612,.114-.428,.243-.826,.369-1.218,.315-.98,.614-1.906,.614-3.16,0-2.766-1.029-4.801-1.927-6.265-.965-1.576-2.073-3.771-2.073-6.788,0-1.884,.772-4.482,1.294-5.646,.113-.252,.409-.365,.661-.251,.252,.113,.364,.409,.251,.661-.421,.938-1.206,3.436-1.206,5.236,0,2.766,1.029,4.801,1.927,6.265,.965,1.576,2.073,3.771,2.073,6.788,0,1.411-.337,2.456-.663,3.467-.121,.375-.244,.758-.354,1.168-.06,.224-.262,.371-.483,.371Zm-7,0c-.042,0-.086-.005-.129-.017-.267-.071-.426-.345-.354-.612,.114-.428,.243-.826,.369-1.218,.315-.98,.614-1.906,.614-3.16,0-2.766-1.029-4.801-1.927-6.265-.965-1.576-2.073-3.771-2.073-6.788,0-1.884,.772-4.482,1.294-5.646,.112-.252,.409-.365,.661-.251,.252,.113,.364,.409,.251,.661-.421,.938-1.206,3.436-1.206,5.236,0,2.766,1.029,4.801,1.927,6.265,.965,1.576,2.073,3.771,2.073,6.788,0,1.411-.337,2.456-.663,3.467-.121,.375-.244,.758-.354,1.168-.06,.224-.262,.371-.483,.371Zm-7,0c-.042,0-.086-.005-.129-.017-.267-.071-.426-.345-.354-.612,.114-.428,.243-.826,.369-1.218,.315-.98,.614-1.906,.614-3.16,0-2.766-1.029-4.801-1.927-6.265-.965-1.576-2.073-3.771-2.073-6.788,0-1.884,.772-4.482,1.294-5.646,.112-.252,.41-.365,.661-.251,.252,.113,.364,.409,.251,.661-.421,.938-1.206,3.436-1.206,5.236,0,2.766,1.029,4.801,1.927,6.265,.965,1.576,2.073,3.771,2.073,6.788,0,1.411-.337,2.456-.663,3.467-.121,.375-.244,.758-.354,1.168-.06,.224-.262,.371-.483,.371Z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1 +1,4 @@
|
||||
<svg id="Ecommerce" enable-background="new 0 0 48 48" height="512" viewBox="0 0 48 48" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m25 7.03c0-.552-.447-1-1-1-5.514 0-10 4.486-10 10 0 .552.447 1 1 1s1-.448 1-1c0-4.411 3.589-8 8-8 .553 0 1-.448 1-1z"/><path d="m22.246 45.79h1.754 1.754c1.652 0 2.999-1.345 3-3v-1.099c1.032-.475 1.755-1.512 1.755-2.721v-3.72c.176-.382.281-.802.281-1.25 0-1.016.226-2 .671-2.927.441-.919 1.086-1.752 1.864-2.409 3.746-3.165 5.709-7.989 5.25-12.909-.7-7.375-6.813-13.189-14.265-13.525l-.31-.01-.354.011c-7.408.335-13.521 6.149-14.222 13.526-.458 4.917 1.505 9.742 5.251 12.906.778.658 1.423 1.491 1.864 2.41.445.927.671 1.911.671 2.927 0 .447.105.868.281 1.25v3.721c0 1.209.722 2.247 1.755 2.721v1.1c.001 1.653 1.348 2.998 3 2.998zm4.508-3c0 .552-.449 1-1 1h-1.754-1.754c-.551 0-1-.449-1-1v-.82h2.754 2.754zm1.755-3.819c0 .551-.448 1-1 1h-3.509-3.509c-.552 0-1-.449-1-1v-2.068c.232.058.47.097.719.097h3.79 3.79c.249 0 .487-.039.719-.097zm-9.299-4.971c0-1.318-.292-2.595-.868-3.793-.563-1.171-1.385-2.233-2.376-3.071-3.247-2.742-4.948-6.926-4.551-11.191.607-6.389 5.903-11.426 12.275-11.715l.31-.01.265.009c6.417.29 11.713 5.327 12.319 11.714.398 4.267-1.303 8.451-4.55 11.193-.991.838-1.813 1.9-2.376 3.071-.576 1.198-.868 2.475-.868 3.793 0 .551-.448 1-1 1h-3.79-3.79c-.552 0-1-.449-1-1z"/></g></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
|
||||
<path d="m6.443,4.08L4.304.567,5.157.048l2.14,3.513-.854.52Zm13.557,7.92c0,2.323-1.01,4.528-2.771,6.051-.781.674-1.229,1.641-1.229,2.653v3.296h-8v-3.295c0-1.007-.456-1.982-1.252-2.675-2.062-1.796-3.058-4.497-2.661-7.227.512-3.521,3.457-6.36,7.003-6.753,2.307-.256,4.527.45,6.245,1.987,1.693,1.517,2.665,3.689,2.665,5.962Zm-5,8.704c0-.239.04-.471.077-.704h-6.156c.038.233.078.467.078.705v2.295h6v-2.296Zm4-8.704c0-1.988-.85-3.89-2.332-5.217-1.502-1.344-3.438-1.964-5.469-1.738-3.1.343-5.675,2.825-6.122,5.903-.348,2.391.522,4.757,2.327,6.328.553.481.963,1.076,1.234,1.724h2.861v-5.551c-1.14-.232-2-1.242-2-2.449h1c0,.827.673,1.5,1.5,1.5s1.5-.673,1.5-1.5h1c0,1.208-.86,2.217-2,2.449v5.551h2.856c.268-.645.672-1.234,1.218-1.706,1.542-1.332,2.426-3.262,2.426-5.294Zm.696-11.433l-.854-.52-2.14,3.513.854.52,2.14-3.513Zm3.86,4.342l-3.536,1.597.412.912,3.536-1.597-.412-.912ZM.031,5.821l3.536,1.597.412-.912L.443,4.909l-.412.912Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1 +1,4 @@
|
||||
<svg id="Layer_1" height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m36 39.47v-32.47a7 7 0 1 0 -14 0v32.47a13.26 13.26 0 1 0 14 0zm-7 22.59a11.32 11.32 0 0 1 -5.53-21.19 1 1 0 0 0 .53-.87v-33a5 5 0 1 1 10 0v33a1 1 0 0 0 .49.84 11.32 11.32 0 0 1 -5.49 21.22z"/><path d="m30 44.89v-30.25a1 1 0 1 0 -1.94 0v30.25a5.94 5.94 0 1 0 1.94 0zm-1 9.85a4 4 0 1 1 4-4 4 4 0 0 1 -4 4z"/><path d="m40.32 9.64h7a1 1 0 0 0 0-1.94h-7a1 1 0 1 0 0 1.94z"/><path d="m40.32 16.06h4.06a1 1 0 0 0 0-1.94h-4.06a1 1 0 0 0 0 1.94z"/><path d="m47.29 20.55h-7a1 1 0 1 0 0 1.93h7a1 1 0 1 0 0-1.93z"/><path d="m40.32 28.91h4.06a1 1 0 0 0 0-1.94h-4.06a1 1 0 0 0 0 1.94z"/><path d="m47.29 33.39h-7a1 1 0 0 0 0 1.94h7a1 1 0 1 0 0-1.94z"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
|
||||
<path d="m12.5,15.051V5h-1v10.051c-1.14.232-2,1.242-2,2.449,0,1.379,1.121,2.5,2.5,2.5s2.5-1.121,2.5-2.5c0-1.208-.86-2.217-2-2.449Zm-.5,3.949c-.827,0-1.5-.673-1.5-1.5s.673-1.5,1.5-1.5,1.5.673,1.5,1.5-.673,1.5-1.5,1.5Zm4.5-6.181V4.5c0-2.481-2.019-4.5-4.5-4.5s-4.5,2.019-4.5,4.5v8.319c-1.627,1.561-2.32,3.805-1.859,6.049.508,2.472,2.506,4.476,4.972,4.987.459.096.92.143,1.376.143,1.495,0,2.942-.503,4.111-1.454,1.525-1.241,2.4-3.08,2.4-5.044,0-1.763-.727-3.456-2-4.681Zm-1.031,8.949c-1.292,1.05-2.989,1.454-4.653,1.108-2.081-.432-3.767-2.124-4.194-4.21-.405-1.968.235-3.933,1.713-5.258l.166-.148V4.5c0-1.93,1.57-3.5,3.5-3.5s3.5,1.57,3.5,3.5v8.761l.166.148c1.166,1.046,1.834,2.537,1.834,4.091,0,1.662-.74,3.218-2.031,4.269Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 770 B After Width: | Height: | Size: 865 B |
@@ -1 +1,4 @@
|
||||
<svg id="layer_1" enable-background="new 0 0 200 200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><g fill="rgb(0,0,0)"><path d="m61.3366 198.7365c-3.8893 0-7.7945-1.0847-11.3086-3.2781-32.6418-20.3714-44.9485-46.703-49.5247-65.2053-6.1066-30.8373 44.9333-36.4535 67.8505-35.056 1.761-6.9634 6.1301-13.08 12.1249-17.0214-9.0714-12.1387-21.2458-19.7867-36.2543-22.7586-19.3391-3.2847-23.4263-31.1087-5.8545-39.8477 33.3392-19.0024 97.1952-22.5083 96.3704 10.811-.6835 19.2868-6.9715 37.8308-18.7038 55.1859 5.1612 4.9953 8.292 11.7803 8.6884 19.0147 15.0429-1.7888 27.7494-8.5077 37.8247-20.0165 6.0052-6.8593 15.3868-9.1156 23.9022-5.7478 8.5361 3.3773 13.8491 11.4631 13.535 20.6005-1.3216 38.4536-17.9721 62.2774-31.708 75.4917-23.6412 20.7064-54.0319-20.679-64.2755-41.2153-6.816 1.9769-14.557 1.2532-20.8202-1.9875-5.969 13.9198-6.5027 28.2804-1.574 42.7574 5.0949 13.3396-6.2694 28.6219-20.2725 28.273zm-1.6188-94.5681c-14.1176.1632-54.2272 5.0317-50.2069 23.856 4.1646 16.8367 15.4261 40.8368 45.43 59.562 5.3197 3.3207 10.6838 1.7539 13.9293-.8201 3.2305-2.5623 5.9658-7.4048 3.9546-13.3126-6.3202-18.5652-4.873-36.9425 4.3011-54.6216.6379-1.2292 1.7915-2.1091 3.1458-2.3995 1.3551-.2882 2.7674.0404 3.8526.8993 3.4512 2.733 7.6019 4.1775 12.0037 4.1775 4.9261.8516 11.3607-5.6527 14.5071.4457 6.0281 15.2396 35.1102 56.5264 51.2108 42.2675 12.4987-12.025 27.6526-33.7782 28.8673-69.1245.0159-11.6249-13.2308-16.8886-21.1812-8.4221-12.9177 14.7553-29.5546 22.6907-49.4492 23.5865-1.3715.0602-2.7195-.4957-3.6482-1.5215-1.8205-1.8024-.9363-4.3611-.9455-6.5928-.0001-6.3459-3.1223-12.2987-8.3528-15.9233-1.0257-.7109-1.7221-1.8051-1.931-3.0357-.2093-1.2306.0861-2.4938.8196-3.5037 10.2153-12.9246 31.3669-58.5212 11.0023-65.4878-16.6655-4.8123-43.08-7.0587-74.2968 9.5628-10.0753 5.7972-8.0115 19.9044 3.2961 22.5543 19.2372 3.809 34.4279 14.2491 45.1507 31.0306 1.5812 2.3482.4574 5.827-2.1953 6.8094-6.7913 2.7009-11.5582 9.1434-12.144 16.4131-.1006 1.2455-.6991 2.3982-1.6611 3.1966-4.5407 2.4665-10.5485-.3007-15.459.4039z"/><path d="m96.1279 112.2302c-7.607 1.0321-15.4151-13.051-5.4426-14.7224 2.5623.0007 4.6397 2.0781 4.6397 4.6403 0 .4354.3675.8028.8029.8028 6.0654.0249 6.0675 9.2545 0 9.2793z"/><path d="m9.2329 99.228c-12.7712-.764 4.8333-40.6245 9.5289-46.5697 3.292-5.0803 11.0691-.1194 7.8098 5.0118-7.1818 11.1897-11.5808 24.0937-12.7217 37.3166-.2085 2.4172-2.2352 4.2413-4.617 4.2413z"/><path d="m39.2577 89.6348c-11.0543-1.6919 1.3997-18.9153 4.3572-24.1282 3.4934-4.9529 11.064.3241 7.6063 5.3167-3.325 4.7567-5.8564 9.9749-7.5229 15.5084-.6044 2.0077-2.4471 3.3031-4.4406 3.3031z"/><path d="m96.2071 193.9939c-3.6525.2417-8.8695-.1084-8.6734-4.9527.12-2.5591 2.2921-4.5182 4.8525-4.4162 13.2881.6302 26.6579-2.0181 38.6783-7.6414 5.4914-2.5456 9.4266 5.808 3.9316 8.4049-12.0948 5.6586-25.4224 8.6054-38.789 8.6054z"/><path d="m96.194 166.1857c-3.9048.076-10.1254.1951-9.7645-5.2634.2215-2.5524 2.4715-4.439 5.0229-4.2215 5.7823.502 11.5673.0847 17.1924-1.2392 2.4947-.584 4.9917.9597 5.5789 3.4539.8588 8.4162-12.5193 6.4771-18.0297 7.2702z"/><path d="m173.5515 66.6619c-1.6806 0-3.3021-.9157-4.1263-2.512-6.0995-11.8142-15.0751-22.0758-25.9565-29.6754-4.9499-3.4817.3164-11.067 5.3132-7.6072 6.2137 3.1209 41.191 37.1803 24.7696 39.7946z"/><path d="m149.9162 81.597c-1.7462 0-3.4186-.99-4.2065-2.6759-2.4571-5.2576-5.7103-10.0585-9.6698-14.27-4.1842-4.3187 2.6992-10.8071 6.7603-6.3556 3.7145 4.588 19.1197 21.1144 7.116 23.3015z"/></g></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
|
||||
<path d="m22.98,10.346c-.721-.804-1.714-1.278-2.798-1.336-1.661-.081-4.16.444-5.969.983-.179-.197-.378-.371-.603-.515.164-.177.324-.352.494-.533,1.375-1.456,2.797-2.96,2.89-4.691C17.113,2.032,15.42.126,13.219.006c-1.067-.062-2.08.302-2.872,1.013-.804.721-1.278,1.715-1.336,2.798-.09,1.676.443,4.161.982,5.969-.197.179-.371.378-.515.603-.178-.164-.354-.325-.534-.495-1.455-1.374-2.96-2.795-4.69-2.888C2.054,6.892.127,8.581.006,10.781c-.059,1.06.302,2.08,1.014,2.873.721.804,1.714,1.278,2.798,1.336.125.006.255.01.389.01,1.655,0,3.907-.494,5.58-.993.178.197.378.371.603.515-.164.177-.324.352-.494.533-1.375,1.456-2.796,2.96-2.889,4.691-.119,2.222,1.574,4.127,3.775,4.247.074.004.147.006.221.006.979,0,1.914-.358,2.651-1.02.804-.721,1.278-1.714,1.336-2.798.09-1.676-.443-4.161-.982-5.969.197-.179.371-.378.515-.603.178.164.354.325.534.495,1.455,1.374,2.96,2.795,4.69,2.888.073.004.146.006.22.006,2.125,0,3.91-1.654,4.027-3.781.059-1.06-.302-2.08-1.014-2.873Zm-10.98,3.654c-1.103,0-2-.897-2-2s.897-2,2-2,2,.897,2,2-.897,2-2,2Zm-1.991-10.129c.044-.817.401-1.565,1.006-2.108.595-.533,1.36-.801,2.149-.759,1.651.09,2.921,1.524,2.832,3.195-.073,1.365-1.367,2.734-2.618,4.059-.258.274-.51.543-.751.81-.202-.043-.412-.068-.627-.068-.4,0-.78.082-1.129.224-.468-1.64-.941-3.886-.863-5.353ZM3.87,13.991c-.816-.043-1.564-.4-2.106-1.005-.533-.594-.803-1.358-.76-2.15.091-1.651,1.554-2.924,3.196-2.831,1.364.073,2.732,1.365,4.057,2.616.274.259.544.511.811.753-.043.203-.068.412-.068.627,0,.4.082.78.224,1.129-1.64.469-3.883.943-5.354.863Zm10.121,6.138c-.044.816-.401,1.565-1.006,2.107-.593.533-1.359.801-2.149.759-1.651-.09-2.921-1.524-2.832-3.195.073-1.365,1.367-2.734,2.618-4.058.258-.275.51-.544.751-.81.202.043.412.068.627.068.4,0,.78-.082,1.129-.224.468,1.64.941,3.886.863,5.353Zm5.809-4.134c-1.364-.073-2.732-1.365-4.057-2.616-.274-.259-.544-.511-.811-.753.043-.202.068-.412.068-.627,0-.4-.082-.78-.224-1.129,1.64-.469,3.886-.938,5.354-.862.816.043,1.564.4,2.106,1.005.533.594.803,1.358.76,2.15-.091,1.651-1.53,2.932-3.196,2.831Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -7,8 +7,8 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DVDHG02m.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DaEsKk03.css">
|
||||
<script type="module" crossorigin src="/assets/index-DKrE4YAX.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-OvloHYxf.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||