Compare commits
	
		
			11 Commits
		
	
	
		
			feature/re
			...
			698f8f4151
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 698f8f4151 | |||
| 7a0293bac7 | |||
|  | d0fd4a5434 | ||
|  | 3c218a548d | ||
|  | 03af51608d | ||
|  | c00285b1b2 | ||
|  | 7f1ae5a24b | ||
|  | 5754e81b72 | ||
|  | cd4103cc71 | ||
|  | 01c6cacf15 | ||
|  | fda4b86cbc | 
							
								
								
									
										3
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | github: [jneilliii] | ||||||
|  | patreon: jneilliii | ||||||
|  | custom: ['https://www.paypal.me/jneilliii'] | ||||||
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | --- | ||||||
|  | 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? | ||||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | --- | ||||||
|  | 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
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | # 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 | ||||||
							
								
								
									
										27
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | 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 | ||||||
| @@ -37,7 +37,12 @@ from .printer.bambu_virtual_printer import BambuVirtualPrinter | |||||||
| @contextmanager | @contextmanager | ||||||
| def measure_elapsed(): | def measure_elapsed(): | ||||||
|     start = perf_counter() |     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( | class BambuPrintPlugin( | ||||||
| @@ -80,7 +85,7 @@ class BambuPrintPlugin( | |||||||
|             "serial": "", |             "serial": "", | ||||||
|             "host": "", |             "host": "", | ||||||
|             "access_code": "", |             "access_code": "", | ||||||
|             "username": "bblp", |             "username": "octobambu", | ||||||
|             "timelapse": False, |             "timelapse": False, | ||||||
|             "bed_leveling": True, |             "bed_leveling": True, | ||||||
|             "flow_cali": False, |             "flow_cali": False, | ||||||
| @@ -281,10 +286,10 @@ class BambuPrintPlugin( | |||||||
|     def get_update_information(self): |     def get_update_information(self): | ||||||
|         return { |         return { | ||||||
|             "bambu_printer": { |             "bambu_printer": { | ||||||
|                 "displayName": "Bambu Printer", |                 "displayName": "Manus Bambu Printer", | ||||||
|                 "displayVersion": self._plugin_version, |                 "displayVersion": self._plugin_version, | ||||||
|                 "type": "github_release", |                 "type": "github_release", | ||||||
|                 "user": "jneilliii", |                 "user": "ManuelW", | ||||||
|                 "repo": "OctoPrint-BambuPrinter", |                 "repo": "OctoPrint-BambuPrinter", | ||||||
|                 "current": self._plugin_version, |                 "current": self._plugin_version, | ||||||
|                 "stable_branch": { |                 "stable_branch": { | ||||||
| @@ -299,6 +304,6 @@ class BambuPrintPlugin( | |||||||
|                         "comittish": ["rc", "master"], |                         "comittish": ["rc", "master"], | ||||||
|                     } |                     } | ||||||
|                 ], |                 ], | ||||||
|                 "pip": "https://github.com/jneilliii/OctoPrint-BambuPrinter/archive/{target_version}.zip", |                 "pip": "https://gitlab.fire-devils.org/3D-Druck/OctoPrint-BambuPrinter/archive/{target_version}.zip", | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -71,6 +71,8 @@ class BambuVirtualPrinter: | |||||||
|         self._current_state = self._state_idle |         self._current_state = self._state_idle | ||||||
|  |  | ||||||
|         self._running = True |         self._running = True | ||||||
|  |         self._print_status_reporter = None | ||||||
|  |         self._print_temp_reporter = None | ||||||
|         self._printer_thread = threading.Thread( |         self._printer_thread = threading.Thread( | ||||||
|             target=self._printer_worker, |             target=self._printer_worker, | ||||||
|             name="octoprint.plugins.bambu_printer.printer_state", |             name="octoprint.plugins.bambu_printer.printer_state", | ||||||
| @@ -184,13 +186,14 @@ class BambuVirtualPrinter: | |||||||
|         self._telemetry.bedTargetTemp = temperatures.target_bed_temp |         self._telemetry.bedTargetTemp = temperatures.target_bed_temp | ||||||
|         self._telemetry.chamberTemp = temperatures.chamber_temp |         self._telemetry.chamberTemp = temperatures.chamber_temp | ||||||
|  |  | ||||||
|  |         self._log.debug(f"Received printer state update: {print_job_state}") | ||||||
|         if ( |         if ( | ||||||
|             print_job_state == "IDLE" |             print_job_state == "IDLE" | ||||||
|             or print_job_state == "FINISH" |             or print_job_state == "FINISH" | ||||||
|             or print_job_state == "FAILED" |             or print_job_state == "FAILED" | ||||||
|         ): |         ): | ||||||
|             self.change_state(self._state_idle) |             self.change_state(self._state_idle) | ||||||
|         elif print_job_state == "RUNNING": |         elif print_job_state == "RUNNING" or print_job_state == "PREPARE": | ||||||
|             self.change_state(self._state_printing) |             self.change_state(self._state_printing) | ||||||
|         elif print_job_state == "PAUSE": |         elif print_job_state == "PAUSE": | ||||||
|             self.change_state(self._state_paused) |             self.change_state(self._state_paused) | ||||||
| @@ -241,11 +244,7 @@ class BambuVirtualPrinter: | |||||||
|             device_type=self._settings.get(["device_type"]), |             device_type=self._settings.get(["device_type"]), | ||||||
|             serial=self._settings.get(["serial"]), |             serial=self._settings.get(["serial"]), | ||||||
|             host=self._settings.get(["host"]), |             host=self._settings.get(["host"]), | ||||||
|             username=( |             username=("bambuocto"), | ||||||
|                 "bblp" |  | ||||||
|                 if self._settings.get_boolean(["local_mqtt"]) |  | ||||||
|                 else self._settings.get(["username"]) |  | ||||||
|             ), |  | ||||||
|             access_code=self._settings.get(["access_code"]), |             access_code=self._settings.get(["access_code"]), | ||||||
|             local_mqtt=self._settings.get_boolean(["local_mqtt"]), |             local_mqtt=self._settings.get_boolean(["local_mqtt"]), | ||||||
|             region=self._settings.get(["region"]), |             region=self._settings.get(["region"]), | ||||||
| @@ -274,9 +273,9 @@ class BambuVirtualPrinter: | |||||||
|             self.lastN = 0 |             self.lastN = 0 | ||||||
|             self._running = False |             self._running = False | ||||||
|  |  | ||||||
|             if self._sdstatus_reporter is not None: |             if self._print_status_reporter is not None: | ||||||
|                 self._sdstatus_reporter.cancel() |                 self._print_status_reporter.cancel() | ||||||
|                 self._sdstatus_reporter = None |                 self._print_status_reporter = None | ||||||
|  |  | ||||||
|             if self._settings.get_boolean(["simulateReset"]): |             if self._settings.get_boolean(["simulateReset"]): | ||||||
|                 for item in self._settings.get(["resetLines"]): |                 for item in self._settings.get(["resetLines"]): | ||||||
| @@ -310,7 +309,16 @@ class BambuVirtualPrinter: | |||||||
|  |  | ||||||
|     def select_project_file(self, file_path: str) -> bool: |     def select_project_file(self, file_path: str) -> bool: | ||||||
|         self._log.debug(f"Select project file: {file_path}") |         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: |         if file_info is None: | ||||||
|             self._log.error(f"Cannot select not existing file: {file_path}") |             self._log.error(f"Cannot select not existing file: {file_path}") | ||||||
|             return False |             return False | ||||||
| @@ -328,7 +336,6 @@ 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_project_files() |  | ||||||
|         return self.select_project_file(filename) |         return self.select_project_file(filename) | ||||||
|  |  | ||||||
|     def _send_file_selected_message(self): |     def _send_file_selected_message(self): | ||||||
| @@ -355,38 +362,81 @@ class BambuVirtualPrinter: | |||||||
|         matchS = re.search(r"S([0-9]+)", data) |         matchS = re.search(r"S([0-9]+)", data) | ||||||
|         if matchS: |         if matchS: | ||||||
|             interval = int(matchS.group(1)) |             interval = int(matchS.group(1)) | ||||||
|             if self._sdstatus_reporter is not None: |  | ||||||
|                 self._sdstatus_reporter.cancel() |  | ||||||
|  |  | ||||||
|             if interval > 0: |             if interval > 0: | ||||||
|                 self._sdstatus_reporter = RepeatedTimer( |                 self.start_continuous_status_report(interval) | ||||||
|                     interval, self.report_print_job_status |                 return False | ||||||
|                 ) |  | ||||||
|                 self._sdstatus_reporter.start() |  | ||||||
|             else: |             else: | ||||||
|                 self._sdstatus_reporter = None |                 self.stop_continuous_status_report() | ||||||
|  |                 return False | ||||||
|  |  | ||||||
|         self.report_print_job_status() |         self.report_print_job_status() | ||||||
|         return True |         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") |     @gcode_executor.register("M30") | ||||||
|     def _delete_sd_file(self, data: str) -> bool: |     def _delete_project_file(self, data: str) -> bool: | ||||||
|         file_path = data.split(None, 1)[1].strip() |         file_path = data.split(maxsplit=1)[1].strip() | ||||||
|         self._list_project_files() |         file_info = self.project_files.get_file_data(file_path) | ||||||
|         self.file_system.delete_file(Path(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 |         return True | ||||||
|  |  | ||||||
|     @gcode_executor.register("M105") |     @gcode_executor.register("M105") | ||||||
|     def _report_temperatures(self, data: str) -> bool: |     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 |     # noinspection PyUnusedLocal | ||||||
|     @gcode_executor.register_no_data("M115") |     @gcode_executor.register_no_data("M115") | ||||||
|     def _report_firmware_info(self) -> bool: |     def _report_firmware_info(self) -> bool: | ||||||
|         self.sendIO("Bambu Printer Integration") |         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:EXTENDED_M20:1") | ||||||
|         self.sendIO("Cap:LFN_WRITE:1") |         self.sendIO("Cap:LFN_WRITE:1") | ||||||
|         self.sendIO("Cap:LFN_WRITE:1") |  | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
|     @gcode_executor.register("M117") |     @gcode_executor.register("M117") | ||||||
| @@ -418,21 +468,53 @@ class BambuVirtualPrinter: | |||||||
|     def _set_feedrate_percent(self, data: str) -> bool: |     def _set_feedrate_percent(self, data: str) -> bool: | ||||||
|         if self.bambu_client.connected: |         if self.bambu_client.connected: | ||||||
|             gcode_command = commands.SEND_GCODE_TEMPLATE |             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: |             def speed_fraction(speed_percent): | ||||||
|                 return True |                 return math.floor(10000 / speed_percent) / 100 | ||||||
|  |  | ||||||
|             speed_fraction = 100 / percent |             def acceleration_magnitude(speed_percent): | ||||||
|             acceleration = math.exp((speed_fraction - 1.0191) / -0.814) |                 return math.exp((speed_fraction(speed_percent) - 1.0191) / -0.8139) | ||||||
|             feed_rate = ( |  | ||||||
|                 2.1645 * (acceleration**3) |             def feed_rate(speed_percent): | ||||||
|                 - 5.3247 * (acceleration**2) |                 return 6.426e-5 * speed_percent ** 2 - 2.484e-3 * speed_percent + 0.654 | ||||||
|                 + 4.342 * acceleration |  | ||||||
|                 - 0.181 |             def linear_interpolate(x, x_points, y_points): | ||||||
|             ) |                 if x <= x_points[0]: return y_points[0] | ||||||
|             speed_level = 1.539 * (acceleration**2) - 0.7032 * acceleration + 4.0834 |                 if x >= x_points[-1]: return y_points[-1] | ||||||
|             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" |                 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) | ||||||
|  |  | ||||||
|             gcode_command["print"]["param"] = speed_command |             gcode_command["print"]["param"] = speed_command | ||||||
|             if self.bambu_client.publish(gcode_command): |             if self.bambu_client.publish(gcode_command): | ||||||
| @@ -464,8 +546,8 @@ class BambuVirtualPrinter: | |||||||
|         return True |         return True | ||||||
|  |  | ||||||
|     @gcode_executor.register("M20") |     @gcode_executor.register("M20") | ||||||
|     def _list_project_files(self, data: str = ""): |     def _update_project_file_list(self, data: str = ""): | ||||||
|         self._project_files_view.update() |         self._project_files_view.update()  # internally sends list to serial io | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
|     def _list_cached_project_files(self): |     def _list_cached_project_files(self): | ||||||
| @@ -475,10 +557,11 @@ class BambuVirtualPrinter: | |||||||
|         ): |         ): | ||||||
|             self.sendIO(item) |             self.sendIO(item) | ||||||
|         self.sendIO("End file list") |         self.sendIO("End file list") | ||||||
|  |         self.sendOk() | ||||||
|  |  | ||||||
|     @gcode_executor.register_no_data("M24") |     @gcode_executor.register_no_data("M24") | ||||||
|     def _start_print(self): |     def _start_resume_sd_print(self): | ||||||
|         self._current_state.start_resume_print() |         self._current_state.start_new_print() | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
|     @gcode_executor.register_no_data("M25") |     @gcode_executor.register_no_data("M25") | ||||||
| @@ -492,14 +575,31 @@ class BambuVirtualPrinter: | |||||||
|         return True |         return True | ||||||
|  |  | ||||||
|     def report_print_job_status(self): |     def report_print_job_status(self): | ||||||
|         print_job = self.current_print_job |         if self.current_print_job is not None: | ||||||
|         if print_job is not None: |             file_position = 1 if self.current_print_job.file_position == 0 else self.current_print_job.file_position | ||||||
|             self.sendIO( |             self.sendIO( | ||||||
|                 f"SD printing byte {print_job.file_position}/{print_job.file_info.size}" |                 f"SD printing byte {file_position}" | ||||||
|  |                 f"/{self.current_print_job.file_info.size}" | ||||||
|             ) |             ) | ||||||
|         else: |         else: | ||||||
|             self.sendIO("Not SD printing") |             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: |     def _create_temperature_message(self) -> str: | ||||||
|         template = "{heater}:{actual:.2f}/ {target:.2f}" |         template = "{heater}:{actual:.2f}/ {target:.2f}" | ||||||
|         temps = collections.OrderedDict() |         temps = collections.OrderedDict() | ||||||
|   | |||||||
| @@ -15,7 +15,9 @@ from octoprint_bambu_printer.printer.file_system.file_info import FileInfo | |||||||
| @dataclass | @dataclass | ||||||
| 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: 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 |     on_update: Callable[[], None] | None = None | ||||||
|  |  | ||||||
|     def __post_init__(self): |     def __post_init__(self): | ||||||
| @@ -25,7 +27,7 @@ class CachedFileView: | |||||||
|     def with_filter( |     def with_filter( | ||||||
|         self, folder: str, extensions: str | list[str] | None = None |         self, folder: str, extensions: str | list[str] | None = None | ||||||
|     ) -> "CachedFileView": |     ) -> "CachedFileView": | ||||||
|         self.folder_view.add((folder, extensions)) |         self.folder_view[(folder, extensions)] = None | ||||||
|         return self |         return self | ||||||
|  |  | ||||||
|     def list_all_views(self): |     def list_all_views(self): | ||||||
| @@ -33,7 +35,7 @@ class CachedFileView: | |||||||
|         result: list[FileInfo] = [] |         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.keys(): | ||||||
|                 result.extend(self.file_system.list_files(*filter, ftp, existing_files)) |                 result.extend(self.file_system.list_files(*filter, ftp, existing_files)) | ||||||
|         return result |         return result | ||||||
|  |  | ||||||
| @@ -44,8 +46,8 @@ class CachedFileView: | |||||||
|             self.on_update() |             self.on_update() | ||||||
|  |  | ||||||
|     def _update_file_list_cache(self, files: list[FileInfo]): |     def _update_file_list_cache(self, files: list[FileInfo]): | ||||||
|         self._file_alias_cache = {info.dosname: info.file_name for info in files} |         self._file_alias_cache = {info.dosname: info.path.as_posix() for info in files} | ||||||
|         self._file_data_cache = {info.file_name: info for info in files} |         self._file_data_cache = {info.path.as_posix(): info for info in files} | ||||||
|  |  | ||||||
|     def get_all_info(self): |     def get_all_info(self): | ||||||
|         self.update() |         self.update() | ||||||
| @@ -54,26 +56,39 @@ class CachedFileView: | |||||||
|     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()) | ||||||
|  |  | ||||||
|     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 == "": |         if file_stem == "": | ||||||
|             return None |             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: |         if file_data is None: | ||||||
|             self.update() |             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 |         return file_data | ||||||
|  |  | ||||||
|     def get_cached_file_data(self, file_name: str) -> FileInfo | None: |     def _get_file_by_stem_cached(self, file_stem: str, allowed_suffixes: list[str]): | ||||||
|         file_name = Path(file_name).name |         for file_path_str in list(self._file_data_cache.keys()) + list(self._file_alias_cache.keys()): | ||||||
|         file_name = self._file_alias_cache.get(file_name, file_name) |             file_path = Path(file_path_str) | ||||||
|         return self._file_data_cache.get(file_name, None) |             if file_stem == file_path.with_suffix("").stem and all( | ||||||
|  |                 suffix in allowed_suffixes for suffix in file_path.suffixes | ||||||
|     def _get_file_by_suffix_cached(self, file_stem: str, allowed_suffixes: list[str]): |             ): | ||||||
|         for suffix in allowed_suffixes: |                 return self.get_file_data_cached(file_path) | ||||||
|             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 |         return None | ||||||
|   | |||||||
| @@ -117,7 +117,7 @@ class IoTFTPSConnection: | |||||||
|                         # But since we operate in prot p mode |                         # But since we operate in prot p mode | ||||||
|                         # we can close the connection always. |                         # we can close the connection always. | ||||||
|                         # This is cursed but it works. |                         # This is cursed but it works. | ||||||
|                         if "vsFTPd" in self.welcome: |                         if "vsFTPd" in self.ftps_session.welcome: | ||||||
|                             conn.unwrap() |                             conn.unwrap() | ||||||
|                         else: |                         else: | ||||||
|                             conn.shutdown(socket.SHUT_RDWR) |                             conn.shutdown(socket.SHUT_RDWR) | ||||||
|   | |||||||
| @@ -7,8 +7,6 @@ import logging.handlers | |||||||
|  |  | ||||||
| from octoprint.util import get_dos_filename | 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 .ftps_client import IoTFTPSClient, IoTFTPSConnection | ||||||
| from .file_info import FileInfo | from .file_info import FileInfo | ||||||
|  |  | ||||||
| @@ -23,7 +21,7 @@ class RemoteSDCardFileList: | |||||||
|     def delete_file(self, file_path: Path) -> None: |     def delete_file(self, file_path: Path) -> None: | ||||||
|         try: |         try: | ||||||
|             with self.get_ftps_client() as ftp: |             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") |                     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") | ||||||
|   | |||||||
| @@ -1,26 +1,33 @@ | |||||||
| from __future__ import annotations | 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 | from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState | ||||||
|  |  | ||||||
|  |  | ||||||
| class IdleState(APrinterState): | class IdleState(APrinterState): | ||||||
|  |  | ||||||
|     def start_resume_print(self): |     def start_new_print(self): | ||||||
|         selected_file = self._printer.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 | ||||||
|  |  | ||||||
|         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): |         if self._printer.bambu_client.publish(print_command): | ||||||
|             self._log.info(f"Started print for {selected_file.file_name}") |             self._log.info(f"Started print for {selected_file.file_name}") | ||||||
|             self._printer.change_state(self._printer._state_printing) |  | ||||||
|         else: |         else: | ||||||
|             self._log.warn(f"Failed to start print for {selected_file.file_name}") |             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_command = { | ||||||
|             "print": { |             "print": { | ||||||
|                 "sequence_id": 0, |                 "sequence_id": 0, | ||||||
| @@ -31,14 +38,9 @@ class IdleState(APrinterState): | |||||||
|                 "project_id": "0", |                 "project_id": "0", | ||||||
|                 "subtask_id": "0", |                 "subtask_id": "0", | ||||||
|                 "task_id": "0", |                 "task_id": "0", | ||||||
|                 "subtask_name": f"{selected_file}", |                 "subtask_name": selected_file.file_name, | ||||||
|                 "file": f"{selected_file}", |                 "url": f"{filesystem_root}{selected_file.path.as_posix()}", | ||||||
|                 "url": ( |                 "bed_type": "auto", | ||||||
|                     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"]), |                 "timelapse": self._printer._settings.get_boolean(["timelapse"]), | ||||||
|                 "bed_leveling": self._printer._settings.get_boolean(["bed_leveling"]), |                 "bed_leveling": self._printer._settings.get_boolean(["bed_leveling"]), | ||||||
|                 "flow_cali": self._printer._settings.get_boolean(["flow_cali"]), |                 "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"]), |                 "layer_inspect": self._printer._settings.get_boolean(["layer_inspect"]), | ||||||
|                 "use_ams": self._printer._settings.get_boolean(["use_ams"]), |                 "use_ams": self._printer._settings.get_boolean(["use_ams"]), | ||||||
|  |                 "ams_mapping": "", | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,35 +19,33 @@ class PausedState(APrinterState): | |||||||
|     def __init__(self, printer: BambuVirtualPrinter) -> None: |     def __init__(self, printer: BambuVirtualPrinter) -> None: | ||||||
|         super().__init__(printer) |         super().__init__(printer) | ||||||
|         self._pausedLock = threading.Event() |         self._pausedLock = threading.Event() | ||||||
|  |         self._paused_repeated_report = None | ||||||
|  |  | ||||||
|     def init(self): |     def init(self): | ||||||
|         if not self._pausedLock.is_set(): |         if not self._pausedLock.is_set(): | ||||||
|             self._pausedLock.set() |             self._pausedLock.set() | ||||||
|  |  | ||||||
|         self._printer.sendIO("// action:paused") |         self._printer.sendIO("// action:paused") | ||||||
|         self._sendPaused() |         self._printer.start_continuous_status_report(3) | ||||||
|  |  | ||||||
|     def finalize(self): |     def finalize(self): | ||||||
|         if self._pausedLock.is_set(): |         if self._pausedLock.is_set(): | ||||||
|             self._pausedLock.clear() |             self._pausedLock.clear() | ||||||
|  |             if self._paused_repeated_report is not None: | ||||||
|  |                 self._paused_repeated_report.join() | ||||||
|  |                 self._paused_repeated_report = None | ||||||
|  |  | ||||||
|     def _sendPaused(self): |     def start_new_print(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.connected: | ||||||
|             if self._printer.bambu_client.publish(pybambu.commands.RESUME): |             if self._printer.bambu_client.publish(pybambu.commands.RESUME): | ||||||
|                 self._log.info("print resumed") |                 self._log.info("print resumed") | ||||||
|                 self._printer.change_state(self._printer._state_printing) |  | ||||||
|             else: |             else: | ||||||
|                 self._log.info("print resume failed") |                 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") | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ class PrintingState(APrinterState): | |||||||
|  |  | ||||||
|     def __init__(self, printer: BambuVirtualPrinter) -> None: |     def __init__(self, printer: BambuVirtualPrinter) -> None: | ||||||
|         super().__init__(printer) |         super().__init__(printer) | ||||||
|  |         self._current_print_job = None | ||||||
|         self._is_printing = False |         self._is_printing = False | ||||||
|         self._sd_printing_thread = None |         self._sd_printing_thread = None | ||||||
|  |  | ||||||
| @@ -36,6 +37,7 @@ class PrintingState(APrinterState): | |||||||
|             self._is_printing = False |             self._is_printing = False | ||||||
|             self._sd_printing_thread.join() |             self._sd_printing_thread.join() | ||||||
|             self._sd_printing_thread = None |             self._sd_printing_thread = None | ||||||
|  |         self._printer.current_print_job = None | ||||||
|  |  | ||||||
|     def _start_worker_thread(self): |     def _start_worker_thread(self): | ||||||
|         if self._sd_printing_thread is None: |         if self._sd_printing_thread is None: | ||||||
| @@ -53,34 +55,33 @@ class PrintingState(APrinterState): | |||||||
|             self._printer.report_print_job_status() |             self._printer.report_print_job_status() | ||||||
|             time.sleep(3) |             time.sleep(3) | ||||||
|  |  | ||||||
|         if self._printer.current_print_job is None: |         self.update_print_job_info() | ||||||
|  |         if ( | ||||||
|             self._log.warn("Printing state was triggered with empty print job") |             self._printer.current_print_job is not None | ||||||
|             return |             and self._printer.current_print_job.progress >= 100 | ||||||
|  |         ): | ||||||
|         if self._printer.current_print_job.progress >= 100: |             self._printer.finalize_print_job() | ||||||
|             self._finish_print() |  | ||||||
|  |  | ||||||
|     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.project_files.get_file_by_suffix( |         project_file_info = self._printer.project_files.get_file_by_stem( | ||||||
|             task_name, [".3mf", ".gcode.3mf"] |             task_name, [".gcode", ".3mf"] | ||||||
|         ) |         ) | ||||||
|         if project_file_info is None: |         if project_file_info is None: | ||||||
|             self._log.debug(f"No 3mf file found for {print_job_info}") |             self._log.debug(f"No 3mf file found for {print_job_info}") | ||||||
|             self._current_print_job = None |             self._current_print_job = None | ||||||
|  |             self._printer.change_state(self._printer._state_idle) | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         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) |         self._printer.select_project_file(project_file_info.path.as_posix()) | ||||||
|  |  | ||||||
|     def pause_print(self): |     def pause_print(self): | ||||||
|         if self._printer.bambu_client.connected: |         if self._printer.bambu_client.connected: | ||||||
|             if self._printer.bambu_client.publish(pybambu.commands.PAUSE): |             if self._printer.bambu_client.publish(pybambu.commands.PAUSE): | ||||||
|                 self._log.info("print paused") |                 self._log.info("print paused") | ||||||
|                 self._printer.change_state(self._printer._state_paused) |  | ||||||
|             else: |             else: | ||||||
|                 self._log.info("print pause failed") |                 self._log.info("print pause failed") | ||||||
|  |  | ||||||
| @@ -88,17 +89,6 @@ class PrintingState(APrinterState): | |||||||
|         if self._printer.bambu_client.connected: |         if self._printer.bambu_client.connected: | ||||||
|             if self._printer.bambu_client.publish(pybambu.commands.STOP): |             if self._printer.bambu_client.publish(pybambu.commands.STOP): | ||||||
|                 self._log.info("print cancelled") |                 self._log.info("print cancelled") | ||||||
|                 self._finish_print() |                 self._printer.finalize_print_job() | ||||||
|                 self._printer.change_state(self._printer._state_idle) |  | ||||||
|             else: |             else: | ||||||
|                 self._log.info("print cancel failed") |                 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) |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								setup.py
									
									
									
									
									
								
							| @@ -14,20 +14,20 @@ plugin_package = "octoprint_bambu_printer" | |||||||
| plugin_name = "OctoPrint-BambuPrinter" | plugin_name = "OctoPrint-BambuPrinter" | ||||||
|  |  | ||||||
| # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module | # 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 = "1.0.0" | ||||||
|  |  | ||||||
| # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin | # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin | ||||||
| # module | # module | ||||||
| plugin_description = """Connects OctoPrint to BambuLabs printers.""" | plugin_description = """Connects OctoPrint to BambuLabs printers.""" | ||||||
|  |  | ||||||
| # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module | # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module | ||||||
| plugin_author = "jneilliii" | plugin_author = "ManuelW" | ||||||
|  |  | ||||||
| # The plugin's author's mail address. | # The plugin's author's mail address. | ||||||
| plugin_author_email = "jneilliii+github@gmail.com" | plugin_author_email = "manuelw@example.com" | ||||||
|  |  | ||||||
| # The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module | # The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module | ||||||
| plugin_url = "https://github.com/jneilliii/OctoPrint-BambuPrinter" | plugin_url = "https://gitlab.fire-devils.org/3D-Druck/OctoPrint-BambuPrinter" | ||||||
|  |  | ||||||
| # The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module | # The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module | ||||||
| plugin_license = "AGPLv3" | plugin_license = "AGPLv3" | ||||||
|   | |||||||
| @@ -2,8 +2,9 @@ from __future__ import annotations | |||||||
| from datetime import datetime, timezone | from datetime import datetime, timezone | ||||||
| import logging | import logging | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | import sys | ||||||
| from typing import Any | 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 | from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView | ||||||
| import pybambu | import pybambu | ||||||
| @@ -29,7 +30,9 @@ def output_test_folder(output_folder: Path): | |||||||
|  |  | ||||||
| @fixture | @fixture | ||||||
| def log_test(): | def log_test(): | ||||||
|     return logging.getLogger("gcode_unittest") |     log = logging.getLogger("gcode_unittest") | ||||||
|  |     log.setLevel(logging.DEBUG) | ||||||
|  |     return log | ||||||
|  |  | ||||||
|  |  | ||||||
| class DictGetter: | class DictGetter: | ||||||
| @@ -89,7 +92,11 @@ def project_files_info_ftp(): | |||||||
| def cache_files_info_ftp(): | def cache_files_info_ftp(): | ||||||
|     return { |     return { | ||||||
|         "cache/print.3mf": (1200, _ftp_date_format(datetime(2024, 5, 7))), |         "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[0] == b"Begin file list" | ||||||
|     assert result[1].endswith(b'"print.3mf"') |     assert result[1].endswith(b'"print.3mf"') | ||||||
|     assert result[2].endswith(b'"print2.3mf"') |     assert result[2].endswith(b'"print2.3mf"') | ||||||
|     assert result[3] == b"End file list" |     assert result[3].endswith(b'"print.3mf"') | ||||||
|     assert result[4] == b"ok" |     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): | 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): | def test_cannot_start_print_without_file(printer: BambuVirtualPrinter): | ||||||
|     printer.write(b"M24\n") |     printer.write(b"M24\n") | ||||||
|     printer.flush() |     printer.flush() | ||||||
| @@ -278,9 +349,13 @@ def test_print_started_with_selected_file(printer: BambuVirtualPrinter, print_jo | |||||||
|  |  | ||||||
|     printer.write(b"M24\n") |     printer.write(b"M24\n") | ||||||
|     printer.flush() |     printer.flush() | ||||||
|  |  | ||||||
|     result = printer.readlines() |     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) |     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"M23 print.3mf\n") | ||||||
|     printer.write(b"M24\n") |     printer.write(b"M24\n") | ||||||
|     printer.flush() |     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 isinstance(printer.current_state, PrintingState) | ||||||
|  |  | ||||||
|     bambu_client_mock.publish.return_value = True |     printer.write(b"M25\n")  # pausing the print | ||||||
|     printer.write(b"M25\n")  # GCode for pausing the print |  | ||||||
|     printer.flush() |     printer.flush() | ||||||
|     result = printer.readlines() |     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) |     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): | def test_events_update_printer_state(printer: BambuVirtualPrinter, print_job_mock): | ||||||
|  |     print_job_mock.subtask_name = "print.3mf" | ||||||
|     print_job_mock.gcode_state = "RUNNING" |     print_job_mock.gcode_state = "RUNNING" | ||||||
|     printer.new_update("event_printer_data_update") |     printer.new_update("event_printer_data_update") | ||||||
|     printer.flush() |     printer.flush() | ||||||
| @@ -338,10 +421,45 @@ def test_printer_info_check(printer: BambuVirtualPrinter): | |||||||
|     assert isinstance(printer.current_state, IdleState) |     assert isinstance(printer.current_state, IdleState) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_abort_print(printer: BambuVirtualPrinter): | def test_abort_print_during_printing(printer: BambuVirtualPrinter, print_job_mock): | ||||||
|     printer.write(b"M26\n")  # GCode for aborting the print |     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.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() |     result = printer.readlines() | ||||||
|     assert result[-1] == b"ok" |     assert result[-1] == b"ok" | ||||||
|     assert isinstance(printer.current_state, IdleState) |     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.write(b"M23 print.3mf\nM24\n") | ||||||
|     printer.flush() |     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 isinstance(printer.current_state, PrintingState) | ||||||
|     assert printer.current_print_job is not None |     assert printer.current_print_job is not None | ||||||
|     assert printer.current_print_job.file_info.file_name == "print.3mf" |     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.write(b"M23 print.3mf\nM24\n") | ||||||
|     printer.flush() |     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 isinstance(printer.current_state, PrintingState) | ||||||
|     assert printer.current_print_job is not None |     assert printer.current_print_job is not None | ||||||
|     assert printer.current_print_job.file_info.file_name == "print.3mf" |     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.current_print_job is None | ||||||
|     assert printer.selected_file is not None |     assert printer.selected_file is not None | ||||||
|     assert printer.selected_file.file_name == "print2.3mf" |     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" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user