Refactor plugin. Add typing
This commit is contained in:
		@@ -1,260 +1,10 @@
 | 
			
		||||
# coding=utf-8
 | 
			
		||||
from __future__ import absolute_import
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import threading
 | 
			
		||||
import time
 | 
			
		||||
import flask
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
import octoprint.plugin
 | 
			
		||||
from octoprint.events import Events
 | 
			
		||||
from octoprint.util import get_formatted_size, get_formatted_datetime, is_hidden_path
 | 
			
		||||
from octoprint.server.util.flask import no_firstrun_access
 | 
			
		||||
from octoprint.server.util.tornado import LargeResponseHandler, UrlProxyHandler, path_validation_factory
 | 
			
		||||
from octoprint.access.permissions import Permissions
 | 
			
		||||
from urllib.parse import quote as urlquote
 | 
			
		||||
from .ftpsclient import IoTFTPSClient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BambuPrintPlugin(octoprint.plugin.SettingsPlugin,
 | 
			
		||||
                       octoprint.plugin.TemplatePlugin,
 | 
			
		||||
                       octoprint.plugin.AssetPlugin,
 | 
			
		||||
                       octoprint.plugin.EventHandlerPlugin,
 | 
			
		||||
                       octoprint.plugin.SimpleApiPlugin,
 | 
			
		||||
                       octoprint.plugin.BlueprintPlugin):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_assets(self):
 | 
			
		||||
        return {'js': ["js/bambu_printer.js"]}
 | 
			
		||||
    def get_template_configs(self):
 | 
			
		||||
        return [{"type": "settings", "custom_bindings": True},
 | 
			
		||||
                {"type": "generic", "custom_bindings": True, "template": "bambu_timelapse.jinja2"}] #, {"type": "generic", "custom_bindings": True, "template": "bambu_printer.jinja2"}]
 | 
			
		||||
 | 
			
		||||
    def get_settings_defaults(self):
 | 
			
		||||
        return {"device_type": "X1C",
 | 
			
		||||
                "serial": "",
 | 
			
		||||
                "host": "",
 | 
			
		||||
                "access_code": "",
 | 
			
		||||
                "username": "bblp",
 | 
			
		||||
                "timelapse": False,
 | 
			
		||||
                "bed_leveling": True,
 | 
			
		||||
                "flow_cali": False,
 | 
			
		||||
                "vibration_cali": True,
 | 
			
		||||
                "layer_inspect": True,
 | 
			
		||||
                "use_ams": False,
 | 
			
		||||
                "local_mqtt": True,
 | 
			
		||||
                "region": "",
 | 
			
		||||
                "email": "",
 | 
			
		||||
                "auth_token": "",
 | 
			
		||||
                "always_use_default_options": False
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
    def is_api_adminonly(self):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def get_api_commands(self):
 | 
			
		||||
        return {"register": ["email", "password", "region", "auth_token"]}
 | 
			
		||||
    def on_api_command(self, command, data):
 | 
			
		||||
        if command == "register":
 | 
			
		||||
            if "email" in data and "password" in data and "region" in data and "auth_token" in data:
 | 
			
		||||
                self._logger.info(f"Registering user {data['email']}")
 | 
			
		||||
                from pybambu import BambuCloud
 | 
			
		||||
                bambu_cloud = BambuCloud(data["region"], data["email"], data["password"], data["auth_token"])
 | 
			
		||||
                bambu_cloud.login(data["region"], data["email"], data["password"])
 | 
			
		||||
                return flask.jsonify({"auth_token": bambu_cloud.auth_token, "username": bambu_cloud.username})
 | 
			
		||||
    def on_event(self, event, payload):
 | 
			
		||||
        if event == Events.TRANSFER_DONE:
 | 
			
		||||
            self._printer.commands("M20 L T", force=True)
 | 
			
		||||
    def support_3mf_files(self):
 | 
			
		||||
        return {'machinecode': {'3mf': ["3mf"]}}
 | 
			
		||||
 | 
			
		||||
    def upload_to_sd(self, printer, filename, path, sd_upload_started, sd_upload_succeeded, sd_upload_failed, *args, **kwargs):
 | 
			
		||||
        self._logger.debug(f"Starting upload from {filename} to {filename}")
 | 
			
		||||
        sd_upload_started(filename, filename)
 | 
			
		||||
        def process():
 | 
			
		||||
            host = self._settings.get(["host"])
 | 
			
		||||
            access_code = self._settings.get(["access_code"])
 | 
			
		||||
            elapsed = time.monotonic()
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
 | 
			
		||||
                if ftp.upload_file(path, f"{filename}"):
 | 
			
		||||
                    elapsed = time.monotonic() - elapsed
 | 
			
		||||
                    sd_upload_succeeded(filename, filename, elapsed)
 | 
			
		||||
                    # remove local file after successful upload to Bambu
 | 
			
		||||
                    # self._file_manager.remove_file("local", filename)
 | 
			
		||||
                else:
 | 
			
		||||
                    raise Exception("upload failed")
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                elapsed = time.monotonic() - elapsed
 | 
			
		||||
                sd_upload_failed(filename, filename, elapsed)
 | 
			
		||||
                self._logger.debug(f"Error uploading file {filename}")
 | 
			
		||||
 | 
			
		||||
        thread = threading.Thread(target=process)
 | 
			
		||||
        thread.daemon = True
 | 
			
		||||
        thread.start()
 | 
			
		||||
 | 
			
		||||
        return filename
 | 
			
		||||
 | 
			
		||||
    def get_template_vars(self):
 | 
			
		||||
        return {"plugin_version": self._plugin_version}
 | 
			
		||||
 | 
			
		||||
    def virtual_printer_factory(self, comm_instance, port, baudrate, read_timeout):
 | 
			
		||||
        if not port == "BAMBU":
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        if self._settings.get(["serial"]) == "" or self._settings.get(["host"]) == "" or self._settings.get(["access_code"]) == "":
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        import logging.handlers
 | 
			
		||||
 | 
			
		||||
        from octoprint.logging.handlers import CleaningTimedRotatingFileHandler
 | 
			
		||||
 | 
			
		||||
        seriallog_handler = CleaningTimedRotatingFileHandler(
 | 
			
		||||
            self._settings.get_plugin_logfile_path(postfix="serial"),
 | 
			
		||||
            when="D",
 | 
			
		||||
            backupCount=3,
 | 
			
		||||
        )
 | 
			
		||||
        seriallog_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
 | 
			
		||||
        seriallog_handler.setLevel(logging.DEBUG)
 | 
			
		||||
 | 
			
		||||
        from . import virtual
 | 
			
		||||
 | 
			
		||||
        serial_obj = virtual.BambuPrinter(
 | 
			
		||||
            self._settings,
 | 
			
		||||
            self._printer_profile_manager,
 | 
			
		||||
            data_folder=self.get_plugin_data_folder(),
 | 
			
		||||
            seriallog_handler=seriallog_handler,
 | 
			
		||||
            read_timeout=float(read_timeout),
 | 
			
		||||
            faked_baudrate=baudrate,
 | 
			
		||||
        )
 | 
			
		||||
        return serial_obj
 | 
			
		||||
 | 
			
		||||
    def get_additional_port_names(self, *args, **kwargs):
 | 
			
		||||
        if self._settings.get(["serial"]) != "" and self._settings.get(["host"]) != "" and self._settings.get(["access_code"]) != "":
 | 
			
		||||
            return ["BAMBU"]
 | 
			
		||||
        else:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
    def get_timelapse_file_list(self):
 | 
			
		||||
        if flask.request.path.startswith('/api/timelapse'):
 | 
			
		||||
            def process():
 | 
			
		||||
                host = self._settings.get(["host"])
 | 
			
		||||
                access_code = self._settings.get(["access_code"])
 | 
			
		||||
                return_file_list = []
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
 | 
			
		||||
                    if self._settings.get(["device_type"]) in ["X1", "X1C"]:
 | 
			
		||||
                        timelapse_file_list = ftp.list_files("timelapse/", ".mp4") or []
 | 
			
		||||
                    else:
 | 
			
		||||
                        timelapse_file_list = ftp.list_files("timelapse/", ".avi") or []
 | 
			
		||||
 | 
			
		||||
                    for entry in timelapse_file_list:
 | 
			
		||||
                        if entry.startswith("/"):
 | 
			
		||||
                            filename = entry[1:].replace("timelapse/", "")
 | 
			
		||||
                        else:
 | 
			
		||||
                            filename = entry.replace("timelapse/", "")
 | 
			
		||||
 | 
			
		||||
                        filesize = ftp.ftps_session.size(f"timelapse/{filename}")
 | 
			
		||||
                        date_str = ftp.ftps_session.sendcmd(f"MDTM timelapse/{filename}").replace("213 ", "")
 | 
			
		||||
                        filedate = datetime.datetime.strptime(date_str, "%Y%m%d%H%M%S").replace(tzinfo=datetime.timezone.utc).timestamp()
 | 
			
		||||
 | 
			
		||||
                        return_file_list.append(
 | 
			
		||||
                            {
 | 
			
		||||
                                "bytes": filesize,
 | 
			
		||||
                                "date": get_formatted_datetime(datetime.datetime.fromtimestamp(filedate)),
 | 
			
		||||
                                "name": filename,
 | 
			
		||||
                                "size": get_formatted_size(filesize),
 | 
			
		||||
                                "thumbnail": "/plugin/bambu_printer/thumbnail/" + filename.replace(".mp4", ".jpg").replace(".avi", ".jpg"),
 | 
			
		||||
                                "timestamp": filedate,
 | 
			
		||||
                                "url": f"/plugin/bambu_printer/timelapse/{filename}"
 | 
			
		||||
                            })
 | 
			
		||||
 | 
			
		||||
                    self._plugin_manager.send_plugin_message(self._identifier, {'files': return_file_list})
 | 
			
		||||
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    self._logger.debug(f"Error getting timelapse files: {e}")
 | 
			
		||||
 | 
			
		||||
            thread = threading.Thread(target=process)
 | 
			
		||||
            thread.daemon = True
 | 
			
		||||
            thread.start()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _hook_octoprint_server_api_before_request(self, *args, **kwargs):
 | 
			
		||||
        return [self.get_timelapse_file_list]
 | 
			
		||||
 | 
			
		||||
    @octoprint.plugin.BlueprintPlugin.route("/timelapse/<filename>", methods=["GET"])
 | 
			
		||||
    @octoprint.server.util.flask.restricted_access
 | 
			
		||||
    @no_firstrun_access
 | 
			
		||||
    @Permissions.TIMELAPSE_DOWNLOAD.require(403)
 | 
			
		||||
    def downloadTimelapse(self, filename):
 | 
			
		||||
        dest_filename = os.path.join(self.get_plugin_data_folder(), filename)
 | 
			
		||||
        host = self._settings.get(["host"])
 | 
			
		||||
        access_code = self._settings.get(["access_code"])
 | 
			
		||||
 | 
			
		||||
        if not os.path.exists(dest_filename):
 | 
			
		||||
            ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
 | 
			
		||||
            download_result = ftp.download_file(
 | 
			
		||||
                source=f"timelapse/{filename}",
 | 
			
		||||
                dest=dest_filename,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return flask.redirect("/plugin/bambu_printer/download/timelapse/" + urlquote(filename), code=302)
 | 
			
		||||
 | 
			
		||||
    @octoprint.plugin.BlueprintPlugin.route("/thumbnail/<filename>", methods=["GET"])
 | 
			
		||||
    @octoprint.server.util.flask.restricted_access
 | 
			
		||||
    @no_firstrun_access
 | 
			
		||||
    @Permissions.TIMELAPSE_DOWNLOAD.require(403)
 | 
			
		||||
    def downloadThumbnail(self, filename):
 | 
			
		||||
        dest_filename = os.path.join(self.get_plugin_data_folder(), filename)
 | 
			
		||||
        host = self._settings.get(["host"])
 | 
			
		||||
        access_code = self._settings.get(["access_code"])
 | 
			
		||||
 | 
			
		||||
        if not os.path.exists(dest_filename):
 | 
			
		||||
            ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
 | 
			
		||||
            download_result = ftp.download_file(
 | 
			
		||||
                source=f"timelapse/thumbnail/{filename}",
 | 
			
		||||
                dest=dest_filename,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return flask.redirect("/plugin/bambu_printer/download/thumbnail/" + urlquote(filename), code=302)
 | 
			
		||||
 | 
			
		||||
    def is_blueprint_csrf_protected(self):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def route_hook(self, server_routes, *args, **kwargs):
 | 
			
		||||
        return [
 | 
			
		||||
            (r"/download/timelapse/(.*)", LargeResponseHandler,
 | 
			
		||||
             {'path': self.get_plugin_data_folder(), 'as_attachment': True, 'path_validation': path_validation_factory(
 | 
			
		||||
                 lambda path: not is_hidden_path(path), status_code=404)}),
 | 
			
		||||
            (r"/download/thumbnail/(.*)", LargeResponseHandler,
 | 
			
		||||
             {'path': self.get_plugin_data_folder(), 'as_attachment': True, 'path_validation': path_validation_factory(
 | 
			
		||||
                 lambda path: not is_hidden_path(path), status_code=404)})
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def get_update_information(self):
 | 
			
		||||
        return {'bambu_printer': {'displayName': "Bambu Printer",
 | 
			
		||||
                                  'displayVersion': self._plugin_version,
 | 
			
		||||
                                  'type': "github_release",
 | 
			
		||||
                                  'user': "jneilliii",
 | 
			
		||||
                                  'repo': "OctoPrint-BambuPrinter",
 | 
			
		||||
                                  'current': self._plugin_version,
 | 
			
		||||
                                  'stable_branch': {'name': "Stable",
 | 
			
		||||
                                                    'branch': "master",
 | 
			
		||||
                                                    'comittish': ["master"]},
 | 
			
		||||
                                  'prerelease_branches': [
 | 
			
		||||
                                      {'name': "Release Candidate",
 | 
			
		||||
                                       'branch': "rc",
 | 
			
		||||
                                       'comittish': ["rc", "master"]}
 | 
			
		||||
                                  ],
 | 
			
		||||
                                  'pip': "https://github.com/jneilliii/OctoPrint-BambuPrinter/archive/{target_version}.zip"}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__plugin_name__ = "Bambu Printer"
 | 
			
		||||
__plugin_pythoncompat__ = ">=3.7,<4"
 | 
			
		||||
 | 
			
		||||
from .bambu_print_plugin import BambuPrintPlugin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def __plugin_load__():
 | 
			
		||||
    plugin = BambuPrintPlugin()
 | 
			
		||||
@@ -270,5 +20,5 @@ def __plugin_load__():
 | 
			
		||||
        "octoprint.printer.sdcardupload": __plugin_implementation__.upload_to_sd,
 | 
			
		||||
        "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information,
 | 
			
		||||
        "octoprint.server.api.before_request": __plugin_implementation__._hook_octoprint_server_api_before_request,
 | 
			
		||||
        "octoprint.server.http.routes": __plugin_implementation__.route_hook
 | 
			
		||||
        "octoprint.server.http.routes": __plugin_implementation__.route_hook,
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										339
									
								
								octoprint_bambu_printer/bambu_print_plugin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										339
									
								
								octoprint_bambu_printer/bambu_print_plugin.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,339 @@
 | 
			
		||||
from __future__ import absolute_import, annotations
 | 
			
		||||
import os
 | 
			
		||||
import threading
 | 
			
		||||
import time
 | 
			
		||||
import flask
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
import octoprint.printer
 | 
			
		||||
import octoprint.server
 | 
			
		||||
import octoprint.plugin
 | 
			
		||||
from octoprint.events import Events
 | 
			
		||||
import octoprint.settings
 | 
			
		||||
from octoprint.util import get_formatted_size, get_formatted_datetime, is_hidden_path
 | 
			
		||||
from octoprint.server.util.flask import no_firstrun_access
 | 
			
		||||
from octoprint.server.util.tornado import (
 | 
			
		||||
    LargeResponseHandler,
 | 
			
		||||
    path_validation_factory,
 | 
			
		||||
)
 | 
			
		||||
from octoprint.access.permissions import Permissions
 | 
			
		||||
from octoprint.logging.handlers import CleaningTimedRotatingFileHandler
 | 
			
		||||
 | 
			
		||||
from pybambu import BambuCloud
 | 
			
		||||
 | 
			
		||||
from urllib.parse import quote as urlquote
 | 
			
		||||
import logging.handlers
 | 
			
		||||
 | 
			
		||||
from octoprint_bambu_printer.bambu_virtual_printer import BambuVirtualPrinter
 | 
			
		||||
from .ftpsclient import IoTFTPSClient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BambuPrintPlugin(
 | 
			
		||||
    octoprint.plugin.SettingsPlugin,
 | 
			
		||||
    octoprint.plugin.TemplatePlugin,
 | 
			
		||||
    octoprint.plugin.AssetPlugin,
 | 
			
		||||
    octoprint.plugin.EventHandlerPlugin,
 | 
			
		||||
    octoprint.plugin.SimpleApiPlugin,
 | 
			
		||||
    octoprint.plugin.BlueprintPlugin,
 | 
			
		||||
):
 | 
			
		||||
    _logger: logging.Logger
 | 
			
		||||
    _printer: octoprint.printer.PrinterInterface
 | 
			
		||||
    _settings: octoprint.settings.Settings
 | 
			
		||||
    _plugin_manager: octoprint.plugin.PluginManager
 | 
			
		||||
 | 
			
		||||
    def get_assets(self):
 | 
			
		||||
        return {"js": ["js/bambu_printer.js"]}
 | 
			
		||||
 | 
			
		||||
    def get_template_configs(self):
 | 
			
		||||
        return [
 | 
			
		||||
            {"type": "settings", "custom_bindings": True},
 | 
			
		||||
            {
 | 
			
		||||
                "type": "generic",
 | 
			
		||||
                "custom_bindings": True,
 | 
			
		||||
                "template": "bambu_timelapse.jinja2",
 | 
			
		||||
            },
 | 
			
		||||
        ]  # , {"type": "generic", "custom_bindings": True, "template": "bambu_printer.jinja2"}]
 | 
			
		||||
 | 
			
		||||
    def get_settings_defaults(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "device_type": "X1C",
 | 
			
		||||
            "serial": "",
 | 
			
		||||
            "host": "",
 | 
			
		||||
            "access_code": "",
 | 
			
		||||
            "username": "bblp",
 | 
			
		||||
            "timelapse": False,
 | 
			
		||||
            "bed_leveling": True,
 | 
			
		||||
            "flow_cali": False,
 | 
			
		||||
            "vibration_cali": True,
 | 
			
		||||
            "layer_inspect": True,
 | 
			
		||||
            "use_ams": False,
 | 
			
		||||
            "local_mqtt": True,
 | 
			
		||||
            "region": "",
 | 
			
		||||
            "email": "",
 | 
			
		||||
            "auth_token": "",
 | 
			
		||||
            "always_use_default_options": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def is_api_adminonly(self):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def get_api_commands(self):
 | 
			
		||||
        return {"register": ["email", "password", "region", "auth_token"]}
 | 
			
		||||
 | 
			
		||||
    def on_api_command(self, command, data):
 | 
			
		||||
        if command == "register":
 | 
			
		||||
            if (
 | 
			
		||||
                "email" in data
 | 
			
		||||
                and "password" in data
 | 
			
		||||
                and "region" in data
 | 
			
		||||
                and "auth_token" in data
 | 
			
		||||
            ):
 | 
			
		||||
                self._logger.info(f"Registering user {data['email']}")
 | 
			
		||||
                bambu_cloud = BambuCloud(
 | 
			
		||||
                    data["region"], data["email"], data["password"], data["auth_token"]
 | 
			
		||||
                )
 | 
			
		||||
                bambu_cloud.login(data["region"], data["email"], data["password"])
 | 
			
		||||
                return flask.jsonify(
 | 
			
		||||
                    {
 | 
			
		||||
                        "auth_token": bambu_cloud.auth_token,
 | 
			
		||||
                        "username": bambu_cloud.username,
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    def on_event(self, event, payload):
 | 
			
		||||
        if event == Events.TRANSFER_DONE:
 | 
			
		||||
            self._printer.commands("M20 L T", force=True)
 | 
			
		||||
 | 
			
		||||
    def support_3mf_files(self):
 | 
			
		||||
        return {"machinecode": {"3mf": ["3mf"]}}
 | 
			
		||||
 | 
			
		||||
    def upload_to_sd(
 | 
			
		||||
        self,
 | 
			
		||||
        printer,
 | 
			
		||||
        filename,
 | 
			
		||||
        path,
 | 
			
		||||
        sd_upload_started,
 | 
			
		||||
        sd_upload_succeeded,
 | 
			
		||||
        sd_upload_failed,
 | 
			
		||||
        *args,
 | 
			
		||||
        **kwargs,
 | 
			
		||||
    ):
 | 
			
		||||
        self._logger.debug(f"Starting upload from {filename} to {filename}")
 | 
			
		||||
        sd_upload_started(filename, filename)
 | 
			
		||||
 | 
			
		||||
        def process():
 | 
			
		||||
            host = self._settings.get(["host"])
 | 
			
		||||
            access_code = self._settings.get(["access_code"])
 | 
			
		||||
            elapsed = time.monotonic()
 | 
			
		||||
            try:
 | 
			
		||||
                ftp = IoTFTPSClient(
 | 
			
		||||
                    f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True
 | 
			
		||||
                )
 | 
			
		||||
                if ftp.upload_file(path, f"{filename}"):
 | 
			
		||||
                    elapsed = time.monotonic() - elapsed
 | 
			
		||||
                    sd_upload_succeeded(filename, filename, elapsed)
 | 
			
		||||
                    # remove local file after successful upload to Bambu
 | 
			
		||||
                    # self._file_manager.remove_file("local", filename)
 | 
			
		||||
                else:
 | 
			
		||||
                    raise Exception("upload failed")
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                elapsed = time.monotonic() - elapsed
 | 
			
		||||
                sd_upload_failed(filename, filename, elapsed)
 | 
			
		||||
                self._logger.debug(f"Error uploading file {filename}")
 | 
			
		||||
 | 
			
		||||
        thread = threading.Thread(target=process)
 | 
			
		||||
        thread.daemon = True
 | 
			
		||||
        thread.start()
 | 
			
		||||
        return filename
 | 
			
		||||
 | 
			
		||||
    def get_template_vars(self):
 | 
			
		||||
        return {"plugin_version": self._plugin_version}
 | 
			
		||||
 | 
			
		||||
    def virtual_printer_factory(self, comm_instance, port, baudrate, read_timeout):
 | 
			
		||||
        if not port == "BAMBU":
 | 
			
		||||
            return None
 | 
			
		||||
        if (
 | 
			
		||||
            self._settings.get(["serial"]) == ""
 | 
			
		||||
            or self._settings.get(["host"]) == ""
 | 
			
		||||
            or self._settings.get(["access_code"]) == ""
 | 
			
		||||
        ):
 | 
			
		||||
            return None
 | 
			
		||||
        seriallog_handler = CleaningTimedRotatingFileHandler(
 | 
			
		||||
            self._settings.get_plugin_logfile_path(postfix="serial"),
 | 
			
		||||
            when="D",
 | 
			
		||||
            backupCount=3,
 | 
			
		||||
        )
 | 
			
		||||
        seriallog_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
 | 
			
		||||
        seriallog_handler.setLevel(logging.DEBUG)
 | 
			
		||||
 | 
			
		||||
        serial_obj = BambuVirtualPrinter(
 | 
			
		||||
            self._settings,
 | 
			
		||||
            self._printer_profile_manager,
 | 
			
		||||
            data_folder=self.get_plugin_data_folder(),
 | 
			
		||||
            seriallog_handler=seriallog_handler,
 | 
			
		||||
            read_timeout=float(read_timeout),
 | 
			
		||||
            faked_baudrate=baudrate,
 | 
			
		||||
        )
 | 
			
		||||
        return serial_obj
 | 
			
		||||
 | 
			
		||||
    def get_additional_port_names(self, *args, **kwargs):
 | 
			
		||||
        if (
 | 
			
		||||
            self._settings.get(["serial"]) != ""
 | 
			
		||||
            and self._settings.get(["host"]) != ""
 | 
			
		||||
            and self._settings.get(["access_code"]) != ""
 | 
			
		||||
        ):
 | 
			
		||||
            return ["BAMBU"]
 | 
			
		||||
        else:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
    def get_timelapse_file_list(self):
 | 
			
		||||
        if flask.request.path.startswith("/api/timelapse"):
 | 
			
		||||
 | 
			
		||||
            def process():
 | 
			
		||||
                host = self._settings.get(["host"])
 | 
			
		||||
                access_code = self._settings.get(["access_code"])
 | 
			
		||||
                return_file_list = []
 | 
			
		||||
                try:
 | 
			
		||||
                    ftp = IoTFTPSClient(
 | 
			
		||||
                        f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True
 | 
			
		||||
                    )
 | 
			
		||||
                    if self._settings.get(["device_type"]) in ["X1", "X1C"]:
 | 
			
		||||
                        timelapse_file_list = ftp.list_files("timelapse/", ".mp4") or []
 | 
			
		||||
                    else:
 | 
			
		||||
                        timelapse_file_list = ftp.list_files("timelapse/", ".avi") or []
 | 
			
		||||
                    for entry in timelapse_file_list:
 | 
			
		||||
                        if entry.startswith("/"):
 | 
			
		||||
                            filename = entry[1:].replace("timelapse/", "")
 | 
			
		||||
                        else:
 | 
			
		||||
                            filename = entry.replace("timelapse/", "")
 | 
			
		||||
                        filesize = ftp.ftps_session.size(f"timelapse/{filename}")
 | 
			
		||||
                        date_str = ftp.ftps_session.sendcmd(
 | 
			
		||||
                            f"MDTM timelapse/{filename}"
 | 
			
		||||
                        ).replace("213 ", "")
 | 
			
		||||
                        filedate = (
 | 
			
		||||
                            datetime.datetime.strptime(date_str, "%Y%m%d%H%M%S")
 | 
			
		||||
                            .replace(tzinfo=datetime.timezone.utc)
 | 
			
		||||
                            .timestamp()
 | 
			
		||||
                        )
 | 
			
		||||
                        return_file_list.append(
 | 
			
		||||
                            {
 | 
			
		||||
                                "bytes": filesize,
 | 
			
		||||
                                "date": get_formatted_datetime(
 | 
			
		||||
                                    datetime.datetime.fromtimestamp(filedate)
 | 
			
		||||
                                ),
 | 
			
		||||
                                "name": filename,
 | 
			
		||||
                                "size": get_formatted_size(filesize),
 | 
			
		||||
                                "thumbnail": "/plugin/bambu_printer/thumbnail/"
 | 
			
		||||
                                + filename.replace(".mp4", ".jpg").replace(
 | 
			
		||||
                                    ".avi", ".jpg"
 | 
			
		||||
                                ),
 | 
			
		||||
                                "timestamp": filedate,
 | 
			
		||||
                                "url": f"/plugin/bambu_printer/timelapse/{filename}",
 | 
			
		||||
                            }
 | 
			
		||||
                        )
 | 
			
		||||
                    self._plugin_manager.send_plugin_message(
 | 
			
		||||
                        self._identifier, {"files": return_file_list}
 | 
			
		||||
                    )
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    self._logger.debug(f"Error getting timelapse files: {e}")
 | 
			
		||||
 | 
			
		||||
            thread = threading.Thread(target=process)
 | 
			
		||||
            thread.daemon = True
 | 
			
		||||
            thread.start()
 | 
			
		||||
 | 
			
		||||
    def _hook_octoprint_server_api_before_request(self, *args, **kwargs):
 | 
			
		||||
        return [self.get_timelapse_file_list]
 | 
			
		||||
 | 
			
		||||
    @octoprint.plugin.BlueprintPlugin.route("/timelapse/<filename>", methods=["GET"])
 | 
			
		||||
    @octoprint.server.util.flask.restricted_access
 | 
			
		||||
    @no_firstrun_access
 | 
			
		||||
    @Permissions.TIMELAPSE_DOWNLOAD.require(403)
 | 
			
		||||
    def downloadTimelapse(self, filename):
 | 
			
		||||
        dest_filename = os.path.join(self.get_plugin_data_folder(), filename)
 | 
			
		||||
        host = self._settings.get(["host"])
 | 
			
		||||
        access_code = self._settings.get(["access_code"])
 | 
			
		||||
        if not os.path.exists(dest_filename):
 | 
			
		||||
            ftp = IoTFTPSClient(
 | 
			
		||||
                f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True
 | 
			
		||||
            )
 | 
			
		||||
            download_result = ftp.download_file(
 | 
			
		||||
                source=f"timelapse/{filename}",
 | 
			
		||||
                dest=dest_filename,
 | 
			
		||||
            )
 | 
			
		||||
        return flask.redirect(
 | 
			
		||||
            "/plugin/bambu_printer/download/timelapse/" + urlquote(filename), code=302
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @octoprint.plugin.BlueprintPlugin.route("/thumbnail/<filename>", methods=["GET"])
 | 
			
		||||
    @octoprint.server.util.flask.restricted_access
 | 
			
		||||
    @no_firstrun_access
 | 
			
		||||
    @Permissions.TIMELAPSE_DOWNLOAD.require(403)
 | 
			
		||||
    def downloadThumbnail(self, filename):
 | 
			
		||||
        dest_filename = os.path.join(self.get_plugin_data_folder(), filename)
 | 
			
		||||
        host = self._settings.get(["host"])
 | 
			
		||||
        access_code = self._settings.get(["access_code"])
 | 
			
		||||
        if not os.path.exists(dest_filename):
 | 
			
		||||
            ftp = IoTFTPSClient(
 | 
			
		||||
                f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True
 | 
			
		||||
            )
 | 
			
		||||
            download_result = ftp.download_file(
 | 
			
		||||
                source=f"timelapse/thumbnail/{filename}",
 | 
			
		||||
                dest=dest_filename,
 | 
			
		||||
            )
 | 
			
		||||
        return flask.redirect(
 | 
			
		||||
            "/plugin/bambu_printer/download/thumbnail/" + urlquote(filename), code=302
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def is_blueprint_csrf_protected(self):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def route_hook(self, server_routes, *args, **kwargs):
 | 
			
		||||
        return [
 | 
			
		||||
            (
 | 
			
		||||
                r"/download/timelapse/(.*)",
 | 
			
		||||
                LargeResponseHandler,
 | 
			
		||||
                {
 | 
			
		||||
                    "path": self.get_plugin_data_folder(),
 | 
			
		||||
                    "as_attachment": True,
 | 
			
		||||
                    "path_validation": path_validation_factory(
 | 
			
		||||
                        lambda path: not is_hidden_path(path), status_code=404
 | 
			
		||||
                    ),
 | 
			
		||||
                },
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                r"/download/thumbnail/(.*)",
 | 
			
		||||
                LargeResponseHandler,
 | 
			
		||||
                {
 | 
			
		||||
                    "path": self.get_plugin_data_folder(),
 | 
			
		||||
                    "as_attachment": True,
 | 
			
		||||
                    "path_validation": path_validation_factory(
 | 
			
		||||
                        lambda path: not is_hidden_path(path), status_code=404
 | 
			
		||||
                    ),
 | 
			
		||||
                },
 | 
			
		||||
            ),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def get_update_information(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "bambu_printer": {
 | 
			
		||||
                "displayName": "Bambu Printer",
 | 
			
		||||
                "displayVersion": self._plugin_version,
 | 
			
		||||
                "type": "github_release",
 | 
			
		||||
                "user": "jneilliii",
 | 
			
		||||
                "repo": "OctoPrint-BambuPrinter",
 | 
			
		||||
                "current": self._plugin_version,
 | 
			
		||||
                "stable_branch": {
 | 
			
		||||
                    "name": "Stable",
 | 
			
		||||
                    "branch": "master",
 | 
			
		||||
                    "comittish": ["master"],
 | 
			
		||||
                },
 | 
			
		||||
                "prerelease_branches": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "name": "Release Candidate",
 | 
			
		||||
                        "branch": "rc",
 | 
			
		||||
                        "comittish": ["rc", "master"],
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "pip": "https://github.com/jneilliii/OctoPrint-BambuPrinter/archive/{target_version}.zip",
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -13,16 +13,18 @@ import time
 | 
			
		||||
from typing import Any, Dict, List, Optional
 | 
			
		||||
import asyncio
 | 
			
		||||
from pybambu import BambuClient, commands
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from serial import SerialTimeoutException
 | 
			
		||||
from octoprint.util import RepeatedTimer, to_bytes, to_unicode, get_dos_filename
 | 
			
		||||
from octoprint.util.files import unix_timestamp_to_m20_timestamp
 | 
			
		||||
 | 
			
		||||
from .char_counting_queue import CharCountingQueue
 | 
			
		||||
from .ftpsclient import IoTFTPSClient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# noinspection PyBroadException
 | 
			
		||||
class BambuPrinter:
 | 
			
		||||
class BambuVirtualPrinter:
 | 
			
		||||
    command_regex = re.compile(r"^([GM])(\d+)")
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
@@ -38,14 +40,14 @@ class BambuPrinter:
 | 
			
		||||
        self._busyInterval = 2.0
 | 
			
		||||
        self.tick_rate = 2.0
 | 
			
		||||
        self._errors = {
 | 
			
		||||
                "checksum_mismatch": "Checksum mismatch",
 | 
			
		||||
                "checksum_missing": "Missing checksum",
 | 
			
		||||
                "lineno_mismatch": "expected line {} got {}",
 | 
			
		||||
                "lineno_missing": "No Line Number with checksum, Last Line: {}",
 | 
			
		||||
                "maxtemp": "MAXTEMP triggered!",
 | 
			
		||||
                "mintemp": "MINTEMP triggered!",
 | 
			
		||||
                "command_unknown": "Unknown command {}",
 | 
			
		||||
            }
 | 
			
		||||
            "checksum_mismatch": "Checksum mismatch",
 | 
			
		||||
            "checksum_missing": "Missing checksum",
 | 
			
		||||
            "lineno_mismatch": "expected line {} got {}",
 | 
			
		||||
            "lineno_missing": "No Line Number with checksum, Last Line: {}",
 | 
			
		||||
            "maxtemp": "MAXTEMP triggered!",
 | 
			
		||||
            "mintemp": "MINTEMP triggered!",
 | 
			
		||||
            "command_unknown": "Unknown command {}",
 | 
			
		||||
        }
 | 
			
		||||
        self._sendBusy = False
 | 
			
		||||
        self._ambient_temperature = 21.3
 | 
			
		||||
        self.temp = [self._ambient_temperature]
 | 
			
		||||
@@ -80,13 +82,7 @@ class BambuPrinter:
 | 
			
		||||
        self._busy = None
 | 
			
		||||
        self._busy_loop = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        import logging
 | 
			
		||||
 | 
			
		||||
        self._logger = logging.getLogger(
 | 
			
		||||
            "octoprint.plugins.bambu_printer.BambuPrinter"
 | 
			
		||||
        )
 | 
			
		||||
        self._logger = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter")
 | 
			
		||||
 | 
			
		||||
        self._settings = settings
 | 
			
		||||
        self._printer_profile_manager = printer_profile_manager
 | 
			
		||||
@@ -119,12 +115,12 @@ class BambuPrinter:
 | 
			
		||||
 | 
			
		||||
        self._last_hms_errors = None
 | 
			
		||||
 | 
			
		||||
        self.bambu = None
 | 
			
		||||
        self._bambu: BambuClient = None
 | 
			
		||||
 | 
			
		||||
        readThread = threading.Thread(
 | 
			
		||||
            target=self._processIncoming,
 | 
			
		||||
            name="octoprint.plugins.bambu_printer.wait_thread",
 | 
			
		||||
            daemon=True
 | 
			
		||||
            daemon=True,
 | 
			
		||||
        )
 | 
			
		||||
        readThread.start()
 | 
			
		||||
 | 
			
		||||
@@ -139,16 +135,25 @@ class BambuPrinter:
 | 
			
		||||
        connectionThread = threading.Thread(
 | 
			
		||||
            target=self._create_connection,
 | 
			
		||||
            name="octoprint.plugins.bambu_printer.connection_thread",
 | 
			
		||||
            daemon=True
 | 
			
		||||
            daemon=True,
 | 
			
		||||
        )
 | 
			
		||||
        connectionThread.start()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def bambu(self):
 | 
			
		||||
        if self._bambu is None:
 | 
			
		||||
            raise ValueError("No connection to Bambulab was established")
 | 
			
		||||
        return self._bambu
 | 
			
		||||
 | 
			
		||||
    def new_update(self, event_type):
 | 
			
		||||
        if event_type == "event_hms_errors":
 | 
			
		||||
            bambu_printer = self.bambu.get_device()
 | 
			
		||||
            if bambu_printer.hms.errors != self._last_hms_errors and bambu_printer.hms.errors["Count"] > 0:
 | 
			
		||||
            if (
 | 
			
		||||
                bambu_printer.hms.errors != self._last_hms_errors
 | 
			
		||||
                and bambu_printer.hms.errors["Count"] > 0
 | 
			
		||||
            ):
 | 
			
		||||
                self._logger.debug(f"HMS Error: {bambu_printer.hms.errors}")
 | 
			
		||||
                for n in range(1, bambu_printer.hms.errors["Count"]+1):
 | 
			
		||||
                for n in range(1, bambu_printer.hms.errors["Count"] + 1):
 | 
			
		||||
                    error = bambu_printer.hms.errors[f"{n}-Error"].strip()
 | 
			
		||||
                    self._send(f"// action:notification {error}")
 | 
			
		||||
                self._last_hms_errors = bambu_printer.hms.errors
 | 
			
		||||
@@ -177,7 +182,7 @@ class BambuPrinter:
 | 
			
		||||
                    self._sdPrintingPausedSemaphore.clear()
 | 
			
		||||
                self._sdPrintStarting = False
 | 
			
		||||
                if not self._sdPrinting:
 | 
			
		||||
                    filename = print_job.get("subtask_name")
 | 
			
		||||
                    filename: str = print_job.get("subtask_name")
 | 
			
		||||
                    if not self._sdFileListCache.get(filename.lower()):
 | 
			
		||||
                        if self._sdFileListCache.get(f"{filename.lower()}.3mf"):
 | 
			
		||||
                            filename = f"{filename.lower()}.3mf"
 | 
			
		||||
@@ -192,7 +197,10 @@ class BambuPrinter:
 | 
			
		||||
                    self._startSdPrint(from_printer=True)
 | 
			
		||||
 | 
			
		||||
                # fuzzy math here to get print percentage to match BambuStudio
 | 
			
		||||
                self._selectedSdFilePos = int(self._selectedSdFileSize * ((print_job.get("print_percentage") + 1)/100))
 | 
			
		||||
                self._selectedSdFilePos = int(
 | 
			
		||||
                    self._selectedSdFileSize
 | 
			
		||||
                    * ((print_job.get("print_percentage") + 1) / 100)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if print_job.get("gcode_state") == "PAUSE":
 | 
			
		||||
                if not self._sdPrintingPausedSemaphore.is_set():
 | 
			
		||||
@@ -202,18 +210,23 @@ class BambuPrinter:
 | 
			
		||||
                    self._send("// action:paused")
 | 
			
		||||
                    self._sendPaused()
 | 
			
		||||
 | 
			
		||||
            if print_job.get("gcode_state") == "FINISH" or print_job.get("gcode_state") == "FAILED":
 | 
			
		||||
            if (
 | 
			
		||||
                print_job.get("gcode_state") == "FINISH"
 | 
			
		||||
                or print_job.get("gcode_state") == "FAILED"
 | 
			
		||||
            ):
 | 
			
		||||
                if self._sdPrintStarting is False:
 | 
			
		||||
                    self._sdPrinting = False
 | 
			
		||||
                if self._sdPrintingSemaphore.is_set():
 | 
			
		||||
                    self._selectedSdFilePos = self._selectedSdFileSize
 | 
			
		||||
                    self._finishSdPrint()
 | 
			
		||||
 | 
			
		||||
    def _create_connection(self):
 | 
			
		||||
        if (self._settings.get(["device_type"]) != "" and
 | 
			
		||||
            self._settings.get(["serial"]) != "" and
 | 
			
		||||
            self._settings.get(["serial"]) != "" and
 | 
			
		||||
            self._settings.get(["username"]) != "" and
 | 
			
		||||
            self._settings.get(["access_code"]) != ""
 | 
			
		||||
        if (
 | 
			
		||||
            self._settings.get(["device_type"]) != ""
 | 
			
		||||
            and self._settings.get(["serial"]) != ""
 | 
			
		||||
            and self._settings.get(["serial"]) != ""
 | 
			
		||||
            and self._settings.get(["username"]) != ""
 | 
			
		||||
            and self._settings.get(["access_code"]) != ""
 | 
			
		||||
        ):
 | 
			
		||||
            asyncio.run(self._create_connection_async())
 | 
			
		||||
 | 
			
		||||
@@ -226,17 +239,24 @@ class BambuPrinter:
 | 
			
		||||
        return on_connect
 | 
			
		||||
 | 
			
		||||
    async def _create_connection_async(self):
 | 
			
		||||
        self._logger.debug(f"connecting via local mqtt: {self._settings.get_boolean(['local_mqtt'])}")
 | 
			
		||||
        self.bambu = 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"])
 | 
			
		||||
                                 )
 | 
			
		||||
        self._logger.debug(
 | 
			
		||||
            f"connecting via local mqtt: {self._settings.get_boolean(['local_mqtt'])}"
 | 
			
		||||
        )
 | 
			
		||||
        self.bambu = 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"]),
 | 
			
		||||
        )
 | 
			
		||||
        self.bambu.on_disconnect = self.on_disconnect(self.bambu.on_disconnect)
 | 
			
		||||
        self.bambu.on_connect = self.on_connect(self.bambu.on_connect)
 | 
			
		||||
        self.bambu.connect(callback=self.new_update)
 | 
			
		||||
@@ -247,7 +267,10 @@ class BambuPrinter:
 | 
			
		||||
        return "BAMBU(read_timeout={read_timeout},write_timeout={write_timeout},options={options})".format(
 | 
			
		||||
            read_timeout=self._read_timeout,
 | 
			
		||||
            write_timeout=self._write_timeout,
 | 
			
		||||
            options={"device_type": self._settings.get(["device_type"]), "host": self._settings.get(["host"])},
 | 
			
		||||
            options={
 | 
			
		||||
                "device_type": self._settings.get(["device_type"]),
 | 
			
		||||
                "host": self._settings.get(["host"]),
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _calculate_resend_every_n(self, resend_ratio):
 | 
			
		||||
@@ -432,7 +455,7 @@ class BambuPrinter:
 | 
			
		||||
            data = to_unicode(data, encoding="ascii", errors="replace").strip()
 | 
			
		||||
 | 
			
		||||
            # actual command handling
 | 
			
		||||
            command_match = BambuPrinter.command_regex.match(data)
 | 
			
		||||
            command_match = BambuVirtualPrinter.command_regex.match(data)
 | 
			
		||||
            if command_match is not None:
 | 
			
		||||
                command = command_match.group(0)
 | 
			
		||||
                letter = command_match.group(1)
 | 
			
		||||
@@ -459,7 +482,7 @@ class BambuPrinter:
 | 
			
		||||
 | 
			
		||||
                    if self.bambu.connected:
 | 
			
		||||
                        GCODE_COMMAND = commands.SEND_GCODE_TEMPLATE
 | 
			
		||||
                        GCODE_COMMAND['print']['param'] = data + "\n"
 | 
			
		||||
                        GCODE_COMMAND["print"]["param"] = data + "\n"
 | 
			
		||||
                        if self.bambu.publish(GCODE_COMMAND):
 | 
			
		||||
                            self._logger.info("command sent successfully")
 | 
			
		||||
                            self._sendOk()
 | 
			
		||||
@@ -611,13 +634,20 @@ class BambuPrinter:
 | 
			
		||||
 | 
			
		||||
            speed_fraction = 100 / percent
 | 
			
		||||
            acceleration = math.exp((speed_fraction - 1.0191) / -0.814)
 | 
			
		||||
            feed_rate = (2.1645 * (acceleration ** 3) - 5.3247 * (acceleration ** 2) + 4.342 * acceleration - 0.181)
 | 
			
		||||
            speed_level = 1.539 * (acceleration ** 2) - 0.7032 * acceleration + 4.0834
 | 
			
		||||
            feed_rate = (
 | 
			
		||||
                2.1645 * (acceleration**3)
 | 
			
		||||
                - 5.3247 * (acceleration**2)
 | 
			
		||||
                + 4.342 * acceleration
 | 
			
		||||
                - 0.181
 | 
			
		||||
            )
 | 
			
		||||
            speed_level = 1.539 * (acceleration**2) - 0.7032 * acceleration + 4.0834
 | 
			
		||||
            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"
 | 
			
		||||
 | 
			
		||||
            gcode_command['print']['param'] = speed_command
 | 
			
		||||
            gcode_command["print"]["param"] = speed_command
 | 
			
		||||
            if self.bambu.publish(gcode_command):
 | 
			
		||||
                self._logger.info(f"{percent}% speed adjustment command sent successfully")
 | 
			
		||||
                self._logger.info(
 | 
			
		||||
                    f"{percent}% speed adjustment command sent successfully"
 | 
			
		||||
                )
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    # noinspection PyUnusedLocal
 | 
			
		||||
@@ -672,7 +702,7 @@ class BambuPrinter:
 | 
			
		||||
            request_resend()
 | 
			
		||||
 | 
			
		||||
    def _listSd(self, incl_long=False, incl_timestamp=False):
 | 
			
		||||
        line = "{dosname} {size} {timestamp} \"{name}\""
 | 
			
		||||
        line = '{dosname} {size} {timestamp} "{name}"'
 | 
			
		||||
 | 
			
		||||
        self._send("Begin file list")
 | 
			
		||||
        for item in map(lambda x: line.format(**x), self._getSdFiles()):
 | 
			
		||||
@@ -694,14 +724,20 @@ class BambuPrinter:
 | 
			
		||||
                filename = entry
 | 
			
		||||
            filesize = ftp.ftps_session.size(entry)
 | 
			
		||||
            date_str = ftp.ftps_session.sendcmd(f"MDTM {entry}").replace("213 ", "")
 | 
			
		||||
            filedate = datetime.datetime.strptime(date_str, "%Y%m%d%H%M%S").replace(tzinfo=datetime.timezone.utc).timestamp()
 | 
			
		||||
            dosname = get_dos_filename(filename, existing_filenames=list(result.keys())).lower()
 | 
			
		||||
            filedate = (
 | 
			
		||||
                datetime.datetime.strptime(date_str, "%Y%m%d%H%M%S")
 | 
			
		||||
                .replace(tzinfo=datetime.timezone.utc)
 | 
			
		||||
                .timestamp()
 | 
			
		||||
            )
 | 
			
		||||
            dosname = get_dos_filename(
 | 
			
		||||
                filename, existing_filenames=list(result.keys())
 | 
			
		||||
            ).lower()
 | 
			
		||||
            data = {
 | 
			
		||||
                "dosname": dosname,
 | 
			
		||||
                "name": filename,
 | 
			
		||||
                "path": filename,
 | 
			
		||||
                "size": filesize,
 | 
			
		||||
                "timestamp": unix_timestamp_to_m20_timestamp(int(filedate))
 | 
			
		||||
                "timestamp": unix_timestamp_to_m20_timestamp(int(filedate)),
 | 
			
		||||
            }
 | 
			
		||||
            result[dosname.lower()] = filename.lower()
 | 
			
		||||
            result[filename.lower()] = data
 | 
			
		||||
@@ -714,15 +750,23 @@ class BambuPrinter:
 | 
			
		||||
            else:
 | 
			
		||||
                filename = entry.replace("cache/", "")
 | 
			
		||||
            filesize = ftp.ftps_session.size(f"cache/{filename}")
 | 
			
		||||
            date_str = ftp.ftps_session.sendcmd(f"MDTM cache/{filename}").replace("213 ", "")
 | 
			
		||||
            filedate = datetime.datetime.strptime(date_str, "%Y%m%d%H%M%S").replace(tzinfo=datetime.timezone.utc).timestamp()
 | 
			
		||||
            dosname = get_dos_filename(filename, existing_filenames=list(result.keys())).lower()
 | 
			
		||||
            date_str = ftp.ftps_session.sendcmd(f"MDTM cache/{filename}").replace(
 | 
			
		||||
                "213 ", ""
 | 
			
		||||
            )
 | 
			
		||||
            filedate = (
 | 
			
		||||
                datetime.datetime.strptime(date_str, "%Y%m%d%H%M%S")
 | 
			
		||||
                .replace(tzinfo=datetime.timezone.utc)
 | 
			
		||||
                .timestamp()
 | 
			
		||||
            )
 | 
			
		||||
            dosname = get_dos_filename(
 | 
			
		||||
                filename, existing_filenames=list(result.keys())
 | 
			
		||||
            ).lower()
 | 
			
		||||
            data = {
 | 
			
		||||
                "dosname": dosname,
 | 
			
		||||
                "name": filename,
 | 
			
		||||
                "path": "cache/"+filename,
 | 
			
		||||
                "path": "cache/" + filename,
 | 
			
		||||
                "size": filesize,
 | 
			
		||||
                "timestamp": unix_timestamp_to_m20_timestamp(int(filedate))
 | 
			
		||||
                "timestamp": unix_timestamp_to_m20_timestamp(int(filedate)),
 | 
			
		||||
            }
 | 
			
		||||
            result[dosname.lower()] = filename.lower()
 | 
			
		||||
            result[filename.lower()] = data
 | 
			
		||||
@@ -743,7 +787,9 @@ class BambuPrinter:
 | 
			
		||||
        return [x for x in self._sdFileListCache.values() if isinstance(x, dict)]
 | 
			
		||||
 | 
			
		||||
    def _selectSdFile(self, filename: str, check_already_open: bool = False) -> None:
 | 
			
		||||
        self._logger.debug(f"_selectSdFile: {filename}, check_already_open={check_already_open}")
 | 
			
		||||
        self._logger.debug(
 | 
			
		||||
            f"_selectSdFile: {filename}, check_already_open={check_already_open}"
 | 
			
		||||
        )
 | 
			
		||||
        if filename.startswith("/"):
 | 
			
		||||
            filename = filename[1:]
 | 
			
		||||
 | 
			
		||||
@@ -770,7 +816,9 @@ class BambuPrinter:
 | 
			
		||||
            if self._sdPrinter is None:
 | 
			
		||||
                self._sdPrinting = True
 | 
			
		||||
                self._sdPrintStarting = True
 | 
			
		||||
                self._sdPrinter = threading.Thread(target=self._sdPrintingWorker, kwargs={"from_printer": from_printer})
 | 
			
		||||
                self._sdPrinter = threading.Thread(
 | 
			
		||||
                    target=self._sdPrintingWorker, kwargs={"from_printer": from_printer}
 | 
			
		||||
                )
 | 
			
		||||
                self._sdPrinter.start()
 | 
			
		||||
        # self._sdPrintingSemaphore.set()
 | 
			
		||||
        if self._sdPrinter is not None:
 | 
			
		||||
@@ -803,8 +851,12 @@ class BambuPrinter:
 | 
			
		||||
        self._newSdFilePos = pos
 | 
			
		||||
 | 
			
		||||
    def _reportSdStatus(self):
 | 
			
		||||
        if ( self._sdPrinter is not None or self._sdPrintStarting is True ) and self._selectedSdFileSize > 0:
 | 
			
		||||
            self._send(f"SD printing byte {self._selectedSdFilePos}/{self._selectedSdFileSize}")
 | 
			
		||||
        if (
 | 
			
		||||
            self._sdPrinter is not None or self._sdPrintStarting is True
 | 
			
		||||
        ) and self._selectedSdFileSize > 0:
 | 
			
		||||
            self._send(
 | 
			
		||||
                f"SD printing byte {self._selectedSdFilePos}/{self._selectedSdFileSize}"
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            self._send("Not SD printing")
 | 
			
		||||
 | 
			
		||||
@@ -858,25 +910,34 @@ class BambuPrinter:
 | 
			
		||||
        self._selectedSdFilePos = 0
 | 
			
		||||
        try:
 | 
			
		||||
            if not from_printer and self.bambu.connected:
 | 
			
		||||
                print_command = {"print": {"sequence_id": 0,
 | 
			
		||||
                                           "command": "project_file",
 | 
			
		||||
                                           "param": "Metadata/plate_1.gcode",
 | 
			
		||||
                                           "md5": "",
 | 
			
		||||
                                           "profile_id": "0",
 | 
			
		||||
                                           "project_id": "0",
 | 
			
		||||
                                           "subtask_id": "0",
 | 
			
		||||
                                           "task_id": "0",
 | 
			
		||||
                                           "subtask_name": f"{self._selectedSdFile}",
 | 
			
		||||
                                           "file": f"{self._selectedSdFile}",
 | 
			
		||||
                                           "url": f"file:///mnt/sdcard/{self._selectedSdFile}" if self._settings.get_boolean(["device_type"]) in ["X1", "X1C"] else f"file:///sdcard/{self._selectedSdFile}",
 | 
			
		||||
                                           "timelapse": self._settings.get_boolean(["timelapse"]),
 | 
			
		||||
                                           "bed_leveling": self._settings.get_boolean(["bed_leveling"]),
 | 
			
		||||
                                           "flow_cali": self._settings.get_boolean(["flow_cali"]),
 | 
			
		||||
                                           "vibration_cali": self._settings.get_boolean(["vibration_cali"]),
 | 
			
		||||
                                           "layer_inspect": self._settings.get_boolean(["layer_inspect"]),
 | 
			
		||||
                                           "use_ams": self._settings.get_boolean(["use_ams"])
 | 
			
		||||
                                           }
 | 
			
		||||
                                 }
 | 
			
		||||
                print_command = {
 | 
			
		||||
                    "print": {
 | 
			
		||||
                        "sequence_id": 0,
 | 
			
		||||
                        "command": "project_file",
 | 
			
		||||
                        "param": "Metadata/plate_1.gcode",
 | 
			
		||||
                        "md5": "",
 | 
			
		||||
                        "profile_id": "0",
 | 
			
		||||
                        "project_id": "0",
 | 
			
		||||
                        "subtask_id": "0",
 | 
			
		||||
                        "task_id": "0",
 | 
			
		||||
                        "subtask_name": f"{self._selectedSdFile}",
 | 
			
		||||
                        "file": f"{self._selectedSdFile}",
 | 
			
		||||
                        "url": (
 | 
			
		||||
                            f"file:///mnt/sdcard/{self._selectedSdFile}"
 | 
			
		||||
                            if self._settings.get_boolean(["device_type"])
 | 
			
		||||
                            in ["X1", "X1C"]
 | 
			
		||||
                            else f"file:///sdcard/{self._selectedSdFile}"
 | 
			
		||||
                        ),
 | 
			
		||||
                        "timelapse": self._settings.get_boolean(["timelapse"]),
 | 
			
		||||
                        "bed_leveling": self._settings.get_boolean(["bed_leveling"]),
 | 
			
		||||
                        "flow_cali": self._settings.get_boolean(["flow_cali"]),
 | 
			
		||||
                        "vibration_cali": self._settings.get_boolean(
 | 
			
		||||
                            ["vibration_cali"]
 | 
			
		||||
                        ),
 | 
			
		||||
                        "layer_inspect": self._settings.get_boolean(["layer_inspect"]),
 | 
			
		||||
                        "use_ams": self._settings.get_boolean(["use_ams"]),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                self.bambu.publish(print_command)
 | 
			
		||||
 | 
			
		||||
            while self._selectedSdFilePos < self._selectedSdFileSize:
 | 
			
		||||
@@ -913,7 +974,9 @@ class BambuPrinter:
 | 
			
		||||
            filename = filename[1:]
 | 
			
		||||
        file = self._getSdFileData(filename)
 | 
			
		||||
        if file is not None:
 | 
			
		||||
            ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
 | 
			
		||||
            ftp = IoTFTPSClient(
 | 
			
		||||
                f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True
 | 
			
		||||
            )
 | 
			
		||||
            try:
 | 
			
		||||
                if ftp.delete_file(file["path"]):
 | 
			
		||||
                    self._logger.debug(f"{filename} deleted")
 | 
			
		||||
@@ -978,7 +1041,9 @@ class BambuPrinter:
 | 
			
		||||
                return len(data)
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                written = self.incoming.put(data, timeout=self._write_timeout, partial=True)
 | 
			
		||||
                written = self.incoming.put(
 | 
			
		||||
                    data, timeout=self._write_timeout, partial=True
 | 
			
		||||
                )
 | 
			
		||||
                self._seriallog.debug(f"<<< {u_data}")
 | 
			
		||||
                return written
 | 
			
		||||
            except queue.Full:
 | 
			
		||||
@@ -1017,9 +1082,18 @@ class BambuPrinter:
 | 
			
		||||
 | 
			
		||||
    def _isPaused(self):
 | 
			
		||||
        return self._sdPrintingPausedSemaphore.is_set()
 | 
			
		||||
 | 
			
		||||
    def _sendPaused(self):
 | 
			
		||||
        paused_timer = RepeatedTimer(interval=3.0, function=self._send, args=[f"SD printing byte {self._selectedSdFilePos}/{self._selectedSdFileSize}"],
 | 
			
		||||
                                     daemon=True, run_first=True, condition=self._isPaused)
 | 
			
		||||
        paused_timer = RepeatedTimer(
 | 
			
		||||
            interval=3.0,
 | 
			
		||||
            function=self._send,
 | 
			
		||||
            args=[
 | 
			
		||||
                f"SD printing byte {self._selectedSdFilePos}/{self._selectedSdFileSize}"
 | 
			
		||||
            ],
 | 
			
		||||
            daemon=True,
 | 
			
		||||
            run_first=True,
 | 
			
		||||
            condition=self._isPaused,
 | 
			
		||||
        )
 | 
			
		||||
        paused_timer.start()
 | 
			
		||||
 | 
			
		||||
    def _send(self, line: str) -> None:
 | 
			
		||||
@@ -1031,68 +1105,3 @@ class BambuPrinter:
 | 
			
		||||
 | 
			
		||||
    def _error(self, error: str, *args, **kwargs) -> str:
 | 
			
		||||
        return f"Error: {self._errors.get(error).format(*args, **kwargs)}"
 | 
			
		||||
 | 
			
		||||
# noinspection PyUnresolvedReferences
 | 
			
		||||
class CharCountingQueue(queue.Queue):
 | 
			
		||||
    def __init__(self, maxsize, name=None):
 | 
			
		||||
        queue.Queue.__init__(self, maxsize=maxsize)
 | 
			
		||||
        self._size = 0
 | 
			
		||||
        self._name = name
 | 
			
		||||
 | 
			
		||||
    def clear(self):
 | 
			
		||||
        with self.mutex:
 | 
			
		||||
            self.queue.clear()
 | 
			
		||||
 | 
			
		||||
    def put(self, item, block=True, timeout=None, partial=False) -> int:
 | 
			
		||||
        self.not_full.acquire()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            if not self._will_it_fit(item) and partial:
 | 
			
		||||
                space_left = self.maxsize - self._qsize()
 | 
			
		||||
                if space_left:
 | 
			
		||||
                    item = item[:space_left]
 | 
			
		||||
 | 
			
		||||
            if not block:
 | 
			
		||||
                if not self._will_it_fit(item):
 | 
			
		||||
                    raise queue.Full
 | 
			
		||||
            elif timeout is None:
 | 
			
		||||
                while not self._will_it_fit(item):
 | 
			
		||||
                    self.not_full.wait()
 | 
			
		||||
            elif timeout < 0:
 | 
			
		||||
                raise ValueError("'timeout' must be a positive number")
 | 
			
		||||
            else:
 | 
			
		||||
                endtime = time.monotonic() + timeout
 | 
			
		||||
                while not self._will_it_fit(item):
 | 
			
		||||
                    remaining = endtime - time.monotonic()
 | 
			
		||||
                    if remaining <= 0:
 | 
			
		||||
                        raise queue.Full
 | 
			
		||||
                    self.not_full.wait(remaining)
 | 
			
		||||
 | 
			
		||||
            self._put(item)
 | 
			
		||||
            self.unfinished_tasks += 1
 | 
			
		||||
            self.not_empty.notify()
 | 
			
		||||
 | 
			
		||||
            return self._len(item)
 | 
			
		||||
        finally:
 | 
			
		||||
            self.not_full.release()
 | 
			
		||||
 | 
			
		||||
    # noinspection PyMethodMayBeStatic
 | 
			
		||||
    def _len(self, item):
 | 
			
		||||
        return len(item)
 | 
			
		||||
 | 
			
		||||
    def _qsize(self, l=len):  # noqa: E741
 | 
			
		||||
        return self._size
 | 
			
		||||
 | 
			
		||||
    # Put a new item in the queue
 | 
			
		||||
    def _put(self, item):
 | 
			
		||||
        self.queue.append(item)
 | 
			
		||||
        self._size += self._len(item)
 | 
			
		||||
 | 
			
		||||
    # Get an item from the queue
 | 
			
		||||
    def _get(self):
 | 
			
		||||
        item = self.queue.popleft()
 | 
			
		||||
        self._size -= self._len(item)
 | 
			
		||||
        return item
 | 
			
		||||
 | 
			
		||||
    def _will_it_fit(self, item):
 | 
			
		||||
        return self.maxsize - self._qsize() >= self._len(item)
 | 
			
		||||
							
								
								
									
										67
									
								
								octoprint_bambu_printer/char_counting_queue.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								octoprint_bambu_printer/char_counting_queue.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
import queue
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CharCountingQueue(queue.Queue):
 | 
			
		||||
    def __init__(self, maxsize, name=None):
 | 
			
		||||
        queue.Queue.__init__(self, maxsize=maxsize)
 | 
			
		||||
        self._size = 0
 | 
			
		||||
        self._name = name
 | 
			
		||||
 | 
			
		||||
    def clear(self):
 | 
			
		||||
        with self.mutex:
 | 
			
		||||
            self.queue.clear()
 | 
			
		||||
 | 
			
		||||
    def put(self, item, block=True, timeout=None, partial=False) -> int:
 | 
			
		||||
        self.not_full.acquire()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            if not self._will_it_fit(item) and partial:
 | 
			
		||||
                space_left = self.maxsize - self._qsize()
 | 
			
		||||
                if space_left:
 | 
			
		||||
                    item = item[:space_left]
 | 
			
		||||
 | 
			
		||||
            if not block:
 | 
			
		||||
                if not self._will_it_fit(item):
 | 
			
		||||
                    raise queue.Full
 | 
			
		||||
            elif timeout is None:
 | 
			
		||||
                while not self._will_it_fit(item):
 | 
			
		||||
                    self.not_full.wait()
 | 
			
		||||
            elif timeout < 0:
 | 
			
		||||
                raise ValueError("'timeout' must be a positive number")
 | 
			
		||||
            else:
 | 
			
		||||
                endtime = time.monotonic() + timeout
 | 
			
		||||
                while not self._will_it_fit(item):
 | 
			
		||||
                    remaining = endtime - time.monotonic()
 | 
			
		||||
                    if remaining <= 0:
 | 
			
		||||
                        raise queue.Full
 | 
			
		||||
                    self.not_full.wait(remaining)
 | 
			
		||||
 | 
			
		||||
            self._put(item)
 | 
			
		||||
            self.unfinished_tasks += 1
 | 
			
		||||
            self.not_empty.notify()
 | 
			
		||||
 | 
			
		||||
            return self._len(item)
 | 
			
		||||
        finally:
 | 
			
		||||
            self.not_full.release()
 | 
			
		||||
 | 
			
		||||
    # noinspection PyMethodMayBeStatic
 | 
			
		||||
    def _len(self, item):
 | 
			
		||||
        return len(item)
 | 
			
		||||
 | 
			
		||||
    def _qsize(self, l=len):  # noqa: E741
 | 
			
		||||
        return self._size
 | 
			
		||||
 | 
			
		||||
    # Put a new item in the queue
 | 
			
		||||
    def _put(self, item):
 | 
			
		||||
        self.queue.append(item)
 | 
			
		||||
        self._size += self._len(item)
 | 
			
		||||
 | 
			
		||||
    # Get an item from the queue
 | 
			
		||||
    def _get(self):
 | 
			
		||||
        item = self.queue.popleft()
 | 
			
		||||
        self._size -= self._len(item)
 | 
			
		||||
        return item
 | 
			
		||||
 | 
			
		||||
    def _will_it_fit(self, item):
 | 
			
		||||
        return self.maxsize - self._qsize() >= self._len(item)
 | 
			
		||||
		Reference in New Issue
	
	Block a user