Compare commits

...

11 Commits

Author SHA1 Message Date
3889efa67a 0.0.14
fix cache file list issues
optimize file listing to only update when retrieving file list and not while selecting or deleting a file
don't remove intermediary file on local storage
2024-02-18 01:57:28 -05:00
cb4b345aa7 0.0.13
use gcode_file instead of subtask_name if it doesn't have 3mf extension
2024-02-13 18:44:24 -05:00
3d0cc26147 0.0.12
fix issue with last PR that broke the ability to recognize currently printing file.
2024-02-12 21:35:09 -05:00
ff58636e41 0.0.11
support cache folder listing for P1 devices
add gcode command support
2024-02-12 19:14:10 -05:00
f54ab5c29f Merge pull request #5 from Pavulon87/use-cache-folder
Allow use of cache folder and custom gcode commands
2024-02-12 18:33:27 -05:00
7a4439c53e allow use of cache folder and custom g-gcode commands 2024-02-12 16:17:17 +01:00
9eb8b0da65 0.0.10
fix cancel command, #4
2024-02-12 00:09:04 -05:00
ef969d3d3b 0.0.9
fix upload_file and delete_file to return boolean as it did before switching the ftps client module for A1/P1 devices
2024-02-11 15:40:28 -05:00
3d92d73879 0.0.8
fix delete command
2024-02-10 17:56:50 -05:00
41dad23c49 pin paho-mqtt to versions less than 2 2024-02-10 13:33:09 -05:00
15538a9d0d switch ftpclient class to support A1/P1 devices hopefully 2024-02-10 11:30:38 -05:00
9 changed files with 377 additions and 202 deletions

View File

@ -10,11 +10,14 @@ from .ftpsclient import IoTFTPSClient
class BambuPrintPlugin(
octoprint.plugin.SettingsPlugin, octoprint.plugin.TemplatePlugin
octoprint.plugin.SettingsPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin
):
def get_assets(self):
return {'js': ["js/bambu_printer.js"]}
def get_template_configs(self):
return [{"type": "settings", "custom_bindings": False}]
return [{"type": "settings", "custom_bindings": False}] #, {"type": "generic", "custom_bindings": True, "template": "bambu_printer.jinja2"}]
def get_settings_defaults(self):
return {"device_type": "X1C",
@ -31,7 +34,9 @@ class BambuPrintPlugin(
"local_mqtt": True,
"region": "",
"email": "",
"auth_token": ""}
"auth_token": "",
"always_use_default_options": False
}
def support_3mf_files(self):
return {'machinecode': {'3mf': ["3mf"]}}
@ -50,7 +55,7 @@ class BambuPrintPlugin(
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)
# self._file_manager.remove_file("local", filename)
else:
raise Exception("upload failed")
except Exception as e:

View File

@ -1,2 +1 @@
from ._client import IoTFTPSClient
from ._version import __version__
from .ftpsclient import IoTFTPSClient

View File

