WIP Refactor sd card logic
This commit is contained in:
parent
4da769da49
commit
75b0a11fef
@ -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:
|
||||
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}")
|
Loading…
x
Reference in New Issue
Block a user