Fix file list update. Decouple filesystem from printer file structure.

This commit is contained in:
Anton Skrypnyk 2024-07-27 02:22:46 +03:00
parent 4ea98036e5
commit f42d3167c5
8 changed files with 142 additions and 146 deletions

View File

@ -21,6 +21,7 @@ from octoprint.server.util.tornado import (
from octoprint.access.permissions import Permissions from octoprint.access.permissions import Permissions
from octoprint.logging.handlers import CleaningTimedRotatingFileHandler from octoprint.logging.handlers import CleaningTimedRotatingFileHandler
from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView
from pybambu import BambuCloud from pybambu import BambuCloud
from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import ( from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import (
@ -50,9 +51,15 @@ class BambuPrintPlugin(
_logger: logging.Logger _logger: logging.Logger
_plugin_manager: octoprint.plugin.PluginManager _plugin_manager: octoprint.plugin.PluginManager
_bambu_file_system: RemoteSDCardFileList _bambu_file_system: RemoteSDCardFileList
_timelapse_files_view: CachedFileView
def on_settings_initialized(self): def on_settings_initialized(self):
self._bambu_file_system = RemoteSDCardFileList(self._settings) self._bambu_file_system = RemoteSDCardFileList(self._settings)
self._timelapse_files_view = CachedFileView(self._bambu_file_system)
if self._settings.get(["device_type"]) in ["X1", "X1C"]:
self._timelapse_files_view.with_filter("timelapse/", ".mp4")
else:
self._timelapse_files_view.with_filter("timelapse/", ".avi")
def get_assets(self): def get_assets(self):
return {"js": ["js/bambu_printer.js"]} return {"js": ["js/bambu_printer.js"]}
@ -196,7 +203,7 @@ class BambuPrintPlugin(
def process(): def process():
return_file_list = [] return_file_list = []
for file_info in self._bambu_file_system.get_all_timelapse_files(): for file_info in self._timelapse_files_view.get_all_info():
timelapse_info = BambuTimelapseFileInfo.from_file_info(file_info) timelapse_info = BambuTimelapseFileInfo.from_file_info(file_info)
return_file_list.append(timelapse_info.to_dict()) return_file_list.append(timelapse_info.to_dict())
self._plugin_manager.send_plugin_message( self._plugin_manager.send_plugin_message(

View File

@ -3,10 +3,13 @@ from __future__ import annotations
import collections import collections
from dataclasses import dataclass, field from dataclasses import dataclass, field
import math import math
from pathlib import Path
import queue import queue
import re import re
import threading import threading
import time import time
from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView
from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
from octoprint_bambu_printer.printer.print_job import PrintJob from octoprint_bambu_printer.printer.print_job import PrintJob
from pybambu import BambuClient, commands from pybambu import BambuClient, commands
import logging import logging
@ -55,6 +58,11 @@ class BambuVirtualPrinter:
read_timeout=5.0, read_timeout=5.0,
faked_baudrate=115200, faked_baudrate=115200,
): ):
self._settings = settings
self._printer_profile_manager = printer_profile_manager
self._faked_baudrate = faked_baudrate
self._data_folder = data_folder
self._last_hms_errors = None
self._log = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter") self._log = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter")
self._state_idle = IdleState(self) self._state_idle = IdleState(self)
@ -85,12 +93,12 @@ class BambuVirtualPrinter:
) )
self.file_system = RemoteSDCardFileList(settings) self.file_system = RemoteSDCardFileList(settings)
self._selected_project_file: FileInfo | None = None
self._settings = settings self._project_files_view = (
self._printer_profile_manager = printer_profile_manager CachedFileView(self.file_system, on_update=self._list_cached_project_files)
self._faked_baudrate = faked_baudrate .with_filter("", ".3mf")
.with_filter("cache/", ".3mf")
self._last_hms_errors = None )
self._serial_io.start() self._serial_io.start()
self._printer_thread.start() self._printer_thread.start()
@ -117,6 +125,44 @@ class BambuVirtualPrinter:
def current_print_job(self, value): def current_print_job(self, value):
self._current_print_job = value self._current_print_job = value
@property
def selected_file(self):
return self._selected_project_file
@property
def has_selected_file(self):
return self._selected_project_file is not None
@property
def timeout(self):
return self._serial_io._read_timeout
@timeout.setter
def timeout(self, value):
self._log.debug(f"Setting read timeout to {value}s")
self._serial_io._read_timeout = value
@property
def write_timeout(self):
return self._serial_io._write_timeout
@write_timeout.setter
def write_timeout(self, value):
self._log.debug(f"Setting write timeout to {value}s")
self._serial_io._write_timeout = value
@property
def port(self):
return "BAMBU"
@property
def baudrate(self):
return self._faked_baudrate
@property
def project_files(self):
return self._project_files_view
def change_state(self, new_state: APrinterState): def change_state(self, new_state: APrinterState):
self._state_change_queue.put(new_state) self._state_change_queue.put(new_state)
@ -238,32 +284,6 @@ class BambuVirtualPrinter:
self._serial_io.reset() self._serial_io.reset()
@property
def timeout(self):
return self._serial_io._read_timeout
@timeout.setter
def timeout(self, value):
self._log.debug(f"Setting read timeout to {value}s")
self._serial_io._read_timeout = value
@property
def write_timeout(self):
return self._serial_io._write_timeout
@write_timeout.setter
def write_timeout(self, value):
self._log.debug(f"Setting write timeout to {value}s")
self._serial_io._write_timeout = value
@property
def port(self):
return "BAMBU"
@property
def baudrate(self):
return self._faked_baudrate
def write(self, data: bytes) -> int: def write(self, data: bytes) -> int:
return self._serial_io.write(data) return self._serial_io.write(data)
@ -283,6 +303,22 @@ class BambuVirtualPrinter:
self._serial_io.flush() self._serial_io.flush()
self._wait_for_state_change() self._wait_for_state_change()
##~~ project file functions
def remove_project_selection(self):
self._selected_project_file = None
def select_project_file(self, file_path: str) -> bool:
self._log.debug(f"Select project file: {file_path}")
file_info = self._project_files_view.get_cached_file_data(file_path)
if file_info is None:
self._log.error(f"Cannot select not existing file: {file_path}")
return False
self._selected_project_file = file_info
self._send_file_selected_message()
return True
##~~ command implementations ##~~ command implementations
@gcode_executor.register_no_data("M21") @gcode_executor.register_no_data("M21")
@ -292,19 +328,18 @@ class BambuVirtualPrinter:
@gcode_executor.register("M23") @gcode_executor.register("M23")
def _select_sd_file(self, data: str) -> bool: def _select_sd_file(self, data: str) -> bool:
filename = data.split(maxsplit=1)[1].strip() filename = data.split(maxsplit=1)[1].strip()
self._list_sd() self._list_project_files()
if not self.file_system.select_project_file(filename): return self.select_project_file(filename)
return False
assert self.file_system.selected_file is not None def _send_file_selected_message(self):
self._current_state.update_print_job_info() if self.selected_file is None:
return
self.sendIO( self.sendIO(
f"File opened: {self.file_system.selected_file.file_name} " f"File opened: {self.selected_file.file_name} "
f"Size: {self.file_system.selected_file.size}" f"Size: {self.selected_file.size}"
) )
self.sendIO("File selected") self.sendIO("File selected")
return True
@gcode_executor.register("M26") @gcode_executor.register("M26")
def _set_sd_position(self, data: str) -> bool: def _set_sd_position(self, data: str) -> bool:
@ -336,9 +371,9 @@ class BambuVirtualPrinter:
@gcode_executor.register("M30") @gcode_executor.register("M30")
def _delete_sd_file(self, data: str) -> bool: def _delete_sd_file(self, data: str) -> bool:
filename = data.split(None, 1)[1].strip() file_path = data.split(None, 1)[1].strip()
self._list_sd() self._list_project_files()
self.file_system.delete_file(filename) self.file_system.delete_file(Path(file_path))
return True return True
@gcode_executor.register("M105") @gcode_executor.register("M105")
@ -429,14 +464,17 @@ class BambuVirtualPrinter:
return True return True
@gcode_executor.register("M20") @gcode_executor.register("M20")
def _list_sd(self, data: str = ""): def _list_project_files(self, data: str = ""):
self._project_files_view.update()
return True
def _list_cached_project_files(self):
self.sendIO("Begin file list") self.sendIO("Begin file list")
for item in map( for item in map(
lambda f: f.get_log_info(), self.file_system.get_all_project_files() FileInfo.get_gcode_info, self._project_files_view.get_all_cached_info()
): ):
self.sendIO(item) self.sendIO(item)
self.sendIO("End file list") self.sendIO("End file list")
return True
@gcode_executor.register_no_data("M24") @gcode_executor.register_no_data("M24")
def _start_print(self): def _start_print(self):

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Callable
if TYPE_CHECKING: if TYPE_CHECKING:
from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import ( from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import (
@ -16,10 +16,11 @@ from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
class CachedFileView: class CachedFileView:
file_system: RemoteSDCardFileList file_system: RemoteSDCardFileList
folder_view: set[tuple[str, str | list[str] | None]] = field(default_factory=set) folder_view: set[tuple[str, str | list[str] | None]] = field(default_factory=set)
on_update: Callable[[], None] | None = None
def __post_init__(self): def __post_init__(self):
self._file_alias_cache = {} self._file_alias_cache: dict[str, str] = {}
self._file_data_cache = {} self._file_data_cache: dict[str, FileInfo] = {}
def with_filter( def with_filter(
self, folder: str, extensions: str | list[str] | None = None self, folder: str, extensions: str | list[str] | None = None
@ -28,8 +29,8 @@ class CachedFileView:
return self return self
def list_all_views(self): def list_all_views(self):
existing_files = [] existing_files: list[str] = []
result = [] result: list[FileInfo] = []
with self.file_system.get_ftps_client() as ftp: with self.file_system.get_ftps_client() as ftp:
for filter in self.folder_view: for filter in self.folder_view:
@ -38,10 +39,17 @@ class CachedFileView:
def update(self): def update(self):
file_info_list = self.list_all_views() file_info_list = self.list_all_views()
self._file_alias_cache = { self._update_file_list_cache(file_info_list)
info.dosname: info.file_name for info in file_info_list if self.on_update:
} self.on_update()
self._file_data_cache = {info.file_name: info for info in file_info_list}
def _update_file_list_cache(self, files: list[FileInfo]):
self._file_alias_cache = {info.dosname: info.file_name for info in files}
self._file_data_cache = {info.file_name: info for info in files}
def get_all_info(self):
self.update()
return self.get_all_cached_info()
def get_all_cached_info(self): def get_all_cached_info(self):
return list(self._file_data_cache.values()) return list(self._file_data_cache.values())

View File

@ -26,7 +26,7 @@ class FileInfo:
def timestamp_m20(self) -> str: def timestamp_m20(self) -> str:
return unix_timestamp_to_m20_timestamp(int(self.timestamp)) return unix_timestamp_to_m20_timestamp(int(self.timestamp))
def get_log_info(self) -> str: def get_gcode_info(self) -> str:
return f'{self.dosname} {self.size} {self.timestamp_m20} "{self.file_name}"' return f'{self.dosname} {self.size} {self.timestamp_m20} "{self.file_name}"'
def to_dict(self): def to_dict(self):

View File

@ -17,63 +17,13 @@ class RemoteSDCardFileList:
def __init__(self, settings) -> None: def __init__(self, settings) -> None:
self._settings = settings self._settings = settings
self._file_alias_cache = {}
self._file_data_cache = {}
self._selected_project_file: FileInfo | None = None self._selected_project_file: FileInfo | None = None
self._logger = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter") self._logger = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter")
self._project_files_view = (
CachedFileView(self).with_filter("", ".3mf").with_filter("cache/", ".3mf")
)
self._timelapse_files_view = CachedFileView(self)
if self._settings.get(["device_type"]) in ["X1", "X1C"]:
self._timelapse_files_view.with_filter("timelapse/", ".mp4")
else:
self._timelapse_files_view.with_filter("timelapse/", ".avi")
@property def delete_file(self, file_path: Path) -> None:
def selected_file(self):
return self._selected_project_file
@property
def has_selected_file(self):
return self._selected_project_file is not None
@property
def project_files(self):
return self._project_files_view
def remove_file_selection(self):
self._selected_project_file = None
def get_all_project_files(self):
self._project_files_view.update()
files = self._project_files_view.get_all_cached_info()
self._logger.debug(f"get project files return: {files}")
return files
def get_all_timelapse_files(self):
self._timelapse_files_view.update()
files = self._timelapse_files_view.get_all_cached_info()
self._logger.debug(f"get timelapse files return: {files}")
return files
def select_project_file(self, file_path: str) -> bool:
self._logger.debug(f"_selectSdFile: {file_path}")
file_name = Path(file_path).name
file_info = self._project_files_view.get_cached_file_data(file_name)
if file_info is None:
self._logger.error(f"{file_name} open failed")
return False
self._selected_project_file = file_info
return True
def delete_file(self, file_path: str) -> None:
file_info = self._project_files_view.get_cached_file_data(file_path)
if file_info is not None:
try: try:
with self.get_ftps_client() as ftp: with self.get_ftps_client() as ftp:
if ftp.delete_file(str(file_info.path)): if ftp.delete_file(str(file_path)):
self._logger.debug(f"{file_path} deleted") self._logger.debug(f"{file_path} deleted")
else: else:
raise RuntimeError(f"Deleting file {file_path} failed") raise RuntimeError(f"Deleting file {file_path} failed")

View File

@ -6,12 +6,8 @@ from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState
class IdleState(APrinterState): class IdleState(APrinterState):
def init(self):
if self._printer.file_system.has_selected_file:
self.update_print_job_info()
def start_new_print(self): def start_new_print(self):
selected_file = self._printer.file_system.selected_file selected_file = self._printer.selected_file
if selected_file is None: if selected_file is None:
self._log.warn("Cannot start print job if file was not selected") self._log.warn("Cannot start print job if file was not selected")
return return
@ -55,9 +51,3 @@ class IdleState(APrinterState):
} }
return print_command return print_command
def update_print_job_info(self):
if self._printer.file_system.selected_file is not None:
self._printer.current_print_job = PrintJob(
self._printer.file_system.selected_file, 0
)

View File

@ -27,7 +27,7 @@ class PrintingState(APrinterState):
def init(self): def init(self):
self._is_printing = True self._is_printing = True
self._printer.file_system.remove_file_selection() self._printer.remove_project_selection()
self.update_print_job_info() self.update_print_job_info()
self._start_worker_thread() self._start_worker_thread()
@ -64,7 +64,7 @@ class PrintingState(APrinterState):
def update_print_job_info(self): def update_print_job_info(self):
print_job_info = self._printer.bambu_client.get_device().print_job print_job_info = self._printer.bambu_client.get_device().print_job
task_name: str = print_job_info.subtask_name task_name: str = print_job_info.subtask_name
project_file_info = self._printer.file_system.project_files.get_file_by_suffix( project_file_info = self._printer.project_files.get_file_by_suffix(
task_name, [".3mf", ".gcode.3mf"] task_name, [".3mf", ".gcode.3mf"]
) )
if project_file_info is None: if project_file_info is None:
@ -74,6 +74,7 @@ class PrintingState(APrinterState):
progress = print_job_info.print_percentage progress = print_job_info.print_percentage
self._printer.current_print_job = PrintJob(project_file_info, progress) self._printer.current_print_job = PrintJob(project_file_info, progress)
self._printer.select_project_file(project_file_info.file_name)
def pause_print(self): def pause_print(self):
if self._printer.bambu_client.connected: if self._printer.bambu_client.connected:
@ -98,5 +99,6 @@ class PrintingState(APrinterState):
f"SD File Print finishing: {self._printer.current_print_job.file_info.file_name}" f"SD File Print finishing: {self._printer.current_print_job.file_info.file_name}"
) )
self._printer.sendIO("Done printing file") self._printer.sendIO("Done printing file")
self._printer.current_print_job = None
self._printer.change_state(self._printer._state_idle) self._printer.change_state(self._printer._state_idle)

View File

@ -5,6 +5,7 @@ from pathlib import Path
from typing import Any from typing import Any
from unittest.mock import MagicMock from unittest.mock import MagicMock
from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView
import pybambu import pybambu
import pybambu.commands import pybambu.commands
from octoprint_bambu_printer.printer.bambu_virtual_printer import BambuVirtualPrinter from octoprint_bambu_printer.printer.bambu_virtual_printer import BambuVirtualPrinter
@ -190,9 +191,9 @@ def test_list_sd_card(printer: BambuVirtualPrinter):
assert result[4] == b"ok" assert result[4] == b"ok"
def test_list_ftp_paths_bambu_p1(settings, ftps_session_mock): def test_list_ftp_paths_p1s(settings, ftps_session_mock):
settings.get.side_effect.options[("device_type",)] = "P1S"
file_system = RemoteSDCardFileList(settings) file_system = RemoteSDCardFileList(settings)
file_view = CachedFileView(file_system).with_filter("timelapse/", ".avi")
timelapse_files = ["timelapse/video.avi", "timelapse/video2.avi"] timelapse_files = ["timelapse/video.avi", "timelapse/video2.avi"]
ftps_session_mock.size.side_effect = DictGetter( ftps_session_mock.size.side_effect = DictGetter(
@ -209,15 +210,15 @@ def test_list_ftp_paths_bambu_p1(settings, ftps_session_mock):
) )
timelapse_paths = list(map(Path, timelapse_files)) timelapse_paths = list(map(Path, timelapse_files))
result_files = file_system.get_all_timelapse_files() result_files = file_view.get_all_info()
assert len(timelapse_files) == len(result_files) and all( assert len(timelapse_files) == len(result_files) and all(
file_info.path in timelapse_paths for file_info in result_files file_info.path in timelapse_paths for file_info in result_files
) )
def test_list_ftp_paths_bambu_x1(settings, ftps_session_mock): def test_list_ftp_paths_x1(settings, ftps_session_mock):
settings.get.side_effect.options[("device_type",)] = "X1"
file_system = RemoteSDCardFileList(settings) file_system = RemoteSDCardFileList(settings)
file_view = CachedFileView(file_system).with_filter("timelapse/", ".mp4")
timelapse_files = ["timelapse/video.mp4", "timelapse/video2.mp4"] timelapse_files = ["timelapse/video.mp4", "timelapse/video2.mp4"]
ftps_session_mock.size.side_effect = DictGetter( ftps_session_mock.size.side_effect = DictGetter(
@ -232,7 +233,7 @@ def test_list_ftp_paths_bambu_x1(settings, ftps_session_mock):
ftps_session_mock.nlst.side_effect = DictGetter({"timelapse/": timelapse_files}) ftps_session_mock.nlst.side_effect = DictGetter({"timelapse/": timelapse_files})
timelapse_paths = list(map(Path, timelapse_files)) timelapse_paths = list(map(Path, timelapse_files))
result_files = file_system.get_all_timelapse_files() result_files = file_view.get_all_info()
assert len(timelapse_files) == len(result_files) and all( assert len(timelapse_files) == len(result_files) and all(
file_info.path in timelapse_paths for file_info in result_files file_info.path in timelapse_paths for file_info in result_files
) )
@ -247,18 +248,18 @@ def test_cannot_start_print_without_file(printer: BambuVirtualPrinter):
def test_non_existing_file_not_selected(printer: BambuVirtualPrinter): def test_non_existing_file_not_selected(printer: BambuVirtualPrinter):
assert printer.file_system.selected_file is None assert printer.selected_file is None
printer.write(b"M23 non_existing.3mf\n") printer.write(b"M23 non_existing.3mf\n")
printer.flush() printer.flush()
result = printer.readlines() result = printer.readlines()
assert result[-2] != b"File selected" assert result[-2] != b"File selected"
assert result[-1] == b"ok" assert result[-1] == b"ok"
assert printer.file_system.selected_file is None assert printer.selected_file is None
def test_print_started_with_selected_file(printer: BambuVirtualPrinter, print_job_mock): def test_print_started_with_selected_file(printer: BambuVirtualPrinter, print_job_mock):
assert printer.file_system.selected_file is None assert printer.selected_file is None
printer.write(b"M20\n") printer.write(b"M20\n")
printer.flush() printer.flush()
@ -270,8 +271,8 @@ def test_print_started_with_selected_file(printer: BambuVirtualPrinter, print_jo
assert result[-2] == b"File selected" assert result[-2] == b"File selected"
assert result[-1] == b"ok" assert result[-1] == b"ok"
assert printer.file_system.selected_file is not None assert printer.selected_file is not None
assert printer.file_system.selected_file.file_name == "print.3mf" assert printer.selected_file.file_name == "print.3mf"
print_job_mock.subtask_name = "print.3mf" print_job_mock.subtask_name = "print.3mf"
@ -403,12 +404,12 @@ def test_finished_print_job_reset_after_new_file_selected(
printer.new_update("event_printer_data_update") printer.new_update("event_printer_data_update")
printer.flush() printer.flush()
assert isinstance(printer.current_state, IdleState) assert isinstance(printer.current_state, IdleState)
assert printer.current_print_job is not None assert printer.current_print_job is None
assert printer.current_print_job.file_info.file_name == "print.3mf" assert printer.selected_file is not None
assert printer.current_print_job.progress == 100 assert printer.selected_file.file_name == "print.3mf"
printer.write(b"M23 print2.3mf\n") printer.write(b"M23 print2.3mf\n")
printer.flush() printer.flush()
assert printer.current_print_job is not None assert printer.current_print_job is None
assert printer.current_print_job.file_info.file_name == "print2.3mf" assert printer.selected_file is not None
assert printer.current_print_job.progress == 0 assert printer.selected_file.file_name == "print2.3mf"