@ -1,159 +0,0 @@
"""wrapper for FTPS server interactions"""
import ftplib
import ssl
from typing import List, Optional, Union
class ImplicitTLS(ftplib.FTP_TLS):
"""ftplib.FTP_TLS sub-class to support implicit SSL FTPS"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._sock = None
@property
def sock(self):
"""return socket"""
return self._sock
@sock.setter
def sock(self, value):
"""wrap and set SSL socket"""
if value is not None and not isinstance(value, ssl.SSLSocket):
value = self.context.wrap_socket(value)
self._sock = value
def ntransfercmd(self, cmd, rest=None):
conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest)
if self._prot_p:
conn = self.context.wrap_socket(conn,
server_hostname=self.host,
session=self.sock.session) # this is the fix
return conn, size
class IoTFTPSClient:
"""iot ftps ftpsclient"""
ftps_host: str
ftps_port: int
ftps_user: str
ftps_pass: str
ssl_implicit: bool
ftps_session: Union[ftplib.FTP, ImplicitTLS]
def __init__(
self,
ftps_host: str,
ftps_port: Optional[int] = 21,
ftps_user: Optional[str] = "",
ftps_pass: Optional[str] = "",
ssl_implicit: Optional[bool] = False,
) -> None:
self.ftps_host = ftps_host
self.ftps_port = ftps_port
self.ftps_user = ftps_user
self.ftps_pass = ftps_pass
self.ssl_implicit = ssl_implicit
self.instantiate_ftps_session()
def __repr__(self) -> str:
return (
"IoT FTPS Client\n"
"--------------------\n"
f"host: {self.ftps_host}\n"
f"port: {self.ftps_port}\n"
f"user: {self.ftps_user}\n"
f"ssl: {self.ssl_implicit}"
)
def instantiate_ftps_session(self) -> None:
"""init ftps_session based on input params"""
try:
if self.ssl_implicit:
self.ftps_session = ImplicitTLS()
else:
self.ftps_session = ftplib.FTP()
self.ftps_session.connect(host=self.ftps_host, port=self.ftps_port)
if self.ftps_user != "" and self.ftps_pass != "":
self.ftps_session.login(user=self.ftps_user, passwd=self.ftps_pass)
else:
self.ftps_session.login()
if self.ssl_implicit:
self.ftps_session.prot_p()
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return
def disconnect(self) -> None:
"""disconnect the current session from the ftps server"""
try:
self.ftps_session.close()
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return
def download_file(self, source: str, dest: str) -> bool:
"""download a file to a path on the local filesystem"""
try:
with open(dest, "wb") as file:
self.ftps_session.retrbinary(f"RETR {source}", file.write)
return True
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return False
def upload_file(self, source: str, dest: str) -> bool:
"""upload a file to a path inside the FTPS server"""
try:
with open(source, "rb") as file:
self.ftps_session.storbinary(f"STOR {dest}", file)
return True
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return False
def delete_file(self, path: str) -> bool:
"""delete a file from under a path inside the FTPS server"""
try:
self.ftps_session.delete(path)
return True
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return False
def move_file(self, source: str, dest: str) -> bool:
"""move a file inside the FTPS server to another path inside the FTPS server"""
try:
self.ftps_session.rename(source, dest)
return True
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return False
def list_files(
self, path: str, file_pattern: Optional[str] = None
) -> Union[List[str], None]:
"""list files under a path inside the FTPS server"""
try:
files = self.ftps_session.nlst(path)
if not files:
return
if file_pattern:
return [f for f in files if file_pattern in f]
return files
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return

View File

@ -1,3 +0,0 @@
VERSION = "1.1.1"
__version__ = VERSION

View File

@ -0,0 +1,228 @@
"""
Based on: <https://github.com/dgonzo27/py-iot-utils>
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
wrapper for FTPS server interactions
"""
import ftplib
import os
import socket
import ssl
from typing import Optional, Union, List
from contextlib import redirect_stdout
import io
import re
class ImplicitTLS(ftplib.FTP_TLS):
"""ftplib.FTP_TLS sub-class to support implicit SSL FTPS"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._sock = None
@property
def sock(self):
"""return socket"""
return self._sock
@sock.setter
def sock(self, value):
"""wrap and set SSL socket"""
if value is not None and not isinstance(value, ssl.SSLSocket):
value = self.context.wrap_socket(value)
self._sock = value
def ntransfercmd(self, cmd, rest=None):
conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest)
if self._prot_p:
conn = self.context.wrap_socket(conn,
server_hostname=self.host,
session=self.sock.session) # this is the fix
return conn, size
class IoTFTPSClient:
"""iot ftps ftpsclient"""
ftps_host: str
ftps_port: int
ftps_user: str
ftps_pass: str
ssl_implicit: bool
ftps_session: Union[ftplib.FTP, ImplicitTLS]
last_error: Optional[str] = None
welcome: str
def __init__(
self,
ftps_host: str,
ftps_port: Optional[int] = 21,
ftps_user: Optional[str] = "",
ftps_pass: Optional[str] = "",
ssl_implicit: Optional[bool] = False,
) -> None:
self.ftps_host = ftps_host
self.ftps_port = ftps_port
self.ftps_user = ftps_user
self.ftps_pass = ftps_pass
self.ssl_implicit = ssl_implicit
self.instantiate_ftps_session()
def __repr__(self) -> str:
return (
"IoT FTPS Client\n"
"--------------------\n"
f"host: {self.ftps_host}\n"
f"port: {self.ftps_port}\n"
f"user: {self.ftps_user}\n"
f"ssl: {self.ssl_implicit}"
)
def instantiate_ftps_session(self) -> None:
"""init ftps_session based on input params"""
self.ftps_session = ImplicitTLS() if self.ssl_implicit else ftplib.FTP()
self.ftps_session.set_debuglevel(0)
self.welcome = self.ftps_session.connect(
host=self.ftps_host, port=self.ftps_port)
if self.ftps_user and self.ftps_pass:
self.ftps_session.login(user=self.ftps_user, passwd=self.ftps_pass)
else:
self.ftps_session.login()
if self.ssl_implicit:
self.ftps_session.prot_p()
def disconnect(self) -> None:
"""disconnect the current session from the ftps server"""
self.ftps_session.close()
def download_file(self, source: str, dest: str):
"""download a file to a path on the local filesystem"""
with open(dest, "wb") as file:
self.ftps_session.retrbinary(f"RETR {source}", file.write)
def upload_file(self, source: str, dest: str, callback=None) -> bool:
"""upload a file to a path inside the FTPS server"""
file_size = os.path.getsize(source)
block_size = max(file_size // 100, 8192)
rest = None
try:
# Taken from ftplib.storbinary but with custom ssl handling
# due to the shitty bambu p1p ftps server TODO fix properly.
with open(source, "rb") as fp:
self.ftps_session.voidcmd('TYPE I')
with self.ftps_session.transfercmd(f"STOR {dest}", rest) as conn:
while 1:
buf = fp.read(block_size)
if not buf:
break
conn.sendall(buf)
if callback:
callback(buf)
# shutdown ssl layer
if ftplib._SSLSocket is not None and isinstance(conn, ftplib._SSLSocket):
# Yeah this is suposed to be conn.unwrap
# But since we operate in prot p mode
# we can close the connection always.
# This is cursed but it works.
if "vsFTPd" in self.welcome:
conn.unwrap()
else:
conn.shutdown(socket.SHUT_RDWR)
return True
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return False
def delete_file(self, path: str) -> bool:
"""delete a file from under a path inside the FTPS server"""
try:
self.ftps_session.delete(path)
return True
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return False
def move_file(self, source: str, dest: str):
"""move a file inside the FTPS server to another path inside the FTPS server"""
self.ftps_session.rename(source, dest)
def mkdir(self, path: str) -> str:
return self.ftps_session.mkd(path)
def list_files(self, path: str, file_pattern: Optional[str] = None) -> Union[List[str], None]:
"""list files under a path inside the FTPS server"""
try:
files = self.ftps_session.nlst(path)
if not files:
return
if file_pattern:
return [f for f in files if file_pattern in f]
return files
except Exception as ex:
print(f"unexpected exception occurred: {ex}")
pass
return
def list_files_ex(self, path: str) -> Union[list[str], None]:
"""list files under a path inside the FTPS server"""
try:
f = io.StringIO()
with redirect_stdout(f):
self.ftps_session.dir(path)
s = f.getvalue()
files = []
for row in s.split("\n"):
if len(row) <= 0: continue
attribs = row.split(" ")
match = re.search(r".*\ (\d\d\:\d\d|\d\d\d\d)\ (.*)", row)
name = ""
if match:
name = match.groups(1)[1]
else:
name = attribs[len(attribs) - 1]
file = ( attribs[0], name )
files.append(file)
return files
except Exception as ex:
print(f"unexpected exception occurred: [{ex}]")
pass
return

View File

@ -4,26 +4,74 @@
* Author: jneilliii
* License: AGPLv3
*/
$(function() {
function Bambu_printerViewModel(parameters) {
var self = this;
// assign the injected parameters, e.g.:
// self.loginStateViewModel = parameters[0];
// self.settingsViewModel = parameters[1];
self.settingsViewModel = parameters[0];
self.filesViewModel = parameters[1];
// TODO: Implement your plugin's view model here.
/*$('#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');
self.onBeforePrintStart = function(start_print_command) {
let confirmation_html = '' +
' <div class="row-fluid form-vertical">\n' +
' <div class="control-group">\n' +
' <label class="control-label">' + gettext("Plate Number") + '</label>\n' +
' <div class="controls">\n' +
' <input type="number" min="1" value="1" id="bambu_printer_plate_number" class="input-mini">\n' +
' </div>\n' +
' </div>\n' +
' </div>';
if(!self.settingsViewModel.settings.plugins.bambu_printer.always_use_default_options()){
confirmation_html += '\n' +
' <div class="row-fluid">\n' +
' <div class="span6">\n' +
' <label class="checkbox"><input id="bambu_printer_timelapse" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.timelapse()) ? ' checked' : '') + '> ' + gettext("Enable timelapse") + '</label>\n' +
' <label class="checkbox"><input id="bambu_printer_bed_leveling" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.bed_leveling()) ? ' checked' : '') + '> ' + gettext("Enable bed leveling") + '</label>\n' +
' <label class="checkbox"><input id="bambu_printer_flow_cali" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.flow_cali()) ? ' checked' : '') + '> ' + gettext("Enable flow calibration") + '</label>\n' +
' </div>\n' +
' <div class="span6">\n' +
' <label class="checkbox"><input id="bambu_printer_vibration_cali" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.vibration_cali()) ? ' checked' : '') + '> ' + gettext("Enable vibration calibration") + '</label>\n' +
' <label class="checkbox"><input id="bambu_printer_layer_inspect" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.layer_inspect()) ? ' checked' : '') + '> ' + gettext("Enable first layer inspection") + '</label>\n' +
' <label class="checkbox"><input id="bambu_printer_use_ams" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.use_ams()) ? ' checked' : '') + '> ' + gettext("Use AMS") + '</label>\n' +
' </div>\n' +
' </div>\n';
}
showConfirmationDialog({
title: "Bambu Print Options",
html: confirmation_html,
cancel: gettext("Cancel"),
proceed: [gettext("Print"), gettext("Always")],
onproceed: function (idx) {
if(idx === 1){
self.settingsViewModel.settings.plugins.bambu_printer.timelapse($('#bambu_printer_timelapse').is(':checked'));
self.settingsViewModel.settings.plugins.bambu_printer.bed_leveling($('#bambu_printer_bed_leveling').is(':checked'));
self.settingsViewModel.settings.plugins.bambu_printer.flow_cali($('#bambu_printer_flow_cali').is(':checked'));
self.settingsViewModel.settings.plugins.bambu_printer.vibration_cali($('#bambu_printer_vibration_cali').is(':checked'));
self.settingsViewModel.settings.plugins.bambu_printer.layer_inspect($('#bambu_printer_layer_inspect').is(':checked'));
self.settingsViewModel.settings.plugins.bambu_printer.use_ams($('#bambu_printer_use_ams').is(':checked'));
self.settingsViewModel.settings.plugins.bambu_printer.always_use_default_options(true);
self.settingsViewModel.saveData();
}
// replace this with our own print command API call?
start_print_command();
},
nofade: true
});
return false;
};*/
}
/* view model class, parameters for constructor, container to bind to
* Please see http://docs.octoprint.org/en/master/plugins/viewmodels.html#registering-custom-viewmodels for more details
* and a full list of the available options.
*/
OCTOPRINT_VIEWMODELS.push({
construct: Bambu_printerViewModel,
// ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ...
dependencies: [ /* "loginStateViewModel", "settingsViewModel" */ ],
dependencies: [ "settingsViewModel", "filesViewModel" ],
// Elements to bind to, e.g. #settings_plugin_bambu_printer, #tab_plugin_bambu_printer, ...
elements: [ /* ... */ ]
elements: [ "#bambu_printer_print_options" ]
});
});

View File

@ -27,7 +27,7 @@
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Print Options') }}</label>
<label class="control-label">{{ _('Default Print Options') }}</label>
<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: settings.plugins.bambu_printer.bed_leveling"> {{ _('Enable bed leveling') }}</label>
@ -37,4 +37,10 @@
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.use_ams"> {{ _('Use AMS') }}</label>
</div>
</div>
{#<div class="control-group">
<label class="control-label">{{ _('Always Use Default') }}</label>
<div class="controls">
<label class="checkbox"><input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.always_use_default_options"> </label>
</div>
</div>#}
</form>

View File

@ -68,8 +68,10 @@ class BambuPrinter:
self._sdCardReady = True
self._sdPrinter = None
self._sdPrinting = False
self._sdPrintStarting = False
self._sdPrintingSemaphore = threading.Event()
self._sdPrintingPausedSemaphore = threading.Event()
self._sdFileListCache = {}
self._selectedSdFile = None
self._selectedSdFileSize = 0
self._selectedSdFilePos = 0
@ -78,6 +80,7 @@ class BambuPrinter:
self._busy_loop = None
import logging
self._logger = logging.getLogger(
@ -163,14 +166,20 @@ class BambuPrinter:
self.bedTargetTemp = temperatures.get("target_bed_temp", 0.0)
self.chamberTemp = temperatures.get("chamber_temp", 0.0)
if print_job.get("gcode_state") == "RUNNING":
if print_job.get("gcode_state") == "RUNNING" or print_job.get("gcode_state") == "PREPARE":
if not self._sdPrintingSemaphore.is_set():
self._sdPrintingSemaphore.set()
if self._sdPrintingPausedSemaphore.is_set():
self._sdPrintingPausedSemaphore.clear()
self._sdPrintStarting = False
if not self._sdPrinting:
filename = print_job.get("subtask_name")
# TODO: swap this out to use 8 dot 3 name based on long name/path
if not self._sdFileListCache.get(filename.lower()):
if self._sdFileListCache.get(f"{filename.lower()}.3mf"):
filename = f"{filename.lower()}.3mf"
elif self._sdFileListCache.get(f"{filename.lower()}.gcode.3mf"):
filename = f"{filename.lower()}.gcode.3mf"
self._selectSdFile(filename)
self._startSdPrint(from_printer=True)
@ -185,9 +194,12 @@ class BambuPrinter:
self._send("// action:paused")
self._sendPaused()
if print_job.get("gcode_state") == "FINISH" and self._sdPrintingSemaphore.is_set():
self._selectedSdFilePos = self._selectedSdFileSize
self._finishSdPrint()
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
@ -241,6 +253,7 @@ class BambuPrinter:
self._sdCardReady = True
self._sdPrinting = False
self._sdPrintStarting = False
if self._sdPrinter:
self._sdPrinting = False
self._sdPrintingSemaphore.clear()
@ -429,6 +442,14 @@ class BambuPrinter:
else:
self._sendOk()
if self.bambu.connected:
GCODE_COMMAND = commands.SEND_GCODE_TEMPLATE
GCODE_COMMAND['print']['param'] = data + "\n"
if self.bambu.publish(GCODE_COMMAND):
self._logger.info("command sent successfully")
self._sendOk()
continue
finally:
self._logger.debug(f"{data}")
@ -474,13 +495,18 @@ class BambuPrinter:
def _gcode_M524(self, data: str) -> bool:
if self._sdCardReady:
self._cancelSdPrint()
return self._cancelSdPrint()
return False
def _gcode_M26(self, data: str) -> bool:
self._logger.debug("ignoring M26 command.")
self._send("M26 disabled for Bambu")
return True
if data == "M26 S0":
if self._sdCardReady:
return self._cancelSdPrint()
return False
else:
self._logger.debug("ignoring M26 command.")
self._send("M26 disabled for Bambu")
return True
def _gcode_M27(self, data: str) -> bool:
def report():
@ -509,8 +535,8 @@ class BambuPrinter:
# noinspection PyUnusedLocal
def _gcode_M29(self, data: str) -> bool:
self._logger.debug("ignoring M28 command.")
self._send("M28 disabled for Bambu")
self._logger.debug("ignoring M29 command.")
self._send("M29 disabled for Bambu")
return True
def _gcode_M30(self, data: str) -> bool:
@ -625,7 +651,7 @@ class BambuPrinter:
access_code = self._settings.get(["access_code"])
ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
filelist = ftp.list_files("", ".3mf")
filelist = ftp.list_files("", ".3mf") or []
for entry in filelist:
if entry.startswith("/"):
@ -643,21 +669,41 @@ class BambuPrinter:
"size": filesize,
"timestamp": unix_timestamp_to_m20_timestamp(int(filedate))
}
result[filename.lower()] = data
result[dosname.lower()] = filename.lower()
result[filename.lower()] = data
filelistcache = ftp.list_files("cache/", ".3mf") or []
for entry in filelistcache:
if entry.startswith("/"):
filename = entry[1:].replace("cache/", "")
else:
filename = entry.replace("cache/", "")
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()
data = {
"dosname": dosname,
"name": filename,
"path": "cache/"+filename,
"size": filesize,
"timestamp": unix_timestamp_to_m20_timestamp(int(filedate))
}
result[dosname.lower()] = filename.lower()
result[filename.lower()] = data
return result
def _getSdFileData(self, filename: str) -> Optional[Dict[str, Any]]:
files = self._mappedSdList()
data = files.get(filename.lower())
data = self._sdFileListCache.get(filename.lower())
if isinstance(data, str):
data = files.get(data.lower())
data = self._sdFileListCache.get(data.lower())
return data
def _getSdFiles(self) -> List[Dict[str, Any]]:
files = self._mappedSdList()
return [x for x in files.values() if isinstance(x, dict)]
self._sdFileListCache = self._mappedSdList()
return [x for x in self._sdFileListCache.values() if isinstance(x, dict)]
def _selectSdFile(self, filename: str, check_already_open: bool = False) -> None:
if filename.startswith("/"):
@ -680,6 +726,7 @@ class BambuPrinter:
if self._selectedSdFile is not None:
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.start()
# self._sdPrintingSemaphore.set()
@ -699,18 +746,21 @@ class BambuPrinter:
else:
self._logger.info("print pause failed")
def _cancelSdPrint(self):
def _cancelSdPrint(self) -> bool:
if self.bambu.connected:
if self.bambu.publish(commands.STOP):
self._logger.info("print cancelled")
self._finishSdPrint()
return True
else:
self._logger.info("print cancel failed")
return False
def _setSdPos(self, pos):
self._newSdFilePos = pos
def _reportSdStatus(self):
if self._sdPrinter is not None and (self._sdPrintingSemaphore.is_set() or self._sdPrintingPausedSemaphore.is_set()):
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")
@ -799,6 +849,7 @@ class BambuPrinter:
self._selectedSdFilePos = 0
self._selectedSdFileSize = 0
self._sdPrinting = False
self._sdPrintStarting = False
self._sdPrinter = None
def _deleteSdFile(self, filename: str) -> None:
@ -811,7 +862,7 @@ class BambuPrinter:
if file is not None:
ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True)
try:
if ftp.delete_file(filename):
if ftp.delete_file(file["path"]):
self._logger.debug(f"{filename} deleted")
else:
raise Exception("delete failed")

View File

@ -14,7 +14,7 @@ plugin_package = "octoprint_bambu_printer"
plugin_name = "OctoPrint-BambuPrinter"
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
plugin_version = "0.0.5"
plugin_version = "0.0.14"
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
# module
@ -33,7 +33,7 @@ plugin_url = "https://github.com/jneilliii/OctoPrint-BambuPrinter"
plugin_license = "AGPLv3"
# Any additional requirements besides OctoPrint should be listed here
plugin_requires = ["paho-mqtt", "python-dateutil", "pybambu>=1.0.1"]
plugin_requires = ["paho-mqtt<2", "python-dateutil", "pybambu>=1.0.1"]
### --------------------------------------------------------------------------------------------------------------------
### More advanced options that you usually shouldn't have to touch follow after this point