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 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"
|
self._logger.debug(f"No 3mf file found for {print_job}")
|
||||||
elif filename.startswith("cache/"):
|
|
||||||
filename = filename[6:]
|
|
||||||
else:
|
|
||||||
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)}"
|
||||||
|
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