WIP Refactor sd card logic

This commit is contained in:
Anton Skrypnyk 2024-07-24 17:15:46 +03:00
parent 4da769da49
commit 75b0a11fef
2 changed files with 185 additions and 162 deletions

View File

@ -3,27 +3,24 @@ __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agp
import collections import collections
import datetime
import math import math
import os import os
import queue import queue
import re import re
import threading import threading
import time import time
from typing import Any, Dict, List, Optional
import asyncio import asyncio
from octoprint_bambu_printer.remote_sd_card_file_list import RemoteSDCardFileList
from pybambu import BambuClient, commands from pybambu import BambuClient, commands
import logging import logging
import logging.handlers import logging.handlers
from serial import SerialTimeoutException from serial import SerialTimeoutException
from octoprint.util import RepeatedTimer, to_bytes, to_unicode, get_dos_filename from octoprint.util import RepeatedTimer, to_bytes, to_unicode
from octoprint.util.files import unix_timestamp_to_m20_timestamp
from octoprint_bambu_printer.gcode_executor import GCodeExecutor from octoprint_bambu_printer.gcode_executor import GCodeExecutor
from .char_counting_queue import CharCountingQueue from .char_counting_queue import CharCountingQueue
from .ftpsclient import IoTFTPSClient
# noinspection PyBroadException # noinspection PyBroadException
@ -77,10 +74,7 @@ class BambuVirtualPrinter:
self._sdPrintStarting = False self._sdPrintStarting = False
self._sdPrintingSemaphore = threading.Event() self._sdPrintingSemaphore = threading.Event()
self._sdPrintingPausedSemaphore = threading.Event() self._sdPrintingPausedSemaphore = threading.Event()
self._sdFileListCache = {} self._sdCardFileSystem = RemoteSDCardFileList(settings)
self._selectedSdFile = None
self._selectedSdFileSize = 0
self._selectedSdFilePos = 0
self._busy = None self._busy = None
self._busy_loop = None self._busy_loop = None
@ -125,14 +119,6 @@ class BambuVirtualPrinter:
) )
readThread.start() 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( connectionThread = threading.Thread(
target=self._create_connection, target=self._create_connection,
name="octoprint.plugins.bambu_printer.connection_thread", name="octoprint.plugins.bambu_printer.connection_thread",
@ -184,17 +170,14 @@ class BambuVirtualPrinter:
self._sdPrintStarting = False self._sdPrintStarting = False
if not self._sdPrinting: if not self._sdPrinting:
filename: str = print_job.get("subtask_name") filename: str = print_job.get("subtask_name")
if not self._sdFileListCache.get(filename.lower()): project_file = self._sdCardFileSystem.search_by_stem(
if self._sdFileListCache.get(f"{filename.lower()}.3mf"): filename, [".3mf", ".gcode.3mf"]
filename = f"{filename.lower()}.3mf" )
elif self._sdFileListCache.get(f"{filename.lower()}.gcode.3mf"): if project_file is None:
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}") 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) self._startSdPrint(from_printer=True)
# fuzzy math here to get print percentage to match BambuStudio # fuzzy math here to get print percentage to match BambuStudio
@ -428,7 +411,7 @@ class BambuVirtualPrinter:
self.current_line += 1 self.current_line += 1
elif self._settings.get_boolean(["forceChecksum"]): elif self._settings.get_boolean(["forceChecksum"]):
self._send(self._error("checksum_missing")) self._send(self._format_error("checksum_missing"))
continue continue
# track N = N + 1 # track N = N + 1
@ -452,18 +435,18 @@ class BambuVirtualPrinter:
data += b"\n" data += b"\n"
data = to_unicode(data, encoding="ascii", errors="replace").strip() command = to_unicode(data, encoding="ascii", errors="replace").strip()
# actual command handling # actual command handling
command_match = BambuVirtualPrinter.command_regex.match(data) command_match = BambuVirtualPrinter.command_regex.match(command)
if command_match is not None: if command_match is not None:
command = command_match.group(0) gcode = command_match.group(0)
letter = command_match.group(1) gcode_letter = command_match.group(1)
if letter in self.gcode_executor: if gcode_letter in self.gcode_executor:
handled = self.run_gcode_handler(letter, data) handled = self.run_gcode_handler(gcode_letter, data)
else: else:
handled = self.run_gcode_handler(command, data) handled = self.run_gcode_handler(gcode, data)
if handled: if handled:
self._sendOk() self._sendOk()
continue continue
@ -626,11 +609,11 @@ class BambuVirtualPrinter:
if actual is None: if actual is None:
if checksum: if checksum:
self._send(self._error("checksum_mismatch")) self._send(self._format_error("checksum_mismatch"))
else: else:
self._send(self._error("checksum_missing")) self._send(self._format_error("checksum_missing"))
else: else:
self._send(self._error("lineno_mismatch", expected, actual)) self._send(self._format_error("lineno_mismatch", expected, actual))
def request_resend(): def request_resend():
self._send("Resend:%d" % expected) self._send("Resend:%d" % expected)
@ -641,114 +624,13 @@ class BambuVirtualPrinter:
@gcode_executor.register_no_data("M20") @gcode_executor.register_no_data("M20")
def _listSd(self): def _listSd(self):
line = '{dosname} {size} {timestamp} "{name}"'
self._send("Begin file list") 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(item)
self._send("End file list") 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") @gcode_executor.register_no_data("M24")
def _startSdPrint(self, from_printer: bool = False) -> bool: def _startSdPrint(self, from_printer: bool = False) -> bool:
self._logger.debug(f"_startSdPrint: from_printer={from_printer}") self._logger.debug(f"_startSdPrint: from_printer={from_printer}")
@ -908,25 +790,6 @@ class BambuVirtualPrinter:
self._sdPrintStarting = False self._sdPrintStarting = False
self._sdPrinter = None 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"): def _setBusy(self, reason="processing"):
if not self._sendBusy: if not self._sendBusy:
return return
@ -1027,5 +890,5 @@ class BambuVirtualPrinter:
if self.outgoing is not None: if self.outgoing is not None:
self.outgoing.put(line) 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)}" return f"Error: {self._errors.get(error).format(*args, **kwargs)}"

View 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}")