Fix response messages. Fix filesystem name transformations.
This commit is contained in:
		| @@ -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") | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user