Compare commits

..

27 Commits

Author SHA1 Message Date
16dc138e9f change name from start_new_print to start_resume_print 2024-07-27 17:01:22 -04:00
f42d3167c5 Fix file list update. Decouple filesystem from printer file structure. 2024-07-27 02:22:46 +03:00
4ea98036e5 Improve ftp error logging. Update ftp tests. 2024-07-26 16:53:20 +03:00
0d16732561 Fix ftp file list parsing 2024-07-26 16:11:09 +03:00
ef305ee6ce Fix bambu filesystem access from plugin class 2024-07-25 17:22:52 +03:00
1f7eed6b23 Move all ftp operations to printer file system. 2024-07-25 16:51:15 +03:00
55b78cea05 Fix reset print job after new file selected. 2024-07-24 17:15:47 +03:00
f35f456eb2 Fix annotations 2024-07-24 17:15:47 +03:00
42ba306e4f Fix response messages. Fix filesystem name transformations. 2024-07-24 17:15:47 +03:00
19cac21db6 Fix file info fetching 2024-07-24 17:15:47 +03:00
4faa240b06 Fix sd card file access. 2024-07-24 17:15:47 +03:00
38a6f58306 Fix bambu client connection. 2024-07-24 17:15:47 +03:00
ed33fd8fb1 Fix state synchronization bugs. Fix unittests. 2024-07-24 17:15:47 +03:00
53e1f88e1a Fix serial io exception handling. Fix file system data fetch. 2024-07-24 17:15:47 +03:00
8178dea15a Fix unittest IO synchronization. 2024-07-24 17:15:46 +03:00
73f77ed659 Fix binary io flush logic. 2024-07-24 17:15:46 +03:00
a13a5a1e2a Fix serial io read loop. 2024-07-24 17:15:46 +03:00
06c9d68390 Fix remote sd card mocks. 2024-07-24 17:15:46 +03:00
07f601694d Fix serial IO synchronization. 2024-07-24 17:15:46 +03:00
98a1f59169 Implement idle state. Fix serial io 2024-07-24 17:15:46 +03:00
ba2eadb064 Add more mocks to printer unittests. 2024-07-24 17:15:46 +03:00
f5017b5631 Fix unittest mocks. 2024-07-24 17:15:46 +03:00
956a261a45 Fix refactoring artifacts. Add initial unittests. 2024-07-24 17:15:46 +03:00
155f3d2bd3 WIP Refactor printer logic into states and subsystem objects. 2024-07-24 17:15:46 +03:00
75b0a11fef WIP Refactor sd card logic 2024-07-24 17:15:46 +03:00
4da769da49 WIP Refactor gcode execution 2024-07-24 17:15:45 +03:00
527ec9ef3c Refactor plugin. Add typing 2024-07-24 17:15:45 +03:00
16 changed files with 134 additions and 493 deletions

3
.github/FUNDING.yml vendored
View File

@ -1,3 +0,0 @@
github: [jneilliii]
patreon: jneilliii
custom: ['https://www.paypal.me/jneilliii']

View File

@ -1,26 +0,0 @@
---
name: Bug report
about: Please make sure to check other issues, including closed ones, prior to submitting a bug report. Debug logs are required and any bug report submitted without them will be ignored and closed.
title: "[BUG]: "
labels: ''
assignees: ''
---
**Describe the Bug**
<!-- A clear and concise description of what the bug is. -->
**Expected Behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Debug Logs**
<!-- If logs are not included in your bug report it will be closed. Enable debug logging for octoprint.plugins.bambu_printer in OctoPrint's logging section of settings and recreate the issue then attach octoprint.log and plugin_bambu_printer_serial.log to this bug report. -->
**Screenshots**
<!-- Please share any relevant screenshots related to the issue. -->
**Printer and Plugin Setting Details**
* Printer model?
* Is your printer connected to Bambu Cloud?
* Is the plugin configured for local access only?

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Create a feature request for an improvement or change you'd like implemented.
title: "[FR]: "
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

16
.github/stale.yml vendored
View File

