Compare commits

...

10 Commits

Author SHA1 Message Date
8e3eb9c64b 0.0.23
* fix thumbnail url for avi based timelapse, #30
2024-05-24 23:37:51 -04:00
e1ea88dbae 0.0.22
* add file listing of avi files for non X1 devices, #29
2024-05-24 19:37:51 -04:00
ac7bb16a2b 0.0.21
add timelapses with thumbnail
2024-05-18 18:45:05 -04:00
112210a3f1 list timelapses on printer with thumbnail and download button 2024-05-18 14:31:03 -04:00
176154cfee 0.0.20 (#27)
* fixes issue related to printing a cloud print again and subtask_name including full path to file, #25
2024-05-14 13:33:37 -04:00
56e5fb4dd2 fixes issue related to printing a cloud print again and subtask_name including full path to file, #25 2024-05-12 17:21:53 -04:00
3e7708429d 0.0.19 (#24)
* attempt to fix A1 related print issues, #9 
* flow rate increase logic for M220 from x1plus community
2024-05-12 13:39:25 -04:00
908173214f adjust start print command for better A1 compatibility 2024-03-17 00:34:42 -04:00
df4bd6cf44 additional logging 2024-03-08 22:30:39 -05:00
bcb1e0f649 0.0.17
expose cloud connection options
2024-03-02 20:38:54 -05:00
6 changed files with 376 additions and 30 deletions

View File

@ -1,25 +1,35 @@
# coding=utf-8 # coding=utf-8
from __future__ import absolute_import from __future__ import absolute_import
import os
import threading import threading
import time import time
import flask
import datetime
import octoprint.plugin import octoprint.plugin
from octoprint.events import Events 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 from .ftpsclient import IoTFTPSClient
class BambuPrintPlugin(octoprint.plugin.SettingsPlugin, class BambuPrintPlugin(octoprint.plugin.SettingsPlugin,
octoprint.plugin.TemplatePlugin, octoprint.plugin.TemplatePlugin,
octoprint.plugin.AssetPlugin, octoprint.plugin.AssetPlugin,
octoprint.plugin.EventHandlerPlugin): octoprint.plugin.EventHandlerPlugin,
octoprint.plugin.SimpleApiPlugin,
octoprint.plugin.BlueprintPlugin):
def get_assets(self): def get_assets(self):
return {'js': ["js/bambu_printer.js"]} return {'js': ["js/bambu_printer.js"]}
def get_template_configs(self): def get_template_configs(self):
return [{"type": "settings", "custom_bindings": False}] #, {"type": "generic", "custom_bindings": True, "template": "bambu_printer.jinja2"}] 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): def get_settings_defaults(self):
return {"device_type": "X1C", return {"device_type": "X1C",
@ -40,6 +50,19 @@ class BambuPrintPlugin(octoprint.plugin.SettingsPlugin,
"always_use_default_options": False "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): def on_event(self, event, payload):
if event == Events.TRANSFER_DONE: if event == Events.TRANSFER_DONE:
self._printer.commands("M20 L T", force=True) self._printer.commands("M20 L T", force=True)
@ -74,6 +97,9 @@ class BambuPrintPlugin(octoprint.plugin.SettingsPlugin,
return filename return filename
def get_template_vars(self):
return {"plugin_version": self._plugin_version}
def virtual_printer_factory(self, comm_instance, port, baudrate, read_timeout): def virtual_printer_factory(self, comm_instance, port, baudrate, read_timeout):
if not port == "BAMBU": if not port == "BAMBU":
return None return None
@ -111,6 +137,103 @@ class BambuPrintPlugin(octoprint.plugin.SettingsPlugin,
else: else:
return [] 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): def get_update_information(self):
return {'bambu_printer': {'displayName': "Bambu Printer", return {'bambu_printer': {'displayName': "Bambu Printer",
'displayVersion': self._plugin_version, 'displayVersion': self._plugin_version,
@ -146,4 +269,6 @@ def __plugin_load__():
"octoprint.filemanager.extension_tree": __plugin_implementation__.support_3mf_files, "octoprint.filemanager.extension_tree": __plugin_implementation__.support_3mf_files,
"octoprint.printer.sdcardupload": __plugin_implementation__.upload_to_sd, "octoprint.printer.sdcardupload": __plugin_implementation__.upload_to_sd,
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, "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
} }

View File

@ -5,12 +5,83 @@
* License: AGPLv3 * License: AGPLv3
*/ */
$(function() { $(function () {
function Bambu_printerViewModel(parameters) { function Bambu_printerViewModel(parameters) {
var self = this; var self = this;
self.settingsViewModel = parameters[0]; self.settingsViewModel = parameters[0];
self.filesViewModel = parameters[1]; self.filesViewModel = parameters[1];
self.loginStateViewModel = parameters[2];
self.accessViewModel = parameters[3];
self.timelapseViewModel = parameters[4];
self.getAuthToken = function (data) {
self.settingsViewModel.settings.plugins.bambu_printer.auth_token("");
OctoPrint.simpleApiCommand("bambu_printer", "register", {
"email": self.settingsViewModel.settings.plugins.bambu_printer.email(),
"password": $("#bambu_cloud_password").val(),
"region": self.settingsViewModel.settings.plugins.bambu_printer.region(),
"auth_token": self.settingsViewModel.settings.plugins.bambu_printer.auth_token()
})
.done(function (response) {
console.log(response);
self.settingsViewModel.settings.plugins.bambu_printer.auth_token(response.auth_token);
self.settingsViewModel.settings.plugins.bambu_printer.username(response.username);
});
};
// initialize list helper
self.listHelper = new ItemListHelper(
"timelapseFiles",
{
name: function (a, b) {
// sorts ascending
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase())
return -1;
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase())
return 1;
return 0;
},
date: function (a, b) {
// sorts descending
if (a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1;
return 0;
},
size: function (a, b) {
// sorts descending
if (a["bytes"] > b["bytes"]) return -1;
if (a["bytes"] < b["bytes"]) return 1;
return 0;
}
},
{},
"name",
[],
[],
CONFIG_TIMELAPSEFILESPERPAGE
);
self.onDataUpdaterPluginMessage = function(plugin, data) {
if (plugin != "bambu_printer") {
return;
}
if (data.files !== undefined) {
console.log(data.files);
self.listHelper.updateItems(data.files);
self.listHelper.resetPage();
}
};
self.onBeforeBinding = function () {
$('#bambu_timelapse').appendTo("#timelapse");
};
self.showTimelapseThumbnail = function(data) {
$("#bambu_printer_timelapse_thumbnail").attr("src", data.thumbnail);
$("#bambu_printer_timelapse_preview").modal('show');
};
/*$('#files div.upload-buttons > span.fileinput-button:first, #files div.folder-button').remove(); /*$('#files div.upload-buttons > span.fileinput-button:first, #files div.folder-button').remove();
$('#files div.upload-buttons > span.fileinput-button:first').removeClass('span6').addClass('input-block-level'); $('#files div.upload-buttons > span.fileinput-button:first').removeClass('span6').addClass('input-block-level');
@ -70,8 +141,8 @@ $(function() {
OCTOPRINT_VIEWMODELS.push({ OCTOPRINT_VIEWMODELS.push({
construct: Bambu_printerViewModel, construct: Bambu_printerViewModel,
// ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ... // ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ...
dependencies: [ "settingsViewModel", "filesViewModel" ], dependencies: ["settingsViewModel", "filesViewModel", "loginStateViewModel", "accessViewModel", "timelapseViewModel"],
// Elements to bind to, e.g. #settings_plugin_bambu_printer, #tab_plugin_bambu_printer, ... // Elements to bind to, e.g. #settings_plugin_bambu_printer, #tab_plugin_bambu_printer, ...
elements: [ "#bambu_printer_print_options" ] elements: ["#bambu_printer_print_options", "#settings_plugin_bambu_printer", "#bambu_timelapse"]
}); });
}); });

View File

@ -1,40 +1,72 @@
<h3>Virtual Printer</h3> <h3>Bambu Printer Settings <small>{{ _('Version') }} {{ plugin_bambu_printer_plugin_version }}</small></h3>
<form class="form-horizontal" onsubmit="return false;"> <form class="form-horizontal" onsubmit="return false;">
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Device Type') }}</label> <label class="control-label">{{ _('Device Type') }}</label>
<div class="controls"> <div class="controls">
<select class="input-block-level" data-bind="options: ['A1', 'A1MINI', 'P1P', 'P1S', 'X1', 'X1C'], value: settings.plugins.bambu_printer.device_type, allowUnset: true"> <select class="input-block-level" data-bind="options: ['A1', 'A1MINI', 'P1P', 'P1S', 'X1', 'X1C'], value: settingsViewModel.settings.plugins.bambu_printer.device_type, allowUnset: true">
</select> </select>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('IP Address') }}</label> <label class="control-label">{{ _('IP Address') }}</label>
<div class="controls"> <div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.plugins.bambu_printer.host" placeholder="192.168.0.2" title="{{ _('IP address or hostname of the printer') }}"></input> <input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.host" placeholder="192.168.0.2" title="{{ _('IP address or hostname of the printer') }}"></input>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Serial Number') }}</label> <label class="control-label">{{ _('Serial Number') }}</label>
<div class="controls"> <div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.plugins.bambu_printer.serial" title="{{ _('Serial number of printer') }}"></input> <input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.serial" title="{{ _('Serial number of printer') }}"></input>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Access Code') }}</label> <label class="control-label">{{ _('Access Code') }}</label>
<div class="controls"> <div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.plugins.bambu_printer.access_code" title="{{ _('Access code of printer') }}"></input> <input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.access_code" title="{{ _('Access code of printer') }}"></input>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.local_mqtt"> {{ _('Use Local Access, disable for cloud connection') }}</label>
</div>
</div>
<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()">
<label class="control-label">{{ _('Region') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.region" title="{{ _('Region used to connect, ie China, US') }}"></input>
</div>
</div>
<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()">
<label class="control-label">{{ _('Email') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.email" title="{{ _('Registered email address') }}"></input>
</div>
</div>
<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()">
<label class="control-label">{{ _('Password') }}</label>
<div class="controls">
<div class="input-block-level input-append">
<input id="bambu_cloud_password" type="password" class="input-text input-block-level" title="{{ _('Password to generate Auth Token') }}"></input>
<span class="btn btn-primary add-on" data-bind="click: getAuthToken">{{ _('Login') }}</span>
</div>
</div>
</div>
<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()">
<label class="control-label">{{ _('Auth Token') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.auth_token" title="{{ _('Auth Token') }}"></input>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Default Print Options') }}</label> <label class="control-label">{{ _('Default Print Options') }}</label>
<div class="controls"> <div class="controls">
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.timelapse"> {{ _('Enable timelapse') }}</label> <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.timelapse"> {{ _('Enable timelapse') }}</label>
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.bed_leveling"> {{ _('Enable bed leveling') }}</label> <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.bed_leveling"> {{ _('Enable bed leveling') }}</label>
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.flow_cali"> {{ _('Enable flow calibration') }}</label> <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.flow_cali"> {{ _('Enable flow calibration') }}</label>
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.vibration_cali"> {{ _('Enable vibration calibration') }}</label> <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.vibration_cali"> {{ _('Enable vibration calibration') }}</label>
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.layer_inspect"> {{ _('Enable first layer inspection') }}</label> <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.layer_inspect"> {{ _('Enable first layer inspection') }}</label>
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.use_ams"> {{ _('Use AMS') }}</label> <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.use_ams"> {{ _('Use AMS') }}</label>
</div> </div>
</div> </div>
{#<div class="control-group"> {#<div class="control-group">

View File

@ -0,0 +1,71 @@
<div class="row-fluid" id="bambu_timelapse">
<h1>{{ _('Bambu Timelapses') }}</h1>
<div class="pull-right">
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="javascript:void(0)" data-bind="click: function() { listHelper.changeSorting('name'); }"><i class="fas fa-check" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> {{ _('Sort by name') }} ({{ _('ascending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { listHelper.changeSorting('date'); }"><i class="fas fa-check" data-bind="style: {visibility: listHelper.currentSorting() == 'date' ? 'visible' : 'hidden'}"></i> {{ _('Sort by date') }} ({{ _('descending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { listHelper.changeSorting('size'); }"><i class="fas fa-check" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> {{ _('Sort by file size') }} ({{ _('descending') }})</a></li>
</ul>
</div>
</div>
<table class="table table-hover table-condensed table-hover" id="bambu_timelapse_files">
<thead>
<tr>
<th class="timelapse_files_thumb"></th>
<th class="timelapse_files_details">{{ _('Details') }}</th>
<th class="timelapse_files_action">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: listHelper.paginatedItems">
<tr data-bind="attr: {title: name}">
<td class="timelapse_files_thumb">
<div class="thumb" data-bind="css: { letterbox: $data.thumbnail }">
<!-- ko if: $data.thumbnail -->
<img data-bind="attr:{src: thumbnail}" loading="lazy" style="aspect-ratio: 3 / 2;"/>
<!-- /ko -->
<a href="javascript:void(0)" data-bind="css: {disabled: !$root.timelapseViewModel.isTimelapseViewable($data)}, click: $root.showTimelapseThumbnail"></a>
</div>
</td>
<td class="timelapse_files_details">
<p class="name" data-bind="text: name"></p>
<p class="detail">{{ _('Recorded:') }} <span data-bind="text: formatTimeAgo(timestamp)"/></p>
<p class="detail">{{ _('Size:') }} <span data-bind="text: size"/></p>
</td>
<td class="timelapse_files_action">
<div class="btn-group action-buttons">
<a href="javascript:void(0)" class="btn btn-mini" data-bind="css: {disabled: !$root.loginStateViewModel.hasPermissionKo($root.accessViewModel.permissions.TIMELAPSE_DOWNLOAD)()}, attr: { href: ($root.loginStateViewModel.hasPermission($root.accessViewModel.permissions.TIMELAPSE_DOWNLOAD)) ? $data.url : 'javascript:void(0)' }"><i class="fas fa-download"></i></a>
</div>
</td>
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="javascript:void(0)" data-bind="click: listHelper.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: listHelper.pages">
<li data-bind="css: { active: $data.number === $root.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="javascript:void(0)" data-bind="text: $data.text, click: function() { $root.listHelper.changePage($data.number); }"></a></li>
</ul>
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="javascript:void(0)" data-bind="click: listHelper.nextPage">»</a></li>
</ul>
</div>
</div>
<div id="bambu_printer_timelapse_preview" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
<h3>{{ _('Timelapse Thumbnail') }}</h3>
</div>
<div class="modal-body">
<div class="row-fluid">
<img id="bambu_printer_timelapse_thumbnail" src="" class="row-fluid" style="aspect-ratio: 3 / 2;"/>
</div>
</div>
<div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Close') }}</a>
</div>
</div>

View File

@ -4,6 +4,7 @@ __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agp
import collections import collections
import datetime import datetime
import math
import os import os
import queue import queue
import re import re
@ -162,13 +163,14 @@ class BambuPrinter:
# self._logger.debug(device_data) # self._logger.debug(device_data)
self.lastTempAt = time.monotonic()
self.temp[0] = temperatures.get("nozzle_temp", 0.0) self.temp[0] = temperatures.get("nozzle_temp", 0.0)
self.targetTemp[0] = temperatures.get("target_nozzle_temp", 0.0) self.targetTemp[0] = temperatures.get("target_nozzle_temp", 0.0)
self.bedTemp = temperatures.get("bed_temp", 0.0) self.bedTemp = temperatures.get("bed_temp", 0.0)
self.bedTargetTemp = temperatures.get("target_bed_temp", 0.0) self.bedTargetTemp = temperatures.get("target_bed_temp", 0.0)
self.chamberTemp = temperatures.get("chamber_temp", 0.0) self.chamberTemp = temperatures.get("chamber_temp", 0.0)
if print_job.get("gcode_state") == "RUNNING" or print_job.get("gcode_state") == "PREPARE": if print_job.get("gcode_state") == "RUNNING":
if not self._sdPrintingSemaphore.is_set(): if not self._sdPrintingSemaphore.is_set():
self._sdPrintingSemaphore.set() self._sdPrintingSemaphore.set()
if self._sdPrintingPausedSemaphore.is_set(): if self._sdPrintingPausedSemaphore.is_set():
@ -181,6 +183,10 @@ class BambuPrinter:
filename = f"{filename.lower()}.3mf" filename = f"{filename.lower()}.3mf"
elif self._sdFileListCache.get(f"{filename.lower()}.gcode.3mf"): elif self._sdFileListCache.get(f"{filename.lower()}.gcode.3mf"):
filename = f"{filename.lower()}.gcode.3mf" filename = f"{filename.lower()}.gcode.3mf"
elif filename.startswith("cache/"):
filename = filename[6:]
else:
self._logger.debug(f"No 3mf file found for {print_job}")
self._selectSdFile(filename) self._selectSdFile(filename)
self._startSdPrint(from_printer=True) self._startSdPrint(from_printer=True)
@ -196,7 +202,7 @@ class BambuPrinter:
self._send("// action:paused") self._send("// action:paused")
self._sendPaused() 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: if self._sdPrintStarting is False:
self._sdPrinting = False self._sdPrinting = False
if self._sdPrintingSemaphore.is_set(): if self._sdPrintingSemaphore.is_set():
@ -211,24 +217,31 @@ class BambuPrinter:
): ):
asyncio.run(self._create_connection_async()) asyncio.run(self._create_connection_async())
def on_disconnect(self, on_disconnect):
self._logger.debug(f"on disconnect called")
return on_disconnect
def on_connect(self, on_connect):
self._logger.debug(f"on connect called")
return on_connect
async def _create_connection_async(self): 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"]), self.bambu = BambuClient(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=self._settings.get(["username"]), username="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=self._settings.get_boolean(["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"])
) )
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) self.bambu.connect(callback=self.new_update)
self._logger.info(f"bambu connection status: {self.bambu.connected}") self._logger.info(f"bambu connection status: {self.bambu.connected}")
self._sendOk() self._sendOk()
# while True:
# await asyncio.sleep(self.tick_rate)
# self._processTemperatureQuery()
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(
@ -554,8 +567,7 @@ class BambuPrinter:
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def _gcode_M105(self, data: str) -> bool: def _gcode_M105(self, data: str) -> bool:
self._processTemperatureQuery() return self._processTemperatureQuery()
return True
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def _gcode_M115(self, data: str) -> bool: def _gcode_M115(self, data: str) -> bool:
@ -588,6 +600,26 @@ class BambuPrinter:
self._send(text) self._send(text)
return True return True
# noinspection PyUnusedLocal
def _gcode_M220(self, data: str) -> bool:
if self.bambu.connected:
gcode_command = commands.SEND_GCODE_TEMPLATE
percent = int(data[1:])
if percent is None or percent < 1 or percent > 166:
return True
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
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
if self.bambu.publish(gcode_command):
self._logger.info(f"{percent}% speed adjustment command sent successfully")
return True
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def _gcode_M400(self, data: str) -> bool: def _gcode_M400(self, data: str) -> bool:
return True return True
@ -698,16 +730,20 @@ class BambuPrinter:
return result return result
def _getSdFileData(self, filename: str) -> Optional[Dict[str, Any]]: def _getSdFileData(self, filename: str) -> Optional[Dict[str, Any]]:
self._logger.debug(f"_getSdFileData: {filename}")
data = self._sdFileListCache.get(filename.lower()) data = self._sdFileListCache.get(filename.lower())
if isinstance(data, str): if isinstance(data, str):
data = self._sdFileListCache.get(data.lower()) data = self._sdFileListCache.get(data.lower())
self._logger.debug(f"_getSdFileData: {data}")
return data return data
def _getSdFiles(self) -> List[Dict[str, Any]]: def _getSdFiles(self) -> List[Dict[str, Any]]:
self._sdFileListCache = self._mappedSdList() self._sdFileListCache = self._mappedSdList()
self._logger.debug(f"_getSdFiles return: {self._sdFileListCache}")
return [x for x in self._sdFileListCache.values() if isinstance(x, dict)] return [x for x in self._sdFileListCache.values() if isinstance(x, dict)]
def _selectSdFile(self, filename: str, check_already_open: bool = False) -> None: def _selectSdFile(self, filename: str, check_already_open: bool = False) -> None:
self._logger.debug(f"_selectSdFile: {filename}, check_already_open={check_already_open}")
if filename.startswith("/"): if filename.startswith("/"):
filename = filename[1:] filename = filename[1:]
@ -729,6 +765,7 @@ class BambuPrinter:
self._send("File selected") self._send("File selected")
def _startSdPrint(self, from_printer: bool = False) -> None: def _startSdPrint(self, from_printer: bool = False) -> None:
self._logger.debug(f"_startSdPrint: from_printer={from_printer}")
if self._selectedSdFile is not None: if self._selectedSdFile is not None:
if self._sdPrinter is None: if self._sdPrinter is None:
self._sdPrinting = True self._sdPrinting = True
@ -788,10 +825,14 @@ class BambuPrinter:
output += " @:64\n" output += " @:64\n"
return output return output
def _processTemperatureQuery(self): def _processTemperatureQuery(self) -> bool:
# includeOk = not self._okBeforeCommandOutput # includeOk = not self._okBeforeCommandOutput
output = self._generateTemperatureOutput() if self.bambu.connected:
self._send(output) output = self._generateTemperatureOutput()
self._send(output)
return True
else:
return False
def _writeSdFile(self, filename: str) -> None: def _writeSdFile(self, filename: str) -> None:
self._send(f"Writing to file: {filename}") self._send(f"Writing to file: {filename}")
@ -820,7 +861,13 @@ class BambuPrinter:
print_command = {"print": {"sequence_id": 0, print_command = {"print": {"sequence_id": 0,
"command": "project_file", "command": "project_file",
"param": "Metadata/plate_1.gcode", "param": "Metadata/plate_1.gcode",
"md5": "",
"profile_id": "0",
"project_id": "0",
"subtask_id": "0",
"task_id": "0",
"subtask_name": f"{self._selectedSdFile}", "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}", "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"]), "timelapse": self._settings.get_boolean(["timelapse"]),
"bed_leveling": self._settings.get_boolean(["bed_leveling"]), "bed_leveling": self._settings.get_boolean(["bed_leveling"]),

View File

@ -14,7 +14,7 @@ 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.0.16" plugin_version = "0.0.23"
# 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