WIP Refactor sd card logic
This commit is contained in:
		| @@ -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)}" | ||||
|   | ||||
							
								
								
									
										160
									
								
								octoprint_bambu_printer/remote_sd_card_file_list.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								octoprint_bambu_printer/remote_sd_card_file_list.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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}") | ||||
		Reference in New Issue
	
	Block a user