Compare commits

...

6 Commits

3 changed files with 501 additions and 179 deletions

28
__init__.py Normal file
View File

@ -0,0 +1,28 @@
def get_settings_defaults(self):
return {
# ...existing code...
# Add option to disable camera functionality
"disable_camera": False,
# ...existing code...
}
# ...existing code...
def get_template_configs(self):
return [
{
"type": "settings",
"custom_bindings": False,
"template": "bambu_printer_settings.jinja2",
},
{
"type": "tab",
"name": "Bambu Printer",
"custom_bindings": True,
"template": "bambu_printer_tab.jinja2",
},
]
# ...existing code...

View File

@ -14,9 +14,8 @@ from octoprint_bambu_printer.printer.print_job import PrintJob
from pybambu import BambuClient, commands from pybambu import BambuClient, commands
import logging import logging
import logging.handlers import logging.handlers
import paho.mqtt.client as mqtt
import json import json
import ssl import paho.mqtt.client as mqtt
from octoprint.util import RepeatedTimer from octoprint.util import RepeatedTimer
@ -48,7 +47,232 @@ class BambuPrinterTelemetry:
extruderCount: int = 1 extruderCount: int = 1
# noinspection PyBroadException class BambuMqttBridgeClient:
"""
Implements compatible interface with BambuClient but uses Paho MQTT
to connect to a MQTT broker that bridges Bambu topics
"""
def __init__(self,
device_type,
serial,
host,
mqtt_port=1883,
**kwargs):
self._log = logging.getLogger("octoprint.plugins.bambu_printer.BambuMqttBridge")
self._device_type = device_type
self._serial = serial
self._host = host
self._mqtt_port = mqtt_port
self.connected = False
self._mqtt_client = mqtt.Client(f"octoprint_bambu_bridge_{serial}")
self._device_data = self._create_empty_device_data()
self._callbacks = {}
# Setup callbacks
self._mqtt_client.on_connect = self._on_connect
self._mqtt_client.on_message = self._on_message
self._mqtt_client.on_disconnect = self._on_disconnect
def _create_empty_device_data(self):
"""Creates empty device data structure compatible with BambuClient"""
from types import SimpleNamespace
# Create basic structure matching BambuClient
device = SimpleNamespace()
device.print_job = SimpleNamespace()
device.print_job.gcode_state = "IDLE"
device.temperature = SimpleNamespace()
device.temperature.nozzle_temp = AMBIENT_TEMPERATURE
device.temperature.target_nozzle_temp = 0.0
device.temperature.bed_temp = AMBIENT_TEMPERATURE
device.temperature.target_bed_temp = 0.0
device.temperature.chamber_temp = AMBIENT_TEMPERATURE
device.hms = SimpleNamespace()
device.hms.errors = {"Count": 0}
return device
def _on_connect(self, client, userdata, flags, rc):
if rc == 0:
self._log.info(f"Connected to MQTT broker at {self._host}:{self._mqtt_port}")
self.connected = True
# Subscribe to Bambu topics
topic_base = f"device/{self._device_type}/{self._serial}"
self._mqtt_client.subscribe(f"{topic_base}/report")
self._mqtt_client.subscribe(f"{topic_base}/report_hms")
self._log.info(f"Subscribed to topic: {topic_base}/#")
if 'callback' in self._callbacks:
self._callbacks['callback']("event_printer_data_update")
if hasattr(self, 'on_connect') and callable(self.on_connect):
self.on_connect(client, userdata, flags, rc)
else:
self._log.error(f"Failed to connect to MQTT broker, return code: {rc}")
def _on_disconnect(self, client, userdata, rc):
self._log.warning(f"Disconnected from MQTT broker with code: {rc}")
self.connected = False
# Add reconnection attempt
if rc != 0: # Non-zero means unexpected disconnect
self._log.info("Attempting to reconnect to MQTT broker...")
try:
self._mqtt_client.reconnect()
except Exception as e:
self._log.error(f"Failed to reconnect to MQTT broker: {str(e)}", exc_info=True)
if hasattr(self, 'on_disconnect') and callable(self.on_disconnect):
self.on_disconnect(client, userdata, rc)
def _on_message(self, client, userdata, msg):
try:
payload = json.loads(msg.payload.decode('utf-8'))
self._log.debug(f"Received message on topic {msg.topic}: {payload}")
if msg.topic.endswith('/report'):
self._process_report_message(payload)
if 'callback' in self._callbacks:
self._callbacks['callback']("event_printer_data_update")
elif msg.topic.endswith('/report_hms'):
self._process_hms_message(payload)
if 'callback' in self._callbacks:
self._callbacks['callback']("event_hms_errors")
except json.JSONDecodeError:
self._log.error(f"Failed to decode JSON from message: {msg.payload}")
except Exception as e:
self._log.error(f"Error processing message: {str(e)}")
def _process_report_message(self, data):
"""Process printer status report messages"""
try:
# Update print state if available
if 'print' in data:
print_data = data['print']
# Update printer state
if 'gcode_state' in print_data:
self._device_data.print_job.gcode_state = print_data['gcode_state']
self._log.debug(f"Updated printer state: {print_data['gcode_state']}")
# Process direct temperature values in print data
if 'nozzle_temper' in print_data:
self._device_data.temperature.nozzle_temp = float(print_data['nozzle_temper'])
if 'nozzle_target_temper' in print_data:
self._device_data.temperature.target_nozzle_temp = float(print_data['nozzle_target_temper'])
if 'bed_temper' in print_data:
self._device_data.temperature.bed_temp = float(print_data['bed_temper'])
if 'bed_target_temper' in print_data:
self._device_data.temperature.target_bed_temp = float(print_data['bed_target_temper'])
if 'chamber_temper' in print_data:
self._device_data.temperature.chamber_temp = float(print_data['chamber_temper'])
# Process temperature section if available
if 'temperature' in data:
temp = self._device_data.temperature
temp_data = data['temperature']
if 'nozzle_temp' in temp_data:
temp.nozzle_temp = float(temp_data['nozzle_temp'])
if 'target_nozzle_temp' in temp_data:
temp.target_nozzle_temp = float(temp_data['target_nozzle_temp'])
if 'bed_temp' in temp_data:
temp.bed_temp = float(temp_data['bed_temp'])
if 'target_bed_temp' in temp_data:
temp.target_bed_temp = float(temp_data['target_bed_temp'])
if 'chamber_temp' in temp_data:
temp.chamber_temp = float(temp_data['chamber_temp'])
self._log.debug(f"Updated temperatures - Nozzle: {self._device_data.temperature.nozzle_temp}/" +
f"{self._device_data.temperature.target_nozzle_temp}, " +
f"Bed: {self._device_data.temperature.bed_temp}/" +
f"{self._device_data.temperature.target_bed_temp}, " +
f"Chamber: {self._device_data.temperature.chamber_temp}")
except Exception as e:
self._log.error(f"Error processing report message: {str(e)}", exc_info=True)
def _process_hms_message(self, data):
"""Process HMS error messages"""
try:
if 'hms' in data:
error_count = 0
hms_errors = {"Count": 0}
for error in data['hms']:
error_count += 1
if isinstance(error, dict) and 'msg' in error:
hms_errors[f"{error_count}-Error"] = error['msg']
else:
hms_errors[f"{error_count}-Error"] = str(error)
hms_errors["Count"] = error_count
self._device_data.hms.errors = hms_errors
if error_count > 0:
self._log.info(f"Found {error_count} HMS errors")
except Exception as e:
self._log.error(f"Error processing HMS message: {str(e)}", exc_info=True)
def connect(self, callback=None):
"""Connect to MQTT broker"""
if callback:
self._callbacks['callback'] = callback
try:
self._log.info(f"Connecting to MQTT broker {self._host}:{self._mqtt_port}")
self._mqtt_client.connect(self._host, self._mqtt_port)
self._mqtt_client.loop_start()
# Wait a bit for the connection to establish
time.sleep(1)
# If not connected after waiting, try again
if not self.connected:
self._log.warning("Initial connection attempt failed, retrying...")
try:
self._mqtt_client.reconnect()
time.sleep(2) # Wait a bit longer for retry
except Exception as e:
self._log.error(f"Reconnection failed: {str(e)}", exc_info=True)
return self.connected
except Exception as e:
self._log.error(f"Failed to connect to MQTT broker: {str(e)}", exc_info=True)
return False
def disconnect(self):
"""Disconnect from MQTT broker"""
if self.connected:
self._mqtt_client.loop_stop()
self._mqtt_client.disconnect()
self.connected = False
def get_device(self):
"""Returns device data structure"""
return self._device_data
def publish(self, command):
"""Publishes command to device"""
if not self.connected:
self._log.error("Cannot publish: Not connected to MQTT broker")
return False
try:
topic_base = f"device/{self._device_type}/{self._serial}"
if 'print' in command and 'param' in command['print']:
# Commands go to command topic
message = json.dumps(command)
result = self._mqtt_client.publish(f"{topic_base}/cmd", message)
self._log.debug(f"Published command to {topic_base}/cmd: {message}")
return result.rc == mqtt.MQTT_ERR_SUCCESS
else:
self._log.warning(f"Invalid command format: {command}")
except Exception as e:
self._log.error(f"Failed to publish command: {str(e)}", exc_info=True)
return False
class BambuVirtualPrinter: class BambuVirtualPrinter:
gcode_executor = GCodeExecutor() gcode_executor = GCodeExecutor()
@ -107,12 +331,20 @@ class BambuVirtualPrinter:
self._serial_io.start() self._serial_io.start()
self._printer_thread.start() self._printer_thread.start()
self._mqtt_client = None self._mqtt_client = None
self._mqtt_connected = False self._mqtt_connected = False
self._bambu_client = None self._bambu_client = None
self._custom_connected = False self._custom_connected = False
# Store initial connection errors to avoid logging the same errors repeatedly
self._connection_error_logged = False
self._camera_error_logged = False
self._last_connection_attempt = 0
self._connection_retry_backoff = 10 # Start with 10 seconds between retries
# Track if we should disable camera functionality due to persistent errors
self._disable_camera = self._settings.get_boolean(["disable_camera"]) or False
self._bambu_client: BambuClient = self._create_client_connection_async() self._bambu_client: BambuClient = self._create_client_connection_async()
@property @property
@ -135,10 +367,6 @@ class BambuVirtualPrinter:
def current_print_job(self, value): def current_print_job(self, value):
self._current_print_job = value self._current_print_job = value
@property
def selected_file(self):
return self._selected_project_file
@property @property
def has_selected_file(self): def has_selected_file(self):
return self._selected_project_file is not None return self._selected_project_file is not None
@ -180,48 +408,34 @@ class BambuVirtualPrinter:
self._log.debug(f"Connection status check: custom_connected={self._custom_connected}, mqtt_connected={self._mqtt_connected}, result={connection_status}") self._log.debug(f"Connection status check: custom_connected={self._custom_connected}, mqtt_connected={self._mqtt_connected}, result={connection_status}")
return connection_status return connection_status
def change_state(self, new_state: APrinterState):
self._state_change_queue.put(new_state)
def new_update(self, event_type): def new_update(self, event_type):
if event_type == "event_hms_errors": """Custom property to track connection status without modifying BambuClient directly"""
if event_type == "event_printer_data_update":
self._log.debug("Received event_printer_data_update")
self._update_hms_errors() self._update_hms_errors()
elif event_type == "event_printer_data_update": # Gib den aktuellen Status detaillierter aus
self._update_printer_info()
def _update_printer_info(self):
# Verwende direkt die Telemetrie-Daten statt der BambuClient-Struktur
self.lastTempAt = time.monotonic()
# Der Rest der Methode kann unverändert bleiben, da wir die Telemetrie
# direkt in _process_print_data aktualisieren
# Gib den aktuellen Status detaillierter aus
self._log.debug(f"Current temperatures - Nozzle: {self._telemetry.temp[0]}/{self._telemetry.targetTemp[0]}, " +
f"Bed: {self._telemetry.bedTemp}/{self._telemetry.bedTargetTemp}, " +
f"Chamber: {self._telemetry.chamberTemp}")
# Rufe trotzdem die BambuClient-Daten ab, falls verfügbar
try:
device_data = self.bambu_client.get_device() device_data = self.bambu_client.get_device()
print_job_state = device_data.print_job.gcode_state print_job_state = device_data.print_job.gcode_state
self._log.debug(f"BambuClient printer state: {print_job_state}") self._log.debug(f"BambuClient printer state: {print_job_state}")
self._log.debug(f"Current temperatures - Nozzle: {self._telemetry.temp[0]}/{self._telemetry.targetTemp[0]}, " +
f"Bed: {self._telemetry.bedTemp}/{self._telemetry.bedTargetTemp}, " +
f"Chamber: {self._telemetry.chamberTemp}")
if ( # Process the printer state even if it's "unknown"
print_job_state == "IDLE" if print_job_state:
or print_job_state == "FINISH" self._process_print_state(print_job_state)
or print_job_state == "FAILED"
):
self.change_state(self._state_idle)
elif print_job_state == "RUNNING" or print_job_state == "PREPARE":
self.change_state(self._state_printing)
elif print_job_state == "PAUSE":
self.change_state(self._state_paused)
else: else:
self._log.warn(f"Unknown print job state: {print_job_state}") self._log.debug("No printer state received, skipping state processing")
except Exception as e:
self._log.error(f"Error reading BambuClient device state: {e}") elif event_type == "event_hms_errors":
self._log.debug("Received event_hms_errors")
bambu_printer = self.bambu_client.get_device()
if bambu_printer.hms.errors != self._last_hms_errors:
self._log.debug(f"HMS Error: {bambu_printer.hms.errors}")
for n in range(1, bambu_printer.hms.errors["Count"] + 1):
error = bambu_printer.hms.errors[f"{n}-Error"].strip()
self.sendIO(f"// action:notification {error}")
self._last_hms_errors = bambu_printer.hms.errors
def _update_hms_errors(self): def _update_hms_errors(self):
bambu_printer = self.bambu_client.get_device() bambu_printer = self.bambu_client.get_device()
@ -246,18 +460,20 @@ class BambuVirtualPrinter:
def _on_mqtt_connect(self, client, userdata, flags, rc): def _on_mqtt_connect(self, client, userdata, flags, rc):
self._log.debug(f"MQTT connected with result code: {rc}") self._log.debug(f"MQTT connected with result code: {rc}")
if rc == 0: if rc == 0:
# Notify that we're connected
self._mqtt_connected = True self._mqtt_connected = True
self._custom_connected = True self._custom_connected = True
# Subscribe to the relevant topics for the Bambu printer # Subscribe to the relevant topics for the Bambu printer
device_topic = f"device/{self._settings.get(['serial'])}/report" device_topic = f"device/{self._settings.get(['serial'])}/report"
client.subscribe(device_topic) client.subscribe(device_topic)
self._log.debug(f"Subscribed to topic: {device_topic}") self._log.debug(f"Subscribed to topic: {device_topic}")
self._log.info(f"MQTT connection successful. Connected: {self.is_connected}") self._log.info(f"MQTT connection successful. Connected: {self.is_connected}")
# Notify that we're connected # Try to patch client for better error handling after successful connection
self.sendOk() try:
self._patch_bambu_client_for_connection_errors()
except Exception as e:
self._log.warning(f"Failed to patch BambuClient for better error handling: {str(e)}")
else: else:
self._mqtt_connected = False self._mqtt_connected = False
self._custom_connected = False self._custom_connected = False
@ -292,7 +508,6 @@ class BambuVirtualPrinter:
self._update_bambu_client_state(payload) self._update_bambu_client_state(payload)
except Exception as e: except Exception as e:
self._log.error(f"Error forwarding to pybambu: {e}", exc_info=True) self._log.error(f"Error forwarding to pybambu: {e}", exc_info=True)
except Exception as e: except Exception as e:
self._log.error(f"Error processing MQTT message: {e}", exc_info=True) self._log.error(f"Error processing MQTT message: {e}", exc_info=True)
@ -308,12 +523,10 @@ class BambuVirtualPrinter:
self._process_direct_temperature_data(print_data) self._process_direct_temperature_data(print_data)
# Status verarbeiten # Status verarbeiten
if 'gcode_state' in print_data: self._process_print_state(print_data['gcode_state'])
self._process_print_state(print_data['gcode_state'])
# Fortschritt verarbeiten # Fortschritt verarbeiten
if 'mc_percent' in print_data: self._process_progress_data(print_data)
self._process_progress_data(print_data)
# Schicht-Informationen verarbeiten # Schicht-Informationen verarbeiten
self._process_layer_data(print_data) self._process_layer_data(print_data)
@ -321,24 +534,20 @@ class BambuVirtualPrinter:
# Lüfter-Informationen verarbeiten # Lüfter-Informationen verarbeiten
self._process_fan_data(print_data) self._process_fan_data(print_data)
# Geschwindigkeit verarbeiten
self._process_speed_data(print_data)
# Datei-Informationen verarbeiten # Datei-Informationen verarbeiten
self._process_file_data(print_data) self._process_file_data(print_data)
# Trigger update # Trigger update
self.new_update("event_printer_data_update") self.new_update("event_printer_data_update")
# Verarbeite info-Daten # Verarbeite info-Daten
if 'info' in payload: if 'info' in payload:
info_data = payload['info'] info_data = payload['info']
self._log.info(f"Processing info data with keys: {list(info_data.keys())}") self._log.info(f"Processing info data with keys: {list(info_data.keys())}")
# HMS-Fehler verarbeiten # HMS-Fehler verarbeiten
if 'hms' in info_data: self._process_hms_errors(info_data['hms'])
self._process_hms_errors(info_data['hms']) self.new_update("event_hms_errors")
self.new_update("event_hms_errors")
except Exception as e: except Exception as e:
self._log.error(f"Error processing MQTT payload: {e}", exc_info=True) self._log.error(f"Error processing MQTT payload: {e}", exc_info=True)
@ -363,13 +572,12 @@ class BambuVirtualPrinter:
if total_layers is not None: if total_layers is not None:
self.current_print_job.total_layers = total_layers self.current_print_job.total_layers = total_layers
# Aktualisiere auch die pybambu-Datenstruktur # Aktualisiere auch die pybambu-Datenstruktur, wenn vorhanden
if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'): if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'):
if current_layer is not None: if current_layer is not None:
self._bambu_client.device.print_job.current_layer = current_layer self._bambu_client.device.print_job.current_layer = current_layer
if total_layers is not None: if total_layers is not None:
self._bambu_client.device.print_job.total_layers = total_layers self._bambu_client.device.print_job.total_layers = total_layers
except Exception as e: except Exception as e:
self._log.error(f"Error processing layer data: {e}", exc_info=True) self._log.error(f"Error processing layer data: {e}", exc_info=True)
@ -443,7 +651,7 @@ class BambuVirtualPrinter:
# Aktualisiere auch die pybambu-Datenstruktur # Aktualisiere auch die pybambu-Datenstruktur
if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'): if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'):
self._bambu_client.device.print_job.gcode_file = filename self._bambu_client.device.print_job.gcode_file = filename
# Subtask Name (oft der Projektname) # Subtask Name (oft der Projektname)
if 'subtask_name' in print_data and print_data['subtask_name']: if 'subtask_name' in print_data and print_data['subtask_name']:
subtask_name = print_data['subtask_name'] subtask_name = print_data['subtask_name']
@ -456,7 +664,6 @@ class BambuVirtualPrinter:
# Aktualisiere auch die pybambu-Datenstruktur # Aktualisiere auch die pybambu-Datenstruktur
if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'): if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'):
self._bambu_client.device.print_job.subtask_name = subtask_name self._bambu_client.device.print_job.subtask_name = subtask_name
except Exception as e: except Exception as e:
self._log.error(f"Error processing file data: {e}", exc_info=True) self._log.error(f"Error processing file data: {e}", exc_info=True)
@ -469,8 +676,9 @@ class BambuVirtualPrinter:
# oder andere Werte haben als die, die wir erwarten # oder andere Werte haben als die, die wir erwarten
print_job_state = print_job_state.upper() if print_job_state else "UNKNOWN" print_job_state = print_job_state.upper() if print_job_state else "UNKNOWN"
# Normalisieren des Status, falls er 'unknown' ist oder nicht erkannt wird # Explicitly handle "unknown" state (both upper and lowercase)
if print_job_state in ["UNKNOWN", ""]: if print_job_state in ["UNKNOWN", ""] or print_job_state.upper() == "UNKNOWN":
self._log.debug("Detected 'unknown' printer state, trying to determine actual state")
# Wenn wir keinen erkannten Status haben, versuchen wir ihn aus anderen Daten abzuleiten # Wenn wir keinen erkannten Status haben, versuchen wir ihn aus anderen Daten abzuleiten
# Prüfe ob Druckfortschritt vorhanden ist # Prüfe ob Druckfortschritt vorhanden ist
if self.current_print_job and self.current_print_job.print_percentage > 0: if self.current_print_job and self.current_print_job.print_percentage > 0:
@ -480,6 +688,9 @@ class BambuVirtualPrinter:
elif self._telemetry.targetTemp[0] > 150 or self._telemetry.bedTargetTemp > 40: elif self._telemetry.targetTemp[0] > 150 or self._telemetry.bedTargetTemp > 40:
print_job_state = "PREPARE" print_job_state = "PREPARE"
self._log.debug(f"Changed unknown state to PREPARE based on target temperatures") self._log.debug(f"Changed unknown state to PREPARE based on target temperatures")
else:
self._log.debug("Keeping state as IDLE since no indicators for print activity were found")
print_job_state = "IDLE" # Default to IDLE if we can't determine state
# Status im PrintJob aktualisieren # Status im PrintJob aktualisieren
if self.current_print_job is None and print_job_state in ["RUNNING", "PREPARE", "PAUSE"]: if self.current_print_job is None and print_job_state in ["RUNNING", "PREPARE", "PAUSE"]:
@ -509,7 +720,7 @@ class BambuVirtualPrinter:
if self._telemetry.temp[0] > 170 and self._telemetry.bedTemp > 40: if self._telemetry.temp[0] > 170 and self._telemetry.bedTemp > 40:
# Hohe aktuelle Temperaturen deuten auf einen laufenden Druck hin # Hohe aktuelle Temperaturen deuten auf einen laufenden Druck hin
is_printing = True is_printing = True
self._log.debug(f"Detected potential printing based on actual temperatures: " self._log.debug(f"Detected potential printing based on actual temperatures: " +
f"Nozzle={self._telemetry.temp[0]}, Bed={self._telemetry.bedTemp}") f"Nozzle={self._telemetry.temp[0]}, Bed={self._telemetry.bedTemp}")
# Statusänderung in den Zustandsautomaten übertragen basierend auf allen Checks # Statusänderung in den Zustandsautomaten übertragen basierend auf allen Checks
@ -524,12 +735,14 @@ class BambuVirtualPrinter:
self.change_state(self._state_paused) self.change_state(self._state_paused)
else: else:
self._log.warn(f"Unknown print job state: {print_job_state}") self._log.warn(f"Unknown print job state: {print_job_state}")
# Aktualisiere auch die pybambu-Datenstruktur # Aktualisiere auch die pybambu-Datenstruktur
if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'): if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'):
self._bambu_client.device.print_job.gcode_state = print_job_state self._bambu_client.device.print_job.gcode_state = print_job_state
except Exception as e: except Exception as e:
self._log.error(f"Error processing print state: {e}", exc_info=True) self._log.error(f"Error processing print state: {e}", exc_info=True)
# Default to a safe state in case of errors
self.change_state(self._state_idle)
def _process_direct_temperature_data(self, print_data): def _process_direct_temperature_data(self, print_data):
"""Verarbeitet Temperaturdaten direkt aus dem print-Objekt""" """Verarbeitet Temperaturdaten direkt aus dem print-Objekt"""
@ -554,7 +767,7 @@ class BambuVirtualPrinter:
if 'chamber_temper' in print_data: if 'chamber_temper' in print_data:
self._telemetry.chamberTemp = float(print_data['chamber_temper']) self._telemetry.chamberTemp = float(print_data['chamber_temper'])
self._log.debug(f"Updated chamber temperature: {self._telemetry.chamberTemp}") self._log.debug(f"Updated chamber temperature: {self._telemetry.chamberTemp}")
# Log der aktualisierten Temperaturen # Log der aktualisierten Temperaturen
self._log.debug(f"Current temperatures - Nozzle: {self._telemetry.temp[0]}/{self._telemetry.targetTemp[0]}, " + self._log.debug(f"Current temperatures - Nozzle: {self._telemetry.temp[0]}/{self._telemetry.targetTemp[0]}, " +
f"Bed: {self._telemetry.bedTemp}/{self._telemetry.bedTargetTemp}, " + f"Bed: {self._telemetry.bedTemp}/{self._telemetry.bedTargetTemp}, " +
@ -575,8 +788,7 @@ class BambuVirtualPrinter:
if 'chamber_temper' in print_data: if 'chamber_temper' in print_data:
temp_obj.chamber_temp = float(print_data['chamber_temper']) temp_obj.chamber_temp = float(print_data['chamber_temper'])
except Exception as e: except Exception as e:
self._log.error(f"Error updating BambuClient temperature: {e}") self._log.error(f"Error updating BambuClient temperature: {e}", exc_info=True)
except Exception as e: except Exception as e:
self._log.error(f"Error processing temperature data: {e}", exc_info=True) self._log.error(f"Error processing temperature data: {e}", exc_info=True)
@ -584,6 +796,7 @@ class BambuVirtualPrinter:
"""Verarbeitet Fortschrittsdaten aus MQTT-Nachrichten""" """Verarbeitet Fortschrittsdaten aus MQTT-Nachrichten"""
try: try:
progress = -1 progress = -1
if 'mc_percent' in print_data: if 'mc_percent' in print_data:
progress = int(print_data['mc_percent']) progress = int(print_data['mc_percent'])
@ -611,7 +824,7 @@ class BambuVirtualPrinter:
if not hasattr(self._bambu_client, 'device'): if not hasattr(self._bambu_client, 'device'):
self._log.debug("BambuClient has no device attribute, initializing") self._log.debug("BambuClient has no device attribute, initializing")
return return
if 'print' in payload: if 'print' in payload:
print_data = payload['print'] print_data = payload['print']
@ -632,7 +845,7 @@ class BambuVirtualPrinter:
if 'chamber_temp' in temp_data: if 'chamber_temp' in temp_data:
temp_obj.chamber_temp = float(temp_data['chamber_temp']) temp_obj.chamber_temp = float(temp_data['chamber_temp'])
except Exception as e: except Exception as e:
self._log.error(f"Error updating BambuClient state: {e}") self._log.error(f"Error updating BambuClient state: {e}", exc_info=True)
def _create_client_connection_async(self): def _create_client_connection_async(self):
self._create_client_connection() self._create_client_connection()
@ -644,112 +857,165 @@ class BambuVirtualPrinter:
if ( if (
self._settings.get(["device_type"]) == "" self._settings.get(["device_type"]) == ""
or self._settings.get(["serial"]) == "" or self._settings.get(["serial"]) == ""
or self._settings.get(["username"]) == ""
or self._settings.get(["access_code"]) == ""
): ):
msg = "invalid settings to start connection with Bambu Printer" msg = "invalid settings to start connection with Bambu Printer"
self._log.debug(msg) self._log.debug(msg)
raise ValueError(msg) raise ValueError(msg)
use_local_mqtt = self._settings.get_boolean(['local_mqtt']) # Check if we should use MQTT bridge mode
self._log.debug(f"connecting via local mqtt: {use_local_mqtt}") use_mqtt_bridge = self._settings.get_boolean(["use_mqtt_bridge"])
# Create a BambuClient but don't let it handle the MQTT connection
bambu_client = BambuClient(
device_type=self._settings.get(["device_type"]),
serial=self._settings.get(["serial"]),
host=self._settings.get(["host"]),
username="bambuocto",
access_code=self._settings.get(["access_code"]),
local_mqtt=use_local_mqtt,
region=self._settings.get(["region"]),
email=self._settings.get(["email"]),
auth_token=self._settings.get(["auth_token"]),
)
# Initialisiere die device-Eigenschaft manuell, ohne connect() zu benutzen
# da die connect()-Methode ein Callback als Parameter erwartet
if not hasattr(bambu_client, 'device'):
self._log.debug("BambuClient has no device attribute, initializing manually")
# Statt eine BambuDevice-Klasse direkt zu importieren oder connect() zu verwenden,
# initialisieren wir die grundlegenden Attribute anders
try:
# Manuell die notwendigen Attribute erstellen
bambu_client.device = type('', (), {})() # Ein leeres Objekt erstellen
# Grundlegende Attribute hinzufügen
bambu_client.device.temperature = type('', (), {
'nozzle_temp': 21.0,
'target_nozzle_temp': 0.0,
'bed_temp': 21.0,
'target_bed_temp': 0.0,
'chamber_temp': 21.0,
})()
bambu_client.device.print_job = type('', (), {
'gcode_state': 'IDLE',
'gcode_file': '',
'mc_percent': 0,
'mc_remaining_time': 0,
})()
bambu_client.device.hms = type('', (), {
'errors': {'Count': 0},
'update_from_payload': lambda x: None
})()
self._log.debug("Created device attributes manually")
except Exception as e:
self._log.error(f"Error initializing BambuClient: {e}", exc_info=True)
# Set up our own MQTT client
self._mqtt_client = mqtt.Client()
self._mqtt_client.on_connect = self._on_mqtt_connect
self._mqtt_client.on_disconnect = self._on_mqtt_disconnect
self._mqtt_client.on_message = self._on_mqtt_message
# Configure connection based on local or cloud
if use_local_mqtt:
host = self._settings.get(["host"])
port = 1883
username = "octobambu"
self._mqtt_client.username_pw_set(username)
else:
# Cloud connection settings
region = self._settings.get(["region"])
host = f"mqtt-{region}.bambulab.com"
port = 8883
username = self._settings.get(["email"])
password = self._settings.get(["auth_token"])
self._mqtt_client.username_pw_set(username, password)
self._mqtt_client.tls_set()
# Connect MQTT
try:
self._mqtt_client.connect(host, port, 60)
self._mqtt_client.loop_start()
self._log.info(f"MQTT client started with {host}:{port}")
# Explicitly set the connection status
self._custom_connected = True
except Exception as e:
self._log.error(f"Failed to connect to MQTT broker: {e}")
raise
# Inject our MQTT client into the BambuClient without modifying 'connected'
bambu_client._mqtt_client = self._mqtt_client
# Instead of modifying bambu_client.connected, we'll use our custom property if use_mqtt_bridge:
self._custom_connected = True self._log.debug(
f"connecting via mqtt bridge: {self._settings.get(['mqtt_host'])}:{self._settings.get(['mqtt_port'])}"
# Store the Bambu client )
self._log.debug(
f"Creating MQTT bridge client: {self._settings.get(['mqtt_host'])}:{self._settings.get(['mqtt_port'])}"
)
# Create MQTT bridge client
bambu_client = BambuMqttBridgeClient(
device_type=self._settings.get(["device_type"]),
serial=self._settings.get(["serial"]),
host=self._settings.get(["mqtt_host"]),
mqtt_port=int(self._settings.get(["mqtt_port"]) or 1883)
)
else:
# Use standard BambuClient
self._log.debug(
f"connecting via local mqtt: {self._settings.get_boolean(['local_mqtt'])}"
)
self._log.debug(
f"Creating standard BambuClient with local_mqtt: {self._settings.get_boolean(['local_mqtt'])}"
)
# Set up client parameters
client_params = {
"device_type": self._settings.get(["device_type"]),
"serial": self._settings.get(["serial"]),
"host": self._settings.get(["host"]),
"username": (
"bblp"
if self._settings.get_boolean(["local_mqtt"])
else self._settings.get(["username"])
),
"access_code": self._settings.get(["access_code"]),
"local_mqtt": self._settings.get_boolean(["local_mqtt"]),
"region": self._settings.get(["region"]),
"email": self._settings.get(["email"]),
"auth_token": self._settings.get(["auth_token"]),
}
# Add disable_camera parameter if it's enabled in settings
if self._disable_camera:
self._log.info("Camera functionality is disabled in settings")
client_params["disable_camera"] = True
client_params["enable_camera_stream"] = False
try:
bambu_client = BambuClient(**client_params)
except TypeError as e:
# Handle the case where pybambu doesn't support these parameters yet
if "disable_camera" in str(e) or "enable_camera_stream" in str(e):
self._log.warning("This version of pybambu doesn't support camera disabling parameters, trying without them")
if "disable_camera" in client_params:
del client_params["disable_camera"]
if "enable_camera_stream" in client_params:
del client_params["enable_camera_stream"]
bambu_client = BambuClient(**client_params)
else:
raise
bambu_client.on_disconnect = self.on_disconnect(bambu_client.on_disconnect)
bambu_client.on_connect = self.on_connect(bambu_client.on_connect)
# Add more robust connection retry logic
connection_attempts = 0
max_attempts = 3
retry_delay = 2
while connection_attempts < max_attempts:
try:
self._log.info(f"Connection attempt {connection_attempts + 1}/{max_attempts}...")
# Check if we need to wait based on backoff
current_time = time.time()
if current_time - self._last_connection_attempt < self._connection_retry_backoff:
wait_time = self._connection_retry_backoff - (current_time - self._last_connection_attempt)
self._log.debug(f"Waiting {wait_time:.1f}s before retry due to backoff")
time.sleep(wait_time)
self._last_connection_attempt = time.time()
bambu_client.connect(callback=self.new_update)
# Wait a moment to verify connection
time.sleep(retry_delay)
if bambu_client.connected:
self._log.info(f"Bambu connection successful: {bambu_client.connected}")
# Reset connection error flags on successful connection
self._connection_error_logged = False
self._connection_retry_backoff = 10 # Reset backoff on success
break
self._log.warning("Connection attempt failed, retrying...")
connection_attempts += 1
# Increase backoff time for future connection attempts
self._connection_retry_backoff = min(300, self._connection_retry_backoff * 2) # Cap at 5 minutes
time.sleep(retry_delay)
except Exception as e:
if not self._connection_error_logged:
self._log.error(f"Error during connection attempt {connection_attempts + 1}: {str(e)}", exc_info=True)
self._connection_error_logged = True
else:
self._log.debug(f"Repeated connection error during attempt {connection_attempts + 1}: {str(e)}")
connection_attempts += 1
# Increase backoff time for future connection attempts
self._connection_retry_backoff = min(300, self._connection_retry_backoff * 2)
if connection_attempts < max_attempts:
time.sleep(retry_delay)
self.sendOk()
self._bambu_client = bambu_client self._bambu_client = bambu_client
self._log.info(f"Custom connection status: {self.is_connected}") self._log.info(f"Custom connection status: {self.is_connected}")
self.sendOk() self.sendOk()
def _patch_bambu_client_for_connection_errors(self):
"""Patch the BambuClient instance to handle connection errors gracefully"""
if not self._bambu_client:
return
# If we need to modify how the library handles connection errors, particularly
# for the Chamber Image functionality, we can patch the relevant methods here
# Check if the client has chamber image functionality and it's causing errors
if hasattr(self._bambu_client, '_chamber_image_thread') and self._bambu_client._chamber_image_thread:
original_run = None
# Find the run function in the thread class
if hasattr(self._bambu_client._chamber_image_thread, 'run'):
original_run = self._bambu_client._chamber_image_thread.run
# Create a wrapper that catches connection errors
def patched_run(*args, **kwargs):
try:
return original_run(*args, **kwargs)
except ConnectionRefusedError as e:
# Only log first occurrence to avoid log spam
if not self._camera_error_logged:
self._log.warning(f"Chamber image connection refused: {str(e)}. Further errors will be suppressed.")
self._camera_error_logged = True
return None
except Exception as e:
self._log.error(f"Chamber image error: {str(e)}", exc_info=True)
return None
# Apply the patched method
self._bambu_client._chamber_image_thread.run = patched_run
self._log.debug("Patched chamber image thread to handle connection errors")
def publish_mqtt(self, topic, payload): def publish_mqtt(self, topic, payload):
"""Publish a message to the MQTT broker""" """Publish a message to the MQTT broker"""
if self._mqtt_client and self._mqtt_connected: if self._mqtt_client and self._mqtt_connected:
@ -765,7 +1031,6 @@ class BambuVirtualPrinter:
serial = self._settings.get(["serial"]) serial = self._settings.get(["serial"])
topic = f"device/{serial}/request" topic = f"device/{serial}/request"
return self.publish_mqtt(topic, command) return self.publish_mqtt(topic, command)
def __str__(self): def __str__(self):
@ -775,6 +1040,10 @@ class BambuVirtualPrinter:
options={ options={
"device_type": self._settings.get(["device_type"]), "device_type": self._settings.get(["device_type"]),
"host": self._settings.get(["host"]), "host": self._settings.get(["host"]),
"local_mqtt": self._settings.get_boolean(["local_mqtt"]),
"region": self._settings.get(["region"]),
"email": self._settings.get(["email"]),
"auth_token": self._settings.get(["auth_token"]),
}, },
) )
@ -790,7 +1059,6 @@ class BambuVirtualPrinter:
if self._settings.get_boolean(["simulateReset"]): if self._settings.get_boolean(["simulateReset"]):
for item in self._settings.get(["resetLines"]): for item in self._settings.get(["resetLines"]):
self.sendIO(item + "\n") self.sendIO(item + "\n")
self._serial_io.reset() self._serial_io.reset()
def write(self, data: bytes) -> int: def write(self, data: bytes) -> int:
@ -822,6 +1090,7 @@ class BambuVirtualPrinter:
file_info = self._project_files_view.get_file_by_stem( file_info = self._project_files_view.get_file_by_stem(
file_path, [".gcode", ".3mf"] file_path, [".gcode", ".3mf"]
) )
if ( if (
self._selected_project_file is not None self._selected_project_file is not None
and file_info is not None and file_info is not None
@ -921,7 +1190,6 @@ class BambuVirtualPrinter:
self.start_continuous_temp_report(interval) self.start_continuous_temp_report(interval)
else: else:
self.stop_continuous_temp_report() self.stop_continuous_temp_report()
self.report_print_job_status() self.report_print_job_status()
return True return True
@ -1022,7 +1290,7 @@ class BambuVirtualPrinter:
acc_mag = acc_mag_scaled(speed_percentage) acc_mag = acc_mag_scaled(speed_percentage)
feed = feed_rate_scaled(speed_percentage) feed = feed_rate_scaled(speed_percentage)
# speed_level = 1.539 * (acc_mag**2) - 0.7032 * acc_mag + 4.0834 # speed_level = 1.539 * (acc_mag**2) - 0.7032 * acc_mag + 4.0834
return f"M204.2 K{acc_mag:.2f}\nM220 K{feed:.2f}\nM73.2 R{speed_frac:.2f}\n" # M1002 set_gcode_claim_speed_level ${speed_level:.0f}\n return f"M204.2 K{acc_mag:.2f}\nM220 K{feed:.2f}\nM73.2 R{speed_frac:.2f}\n" # M1002 set_gcode_claim_speed_level ${speed_level:.0f}\n"
speed_command = speed_adjust(percent) speed_command = speed_adjust(percent)
@ -1133,7 +1401,6 @@ class BambuVirtualPrinter:
def _processTemperatureQuery(self) -> bool: def _processTemperatureQuery(self) -> bool:
# Debug-Log hinzufügen, um zu prüfen, ob die Methode aufgerufen wird # Debug-Log hinzufügen, um zu prüfen, ob die Methode aufgerufen wird
self._log.debug(f"Processing temperature query - connected: {self.is_connected}") self._log.debug(f"Processing temperature query - connected: {self.is_connected}")
# Aktuelle Temperaturdaten ausgeben # Aktuelle Temperaturdaten ausgeben
self._log.debug(f"Current temperature data: Nozzle={self._telemetry.temp[0]}/{self._telemetry.targetTemp[0]}, " + self._log.debug(f"Current temperature data: Nozzle={self._telemetry.temp[0]}/{self._telemetry.targetTemp[0]}, " +
f"Bed={self._telemetry.bedTemp}/{self._telemetry.bedTargetTemp}") f"Bed={self._telemetry.bedTemp}/{self._telemetry.bedTargetTemp}")
@ -1142,39 +1409,49 @@ class BambuVirtualPrinter:
output = self._create_temperature_message() output = self._create_temperature_message()
self._log.debug(f"Sending temperature message: {output.strip()}") self._log.debug(f"Sending temperature message: {output.strip()}")
self.sendIO(output) self.sendIO(output)
return True return True
def close(self): def close(self):
"""Safely close all connections.""" """Safely close all connections."""
try: try:
# Log that we're starting to close connections
self._log.debug("Starting to close all connections...")
if self._mqtt_client and self._mqtt_connected: if self._mqtt_client and self._mqtt_connected:
self._log.debug("Stopping MQTT client loop and disconnecting...")
self._mqtt_client.loop_stop() self._mqtt_client.loop_stop()
self._mqtt_client.disconnect() self._mqtt_client.disconnect()
self._mqtt_connected = False self._mqtt_connected = False
self._custom_connected = False self._custom_connected = False
self._log.debug("MQTT client disconnected")
# Sicherstellen, dass wir keinen AttributError bekommen, wenn wir den BambuClient trennen # Sicherstellen, dass wir keinen AttributError bekommen, wenn wir den BambuClient trennen
if self._bambu_client: if self._bambu_client:
self._log.debug("Disconnecting BambuClient...")
try: try:
self._bambu_client.disconnect() self._bambu_client.disconnect()
self._log.debug("BambuClient disconnected successfully")
except AttributeError: except AttributeError:
# BambuClient hat keinen client-Attribut oder die disconnect-Methode funktioniert nicht wie erwartet # BambuClient hat keinen client-Attribut oder die disconnect-Methode funktioniert nicht wie erwartet
self._log.warning("BambuClient disconnect failed, cleaning up manually") self._log.warning("BambuClient disconnect failed, cleaning up manually")
# Manuell aufräumen # Manuell aufräumen
if hasattr(self._bambu_client, '_mqtt_client') and self._bambu_client._mqtt_client: if hasattr(self._bambu_client, '_mqtt_client') and self._bambu_client._mqtt_client:
try: try:
self._log.debug("Manually stopping BambuClient's MQTT client...")
self._bambu_client._mqtt_client.loop_stop() self._bambu_client._mqtt_client.loop_stop()
self._bambu_client._mqtt_client.disconnect() self._bambu_client._mqtt_client.disconnect()
except: self._log.debug("BambuClient's MQTT client manually disconnected")
pass except Exception as ex:
self._log.error(f"Error during manual MQTT client cleanup: {str(ex)}")
except Exception as e: except Exception as e:
self._log.error(f"Error during close: {e}", exc_info=True) self._log.error(f"Error during close: {e}", exc_info=True)
finally: finally:
# Immer in einen sicheren Zustand zurückkehren # Immer in einen sicheren Zustand zurückkehren
self._log.debug("Final cleanup in close() method")
self.change_state(self._state_idle) self.change_state(self._state_idle)
self._serial_io.close() self._serial_io.close()
self.stop() self.stop()
self._log.debug("Connection cleanup completed")
def stop(self): def stop(self):
self._running = False self._running = False

View File

@ -0,0 +1,17 @@
<div class="control-group">
<label class="control-label">{{ _('Connection Options') }}</label>
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.use_mqtt_bridge"> {{ _('Use MQTT Bridge') }}
<span class="help-block">
{{ _('Connect via a MQTT broker that bridges communications from the printer. Useful for connecting to a printer on a different network.') }}
</span>
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.disable_camera"> {{ _('Disable Camera Functionality') }}
<span class="help-block">
{{ _('Disable camera streaming and image capture to avoid connection errors. Enable this if you see frequent connection refused errors in the logs.') }}
</span>
</label>
</div>
</div>