Wiring up print options: first try. needs mofre reverse engineering for sync process

This commit is contained in:
Martin Ziegler
2025-12-03 11:36:08 +01:00
parent 50a8ed8ea9
commit 071ca1cf4e
23 changed files with 1401 additions and 431 deletions
+1
View File
@@ -42,3 +42,4 @@ archive/
# Logs
*.log
logs/
*.log*
+92
View File
@@ -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,
}
+23
View File
@@ -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
+385 -7
View File
@@ -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:
+2 -1
View File
@@ -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
View File
@@ -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

+2 -1
View File
@@ -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

+4 -1
View File
@@ -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

+4 -1
View File
@@ -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

+4 -1
View File
@@ -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

+47
View File
@@ -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>
);
}
+15 -1
View File
@@ -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>
);
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -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
View File
@@ -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

+2 -1
View File
@@ -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

+4 -1
View File
@@ -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

+4 -1
View File
@@ -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

+4 -1
View File
@@ -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

+2 -2
View File
@@ -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>