@ -1,16 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 14
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- enhancement
- bug
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
activity in 14 days. It will be closed if no further activity occurs in 7 days.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@ -1,27 +0,0 @@
name: Mark Stale Issues
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
permissions:
actions: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been automatically marked as stale because it has not had activity in 14 days. It will be closed if no further activity occurs in 7 days'
days-before-stale: 14
days-before-close: 7
stale-issue-label: 'stale'
days-before-issue-stale: 14
days-before-pr-stale: -1
days-before-issue-close: 7
days-before-pr-close: -1
exempt-issue-labels: 'bug,enhancement'
- uses: actions/checkout@v4
- uses: gautamkrishnar/keepalive-workflow@v2
with:
use_api: true

View File

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

View File

@ -71,8 +71,6 @@ 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",
@ -186,14 +184,13 @@ 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"
or print_job_state == "FAILED"
):
self.change_state(self._state_idle)
elif print_job_state == "RUNNING" or print_job_state == "PREPARE":
elif print_job_state == "RUNNING":
self.change_state(self._state_printing)
elif print_job_state == "PAUSE":
self.change_state(self._state_paused)
@ -277,9 +274,9 @@ class BambuVirtualPrinter:
self.lastN = 0
self._running = False
if self._print_status_reporter is not None:
self._print_status_reporter.cancel()
self._print_status_reporter = None
if self._sdstatus_reporter is not None:
self._sdstatus_reporter.cancel()
self._sdstatus_reporter = None
if self._settings.get_boolean(["simulateReset"]):
for item in self._settings.get(["resetLines"]):
@ -313,16 +310,7 @@ 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_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
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
@ -340,6 +328,7 @@ 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):
@ -366,81 +355,38 @@ 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.start_continuous_status_report(interval)
return False
self._sdstatus_reporter = RepeatedTimer(
interval, self.report_print_job_status
)
self._sdstatus_reporter.start()
else:
self.stop_continuous_status_report()
return False
self._sdstatus_reporter = None
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_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}")
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))
return True
@gcode_executor.register("M105")
def _report_temperatures(self, data: str) -> bool:
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
return self._processTemperatureQuery()
# 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")
@ -472,53 +418,21 @@ class BambuVirtualPrinter:
def _set_feedrate_percent(self, data: str) -> bool:
if self.bambu_client.connected:
gcode_command = commands.SEND_GCODE_TEMPLATE
percent = int(data.replace("M220 S", ""))
percent = int(data[1:])
def speed_fraction(speed_percent):
return math.floor(10000 / speed_percent) / 100
if percent is None or percent < 1 or percent > 166:
return True
def acceleration_magnitude(speed_percent):
return math.exp((speed_fraction(speed_percent) - 1.0191) / -0.8139)
def feed_rate(speed_percent):
return 6.426e-5 * speed_percent ** 2 - 2.484e-3 * speed_percent + 0.654
def linear_interpolate(x, x_points, y_points):
if x <= x_points[0]: return y_points[0]
if x >= x_points[-1]: return y_points[-1]
for i in range(len(x_points) - 1):
if x_points[i] <= x < x_points[i + 1]:
t = (x - x_points[i]) / (x_points[i + 1] - x_points[i])
return y_points[i] * (1 - t) + y_points[i + 1] * t
def scale_to_data_points(func, data_points):
data_points.sort(key=lambda x: x[0])
speeds, values = zip(*data_points)
scaling_factors = [v / func(s) for s, v in zip(speeds, values)]
return lambda x: func(x) * linear_interpolate(x, speeds, scaling_factors)
def speed_adjust(speed_percentage):
if not 30 <= speed_percentage <= 180:
speed_percentage = 100
bambu_params = {
"speed": [50, 100, 124, 166],
"acceleration": [0.3, 1.0, 1.4, 1.6],
"feed_rate": [0.7, 1.0, 1.4, 2.0]
}
acc_mag_scaled = scale_to_data_points(acceleration_magnitude,
list(zip(bambu_params["speed"], bambu_params["acceleration"])))
feed_rate_scaled = scale_to_data_points(feed_rate,
list(zip(bambu_params["speed"], bambu_params["feed_rate"])))
speed_frac = speed_fraction(speed_percentage)
acc_mag = acc_mag_scaled(speed_percentage)
feed = feed_rate_scaled(speed_percentage)
# speed_level = 1.539 * (acc_mag**2) - 0.7032 * acc_mag + 4.0834
return f"M204.2 K{acc_mag:.2f}\nM220 K{feed:.2f}\nM73.2 R{speed_frac:.2f}\n" # M1002 set_gcode_claim_speed_level ${speed_level:.0f}\n
speed_command = speed_adjust(percent)
speed_fraction = 100 / percent
acceleration = math.exp((speed_fraction - 1.0191) / -0.814)
feed_rate = (
2.1645 * (acceleration**3)
- 5.3247 * (acceleration**2)
+ 4.342 * acceleration
- 0.181
)
speed_level = 1.539 * (acceleration**2) - 0.7032 * acceleration + 4.0834
speed_command = f"M204.2 K${acceleration:.2f} \nM220 K${feed_rate:.2f} \nM73.2 R${speed_fraction:.2f} \nM1002 set_gcode_claim_speed_level ${speed_level:.0f}\n"
gcode_command["print"]["param"] = speed_command
if self.bambu_client.publish(gcode_command):
@ -550,8 +464,8 @@ class BambuVirtualPrinter:
return True
@gcode_executor.register("M20")
def _update_project_file_list(self, data: str = ""):
self._project_files_view.update() # internally sends list to serial io
def _list_project_files(self, data: str = ""):
self._project_files_view.update()
return True
def _list_cached_project_files(self):
@ -561,11 +475,10 @@ class BambuVirtualPrinter:
):
self.sendIO(item)
self.sendIO("End file list")
self.sendOk()
@gcode_executor.register_no_data("M24")
def _start_resume_sd_print(self):
self._current_state.start_new_print()
def _start_print(self):
self._current_state.start_resume_print()
return True
@gcode_executor.register_no_data("M25")
@ -579,31 +492,14 @@ class BambuVirtualPrinter:
return True
def report_print_job_status(self):
if self.current_print_job is not None:
file_position = 1 if self.current_print_job.file_position == 0 else self.current_print_job.file_position
print_job = self.current_print_job
if print_job is not None:
self.sendIO(
f"SD printing byte {file_position}"
f"/{self.current_print_job.file_info.size}"
f"SD printing byte {print_job.file_position}/{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,9 +15,7 @@ from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
@dataclass
class CachedFileView:
file_system: RemoteSDCardFileList
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
folder_view: set[tuple[str, str | list[str] | None]] = field(default_factory=set)
on_update: Callable[[], None] | None = None
def __post_init__(self):
@ -27,7 +25,7 @@ class CachedFileView:
def with_filter(
self, folder: str, extensions: str | list[str] | None = None
) -> "CachedFileView":
self.folder_view[(folder, extensions)] = None
self.folder_view.add((folder, extensions))
return self
def list_all_views(self):
@ -35,7 +33,7 @@ class CachedFileView:
result: list[FileInfo] = []
with self.file_system.get_ftps_client() as ftp:
for filter in self.folder_view.keys():
for filter in self.folder_view:
result.extend(self.file_system.list_files(*filter, ftp, existing_files))
return result
@ -46,8 +44,8 @@ class CachedFileView:
self.on_update()
def _update_file_list_cache(self, files: list[FileInfo]):
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}
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()
@ -56,39 +54,26 @@ class CachedFileView:
def get_all_cached_info(self):
return list(self._file_data_cache.values())
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]):
def get_file_by_suffix(self, file_stem: str, allowed_suffixes: list[str]):
if file_stem == "":
return None
file_stem = Path(file_stem).with_suffix("").stem
file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes)
file_data = self._get_file_by_suffix_cached(file_stem, allowed_suffixes)
if file_data is None:
self.update()
file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes)
file_data = self._get_file_by_suffix_cached(file_stem, allowed_suffixes)
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)
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
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.ftps_session.welcome:
if "vsFTPd" in self.welcome:
conn.unwrap()
else:
conn.shutdown(socket.SHUT_RDWR)

