diff --git a/octoprint_bambu_printer/bambu_print_plugin.py b/octoprint_bambu_printer/bambu_print_plugin.py index bd8e45e..06bf4e6 100644 --- a/octoprint_bambu_printer/bambu_print_plugin.py +++ b/octoprint_bambu_printer/bambu_print_plugin.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, annotations import os +from pathlib import Path import threading import time import flask @@ -195,17 +196,14 @@ class BambuPrintPlugin( 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 [] + timelapse_file_list = ftp.list_files("timelapse/", ".mp4") else: - timelapse_file_list = ftp.list_files("timelapse/", ".avi") or [] + timelapse_file_list = ftp.list_files("timelapse/", ".avi") 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}") + filename = entry.name + filesize = ftp.ftps_session.size(entry.as_posix()) date_str = ftp.ftps_session.sendcmd( - f"MDTM timelapse/{filename}" + f"MDTM {entry.as_posix()}" ).replace("213 ", "") filedate = ( datetime.datetime.strptime(date_str, "%Y%m%d%H%M%S") diff --git a/octoprint_bambu_printer/printer/bambu_virtual_printer.py b/octoprint_bambu_printer/printer/bambu_virtual_printer.py index ee4e49d..da58be5 100644 --- a/octoprint_bambu_printer/printer/bambu_virtual_printer.py +++ b/octoprint_bambu_printer/printer/bambu_virtual_printer.py @@ -67,7 +67,7 @@ class BambuVirtualPrinter: self._running = True self._printer_thread = threading.Thread( target=self._printer_worker, - name="octoprint.plugins.bambu_printer.printer_worker", + name="octoprint.plugins.bambu_printer.printer_state", ) self._state_change_queue = queue.Queue() @@ -229,7 +229,7 @@ class BambuVirtualPrinter: bambu_client.on_connect = self.on_connect(bambu_client.on_connect) bambu_client.connect(callback=self.new_update) self._log.info(f"bambu connection status: {bambu_client.connected}") - self._serial_io.sendOk() + self.sendOk() self._bambu_client = bambu_client def __str__(self): @@ -304,11 +304,21 @@ class BambuVirtualPrinter: ##~~ command implementations + @gcode_executor.register_no_data("M21") + def _sd_status(self) -> None: + self.sendIO("SD card ok") + @gcode_executor.register("M23") def _select_sd_file(self, data: str) -> bool: filename = data.split(maxsplit=1)[1].strip() self._list_sd() - return self.file_system.select_file(filename) + if self.file_system.select_file(filename): + assert self.file_system.selected_file is not None + self.sendIO( + f"File opened: {self.file_system.selected_file.file_name} " + f"Size: {self.file_system.selected_file.size}" + ) + self.sendIO("File selected") @gcode_executor.register("M26") def _set_sd_position(self, data: str) -> bool: @@ -335,6 +345,7 @@ class BambuVirtualPrinter: else: self._sdstatus_reporter = None + self.update_print_job_info() self.report_print_job_status() return True @@ -350,8 +361,8 @@ class BambuVirtualPrinter: return self._processTemperatureQuery() # noinspection PyUnusedLocal - @gcode_executor.register("M115") - def _report_firmware_info(self, data: str) -> bool: + @gcode_executor.register_no_data("M115") + def _report_firmware_info(self) -> bool: self.sendIO("Bambu Printer Integration") self.sendIO("Cap:EXTENDED_M20:1") self.sendIO("Cap:LFN_WRITE:1") @@ -412,7 +423,7 @@ class BambuVirtualPrinter: self._log.debug(f"processing gcode {gcode} command = {full_command}") handled = self.gcode_executor.execute(self, gcode, full_command) if handled: - self._serial_io.sendOk() + self.sendOk() return # post gcode to printer otherwise @@ -421,7 +432,7 @@ class BambuVirtualPrinter: GCODE_COMMAND["print"]["param"] = full_command + "\n" if self.bambu_client.publish(GCODE_COMMAND): self._log.info("command sent successfully") - self._serial_io.sendOk() + self.sendOk() @gcode_executor.register_no_data("M112") def _shutdown(self): @@ -432,8 +443,8 @@ class BambuVirtualPrinter: self._serial_io.close() return True - @gcode_executor.register_no_data("M20") - def _list_sd(self): + @gcode_executor.register("M20") + def _list_sd(self, data: str = ""): self.sendIO("Begin file list") for item in map(lambda f: f.get_log_info(), self.file_system.get_all_files()): self.sendIO(item) @@ -464,7 +475,7 @@ class BambuVirtualPrinter: else: self.sendIO("Not SD printing") - def _generateTemperatureOutput(self) -> str: + def _create_temperature_message(self) -> str: template = "{heater}:{actual:.2f}/ {target:.2f}" temps = collections.OrderedDict() temps["T"] = (self._telemetry.temp[0], self._telemetry.targetTemp[0]) @@ -487,7 +498,7 @@ class BambuVirtualPrinter: def _processTemperatureQuery(self) -> bool: # includeOk = not self._okBeforeCommandOutput if self.bambu_client.connected: - output = self._generateTemperatureOutput() + output = self._create_temperature_message() self.sendIO(output) return True else: diff --git a/octoprint_bambu_printer/printer/ftpsclient/ftpsclient.py b/octoprint_bambu_printer/printer/ftpsclient/ftpsclient.py index 638e3e8..ca67a40 100644 --- a/octoprint_bambu_printer/printer/ftpsclient/ftpsclient.py +++ b/octoprint_bambu_printer/printer/ftpsclient/ftpsclient.py @@ -26,6 +26,7 @@ wrapper for FTPS server interactions import ftplib import os +from pathlib import Path import socket import ssl from typing import Optional, Union, List @@ -34,6 +35,7 @@ from contextlib import redirect_stdout import io import re + class ImplicitTLS(ftplib.FTP_TLS): """ftplib.FTP_TLS sub-class to support implicit SSL FTPS""" @@ -57,9 +59,9 @@ class ImplicitTLS(ftplib.FTP_TLS): 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 + conn = self.context.wrap_socket( + conn, server_hostname=self.host, session=self.sock.session + ) # this is the fix return conn, size @@ -76,12 +78,12 @@ class IoTFTPSClient: 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, + 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 @@ -106,7 +108,8 @@ class IoTFTPSClient: self.ftps_session.set_debuglevel(0) self.welcome = self.ftps_session.connect( - host=self.ftps_host, port=self.ftps_port) + 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) @@ -137,7 +140,7 @@ class IoTFTPSClient: # 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') + self.ftps_session.voidcmd("TYPE I") with self.ftps_session.transfercmd(f"STOR {dest}", rest) as conn: while 1: @@ -152,7 +155,9 @@ class IoTFTPSClient: callback(buf) # shutdown ssl layer - if ftplib._SSLSocket is not None and isinstance(conn, ftplib._SSLSocket): + 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. @@ -185,19 +190,24 @@ class IoTFTPSClient: 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]: + def list_files(self, list_path: str, extensions: str | list[str] | None = None): """list files under a path inside the FTPS server""" + + if extensions is None: + _extension_acceptable = lambda p: True + else: + if isinstance(extensions, str): + extensions = [extensions] + _extension_acceptable = lambda p: any(s in p.suffixes for s in extensions) + 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 + list_result = self.ftps_session.nlst(list_path) or [] + for file_name in list_result: + path = Path(list_path) / file_name + if _extension_acceptable(path): + yield path 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""" @@ -208,7 +218,8 @@ class IoTFTPSClient: s = f.getvalue() files = [] for row in s.split("\n"): - if len(row) <= 0: continue + if len(row) <= 0: + continue attribs = row.split(" ") @@ -219,7 +230,7 @@ class IoTFTPSClient: else: name = attribs[len(attribs) - 1] - file = ( attribs[0], name ) + file = (attribs[0], name) files.append(file) return files except Exception as ex: diff --git a/octoprint_bambu_printer/printer/print_job.py b/octoprint_bambu_printer/printer/print_job.py index 5e383d1..d6d6f9b 100644 --- a/octoprint_bambu_printer/printer/print_job.py +++ b/octoprint_bambu_printer/printer/print_job.py @@ -17,4 +17,4 @@ class PrintJob: @progress.setter def progress(self, value): - self.file_position = int(self.file_info.size * ((value + 1) / 100)) + self.file_position = int(self.file_info.size * value / 100) diff --git a/octoprint_bambu_printer/printer/printer_serial_io.py b/octoprint_bambu_printer/printer/printer_serial_io.py index ccb2d70..5a51496 100644 --- a/octoprint_bambu_printer/printer/printer_serial_io.py +++ b/octoprint_bambu_printer/printer/printer_serial_io.py @@ -27,7 +27,7 @@ class PrinterSerialIO(threading.Thread, BufferedIOBase): write_timeout=10.0, ) -> None: super().__init__( - name="octoprint.plugins.bambu_printer.serial_io_thread", daemon=True + name="octoprint.plugins.bambu_printer.printer_worker", daemon=True ) self._handle_command_callback = handle_command_callback self._settings = settings @@ -115,8 +115,8 @@ class PrinterSerialIO(threading.Thread, BufferedIOBase): return 0 try: - self.input_bytes.put(data, timeout=self._write_timeout) self._log.debug(f"<<< {u_data}") + self.input_bytes.put(data, timeout=self._write_timeout) return len(data) except queue.Full: self._log.error( diff --git a/octoprint_bambu_printer/printer/remote_sd_card_file_list.py b/octoprint_bambu_printer/printer/remote_sd_card_file_list.py index fcf91df..cb9d9dd 100644 --- a/octoprint_bambu_printer/printer/remote_sd_card_file_list.py +++ b/octoprint_bambu_printer/printer/remote_sd_card_file_list.py @@ -77,18 +77,14 @@ class RemoteSDCardFileList: yield file_info existing_files.append(file_info.file_name) + existing_files.append(file_info.dosname) def _get_existing_files_info(self): ftp = self._connect_ftps_server() file_list = [] - file_list.extend(ftp.list_files("", ".3mf") or []) - file_list.extend( - [ - (Path("cache/") / f).as_posix() - for f in (ftp.list_files("cache/", ".3mf") or []) - ] - ) + file_list.extend(ftp.list_files("", ".3mf")) + file_list.extend(ftp.list_files("cache/", ".3mf")) existing_files = [] return list(self._scan_ftp_file_list(ftp, file_list, existing_files)) @@ -112,7 +108,7 @@ class RemoteSDCardFileList: def get_all_files(self): self._update_existing_files_info() - self._logger.debug(f"_getSdFiles return: {self._file_data_cache}") + self._logger.debug(f"get_all_files return: {self._file_data_cache}") return list(self._file_data_cache.values()) def _update_existing_files_info(self): @@ -124,7 +120,9 @@ class RemoteSDCardFileList: def _get_cached_data_by_suffix(self, file_stem: str, allowed_suffixes: list[str]): for suffix in allowed_suffixes: - file_data = self._get_cached_file_data(f"{file_stem}{suffix}") + file_data = self._get_cached_file_data( + Path(file_stem).with_suffix(suffix).as_posix() + ) if file_data is not None: return file_data return None @@ -154,9 +152,6 @@ class RemoteSDCardFileList: return True self._selected_file_info = file_info - self._logger.info( - f"File opened: {self._selected_file_info.file_name} Size: {self._selected_file_info.size}" - ) return True def delete_file(self, file_path: str) -> None: diff --git a/octoprint_bambu_printer/printer/states/printing_state.py b/octoprint_bambu_printer/printer/states/printing_state.py index 17b25fe..a285d14 100644 --- a/octoprint_bambu_printer/printer/states/printing_state.py +++ b/octoprint_bambu_printer/printer/states/printing_state.py @@ -36,7 +36,6 @@ class PrintingState(APrinterState): self._start_worker_thread() def finalize(self): - if self._sd_printing_thread is not None and self._sd_printing_thread.is_alive(): self._is_printing = False self._sd_printing_thread.join() @@ -82,7 +81,8 @@ class PrintingState(APrinterState): if self._printer.bambu_client.connected: if self._printer.bambu_client.publish(pybambu.commands.STOP): self._log.info("print cancelled") - self._printer.change_state(self._printer._state_finished) + self._finish_print() + self._printer.change_state(self._printer._state_idle) else: self._log.info("print cancel failed") @@ -91,5 +91,6 @@ class PrintingState(APrinterState): self._log.debug( f"SD File Print finishing: {self._printer.current_print_job.file_info.file_name}" ) + self._printer.sendIO("Done printing file") self._printer.change_state(self._printer._state_idle) diff --git a/test/test_gcode_execution.py b/test/test_gcode_execution.py index 56510a4..8cedaab 100644 --- a/test/test_gcode_execution.py +++ b/test/test_gcode_execution.py @@ -107,7 +107,10 @@ def ftps_session_mock(files_info_ftp): filter(lambda f: Path(f).parent == Path("."), all_files) ), ("cache/", ".3mf"): list( - filter(lambda f: Path(f).parent == Path("cache/"), all_files) + map( + lambda f: Path(f).name, + filter(lambda f: Path(f).parent == Path("cache/"), all_files), + ) ), } ) @@ -164,7 +167,7 @@ def printer( pass BambuVirtualPrinter._create_client_connection_async = _mock_connection - serial_obj = BambuVirtualPrinter( + printer_test = BambuVirtualPrinter( settings, profile_manager, data_folder=output_test_folder, @@ -172,9 +175,11 @@ def printer( read_timeout=0.01, faked_baudrate=115200, ) - serial_obj._bambu_client = bambu_client_mock - yield serial_obj - serial_obj.close() + printer_test._bambu_client = bambu_client_mock + printer_test.flush() + printer_test.readlines() + yield printer_test + printer_test.close() def test_initial_state(printer: BambuVirtualPrinter): @@ -206,7 +211,8 @@ def test_non_existing_file_not_selected(printer: BambuVirtualPrinter): printer.write(b"M23 non_existing.3mf\n") printer.flush() result = printer.readlines() - assert result[0] == b"ok" + assert result[-2] != b"File selected" + assert result[-1] == b"ok" assert printer.file_system.selected_file is None @@ -220,7 +226,8 @@ def test_print_started_with_selected_file(printer: BambuVirtualPrinter, print_jo printer.write(b"M23 print.3mf\n") printer.flush() result = printer.readlines() - assert result[0] == b"ok" + assert result[-2] == b"File selected" + assert result[-1] == b"ok" assert printer.file_system.selected_file is not None assert printer.file_system.selected_file.file_name == "print.3mf"