Compare commits

..

5 Commits

Author SHA1 Message Date
7f1ae5a24b 0.1.4 (#43)
* fix stuck Printing from SD state when canceled in slicer or on printer, #42
2024-09-04 16:48:16 -04:00
5754e81b72 0.1.3
fix file uploads
2024-08-25 14:20:45 -04:00
cd4103cc71 0.1.2 (#40)
* fix issues related to 8dot3 filenames used in M23 command, #39 
* switch to auto reporting temp and sd status
2024-08-18 01:06:57 -04:00
01c6cacf15 0.1.1
* fix M220 command, #35
2024-07-31 00:01:44 -04:00
fda4b86cbc 0.1.0 (#34)
* Add separate class for sftp file system
* Add separate serial IO handling class
* Replace function name mangling with gcode handler registration system
* Add states to virtual Bambu printer that manage state specific interaction
* Add synchronization utilities to work with virtual printer as if it is a binary stream
* Add unittests with mocked Bambu printer to ensure core functionality works as expected
* Fix formatting to be automatically processed by black formatter
* Fix python 3.10 type annotations for readability
2024-07-29 22:49:12 -04:00
11 changed files with 355 additions and 121 deletions

View File

@ -37,7 +37,12 @@ from .printer.bambu_virtual_printer import BambuVirtualPrinter
@contextmanager
def measure_elapsed():
start = perf_counter()
yield lambda: perf_counter() - start
def _get_elapsed():
return perf_counter() - start
yield _get_elapsed
print(f"Total elapsed: {_get_elapsed()}")
class BambuPrintPlugin(

View File

@ -71,6 +71,8 @@ class BambuVirtualPrinter:
self._current_state = self._state_idle
self._running = True
self._print_status_reporter = None
self._print_temp_reporter = None
self._printer_thread = threading.Thread(
target=self._printer_worker,
name="octoprint.plugins.bambu_printer.printer_state",
@ -184,6 +186,7 @@ class BambuVirtualPrinter:
self._telemetry.bedTargetTemp = temperatures.target_bed_temp
self._telemetry.chamberTemp = temperatures.chamber_temp
self._log.debug(f"Received printer state update: {print_job_state}")
if (
print_job_state == "IDLE"
or print_job_state == "FINISH"
@ -274,9 +277,9 @@ class BambuVirtualPrinter:
self.lastN = 0
self._running = False
if self._sdstatus_reporter is not None:
self._sdstatus_reporter.cancel()
self._sdstatus_reporter = None
if self._print_status_reporter is not None:
self._print_status_reporter.cancel()
self._print_status_reporter = None
if self._settings.get_boolean(["simulateReset"]):
for item in self._settings.get(["resetLines"]):
@ -310,7 +313,16 @@ class BambuVirtualPrinter:
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)
file_info = self._project_files_view.get_file_by_stem(
file_path, [".gcode", ".3mf"]
)
if (
self._selected_project_file is not None
and file_info is not None
and self._selected_project_file.path == file_info.path
):
return True
if file_info is None:
self._log.error(f"Cannot select not existing file: {file_path}")
return False
@ -328,7 +340,6 @@ class BambuVirtualPrinter:
@gcode_executor.register("M23")
def _select_sd_file(self, data: str) -> bool:
filename = data.split(maxsplit=1)[1].strip()
self._list_project_files()
return self.select_project_file(filename)
def _send_file_selected_message(self):
@ -355,38 +366,81 @@ class BambuVirtualPrinter:
matchS = re.search(r"S([0-9]+)", data)
if matchS:
interval = int(matchS.group(1))
if self._sdstatus_reporter is not None:
self._sdstatus_reporter.cancel()
if interval > 0:
self._sdstatus_reporter = RepeatedTimer(
interval, self.report_print_job_status
)
self._sdstatus_reporter.start()
self.start_continuous_status_report(interval)
return False
else:
self._sdstatus_reporter = None
self.stop_continuous_status_report()
return False
self.report_print_job_status()
return True
def start_continuous_status_report(self, interval: int):
if self._print_status_reporter is not None:
self._print_status_reporter.cancel()
self._print_status_reporter = RepeatedTimer(
interval, self.report_print_job_status
)
self._print_status_reporter.start()
def stop_continuous_status_report(self):
if self._print_status_reporter is not None:
self._print_status_reporter.cancel()
self._print_status_reporter = None
@gcode_executor.register("M30")
def _delete_sd_file(self, data: str) -> bool:
file_path = data.split(None, 1)[1].strip()
self._list_project_files()
self.file_system.delete_file(Path(file_path))
def _delete_project_file(self, data: str) -> bool:
file_path = data.split(maxsplit=1)[1].strip()
file_info = self.project_files.get_file_data(file_path)
if file_info is not None:
self.file_system.delete_file(file_info.path)
self._update_project_file_list()
else:
self._log.error(f"File not found to delete {file_path}")
return True
@gcode_executor.register("M105")
def _report_temperatures(self, data: str) -> bool:
return self._processTemperatureQuery()
self._processTemperatureQuery()
return True
@gcode_executor.register("M155")
def _auto_report_temperatures(self, data: str) -> bool:
matchS = re.search(r"S([0-9]+)", data)
if matchS:
interval = int(matchS.group(1))
if interval > 0:
self.start_continuous_temp_report(interval)
else:
self.stop_continuous_temp_report()
self.report_print_job_status()
return True
def start_continuous_temp_report(self, interval: int):
if self._print_temp_reporter is not None:
self._print_temp_reporter.cancel()
self._print_temp_reporter = RepeatedTimer(
interval, self._processTemperatureQuery
)
self._print_temp_reporter.start()
def stop_continuous_temp_report(self):
if self._print_temp_reporter is not None:
self._print_temp_reporter.cancel()
self._print_temp_reporter = None
# noinspection PyUnusedLocal
@gcode_executor.register_no_data("M115")
def _report_firmware_info(self) -> bool:
self.sendIO("Bambu Printer Integration")
self.sendIO("Cap:AUTOREPORT_SD_STATUS:1")
self.sendIO("Cap:AUTOREPORT_TEMP:1")
self.sendIO("Cap:EXTENDED_M20:1")
self.sendIO("Cap:LFN_WRITE:1")
self.sendIO("Cap:LFN_WRITE:1")
return True
@gcode_executor.register("M117")
@ -418,7 +472,7 @@ class BambuVirtualPrinter:
def _set_feedrate_percent(self, data: str) -> bool:
if self.bambu_client.connected:
gcode_command = commands.SEND_GCODE_TEMPLATE
percent = int(data[1:])
percent = int(data.replace("M220 S", ""))
if percent is None or percent < 1 or percent > 166:
return True
@ -464,8 +518,8 @@ class BambuVirtualPrinter:
return True
@gcode_executor.register("M20")
def _list_project_files(self, data: str = ""):
self._project_files_view.update()
def _update_project_file_list(self, data: str = ""):
self._project_files_view.update() # internally sends list to serial io
return True
def _list_cached_project_files(self):
@ -475,10 +529,11 @@ class BambuVirtualPrinter:
):
self.sendIO(item)
self.sendIO("End file list")
self.sendOk()
@gcode_executor.register_no_data("M24")
def _start_print(self):
self._current_state.start_resume_print()
def _start_resume_sd_print(self):
self._current_state.start_new_print()
return True
@gcode_executor.register_no_data("M25")
@ -492,14 +547,30 @@ class BambuVirtualPrinter:
return True
def report_print_job_status(self):
print_job = self.current_print_job
if print_job is not None:
if self.current_print_job is not None:
self.sendIO(
f"SD printing byte {print_job.file_position}/{print_job.file_info.size}"
f"SD printing byte {self.current_print_job.file_position}"
f"/{self.current_print_job.file_info.size}"
)
else:
self.sendIO("Not SD printing")
def report_print_finished(self):
if self.current_print_job is None:
return
self._log.debug(
f"SD File Print finishing: {self.current_print_job.file_info.file_name}"
)
self.sendIO("Done printing file")
def finalize_print_job(self):
if self.current_print_job is not None:
self.report_print_job_status()
self.report_print_finished()
self.current_print_job = None
self.report_print_job_status()
self.change_state(self._state_idle)
def _create_temperature_message(self) -> str:
template = "{heater}:{actual:.2f}/ {target:.2f}"
temps = collections.OrderedDict()

View File

@ -15,7 +15,9 @@ from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
@dataclass
class CachedFileView:
file_system: RemoteSDCardFileList
folder_view: set[tuple[str, str | list[str] | None]] = field(default_factory=set)
folder_view: dict[tuple[str, str | list[str] | None], None] = field(
default_factory=dict
) # dict preserves order, but set does not. We use only dict keys as storage
on_update: Callable[[], None] | None = None
def __post_init__(self):
@ -25,7 +27,7 @@ class CachedFileView:
def with_filter(
self, folder: str, extensions: str | list[str] | None = None
) -> "CachedFileView":
self.folder_view.add((folder, extensions))
self.folder_view[(folder, extensions)] = None
return self
def list_all_views(self):
@ -33,7 +35,7 @@ class CachedFileView:
result: list[FileInfo] = []
with self.file_system.get_ftps_client() as ftp:
for filter in self.folder_view:
for filter in self.folder_view.keys():
result.extend(self.file_system.list_files(*filter, ftp, existing_files))
return result
@ -44,8 +46,8 @@ class CachedFileView:
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}
self._file_alias_cache = {info.dosname: info.path.as_posix() for info in files}
self._file_data_cache = {info.path.as_posix(): info for info in files}
def get_all_info(self):
self.update()
@ -54,26 +56,39 @@ class CachedFileView:
def get_all_cached_info(self):
return list(self._file_data_cache.values())
def get_file_by_suffix(self, file_stem: str, allowed_suffixes: list[str]):
def get_file_data(self, file_path: str | Path) -> FileInfo | None:
file_data = self.get_file_data_cached(file_path)
if file_data is None:
self.update()
file_data = self.get_file_data_cached(file_path)
return file_data
def get_file_data_cached(self, file_path: str | Path) -> FileInfo | None:
if isinstance(file_path, str):
file_path = Path(file_path).as_posix().strip("/")
else:
file_path = file_path.as_posix().strip("/")
if file_path not in self._file_data_cache:
file_path = self._file_alias_cache.get(file_path, file_path)
return self._file_data_cache.get(file_path, None)
def get_file_by_stem(self, file_stem: str, allowed_suffixes: list[str]):
if file_stem == "":
return None
file_data = self._get_file_by_suffix_cached(file_stem, allowed_suffixes)
file_stem = Path(file_stem).with_suffix("").stem
file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes)
if file_data is None:
self.update()
file_data = self._get_file_by_suffix_cached(file_stem, allowed_suffixes)
file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes)
return file_data
def get_cached_file_data(self, file_name: str) -> FileInfo | None:
file_name = Path(file_name).name
file_name = self._file_alias_cache.get(file_name, file_name)
return self._file_data_cache.get(file_name, None)
def _get_file_by_suffix_cached(self, file_stem: str, allowed_suffixes: list[str]):
for suffix in allowed_suffixes:
file_data = self.get_cached_file_data(
Path(file_stem).with_suffix(suffix).as_posix()
)
if file_data is not None:
return file_data
def _get_file_by_stem_cached(self, file_stem: str, allowed_suffixes: list[str]):
for file_path_str in list(self._file_data_cache.keys()) + list(self._file_alias_cache.keys()):
file_path = Path(file_path_str)
if file_stem == file_path.with_suffix("").stem and all(
suffix in allowed_suffixes for suffix in file_path.suffixes
):
return self.get_file_data_cached(file_path)
return None

View File

@ -117,7 +117,7 @@ class IoTFTPSConnection:
# But since we operate in prot p mode
# we can close the connection always.
# This is cursed but it works.
if "vsFTPd" in self.welcome:
if "vsFTPd" in self.ftps_session.welcome:
conn.unwrap()
else:
conn.shutdown(socket.SHUT_RDWR)

View File

@ -7,8 +7,6 @@ import logging.handlers
from octoprint.util import get_dos_filename
from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView
from .ftps_client import IoTFTPSClient, IoTFTPSConnection
from .file_info import FileInfo
@ -23,7 +21,7 @@ class RemoteSDCardFileList:
def delete_file(self, file_path: Path) -> None:
try:
with self.get_ftps_client() as ftp:
if ftp.delete_file(str(file_path)):
if ftp.delete_file(file_path.as_posix()):
self._logger.debug(f"{file_path} deleted")
else:
raise RuntimeError(f"Deleting file {file_path} failed")

View File

@ -1,26 +1,33 @@
from __future__ import annotations
from octoprint_bambu_printer.printer.print_job import PrintJob
from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState
class IdleState(APrinterState):
def start_resume_print(self):
def start_new_print(self):
selected_file = self._printer.selected_file
if selected_file is None:
self._log.warn("Cannot start print job if file was not selected")
return
print_command = self._get_print_command_for_file(selected_file.file_name)
print_command = self._get_print_command_for_file(selected_file)
self._log.debug(f"Sending print command: {print_command}")
if self._printer.bambu_client.publish(print_command):
self._log.info(f"Started print for {selected_file.file_name}")
self._printer.change_state(self._printer._state_printing)
else:
self._log.warn(f"Failed to start print for {selected_file.file_name}")
self._printer.change_state(self._printer._state_idle)
def _get_print_command_for_file(self, selected_file):
def _get_print_command_for_file(self, selected_file: FileInfo):
# URL to print. Root path, protocol can vary. E.g., if sd card, "ftp:///myfile.3mf", "ftp:///cache/myotherfile.3mf"
filesystem_root = (
"file:///mnt/sdcard/"
if self._printer._settings.get(["device_type"]) in ["X1", "X1C"]
else "file:///"
)
print_command = {
"print": {
"sequence_id": 0,
@ -31,14 +38,9 @@ class IdleState(APrinterState):
"project_id": "0",
"subtask_id": "0",
"task_id": "0",
"subtask_name": f"{selected_file}",
"file": f"{selected_file}",
"url": (
f"file:///mnt/sdcard/{selected_file}"
if self._printer._settings.get_boolean(["device_type"])
in ["X1", "X1C"]
else f"file:///sdcard/{selected_file}"
),
"subtask_name": selected_file.file_name,
"url": f"{filesystem_root}{selected_file.path.as_posix()}",
"bed_type": "auto",
"timelapse": self._printer._settings.get_boolean(["timelapse"]),
"bed_leveling": self._printer._settings.get_boolean(["bed_leveling"]),
"flow_cali": self._printer._settings.get_boolean(["flow_cali"]),
@ -47,6 +49,7 @@ class IdleState(APrinterState):
),
"layer_inspect": self._printer._settings.get_boolean(["layer_inspect"]),
"use_ams": self._printer._settings.get_boolean(["use_ams"]),
"ams_mapping": "",
}
}

