Implement MQTT bridge client for Bambu printer integration

This commit is contained in:
Manuel Weiser 2025-03-02 15:27:57 +01:00
parent d0fd4a5434
commit f64fa7aea2

View File

@ -14,6 +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 json
import paho.mqtt.client as mqtt
from octoprint.util import RepeatedTimer from octoprint.util import RepeatedTimer
@ -45,6 +47,167 @@ class BambuPrinterTelemetry:
extruderCount: int = 1 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 # noinspection PyBroadException
class BambuVirtualPrinter: class BambuVirtualPrinter:
gcode_executor = GCodeExecutor() gcode_executor = GCodeExecutor()
@ -230,13 +393,27 @@ 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)
# Check if we should use MQTT bridge mode
use_mqtt_bridge = self._settings.get_boolean(["use_mqtt_bridge"])
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( 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'])}"
) )
@ -255,6 +432,7 @@ class BambuVirtualPrinter:
email=self._settings.get(["email"]), email=self._settings.get(["email"]),
auth_token=self._settings.get(["auth_token"]), auth_token=self._settings.get(["auth_token"]),
) )
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)
bambu_client.connect(callback=self.new_update) bambu_client.connect(callback=self.new_update)