diff --git a/octoprint_bambu_printer/printer/bambu_virtual_printer.py b/octoprint_bambu_printer/printer/bambu_virtual_printer.py index 7de5f6c..64c29d9 100644 --- a/octoprint_bambu_printer/printer/bambu_virtual_printer.py +++ b/octoprint_bambu_printer/printer/bambu_virtual_printer.py @@ -14,9 +14,8 @@ from octoprint_bambu_printer.printer.print_job import PrintJob from pybambu import BambuClient, commands import logging import logging.handlers -import paho.mqtt.client as mqtt import json -import ssl +import paho.mqtt.client as mqtt from octoprint.util import RepeatedTimer @@ -48,6 +47,167 @@ class BambuPrinterTelemetry: extruderCount: int = 1 +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() + 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") + + 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 + 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""" + if 'print' in data and 'gcode_state' in data['print']: + self._device_data.print_job.gcode_state = data['print']['gcode_state'] + + if 'temperature' in data: + temp = self._device_data.temperature + temp_data = data['temperature'] + + if 'nozzle_temp' in temp_data: + temp.nozzle_temp = temp_data['nozzle_temp'] + if 'target_nozzle_temp' in temp_data: + temp.target_nozzle_temp = temp_data['target_nozzle_temp'] + if 'bed_temp' in temp_data: + temp.bed_temp = temp_data['bed_temp'] + if 'target_bed_temp' in temp_data: + temp.target_bed_temp = temp_data['target_bed_temp'] + if 'chamber_temp' in temp_data: + temp.chamber_temp = temp_data['chamber_temp'] + + def _process_hms_message(self, data): + """Process HMS error messages""" + if 'hms' in data: + error_count = 0 + hms_errors = {"Count": 0} + + for error in data['hms']: + error_count += 1 + hms_errors[f"{error_count}-Error"] = error['msg'] + + hms_errors["Count"] = error_count + self._device_data.hms.errors = hms_errors + + def connect(self, callback=None): + """Connect to MQTT broker""" + if callback: + self._callbacks['callback'] = callback + + try: + self._mqtt_client.connect(self._host, self._mqtt_port) + self._mqtt_client.loop_start() + return True + except Exception as e: + self._log.error(f"Failed to connect to MQTT broker: {str(e)}") + 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: + return False + + try: + topic_base = f"device/{self._device_type}/{self._serial}" + if 'print' in command and 'param' in command['print']: + # Assuming commands go to command topic + message = json.dumps(command) + self._mqtt_client.publish(f"{topic_base}/cmd", message) + return True + except Exception as e: + self._log.error(f"Failed to publish command: {str(e)}") + + return False + + # noinspection PyBroadException class BambuVirtualPrinter: gcode_executor = GCodeExecutor() @@ -644,107 +804,51 @@ class BambuVirtualPrinter: if ( self._settings.get(["device_type"]) == "" 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" self._log.debug(msg) raise ValueError(msg) - use_local_mqtt = self._settings.get_boolean(['local_mqtt']) - self._log.debug(f"connecting via local mqtt: {use_local_mqtt}") - - # 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 + # Check if we should use MQTT bridge mode + use_mqtt_bridge = self._settings.get_boolean(["use_mqtt_bridge"]) - # Instead of modifying bambu_client.connected, we'll use our custom property - self._custom_connected = True - - # Store the Bambu client + if use_mqtt_bridge: + self._log.debug( + f"connecting via mqtt bridge: {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'])}" + ) + bambu_client = BambuClient( + 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"]), + ) + + bambu_client.on_disconnect = self.on_disconnect(bambu_client.on_disconnect) + bambu_client.on_connect = self.on_connect(bambu_client.on_connect) + bambu_client.connect(callback=self.new_update) + self._log.info(f"bambu connection status: {bambu_client.connected}") + self.sendOk() self._bambu_client = bambu_client self._log.info(f"Custom connection status: {self.is_connected}")