View File

@ -7,6 +7,8 @@ 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
@ -21,7 +23,7 @@ class RemoteSDCardFileList:
def delete_file(self, file_path: Path) -> None:
try:
with self.get_ftps_client() as ftp:
if ftp.delete_file(file_path.as_posix()):
if ftp.delete_file(str(file_path)):
self._logger.debug(f"{file_path} deleted")
else:
raise RuntimeError(f"Deleting file {file_path} failed")

View File

@ -1,33 +1,26 @@
from __future__ import annotations
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.states.a_printer_state import APrinterState
class IdleState(APrinterState):
def start_new_print(self):
def start_resume_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)
self._log.debug(f"Sending print command: {print_command}")
print_command = self._get_print_command_for_file(selected_file.file_name)
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: 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:///"
)
def _get_print_command_for_file(self, selected_file):
print_command = {
"print": {
"sequence_id": 0,
@ -38,9 +31,14 @@ class IdleState(APrinterState):
"project_id": "0",
"subtask_id": "0",
"task_id": "0",
"subtask_name": selected_file.file_name,
"url": f"{filesystem_root}{selected_file.path.as_posix()}",
"bed_type": "auto",
"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}"
),
"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"]),
@ -49,7 +47,6 @@ 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,33 +19,35 @@ 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._printer.start_continuous_status_report(3)
self._sendPaused()
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 start_new_print(self):
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):
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,7 +22,6 @@ 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
@ -37,7 +36,6 @@ 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:
@ -55,33 +53,34 @@ class PrintingState(APrinterState):
self._printer.report_print_job_status()
time.sleep(3)
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()
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()
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_stem(
task_name, [".gcode", ".3mf"]
project_file_info = self._printer.project_files.get_file_by_suffix(
task_name, [".3mf", ".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.path.as_posix())
self._printer.select_project_file(project_file_info.file_name)
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")
@ -89,6 +88,17 @@ class PrintingState(APrinterState):
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()
self._finish_print()
self._printer.change_state(self._printer._state_idle)
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,10 +7,3 @@
###
.
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.1.7"
plugin_version = "0.0.23"
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
# module

View File

@ -2,9 +2,8 @@ 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, patch
from unittest.mock import MagicMock
from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView
import pybambu
@ -30,9 +29,7 @@ def output_test_folder(output_folder: Path):
@fixture
def log_test():
log = logging.getLogger("gcode_unittest")
log.setLevel(logging.DEBUG)
return log
return logging.getLogger("gcode_unittest")
class DictGetter:
@ -92,11 +89,7 @@ def project_files_info_ftp():
def cache_files_info_ftp():
return {
"cache/print.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)),
),
"cache/print2.3mf": (1200, _ftp_date_format(datetime(2024, 5, 7))),
}
@ -194,11 +187,8 @@ 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].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"
assert result[3] == b"End file list"
assert result[4] == b"ok"
def test_list_ftp_paths_p1s(settings, ftps_session_mock):
@ -249,67 +239,6 @@ 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()
@ -349,13 +278,9 @@ def test_print_started_with_selected_file(printer: BambuVirtualPrinter, print_jo
printer.write(b"M24\n")
printer.flush()
result = printer.readlines()
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()
result = printer.readlines()
assert result[0] == b"ok"
assert isinstance(printer.current_state, PrintingState)
@ -366,26 +291,18 @@ 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()
print_job_mock.gcode_state = "RUNNING"
printer.new_update("event_printer_data_update")
printer.flush()
printer.readlines()
assert isinstance(printer.current_state, PrintingState)
printer.write(b"M25\n") # pausing the print
bambu_client_mock.publish.return_value = True
printer.write(b"M25\n") # GCode for pausing the print
printer.flush()
result = printer.readlines()
assert result[-1] == b"ok"
print_job_mock.gcode_state = "PAUSE"
printer.new_update("event_printer_data_update")
printer.flush()
assert result[0] == b"ok"
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()
@ -421,45 +338,10 @@ def test_printer_info_check(printer: BambuVirtualPrinter):
assert isinstance(printer.current_state, IdleState)
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")
def test_abort_print(printer: BambuVirtualPrinter):
printer.write(b"M26\n") # GCode for aborting the print
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)
@ -487,9 +369,7 @@ def test_file_selection_does_not_affect_current_print(
printer.write(b"M23 print.3mf\nM24\n")
printer.flush()
print_job_mock.gcode_state = "RUNNING"
printer.new_update("event_printer_data_update")
printer.flush()
printer.readlines()
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"
@ -509,9 +389,7 @@ def test_finished_print_job_reset_after_new_file_selected(
printer.write(b"M23 print.3mf\nM24\n")
printer.flush()
print_job_mock.gcode_state = "RUNNING"
printer.new_update("event_printer_data_update")
printer.flush()
printer.readlines()
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"
@ -535,28 +413,3 @@ 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"