diff --git a/octoprint_bambu_printer/bambu_print_plugin.py b/octoprint_bambu_printer/bambu_print_plugin.py index 9d13377..17a2c32 100644 --- a/octoprint_bambu_printer/bambu_print_plugin.py +++ b/octoprint_bambu_printer/bambu_print_plugin.py @@ -21,6 +21,7 @@ from octoprint.server.util.tornado import ( from octoprint.access.permissions import Permissions from octoprint.logging.handlers import CleaningTimedRotatingFileHandler +from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView from pybambu import BambuCloud from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import ( @@ -50,9 +51,15 @@ class BambuPrintPlugin( _logger: logging.Logger _plugin_manager: octoprint.plugin.PluginManager _bambu_file_system: RemoteSDCardFileList + _timelapse_files_view: CachedFileView def on_settings_initialized(self): 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): return {"js": ["js/bambu_printer.js"]} @@ -196,7 +203,7 @@ class BambuPrintPlugin( def process(): 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) return_file_list.append(timelapse_info.to_dict()) self._plugin_manager.send_plugin_message( diff --git a/octoprint_bambu_printer/printer/bambu_virtual_printer.py b/octoprint_bambu_printer/printer/bambu_virtual_printer.py index 94217eb..dacebc7 100644 --- a/octoprint_bambu_printer/printer/bambu_virtual_printer.py +++ b/octoprint_bambu_printer/printer/bambu_virtual_printer.py @@ -3,10 +3,13 @@ from __future__ import annotations import collections from dataclasses import dataclass, field import math +from pathlib import Path import queue import re import threading 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 pybambu import BambuClient, commands import logging @@ -55,6 +58,11 @@ class BambuVirtualPrinter: read_timeout=5.0, 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._state_idle = IdleState(self) @@ -85,12 +93,12 @@ class BambuVirtualPrinter: ) self.file_system = RemoteSDCardFileList(settings) - - self._settings = settings - self._printer_profile_manager = printer_profile_manager - self._faked_baudrate = faked_baudrate - - self._last_hms_errors = None + self._selected_project_file: FileInfo | None = None + self._project_files_view = ( + CachedFileView(self.file_system, on_update=self._list_cached_project_files) + .with_filter("", ".3mf") + .with_filter("cache/", ".3mf") + ) self._serial_io.start() self._printer_thread.start() @@ -117,6 +125,44 @@ class BambuVirtualPrinter: def current_print_job(self, 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): self._state_change_queue.put(new_state) @@ -238,32 +284,6 @@ class BambuVirtualPrinter: 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: return self._serial_io.write(data) @@ -283,6 +303,22 @@ class BambuVirtualPrinter: self._serial_io.flush() 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 @gcode_executor.register_no_data("M21") @@ -292,19 +328,18 @@ class BambuVirtualPrinter: @gcode_executor.register("M23") def _select_sd_file(self, data: str) -> bool: filename = data.split(maxsplit=1)[1].strip() - self._list_sd() - if not self.file_system.select_project_file(filename): - return False + self._list_project_files() + return self.select_project_file(filename) - assert self.file_system.selected_file is not None - self._current_state.update_print_job_info() + def _send_file_selected_message(self): + if self.selected_file is None: + return self.sendIO( - f"File opened: {self.file_system.selected_file.file_name} " - f"Size: {self.file_system.selected_file.size}" + f"File opened: {self.selected_file.file_name} " + f"Size: {self.selected_file.size}" ) self.sendIO("File selected") - return True @gcode_executor.register("M26") def _set_sd_position(self, data: str) -> bool: @@ -336,9 +371,9 @@ class BambuVirtualPrinter: @gcode_executor.register("M30") def _delete_sd_file(self, data: str) -> bool: - filename = data.split(None, 1)[1].strip() - self._list_sd() - self.file_system.delete_file(filename) + file_path = data.split(None, 1)[1].strip() + self._list_project_files() + self.file_system.delete_file(Path(file_path)) return True @gcode_executor.register("M105") @@ -429,14 +464,17 @@ class BambuVirtualPrinter: return True @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") 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("End file list") - return True @gcode_executor.register_no_data("M24") def _start_print(self): diff --git a/octoprint_bambu_printer/printer/file_system/cached_file_view.py b/octoprint_bambu_printer/printer/file_system/cached_file_view.py index 162ad99..9cfb56d 100644 --- a/octoprint_bambu_printer/printer/file_system/cached_file_view.py +++ b/octoprint_bambu_printer/printer/file_system/cached_file_view.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable if TYPE_CHECKING: 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: file_system: RemoteSDCardFileList folder_view: set[tuple[str, str | list[str] | None]] = field(default_factory=set) + on_update: Callable[[], None] | None = None def __post_init__(self): - self._file_alias_cache = {} - self._file_data_cache = {} + self._file_alias_cache: dict[str, str] = {} + self._file_data_cache: dict[str, FileInfo] = {} def with_filter( self, folder: str, extensions: str | list[str] | None = None @@ -28,8 +29,8 @@ class CachedFileView: return self def list_all_views(self): - existing_files = [] - result = [] + existing_files: list[str] = [] + result: list[FileInfo] = [] with self.file_system.get_ftps_client() as ftp: for filter in self.folder_view: @@ -38,10 +39,17 @@ class CachedFileView: def update(self): file_info_list = self.list_all_views() - 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} + self._update_file_list_cache(file_info_list) + if self.on_update: + self.on_update() + + 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): return list(self._file_data_cache.values()) diff --git a/octoprint_bambu_printer/printer/file_system/file_info.py b/octoprint_bambu_printer/printer/file_system/file_info.py index 8f6b912..95b0256 100644 --- a/octoprint_bambu_printer/printer/file_system/file_info.py +++ b/octoprint_bambu_printer/printer/file_system/file_info.py @@ -26,7 +26,7 @@ class FileInfo: def timestamp_m20(self) -> str: 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}"' def to_dict(self): diff --git a/octoprint_bambu_printer/printer/file_system/remote_sd_card_file_list.py b/octoprint_bambu_printer/printer/file_system/remote_sd_card_file_list.py index 1bc255b..6739701 100644 --- a/octoprint_bambu_printer/printer/file_system/remote_sd_card_file_list.py +++ b/octoprint_bambu_printer/printer/file_system/remote_sd_card_file_list.py @@ -17,68 +17,18 @@ class RemoteSDCardFileList: def __init__(self, settings) -> None: self._settings = settings - self._file_alias_cache = {} - self._file_data_cache = {} self._selected_project_file: FileInfo | None = None 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 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: - with self.get_ftps_client() as ftp: - if ftp.delete_file(str(file_info.path)): - self._logger.debug(f"{file_path} deleted") - else: - raise RuntimeError(f"Deleting file {file_path} failed") - except Exception as e: - self._logger.exception(e) + def delete_file(self, file_path: Path) -> None: + try: + with self.get_ftps_client() as ftp: + if ftp.delete_file(str(file_path)): + self._logger.debug(f"{file_path} deleted") + else: + raise RuntimeError(f"Deleting file {file_path} failed") + except Exception as e: + self._logger.exception(e) def list_files( self, diff --git a/octoprint_bambu_printer/printer/states/idle_state.py b/octoprint_bambu_printer/printer/states/idle_state.py index 4625912..733639a 100644 --- a/octoprint_bambu_printer/printer/states/idle_state.py +++ b/octoprint_bambu_printer/printer/states/idle_state.py @@ -6,12 +6,8 @@ from octoprint_bambu_printer.printer.states.a_printer_state import 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): - selected_file = self._printer.file_system.selected_file + selected_file = self._printer.selected_file if selected_file is None: self._log.warn("Cannot start print job if file was not selected") return @@ -55,9 +51,3 @@ class IdleState(APrinterState): } 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 - ) diff --git a/octoprint_bambu_printer/printer/states/printing_state.py b/octoprint_bambu_printer/printer/states/printing_state.py index 304a3dc..aaa110d 100644 --- a/octoprint_bambu_printer/printer/states/printing_state.py +++ b/octoprint_bambu_printer/printer/states/printing_state.py @@ -27,7 +27,7 @@ class PrintingState(APrinterState): def init(self): self._is_printing = True - self._printer.file_system.remove_file_selection() + self._printer.remove_project_selection() self.update_print_job_info() self._start_worker_thread() @@ -64,7 +64,7 @@ class PrintingState(APrinterState): def update_print_job_info(self): print_job_info = self._printer.bambu_client.get_device().print_job 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"] ) if project_file_info is None: @@ -74,6 +74,7 @@ class PrintingState(APrinterState): progress = print_job_info.print_percentage self._printer.current_print_job = PrintJob(project_file_info, progress) + self._printer.select_project_file(project_file_info.file_name) def pause_print(self): 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}" ) self._printer.sendIO("Done printing file") + self._printer.current_print_job = None self._printer.change_state(self._printer._state_idle) diff --git a/test/test_gcode_execution.py b/test/test_gcode_execution.py index 17c444c..75c072a 100644 --- a/test/test_gcode_execution.py +++ b/test/test_gcode_execution.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import Any from unittest.mock import MagicMock +from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView import pybambu import pybambu.commands 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" -def test_list_ftp_paths_bambu_p1(settings, ftps_session_mock): - settings.get.side_effect.options[("device_type",)] = "P1S" +def test_list_ftp_paths_p1s(settings, ftps_session_mock): file_system = RemoteSDCardFileList(settings) + file_view = CachedFileView(file_system).with_filter("timelapse/", ".avi") timelapse_files = ["timelapse/video.avi", "timelapse/video2.avi"] 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)) - result_files = file_system.get_all_timelapse_files() + result_files = file_view.get_all_info() assert len(timelapse_files) == len(result_files) and all( file_info.path in timelapse_paths for file_info in result_files ) -def test_list_ftp_paths_bambu_x1(settings, ftps_session_mock): - settings.get.side_effect.options[("device_type",)] = "X1" +def test_list_ftp_paths_x1(settings, ftps_session_mock): file_system = RemoteSDCardFileList(settings) + file_view = CachedFileView(file_system).with_filter("timelapse/", ".mp4") timelapse_files = ["timelapse/video.mp4", "timelapse/video2.mp4"] 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}) 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( 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): - assert printer.file_system.selected_file is None + assert printer.selected_file is None printer.write(b"M23 non_existing.3mf\n") printer.flush() result = printer.readlines() assert result[-2] != b"File selected" 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): - assert printer.file_system.selected_file is None + assert printer.selected_file is None printer.write(b"M20\n") 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[-1] == b"ok" - assert printer.file_system.selected_file is not None - assert printer.file_system.selected_file.file_name == "print.3mf" + assert printer.selected_file is not None + assert printer.selected_file.file_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.flush() assert isinstance(printer.current_state, IdleState) - assert printer.current_print_job is not None - assert printer.current_print_job.file_info.file_name == "print.3mf" - assert printer.current_print_job.progress == 100 + assert printer.current_print_job is None + assert printer.selected_file is not None + assert printer.selected_file.file_name == "print.3mf" printer.write(b"M23 print2.3mf\n") printer.flush() - assert printer.current_print_job is not None - assert printer.current_print_job.file_info.file_name == "print2.3mf" - assert printer.current_print_job.progress == 0 + assert printer.current_print_job is None + assert printer.selected_file is not None + assert printer.selected_file.file_name == "print2.3mf"