View File

@ -19,35 +19,33 @@ class PausedState(APrinterState):
def __init__(self, printer: BambuVirtualPrinter) -> None:
super().__init__(printer)
self._pausedLock = threading.Event()
self._paused_repeated_report = None
def init(self):
if not self._pausedLock.is_set():
self._pausedLock.set()
self._printer.sendIO("// action:paused")
self._sendPaused()
self._printer.start_continuous_status_report(3)
def finalize(self):
if self._pausedLock.is_set():
self._pausedLock.clear()
if self._paused_repeated_report is not None:
self._paused_repeated_report.join()
self._paused_repeated_report = None
def _sendPaused(self):
if self._printer.current_print_job is None:
self._log.warn("job paused, but no print job available?")
return
paused_timer = RepeatedTimer(
interval=3.0,
function=self._printer.report_print_job_status,
daemon=True,
run_first=True,
condition=self._pausedLock.is_set,
)
paused_timer.start()
def start_resume_print(self):
def start_new_print(self):
if self._printer.bambu_client.connected:
if self._printer.bambu_client.publish(pybambu.commands.RESUME):
self._log.info("print resumed")
self._printer.change_state(self._printer._state_printing)
else:
self._log.info("print resume failed")
def cancel_print(self):
if self._printer.bambu_client.connected:
if self._printer.bambu_client.publish(pybambu.commands.STOP):
self._log.info("print cancelled")
self._printer.finalize_print_job()
else:
self._log.info("print cancel failed")

