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
import logging
import logging.handlers
import json
import paho.mqtt.client as mqtt
from octoprint.util import RepeatedTimer
@ -45,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()
@ -230,31 +393,46 @@ 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)
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"]),
)
# 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(
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)