Compare commits

...

4 Commits

3 changed files with 348 additions and 130 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

@ -64,7 +64,7 @@ class BambuMqttBridgeClient:
self._host = host self._host = host
self._mqtt_port = mqtt_port self._mqtt_port = mqtt_port
self.connected = False self.connected = False
self._mqtt_client = mqtt.Client() self._mqtt_client = mqtt.Client(f"octoprint_bambu_bridge_{serial}")
self._device_data = self._create_empty_device_data() self._device_data = self._create_empty_device_data()
self._callbacks = {} self._callbacks = {}
@ -100,6 +100,7 @@ class BambuMqttBridgeClient:
topic_base = f"device/{self._device_type}/{self._serial}" 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")
self._mqtt_client.subscribe(f"{topic_base}/report_hms") self._mqtt_client.subscribe(f"{topic_base}/report_hms")
self._log.info(f"Subscribed to topic: {topic_base}/#")
if 'callback' in self._callbacks: if 'callback' in self._callbacks:
self._callbacks['callback']("event_printer_data_update") self._callbacks['callback']("event_printer_data_update")
@ -112,6 +113,15 @@ class BambuMqttBridgeClient:
def _on_disconnect(self, client, userdata, rc): def _on_disconnect(self, client, userdata, rc):
self._log.warning(f"Disconnected from MQTT broker with code: {rc}") self._log.warning(f"Disconnected from MQTT broker with code: {rc}")
self.connected = False 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): if hasattr(self, 'on_disconnect') and callable(self.on_disconnect):
self.on_disconnect(client, userdata, rc) self.on_disconnect(client, userdata, rc)
@ -135,48 +145,99 @@ class BambuMqttBridgeClient:
def _process_report_message(self, data): def _process_report_message(self, data):
"""Process printer status report messages""" """Process printer status report messages"""
if 'print' in data and 'gcode_state' in data['print']: try:
self._device_data.print_job.gcode_state = data['print']['gcode_state'] # 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: if 'temperature' in data:
temp = self._device_data.temperature temp = self._device_data.temperature
temp_data = data['temperature'] temp_data = data['temperature']
if 'nozzle_temp' in temp_data: if 'nozzle_temp' in temp_data:
temp.nozzle_temp = temp_data['nozzle_temp'] temp.nozzle_temp = float(temp_data['nozzle_temp'])
if 'target_nozzle_temp' in temp_data: if 'target_nozzle_temp' in temp_data:
temp.target_nozzle_temp = temp_data['target_nozzle_temp'] temp.target_nozzle_temp = float(temp_data['target_nozzle_temp'])
if 'bed_temp' in temp_data: if 'bed_temp' in temp_data:
temp.bed_temp = temp_data['bed_temp'] temp.bed_temp = float(temp_data['bed_temp'])
if 'target_bed_temp' in temp_data: if 'target_bed_temp' in temp_data:
temp.target_bed_temp = temp_data['target_bed_temp'] temp.target_bed_temp = float(temp_data['target_bed_temp'])
if 'chamber_temp' in temp_data: if 'chamber_temp' in temp_data:
temp.chamber_temp = temp_data['chamber_temp'] 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): def _process_hms_message(self, data):
"""Process HMS error messages""" """Process HMS error messages"""
try:
if 'hms' in data: if 'hms' in data:
error_count = 0 error_count = 0
hms_errors = {"Count": 0} hms_errors = {"Count": 0}
for error in data['hms']: for error in data['hms']:
error_count += 1 error_count += 1
if isinstance(error, dict) and 'msg' in error:
hms_errors[f"{error_count}-Error"] = error['msg'] hms_errors[f"{error_count}-Error"] = error['msg']
else:
hms_errors[f"{error_count}-Error"] = str(error)
hms_errors["Count"] = error_count hms_errors["Count"] = error_count
self._device_data.hms.errors = hms_errors 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): def connect(self, callback=None):
"""Connect to MQTT broker""" """Connect to MQTT broker"""
if callback: if callback:
self._callbacks['callback'] = callback self._callbacks['callback'] = callback
try: 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.connect(self._host, self._mqtt_port)
self._mqtt_client.loop_start() self._mqtt_client.loop_start()
return True
# 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: except Exception as e:
self._log.error(f"Failed to connect to MQTT broker: {str(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 return False
def disconnect(self): def disconnect(self):
@ -193,22 +254,25 @@ class BambuMqttBridgeClient:
def publish(self, command): def publish(self, command):
"""Publishes command to device""" """Publishes command to device"""
if not self.connected: if not self.connected:
self._log.error("Cannot publish: Not connected to MQTT broker")
return False return False
try: try:
topic_base = f"device/{self._device_type}/{self._serial}" topic_base = f"device/{self._device_type}/{self._serial}"
if 'print' in command and 'param' in command['print']: if 'print' in command and 'param' in command['print']:
# Assuming commands go to command topic # Commands go to command topic
message = json.dumps(command) message = json.dumps(command)
self._mqtt_client.publish(f"{topic_base}/cmd", message) result = self._mqtt_client.publish(f"{topic_base}/cmd", message)
return True 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: except Exception as e:
self._log.error(f"Failed to publish command: {str(e)}") self._log.error(f"Failed to publish command: {str(e)}", exc_info=True)
return False return False
# noinspection PyBroadException
class BambuVirtualPrinter: class BambuVirtualPrinter:
gcode_executor = GCodeExecutor() gcode_executor = GCodeExecutor()
@ -267,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
@ -295,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
@ -340,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":
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 # Gib den aktuellen Status detaillierter aus
device_data = self.bambu_client.get_device()
print_job_state = device_data.print_job.gcode_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]}, " + 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}, " +
f"Chamber: {self._telemetry.chamberTemp}") f"Chamber: {self._telemetry.chamberTemp}")
# Rufe trotzdem die BambuClient-Daten ab, falls verfügbar # Process the printer state even if it's "unknown"
try: if print_job_state:
device_data = self.bambu_client.get_device() self._process_print_state(print_job_state)
print_job_state = device_data.print_job.gcode_state
self._log.debug(f"BambuClient printer state: {print_job_state}")
if (
print_job_state == "IDLE"
or print_job_state == "FINISH"
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()
@ -406,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
@ -452,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)
@ -468,11 +523,9 @@ 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
@ -481,9 +534,6 @@ 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)
@ -496,7 +546,6 @@ class BambuVirtualPrinter:
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:
@ -523,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)
@ -616,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)
@ -629,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:
@ -640,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"]:
@ -669,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
@ -690,6 +741,8 @@ class BambuVirtualPrinter:
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"""
@ -735,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)
@ -744,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'])
@ -792,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()
@ -816,6 +869,9 @@ class BambuVirtualPrinter:
self._log.debug( self._log.debug(
f"connecting via mqtt bridge: {self._settings.get(['mqtt_host'])}:{self._settings.get(['mqtt_port'])}" f"connecting via mqtt bridge: {self._settings.get(['mqtt_host'])}:{self._settings.get(['mqtt_port'])}"
) )
self._log.debug(
f"Creating MQTT bridge client: {self._settings.get(['mqtt_host'])}:{self._settings.get(['mqtt_port'])}"
)
# Create MQTT bridge client # Create MQTT bridge client
bambu_client = BambuMqttBridgeClient( bambu_client = BambuMqttBridgeClient(
device_type=self._settings.get(["device_type"]), device_type=self._settings.get(["device_type"]),
@ -828,32 +884,138 @@ class BambuVirtualPrinter:
self._log.debug( self._log.debug(
f"connecting via local mqtt: {self._settings.get_boolean(['local_mqtt'])}" f"connecting via local mqtt: {self._settings.get_boolean(['local_mqtt'])}"
) )
bambu_client = BambuClient( self._log.debug(
device_type=self._settings.get(["device_type"]), f"Creating standard BambuClient with local_mqtt: {self._settings.get_boolean(['local_mqtt'])}"
serial=self._settings.get(["serial"]), )
host=self._settings.get(["host"]),
username=( # 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" "bblp"
if self._settings.get_boolean(["local_mqtt"]) if self._settings.get_boolean(["local_mqtt"])
else self._settings.get(["username"]) else self._settings.get(["username"])
), ),
access_code=self._settings.get(["access_code"]), "access_code": self._settings.get(["access_code"]),
local_mqtt=self._settings.get_boolean(["local_mqtt"]), "local_mqtt": self._settings.get_boolean(["local_mqtt"]),
region=self._settings.get(["region"]), "region": self._settings.get(["region"]),
email=self._settings.get(["email"]), "email": self._settings.get(["email"]),
auth_token=self._settings.get(["auth_token"]), "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_disconnect = self.on_disconnect(bambu_client.on_disconnect)
bambu_client.on_connect = self.on_connect(bambu_client.on_connect) 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) bambu_client.connect(callback=self.new_update)
self._log.info(f"bambu connection status: {bambu_client.connected}")
# 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.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:
@ -869,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):
@ -879,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"]),
}, },
) )
@ -894,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:
@ -926,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
@ -1025,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
@ -1126,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)
@ -1237,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}")
@ -1246,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>