View File

@ -22,6 +22,7 @@ class PrintingState(APrinterState):
def __init__(self, printer: BambuVirtualPrinter) -> None:
super().__init__(printer)
self._current_print_job = None
self._is_printing = False
self._sd_printing_thread = None
@ -36,6 +37,7 @@ class PrintingState(APrinterState):
self._is_printing = False
self._sd_printing_thread.join()
self._sd_printing_thread = None
self._printer.current_print_job = None
def _start_worker_thread(self):
if self._sd_printing_thread is None:
@ -53,34 +55,33 @@ class PrintingState(APrinterState):
self._printer.report_print_job_status()
time.sleep(3)
if self._printer.current_print_job is None:
self._log.warn("Printing state was triggered with empty print job")
return
if self._printer.current_print_job.progress >= 100:
self._finish_print()
self.update_print_job_info()
if (
self._printer.current_print_job is not None
and self._printer.current_print_job.progress >= 100
):
self._printer.finalize_print_job()
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.project_files.get_file_by_suffix(
task_name, [".3mf", ".gcode.3mf"]
project_file_info = self._printer.project_files.get_file_by_stem(
task_name, [".gcode", ".3mf"]
)
if project_file_info is None:
self._log.debug(f"No 3mf file found for {print_job_info}")
self._current_print_job = None
self._printer.change_state(self._printer._state_idle)
return
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)
self._printer.select_project_file(project_file_info.path.as_posix())
def pause_print(self):
if self._printer.bambu_client.connected:
if self._printer.bambu_client.publish(pybambu.commands.PAUSE):
self._log.info("print paused")
self._printer.change_state(self._printer._state_paused)
else:
self._log.info("print pause failed")
@ -88,17 +89,6 @@ class PrintingState(APrinterState):
if self._printer.bambu_client.connected:
if self._printer.bambu_client.publish(pybambu.commands.STOP):
self._log.info("print cancelled")
self._finish_print()
self._printer.change_state(self._printer._state_idle)
self._printer.finalize_print_job()
else:
self._log.info("print cancel failed")
def _finish_print(self):
if self._printer.current_print_job is not None:
self._log.debug(
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)

