2024-07-24 17:15:45 +03:00
|
|
|
from __future__ import absolute_import, annotations
|
|
|
|
import os
|
2024-07-24 17:15:47 +03:00
|
|
|
from pathlib import Path
|
2024-07-24 17:15:45 +03:00
|
|
|
import threading
|
|
|
|
import time
|
|
|
|
import flask
|
|
|
|
import datetime
|
2024-07-24 17:15:46 +03:00
|
|
|
import logging.handlers
|
|
|
|
from urllib.parse import quote as urlquote
|
2024-07-24 17:15:45 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2024-07-24 17:15:46 +03:00
|
|
|
from .printer.ftpsclient.ftpsclient import IoTFTPSClient
|
|
|
|
from .printer.bambu_virtual_printer import BambuVirtualPrinter
|
2024-07-24 17:15:45 +03:00
|
|
|
|
|
|
|
|
|
|
|
class BambuPrintPlugin(
|
|
|
|
octoprint.plugin.SettingsPlugin,
|
|
|
|
octoprint.plugin.TemplatePlugin,
|
|
|
|
octoprint.plugin.AssetPlugin,
|
|
|
|
octoprint.plugin.EventHandlerPlugin,
|
|
|
|
octoprint.plugin.SimpleApiPlugin,
|
|
|
|
octoprint.plugin.BlueprintPlugin,
|
|
|
|
):
|
|
|
|
_logger: logging.Logger
|
|
|
|
_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,
|
2024-07-24 17:15:45 +03:00
|
|
|
"layer_inspect": False,
|
2024-07-24 17:15:45 +03:00
|
|
|
"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(),
|
2024-07-24 17:15:46 +03:00
|
|
|
serial_log_handler=seriallog_handler,
|
2024-07-24 17:15:45 +03:00
|
|
|
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"]:
|
2024-07-24 17:15:47 +03:00
|
|
|
timelapse_file_list = ftp.list_files("timelapse/", ".mp4")
|
2024-07-24 17:15:45 +03:00
|
|
|
else:
|
2024-07-24 17:15:47 +03:00
|
|
|
timelapse_file_list = ftp.list_files("timelapse/", ".avi")
|
2024-07-24 17:15:45 +03:00
|
|
|
for entry in timelapse_file_list:
|
2024-07-24 17:15:47 +03:00
|
|
|
filename = entry.name
|
|
|
|
filesize = ftp.ftps_session.size(entry.as_posix())
|
2024-07-24 17:15:45 +03:00
|
|
|
date_str = ftp.ftps_session.sendcmd(
|
2024-07-24 17:15:47 +03:00
|
|
|
f"MDTM {entry.as_posix()}"
|
2024-07-24 17:15:45 +03:00
|
|
|
).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",
|
|
|
|
}
|
|
|
|
}
|