From 75b0a11fef6df15b32a330199fcd574e3b2839dd Mon Sep 17 00:00:00 2001 From: Anton Skrypnyk Date: Wed, 24 Jul 2024 17:15:46 +0300 Subject: [PATCH] WIP Refactor sd card logic --- .../bambu_virtual_printer.py | 187 +++--------------- .../remote_sd_card_file_list.py | 160 +++++++++++++++ 2 files changed, 185 insertions(+), 162 deletions(-) create mode 100644 octoprint_bambu_printer/remote_sd_card_file_list.py diff --git a/octoprint_bambu_printer/bambu_virtual_printer.py b/octoprint_bambu_printer/bambu_virtual_printer.py index 67426f7..7b87349 100644 --- a/octoprint_bambu_printer/bambu_virtual_printer.py +++ b/octoprint_bambu_printer/bambu_virtual_printer.py @@ -3,27 +3,24 @@ __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agp import collections -import datetime import math import os import queue import re import threading import time -from typing import Any, Dict, List, Optional import asyncio +from octoprint_bambu_printer.remote_sd_card_file_list import RemoteSDCardFileList from pybambu import BambuClient, commands import logging import logging.handlers from serial import SerialTimeoutException -from octoprint.util import RepeatedTimer, to_bytes, to_unicode, get_dos_filename -from octoprint.util.files import unix_timestamp_to_m20_timestamp +from octoprint.util import RepeatedTimer, to_bytes, to_unicode from octoprint_bambu_printer.gcode_executor import GCodeExecutor from .char_counting_queue import CharCountingQueue -from .ftpsclient import IoTFTPSClient # noinspection PyBroadException @@ -77,10 +74,7 @@ class BambuVirtualPrinter: self._sdPrintStarting = False self._sdPrintingSemaphore = threading.Event() self._sdPrintingPausedSemaphore = threading.Event() - self._sdFileListCache = {} - self._selectedSdFile = None - self._selectedSdFileSize = 0 - self._selectedSdFilePos = 0 + self._sdCardFileSystem = RemoteSDCardFileList(settings) self._busy = None self._busy_loop = None @@ -125,14 +119,6 @@ class BambuVirtualPrinter: ) readThread.start() - # bufferThread = threading.Thread( - # target=self._processBuffer, - # name="octoprint.plugins.bambu_printer.buffer_thread", - # daemon=True - # ) - # bufferThread.start() - - # Move this into M110 command response? connectionThread = threading.Thread( target=self._create_connection, name="octoprint.plugins.bambu_printer.connection_thread", @@ -184,17 +170,14 @@ class BambuVirtualPrinter: self._sdPrintStarting = False if not self._sdPrinting: filename: str = print_job.get("subtask_name") - 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" - elif filename.startswith("cache/"): - filename = filename[6:] - else: - self._logger.debug(f"No 3mf file found for {print_job}") + project_file = self._sdCardFileSystem.search_by_stem( + filename, [".3mf", ".gcode.3mf"] + ) + if project_file is None: + self._logger.debug(f"No 3mf file found for {print_job}") - self._selectSdFile(filename) + if self._sdCardFileSystem.select_file(filename): + self._sendOk() self._startSdPrint(from_printer=True) # fuzzy math here to get print percentage to match BambuStudio @@ -428,7 +411,7 @@ class BambuVirtualPrinter: self.current_line += 1 elif self._settings.get_boolean(["forceChecksum"]): - self._send(self._error("checksum_missing")) + self._send(self._format_error("checksum_missing")) continue # track N = N + 1 @@ -452,18 +435,18 @@ class BambuVirtualPrinter: data += b"\n" - data = to_unicode(data, encoding="ascii", errors="replace").strip() + command = to_unicode(data, encoding="ascii", errors="replace").strip() # actual command handling - command_match = BambuVirtualPrinter.command_regex.match(data) + command_match = BambuVirtualPrinter.command_regex.match(command) if command_match is not None: - command = command_match.group(0) - letter = command_match.group(1) + gcode = command_match.group(0) + gcode_letter = command_match.group(1) - if letter in self.gcode_executor: - handled = self.run_gcode_handler(letter, data) + if gcode_letter in self.gcode_executor: + handled = self.run_gcode_handler(gcode_letter, data) else: - handled = self.run_gcode_handler(command, data) + handled = self.run_gcode_handler(gcode, data) if handled: self._sendOk() continue @@ -626,11 +609,11 @@ class BambuVirtualPrinter: if actual is None: if checksum: - self._send(self._error("checksum_mismatch")) + self._send(self._format_error("checksum_mismatch")) else: - self._send(self._error("checksum_missing")) + self._send(self._format_error("checksum_missing")) else: - self._send(self._error("lineno_mismatch", expected, actual)) + self._send(self._format_error("lineno_mismatch", expected, actual)) def request_resend(): self._send("Resend:%d" % expected) @@ -641,114 +624,13 @@ class BambuVirtualPrinter: @gcode_executor.register_no_data("M20") def _listSd(self): - line = '{dosname} {size} {timestamp} "{name}"' - self._send("Begin file list") - for item in map(lambda x: line.format(**x), self._getSdFiles()): + for item in map( + lambda f: f.get_log_info(), self._sdCardFileSystem.get_all_files() + ): self._send(item) self._send("End file list") - def _mappedSdList(self) -> Dict[str, Dict[str, Any]]: - result = {} - host = self._settings.get(["host"]) - access_code = self._settings.get(["access_code"]) - - ftp = IoTFTPSClient(f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True) - filelist = ftp.list_files("", ".3mf") or [] - - for entry in filelist: - if entry.startswith("/"): - filename = entry[1:] - else: - filename = entry - 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": filename, - "size": filesize, - "timestamp": unix_timestamp_to_m20_timestamp(int(filedate)), - } - 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(f"cache/{filename}") - date_str = ftp.ftps_session.sendcmd(f"MDTM cache/{filename}").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]]: - self._logger.debug(f"_getSdFileData: {filename}") - data = self._sdFileListCache.get(filename.lower()) - if isinstance(data, str): - data = self._sdFileListCache.get(data.lower()) - self._logger.debug(f"_getSdFileData: {data}") - return data - - def _getSdFiles(self) -> List[Dict[str, Any]]: - self._sdFileListCache = self._mappedSdList() - self._logger.debug(f"_getSdFiles return: {self._sdFileListCache}") - return [x for x in self._sdFileListCache.values() if isinstance(x, dict)] - - 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("/"): - filename = filename[1:] - - file = self._getSdFileData(filename) - if file is None: - self._listSd() - self._sendOk() - file = self._getSdFileData(filename) - if file is None: - self._send(f"{filename} open failed") - return - - if self._selectedSdFile == file["path"] and check_already_open: - return - - self._selectedSdFile = file["path"] - self._selectedSdFileSize = file["size"] - self._send(f"File opened: {file['name']} Size: {self._selectedSdFileSize}") - self._send("File selected") - @gcode_executor.register_no_data("M24") def _startSdPrint(self, from_printer: bool = False) -> bool: self._logger.debug(f"_startSdPrint: from_printer={from_printer}") @@ -908,25 +790,6 @@ class BambuVirtualPrinter: self._sdPrintStarting = False self._sdPrinter = None - def _deleteSdFile(self, filename: str) -> None: - host = self._settings.get(["host"]) - access_code = self._settings.get(["access_code"]) - - if filename.startswith("/"): - filename = filename[1:] - file = self._getSdFileData(filename) - if file is not None: - ftp = IoTFTPSClient( - f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True - ) - try: - if ftp.delete_file(file["path"]): - self._logger.debug(f"{filename} deleted") - else: - raise Exception("delete failed") - except Exception as e: - self._logger.debug(f"Error deleting file {filename}") - def _setBusy(self, reason="processing"): if not self._sendBusy: return @@ -1027,5 +890,5 @@ class BambuVirtualPrinter: if self.outgoing is not None: self.outgoing.put(line) - def _error(self, error: str, *args, **kwargs) -> str: + def _format_error(self, error: str, *args, **kwargs) -> str: return f"Error: {self._errors.get(error).format(*args, **kwargs)}" diff --git a/octoprint_bambu_printer/remote_sd_card_file_list.py b/octoprint_bambu_printer/remote_sd_card_file_list.py new file mode 100644 index 0000000..202c1d4 --- /dev/null +++ b/octoprint_bambu_printer/remote_sd_card_file_list.py @@ -0,0 +1,160 @@ +from dataclasses import asdict, dataclass +import datetime +import itertools +from pathlib import Path +from typing import Any, Dict, Iterator, List, Optional +import logging.handlers + +from octoprint.util import get_dos_filename +from octoprint.util.files import unix_timestamp_to_m20_timestamp + + +from .ftpsclient import IoTFTPSClient + + +@dataclass +class FileInfo: + dosname: str + path: Path + size: int | None + timestamp: str + + @property + def file_name(self): + return self.path.name.lower() + + def get_log_info(self): + return f'{self.dosname} {self.size} {self.timestamp} "{self.file_name}"' + + def to_dict(self): + return asdict(self) + + +class RemoteSDCardFileList: + + def __init__(self, settings) -> None: + self._settings = settings + self._file_alias_cache = {} + self._file_data_cache = {} + self._selectedFilePath = None + self._selectedSdFileSize = 0 + self._selectedSdFilePos = 0 + self._logger = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter") + + def _get_ftp_file_info( + self, ftp: IoTFTPSClient, ftp_path, file_path: Path, existing_files: list[str] + ): + file_size = ftp.ftps_session.size(ftp_path) + date_str = ftp.ftps_session.sendcmd(f"MDTM {ftp_path}").replace("213 ", "") + filedate = ( + datetime.datetime.strptime(date_str, "%Y%m%d%H%M%S") + .replace(tzinfo=datetime.timezone.utc) + .timestamp() + ) + file_name = file_path.name.lower() + dosname = get_dos_filename(file_name, existing_filenames=existing_files).lower() + return FileInfo( + dosname, + file_path, + file_size, + unix_timestamp_to_m20_timestamp(int(filedate)), + ) + + def _scan_ftp_file_list( + self, ftp, files: list[str], existing_files: list[str] + ) -> Iterator[FileInfo]: + for entry in files: + ftp_path = Path(entry) + file_info = self._get_ftp_file_info(ftp, entry, ftp_path, existing_files) + + yield file_info + existing_files.append(file_info.file_name) + + def _get_existing_files_info(self): + host = self._settings.get(["host"]) + access_code = self._settings.get(["access_code"]) + ftp = IoTFTPSClient(str(host), 990, "bblp", str(access_code), ssl_implicit=True) + + all_files_info: list[FileInfo] = [] + existing_files = [] + + filelist = ftp.list_files("", ".3mf") or [] + all_files_info.extend(self._scan_ftp_file_list(ftp, filelist, existing_files)) + + filelist_cache = ftp.list_files("cache/", ".3mf") or [] + all_files_info.extend( + self._scan_ftp_file_list(ftp, filelist_cache, existing_files) + ) + + return all_files_info + + def _get_file_data(self, file_path: str) -> FileInfo | None: + self._logger.debug(f"_getSdFileData: {file_path}") + file_name = Path(file_path).name.lower() + full_file_name = self._file_alias_cache.get(file_name, None) + if full_file_name is not None: + data = self._file_data_cache.get(file_name, None) + self._logger.debug(f"_getSdFileData: {data}") + return data + + def get_all_files(self): + self._update_existing_files_info() + self._logger.debug(f"_getSdFiles return: {self._file_data_cache}") + return list(self._file_data_cache.values()) + + def _update_existing_files_info(self): + file_info_list = self._get_existing_files_info() + self._file_alias_cache = { + info.dosname: info.file_name for info in file_info_list + } + self._file_data_cache = {info.file_name: info for info in file_info_list} + + def search_by_stem(self, file_stem: str, allowed_suffixes: list[str]): + for file_name in self._file_data_cache: + file_data = self._get_file_data(file_name) + if file_data is None: + continue + file_path = file_data.path + if file_path.stem == file_stem and any( + s in allowed_suffixes for s in file_path.suffixes + ): + return file_data + return None + + def select_file(self, file_path: str, check_already_open: bool = False) -> bool: + self._logger.debug( + f"_selectSdFile: {file_path}, check_already_open={check_already_open}" + ) + file_name = Path(file_path).name + file_info = self._get_file_data(file_name) + if file_info is None: + file_info = self._get_file_data(file_name) + if file_info is None: + self._logger.error(f"{file_name} open failed") + return False + + if self._selectedFilePath == file_info.path and check_already_open: + return True + + self._selectedFilePath = file_info.path + self._selectedSdFileSize = file_info.size + self._logger.info( + f"File opened: {file_info.file_name} Size: {self._selectedSdFileSize}" + ) + + def delete_file(self, file_path: str) -> None: + host = self._settings.get(["host"]) + access_code = self._settings.get(["access_code"]) + + file_info = self._get_file_data(file_path) + if file_info is not None: + ftp = IoTFTPSClient( + f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True + ) + try: + if ftp.delete_file(str(file_info.path)): + self._logger.debug(f"{file_path} deleted") + else: + raise Exception("delete failed") + except Exception as e: + self._logger.debug(f"Error deleting file {file_path}")