View File

@ -7,3 +7,10 @@
###
.
pytest~=7.4.4
pybambu~=1.0.1
OctoPrint~=1.10.2
setuptools~=70.0.0
pyserial~=3.5
Flask~=2.2.5

View File

@ -14,7 +14,7 @@ plugin_package = "octoprint_bambu_printer"
plugin_name = "OctoPrint-BambuPrinter"
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
plugin_version = "0.0.23"
plugin_version = "0.1.4"
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
# module

View File

@ -2,8 +2,9 @@ from __future__ import annotations
from datetime import datetime, timezone
import logging
from pathlib import Path
import sys
from typing import Any
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView
import pybambu
@ -29,7 +30,9 @@ def output_test_folder(output_folder: Path):
@fixture
def log_test():
return logging.getLogger("gcode_unittest")
log = logging.getLogger("gcode_unittest")
log.setLevel(logging.DEBUG)
return log
class DictGetter:
@ -89,7 +92,11 @@ def project_files_info_ftp():
def cache_files_info_ftp():
return {
"cache/print.3mf": (1200, _ftp_date_format(datetime(2024, 5, 7))),
"cache/print2.3mf": (1200, _ftp_date_format(datetime(2024, 5, 7))),
"cache/print3.gcode.3mf": (1200, _ftp_date_format(datetime(2024, 5, 7))),
"cache/long file path with spaces.gcode.3mf": (
1200,
_ftp_date_format(datetime(2024, 5, 7)),
),
}
@ -187,8 +194,11 @@ def test_list_sd_card(printer: BambuVirtualPrinter):
assert result[0] == b"Begin file list"
assert result[1].endswith(b'"print.3mf"')
assert result[2].endswith(b'"print2.3mf"')
assert result[3] == b"End file list"
assert result[4] == b"ok"
assert result[3].endswith(b'"print.3mf"')
assert result[4].endswith(b'"print3.gcode.3mf"')
assert result[-3] == b"End file list"
assert result[-2] == b"ok"
assert result[-1] == b"ok"
def test_list_ftp_paths_p1s(settings, ftps_session_mock):
@ -239,6 +249,67 @@ def test_list_ftp_paths_x1(settings, ftps_session_mock):
)
def test_delete_sd_file_gcode(printer: BambuVirtualPrinter):
with patch(
"octoprint_bambu_printer.printer.file_system.ftps_client.IoTFTPSConnection.delete_file"
) as delete_function:
printer.write(b"M30 print.3mf\n")
printer.flush()
result = printer.readlines()
assert result[-1] == b"ok"
delete_function.assert_called_with("print.3mf")
printer.write(b"M30 cache/print.3mf\n")
printer.flush()
result = printer.readlines()
assert result[-1] == b"ok"
delete_function.assert_called_with("cache/print.3mf")
def test_delete_sd_file_by_dosname(printer: BambuVirtualPrinter):
with patch(
"octoprint_bambu_printer.printer.file_system.ftps_client.IoTFTPSConnection.delete_file"
) as delete_function:
file_info = printer.project_files.get_file_data("cache/print.3mf")
assert file_info is not None
printer.write(b"M30 " + file_info.dosname.encode() + b"\n")
printer.flush()
assert printer.readlines()[-1] == b"ok"
assert delete_function.call_count == 1
delete_function.assert_called_with("cache/print.3mf")
printer.write(b"M30 cache/print.3mf\n")
printer.flush()
assert printer.readlines()[-1] == b"ok"
assert delete_function.call_count == 2
delete_function.assert_called_with("cache/print.3mf")
def test_select_project_file_by_stem(printer: BambuVirtualPrinter):
printer.write(b"M23 print3\n")
printer.flush()
result = printer.readlines()
assert printer.selected_file is not None
assert printer.selected_file.path == Path("cache/print3.gcode.3mf")
assert result[-2] == b"File selected"
assert result[-1] == b"ok"
def test_select_project_long_name_file_with_multiple_extensions(
printer: BambuVirtualPrinter,
):
printer.write(b"M23 long file path with spaces.gcode.3mf\n")
printer.flush()
result = printer.readlines()
assert printer.selected_file is not None
assert printer.selected_file.path == Path(
"cache/long file path with spaces.gcode.3mf"
)
assert result[-2] == b"File selected"
assert result[-1] == b"ok"
def test_cannot_start_print_without_file(printer: BambuVirtualPrinter):
printer.write(b"M24\n")
printer.flush()
@ -278,9 +349,13 @@ def test_print_started_with_selected_file(printer: BambuVirtualPrinter, print_jo
printer.write(b"M24\n")
printer.flush()
result = printer.readlines()
assert result[0] == b"ok"
assert result[-1] == b"ok"
# emulate printer reporting it's status
print_job_mock.gcode_state = "RUNNING"
printer.new_update("event_printer_data_update")
printer.flush()
assert isinstance(printer.current_state, PrintingState)
@ -291,18 +366,26 @@ def test_pause_print(printer: BambuVirtualPrinter, bambu_client_mock, print_job_
printer.write(b"M23 print.3mf\n")
printer.write(b"M24\n")
printer.flush()
printer.readlines()
print_job_mock.gcode_state = "RUNNING"
printer.new_update("event_printer_data_update")
printer.flush()
assert isinstance(printer.current_state, PrintingState)
bambu_client_mock.publish.return_value = True
printer.write(b"M25\n") # GCode for pausing the print
printer.write(b"M25\n") # pausing the print
printer.flush()
result = printer.readlines()
assert result[0] == b"ok"
assert result[-1] == b"ok"
print_job_mock.gcode_state = "PAUSE"
printer.new_update("event_printer_data_update")
printer.flush()
assert isinstance(printer.current_state, PausedState)
bambu_client_mock.publish.assert_called_with(pybambu.commands.PAUSE)
def test_events_update_printer_state(printer: BambuVirtualPrinter, print_job_mock):
print_job_mock.subtask_name = "print.3mf"
print_job_mock.gcode_state = "RUNNING"
printer.new_update("event_printer_data_update")
printer.flush()
@ -338,10 +421,45 @@ def test_printer_info_check(printer: BambuVirtualPrinter):
assert isinstance(printer.current_state, IdleState)
def test_abort_print(printer: BambuVirtualPrinter):
printer.write(b"M26\n") # GCode for aborting the print
def test_abort_print_during_printing(printer: BambuVirtualPrinter, print_job_mock):
print_job_mock.subtask_name = "print.3mf"
printer.write(b"M20\nM23 print.3mf\nM24\n")
printer.flush()
print_job_mock.gcode_state = "RUNNING"
print_job_mock.print_percentage = 50
printer.new_update("event_printer_data_update")
printer.flush()
printer.readlines()
assert isinstance(printer.current_state, PrintingState)
printer.write(b"M26 S0\n")
printer.flush()
result = printer.readlines()
assert result[-1] == b"ok"
assert isinstance(printer.current_state, IdleState)
def test_abort_print_during_pause(printer: BambuVirtualPrinter, print_job_mock):
print_job_mock.subtask_name = "print.3mf"
printer.write(b"M20\nM23 print.3mf\nM24\n")
printer.flush()
print_job_mock.gcode_state = "RUNNING"
printer.new_update("event_printer_data_update")
printer.flush()
printer.write(b"M25\n")
printer.flush()
print_job_mock.gcode_state = "PAUSE"
printer.new_update("event_printer_data_update")
printer.flush()
printer.readlines()
assert isinstance(printer.current_state, PausedState)
printer.write(b"M26 S0\n")
printer.flush()
result = printer.readlines()
assert result[-1] == b"ok"
assert isinstance(printer.current_state, IdleState)
@ -369,7 +487,9 @@ def test_file_selection_does_not_affect_current_print(
printer.write(b"M23 print.3mf\nM24\n")
printer.flush()
printer.readlines()
print_job_mock.gcode_state = "RUNNING"
printer.new_update("event_printer_data_update")
printer.flush()
assert isinstance(printer.current_state, PrintingState)
assert printer.current_print_job is not None
assert printer.current_print_job.file_info.file_name == "print.3mf"
@ -389,7 +509,9 @@ def test_finished_print_job_reset_after_new_file_selected(
printer.write(b"M23 print.3mf\nM24\n")
printer.flush()
printer.readlines()
print_job_mock.gcode_state = "RUNNING"
printer.new_update("event_printer_data_update")
printer.flush()
assert isinstance(printer.current_state, PrintingState)
assert printer.current_print_job is not None
assert printer.current_print_job.file_info.file_name == "print.3mf"
@ -413,3 +535,28 @@ def test_finished_print_job_reset_after_new_file_selected(
assert printer.current_print_job is None
assert printer.selected_file is not None
assert printer.selected_file.file_name == "print2.3mf"
def test_finish_detected_correctly(printer: BambuVirtualPrinter, print_job_mock):
print_job_mock.subtask_name = "print.3mf"
print_job_mock.gcode_state = "RUNNING"
print_job_mock.print_percentage = 99
printer.new_update("event_printer_data_update")
printer.flush()
assert isinstance(printer.current_state, PrintingState)
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 == 99
print_job_mock.print_percentage = 100
print_job_mock.gcode_state = "FINISH"
printer.new_update("event_printer_data_update")
printer.flush()
result = printer.readlines()
assert result[-3].endswith(b"1000/1000")
assert result[-2] == b"Done printing file"
assert result[-1] == b"Not SD printing"
assert isinstance(printer.current_state, IdleState)
assert printer.current_print_job is None
assert printer.selected_file is not None
assert printer.selected_file.file_name == "print.3mf"