Compare commits

..

10 Commits

Author SHA1 Message Date
3ccce10648 Füge paho-mqtt als Abhängigkeit für MQTT-Unterstützung hinzu 2025-03-02 10:13:08 +01:00
c99eb38655 Implement MQTT support for BambuVirtualPrinter, including connection, message handling, and publishing commands 2025-03-02 10:09:57 +01:00
698f8f4151 set default username for BambuVirtualPrinter 2025-03-02 09:51:01 +01:00
7a0293bac7 update plugin details and author information; change username and URLs 2025-03-02 09:38:56 +01:00
d0fd4a5434 0.1.7
add back missing PREPARE printing state and associate printing status
2024-09-27 09:38:23 -04:00
3c218a548d add issue templates, funding, and stale bot 2024-09-12 19:56:40 -04:00
03af51608d 0.1.6
* replace 0 with 1 bytes during reporting print status to trigger state change in OctoPrint sooner.
2024-09-06 01:39:48 -04:00
c00285b1b2 0.1.5
* adjust M220 feed rate modifier calculations
2024-09-05 22:35:34 -04:00
7f1ae5a24b 0.1.4 (#43)
* fix stuck Printing from SD state when canceled in slicer or on printer, #42
2024-09-04 16:48:16 -04:00
5754e81b72 0.1.3
fix file uploads
2024-08-25 14:20:45 -04:00
12 changed files with 275 additions and 39 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
github: [jneilliii]
patreon: jneilliii
custom: ['https://www.paypal.me/jneilliii']

26
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,26 @@
---
name: Bug report
about: Please make sure to check other issues, including closed ones, prior to submitting a bug report. Debug logs are required and any bug report submitted without them will be ignored and closed.
title: "[BUG]: "
labels: ''
assignees: ''
---
**Describe the Bug**
<!-- A clear and concise description of what the bug is. -->
**Expected Behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Debug Logs**
<!-- If logs are not included in your bug report it will be closed. Enable debug logging for octoprint.plugins.bambu_printer in OctoPrint's logging section of settings and recreate the issue then attach octoprint.log and plugin_bambu_printer_serial.log to this bug report. -->
**Screenshots**
<!-- Please share any relevant screenshots related to the issue. -->
**Printer and Plugin Setting Details**
* Printer model?
* Is your printer connected to Bambu Cloud?
* Is the plugin configured for local access only?

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Create a feature request for an improvement or change you'd like implemented.
title: "[FR]: "
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

16
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,16 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 14
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- enhancement
- bug
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
activity in 14 days. It will be closed if no further activity occurs in 7 days.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

27
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Mark Stale Issues
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
permissions:
actions: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been automatically marked as stale because it has not had activity in 14 days. It will be closed if no further activity occurs in 7 days'
days-before-stale: 14
days-before-close: 7
stale-issue-label: 'stale'
days-before-issue-stale: 14
days-before-pr-stale: -1
days-before-issue-close: 7
days-before-pr-close: -1
exempt-issue-labels: 'bug,enhancement'
- uses: actions/checkout@v4
- uses: gautamkrishnar/keepalive-workflow@v2
with:
use_api: true

View File

@ -85,7 +85,7 @@ class BambuPrintPlugin(
"serial": "", "serial": "",
"host": "", "host": "",
"access_code": "", "access_code": "",
"username": "bblp", "username": "octobambu",
"timelapse": False, "timelapse": False,
"bed_leveling": True, "bed_leveling": True,
"flow_cali": False, "flow_cali": False,
@ -286,10 +286,10 @@ class BambuPrintPlugin(
def get_update_information(self): def get_update_information(self):
return { return {
"bambu_printer": { "bambu_printer": {
"displayName": "Bambu Printer", "displayName": "Manus Bambu Printer",
"displayVersion": self._plugin_version, "displayVersion": self._plugin_version,
"type": "github_release", "type": "github_release",
"user": "jneilliii", "user": "ManuelW",
"repo": "OctoPrint-BambuPrinter", "repo": "OctoPrint-BambuPrinter",
"current": self._plugin_version, "current": self._plugin_version,
"stable_branch": { "stable_branch": {
@ -304,6 +304,6 @@ class BambuPrintPlugin(
"comittish": ["rc", "master"], "comittish": ["rc", "master"],
} }
], ],
"pip": "https://github.com/jneilliii/OctoPrint-BambuPrinter/archive/{target_version}.zip", "pip": "https://gitlab.fire-devils.org/3D-Druck/OctoPrint-BambuPrinter/archive/{target_version}.zip",
} }
} }

View File

@ -14,6 +14,9 @@ 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 ssl
from octoprint.util import RepeatedTimer from octoprint.util import RepeatedTimer
@ -105,6 +108,10 @@ class BambuVirtualPrinter:
self._serial_io.start() self._serial_io.start()
self._printer_thread.start() self._printer_thread.start()
self._mqtt_client = None
self._mqtt_connected = False
self._bambu_client = None
self._bambu_client: BambuClient = self._create_client_connection_async() self._bambu_client: BambuClient = self._create_client_connection_async()
@property @property
@ -193,7 +200,7 @@ class BambuVirtualPrinter:
or print_job_state == "FAILED" or print_job_state == "FAILED"
): ):
self.change_state(self._state_idle) self.change_state(self._state_idle)
elif print_job_state == "RUNNING": elif print_job_state == "RUNNING" or print_job_state == "PREPARE":
self.change_state(self._state_printing) self.change_state(self._state_printing)
elif print_job_state == "PAUSE": elif print_job_state == "PAUSE":
self.change_state(self._state_paused) self.change_state(self._state_paused)
@ -220,6 +227,48 @@ class BambuVirtualPrinter:
self._log.debug(f"on connect called") self._log.debug(f"on connect called")
return on_connect return on_connect
def _on_mqtt_connect(self, client, userdata, flags, rc):
self._log.debug(f"MQTT connected with result code: {rc}")
if rc == 0:
self._mqtt_connected = True
# Subscribe to the relevant topics for the Bambu printer
device_topic = f"device/{self._settings.get(['serial'])}/report"
client.subscribe(device_topic)
self._log.debug(f"Subscribed to topic: {device_topic}")
# Notify that we're connected
self.sendOk()
else:
self._mqtt_connected = False
self._log.error(f"Failed to connect to MQTT broker with result code: {rc}")
def _on_mqtt_disconnect(self, client, userdata, rc):
self._mqtt_connected = False
self._log.debug(f"MQTT disconnected with result code: {rc}")
def _on_mqtt_message(self, client, userdata, msg):
try:
# Decode message and update client data
payload = json.loads(msg.payload.decode('utf-8'))
# If this is a Bambu Lab printer message, process it
if 'print' in payload or 'info' in payload:
# Forward the message to pybambu for processing
if self._bambu_client:
self._bambu_client._process_message(msg.topic, payload)
# Trigger our update handler
if 'print' in payload:
self.new_update("event_printer_data_update")
if 'info' in payload and 'hms' in payload['info']:
self.new_update("event_hms_errors")
self._log.debug(f"MQTT message received on topic {msg.topic}")
except Exception as e:
self._log.error(f"Error processing MQTT message: {e}")
def _create_client_connection_async(self): def _create_client_connection_async(self):
self._create_client_connection() self._create_client_connection()
if self._bambu_client is None: if self._bambu_client is None:
@ -237,31 +286,82 @@ class BambuVirtualPrinter:
self._log.debug(msg) self._log.debug(msg)
raise ValueError(msg) raise ValueError(msg)
self._log.debug( use_local_mqtt = self._settings.get_boolean(['local_mqtt'])
f"connecting via 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( bambu_client = BambuClient(
device_type=self._settings.get(["device_type"]), device_type=self._settings.get(["device_type"]),
serial=self._settings.get(["serial"]), serial=self._settings.get(["serial"]),
host=self._settings.get(["host"]), host=self._settings.get(["host"]),
username=( username="bambuocto",
"bblp"
if self._settings.get_boolean(["local_mqtt"])
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=use_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"]),
) )
bambu_client.on_disconnect = self.on_disconnect(bambu_client.on_disconnect)
bambu_client.on_connect = self.on_connect(bambu_client.on_connect) # Set up our own MQTT client
bambu_client.connect(callback=self.new_update) self._mqtt_client = mqtt.Client()
self._log.info(f"bambu connection status: {bambu_client.connected}") self._mqtt_client.on_connect = self._on_mqtt_connect
self.sendOk() 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}")
except Exception as e:
self._log.error(f"Failed to connect to MQTT broker: {e}")
raise
# Inject our MQTT client into the BambuClient
bambu_client._mqtt_client = self._mqtt_client
bambu_client.connected = True
# Store the Bambu client
self._bambu_client = bambu_client self._bambu_client = bambu_client
self._log.info(f"Bambu connection status: {bambu_client.connected}")
def publish_mqtt(self, topic, payload):
"""Publish a message to the MQTT broker"""
if self._mqtt_client and self._mqtt_connected:
return self._mqtt_client.publish(topic, json.dumps(payload))
return False
# Override BambuClient's publish method to use our MQTT client
def publish(self, command):
"""Publish a command using our MQTT client"""
if not self._mqtt_connected:
self._log.error("Cannot publish command: MQTT not connected")
return False
serial = self._settings.get(["serial"])
topic = f"device/{serial}/request"
return self.publish_mqtt(topic, command)
def __str__(self): def __str__(self):
return "BAMBU(read_timeout={read_timeout},write_timeout={write_timeout},options={options})".format( return "BAMBU(read_timeout={read_timeout},write_timeout={write_timeout},options={options})".format(
read_timeout=self.timeout, read_timeout=self.timeout,
@ -474,19 +574,51 @@ class BambuVirtualPrinter:
gcode_command = commands.SEND_GCODE_TEMPLATE gcode_command = commands.SEND_GCODE_TEMPLATE
percent = int(data.replace("M220 S", "")) percent = int(data.replace("M220 S", ""))
if percent is None or percent < 1 or percent > 166: def speed_fraction(speed_percent):
return True return math.floor(10000 / speed_percent) / 100
speed_fraction = 100 / percent def acceleration_magnitude(speed_percent):
acceleration = math.exp((speed_fraction - 1.0191) / -0.814) return math.exp((speed_fraction(speed_percent) - 1.0191) / -0.8139)
feed_rate = (
2.1645 * (acceleration**3) def feed_rate(speed_percent):
- 5.3247 * (acceleration**2) return 6.426e-5 * speed_percent ** 2 - 2.484e-3 * speed_percent + 0.654
+ 4.342 * acceleration
- 0.181 def linear_interpolate(x, x_points, y_points):
) if x <= x_points[0]: return y_points[0]
speed_level = 1.539 * (acceleration**2) - 0.7032 * acceleration + 4.0834 if x >= x_points[-1]: return y_points[-1]
speed_command = f"M204.2 K${acceleration:.2f} \nM220 K${feed_rate:.2f} \nM73.2 R${speed_fraction:.2f} \nM1002 set_gcode_claim_speed_level ${speed_level:.0f}\n" for i in range(len(x_points) - 1):
if x_points[i] <= x < x_points[i + 1]:
t = (x - x_points[i]) / (x_points[i + 1] - x_points[i])
return y_points[i] * (1 - t) + y_points[i + 1] * t
def scale_to_data_points(func, data_points):
data_points.sort(key=lambda x: x[0])
speeds, values = zip(*data_points)
scaling_factors = [v / func(s) for s, v in zip(speeds, values)]
return lambda x: func(x) * linear_interpolate(x, speeds, scaling_factors)
def speed_adjust(speed_percentage):
if not 30 <= speed_percentage <= 180:
speed_percentage = 100
bambu_params = {
"speed": [50, 100, 124, 166],
"acceleration": [0.3, 1.0, 1.4, 1.6],
"feed_rate": [0.7, 1.0, 1.4, 2.0]
}
acc_mag_scaled = scale_to_data_points(acceleration_magnitude,
list(zip(bambu_params["speed"], bambu_params["acceleration"])))
feed_rate_scaled = scale_to_data_points(feed_rate,
list(zip(bambu_params["speed"], bambu_params["feed_rate"])))
speed_frac = speed_fraction(speed_percentage)
acc_mag = acc_mag_scaled(speed_percentage)
feed = feed_rate_scaled(speed_percentage)
# 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
speed_command = speed_adjust(percent)
gcode_command["print"]["param"] = speed_command gcode_command["print"]["param"] = speed_command
if self.bambu_client.publish(gcode_command): if self.bambu_client.publish(gcode_command):
@ -548,8 +680,9 @@ class BambuVirtualPrinter:
def report_print_job_status(self): def report_print_job_status(self):
if self.current_print_job is not None: if self.current_print_job is not None:
file_position = 1 if self.current_print_job.file_position == 0 else self.current_print_job.file_position
self.sendIO( self.sendIO(
f"SD printing byte {self.current_print_job.file_position}" f"SD printing byte {file_position}"
f"/{self.current_print_job.file_info.size}" f"/{self.current_print_job.file_info.size}"
) )
else: else:
@ -601,6 +734,9 @@ class BambuVirtualPrinter:
return False return False
def close(self): def close(self):
if self._mqtt_client and self._mqtt_connected:
self._mqtt_client.loop_stop()
self._mqtt_client.disconnect()
if self.bambu_client.connected: if self.bambu_client.connected:
self.bambu_client.disconnect() self.bambu_client.disconnect()
self.change_state(self._state_idle) self.change_state(self._state_idle)

View File

@ -117,7 +117,7 @@ class IoTFTPSConnection:
# But since we operate in prot p mode # But since we operate in prot p mode
# we can close the connection always. # we can close the connection always.
# This is cursed but it works. # This is cursed but it works.
if "vsFTPd" in self.welcome: if "vsFTPd" in self.ftps_session.welcome:
conn.unwrap() conn.unwrap()
else: else:
conn.shutdown(socket.SHUT_RDWR) conn.shutdown(socket.SHUT_RDWR)

View File

@ -1,8 +1,6 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from octoprint_bambu_printer.printer.file_system.file_info import FileInfo from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
from octoprint_bambu_printer.printer.print_job import PrintJob
from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState

View File

@ -22,6 +22,7 @@ class PrintingState(APrinterState):
def __init__(self, printer: BambuVirtualPrinter) -> None: def __init__(self, printer: BambuVirtualPrinter) -> None:
super().__init__(printer) super().__init__(printer)
self._current_print_job = None
self._is_printing = False self._is_printing = False
self._sd_printing_thread = None self._sd_printing_thread = None
@ -36,6 +37,7 @@ class PrintingState(APrinterState):
self._is_printing = False self._is_printing = False
self._sd_printing_thread.join() self._sd_printing_thread.join()
self._sd_printing_thread = None self._sd_printing_thread = None
self._printer.current_print_job = None
def _start_worker_thread(self): def _start_worker_thread(self):
if self._sd_printing_thread is None: if self._sd_printing_thread is None:

View File

@ -7,3 +7,11 @@
### ###
. .
pytest~=7.4.4
pybambu~=1.0.1
OctoPrint~=1.10.2
setuptools~=70.0.0
pyserial~=3.5
Flask~=2.2.5
paho-mqtt~=2.1.0

View File

@ -14,20 +14,20 @@ plugin_package = "octoprint_bambu_printer"
plugin_name = "OctoPrint-BambuPrinter" plugin_name = "OctoPrint-BambuPrinter"
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
plugin_version = "0.1.2" plugin_version = "1.0.0"
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
# module # module
plugin_description = """Connects OctoPrint to BambuLabs printers.""" plugin_description = """Connects OctoPrint to BambuLabs printers."""
# The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module
plugin_author = "jneilliii" plugin_author = "ManuelW"
# The plugin's author's mail address. # The plugin's author's mail address.
plugin_author_email = "jneilliii+github@gmail.com" plugin_author_email = "manuelw@example.com"
# The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module # The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module
plugin_url = "https://github.com/jneilliii/OctoPrint-BambuPrinter" plugin_url = "https://gitlab.fire-devils.org/3D-Druck/OctoPrint-BambuPrinter"
# The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module # The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module
plugin_license = "AGPLv3" plugin_license = "AGPLv3"