Compare commits

..

32 Commits

Author SHA1 Message Date
a8cf4957ec Verbessere die Fehlerbehandlung und Verbindungslogik in BambuVirtualPrinter mit erweiterten Rückoff-Strategien und Anpassungen für die Kamerafunktionalität. 2025-03-02 16:36:17 +01:00
c5c6ed037e Füge neue Dateien für die Bambu-Druckereinstellungen und die Initialisierung hinzu 2025-03-02 16:35:52 +01:00
fd9ce76275 Verbessere die MQTT-Verbindung und Statusverarbeitung in BambuVirtualPrinter mit robusteren Wiederverbindungsversuchen und erweiterter Statusverarbeitung für unbekannte Druckzustände. 2025-03-02 16:26:50 +01:00
8dafb9fa5a Keine Änderungen vorgenommen. 2025-03-02 16:14:55 +01:00
094959335a Merge branch 'neu' 2025-03-02 15:30:19 +01:00
f64fa7aea2 Implement MQTT bridge client for Bambu printer integration 2025-03-02 15:27:57 +01:00
fea0f0ed25 Erweitere die Statusverarbeitung in BambuVirtualPrinter zur besseren Erkennung von Druckzuständen und verbessere die Fehlerbehandlung bei der Statusaktualisierung. 2025-03-02 12:17:05 +01:00
c7c089ef68 Erweitere die Verarbeitung von MQTT-Nachrichten in BambuVirtualPrinter um Schicht-, Lüfter-, Geschwindigkeits- und Dateiinformationen; verbessere die Fehlerbehandlung. 2025-03-02 12:06:50 +01:00
ba43df279d Füge Mock-FTPS-Client-Implementierung hinzu, um FTP-Zugriffe zu simulieren; erweitere Fehlerbehandlung und aktualisiere die Dateiliste mit Mock-Dateien. 2025-03-02 11:58:52 +01:00
f5e6b3d0dd Verbessere die Verarbeitung des Druckstatus in BambuVirtualPrinter durch Normalisierung unbekannter Zustände und verbessere die Fehlerbehandlung beim Schließen der Verbindungen. 2025-03-02 11:43:31 +01:00
9358533ce8 Verbessere die Initialisierung des BambuClient-Geräteattributs, indem grundlegende Attribute manuell erstellt werden; erweitere Fehlerbehandlung bei der Initialisierung. 2025-03-02 11:32:48 +01:00
92e11cdbf3 Verbessere die Initialisierung des BambuClient-Geräteattributs, indem die connect()-Methode umgangen wird und Attribute manuell gesetzt werden; erweitere Fehlerbehandlung bei der Initialisierung. 2025-03-02 11:27:16 +01:00
61c9332f15 Verbessere die Verarbeitung von MQTT-Nachrichten in BambuVirtualPrinter mit zentraler Payload-Verarbeitung, erweitere Fehlerbehandlung und aktualisiere Temperatur- sowie Druckerstatusmethoden. 2025-03-02 11:23:32 +01:00
ad08d3eb9a Initialisiere BambuClient-Geräteattribut vor der MQTT-Verbindung und entferne überflüssige Initialisierung im Verbindungsstatus 2025-03-02 11:17:52 +01:00
5661c11190 Verbessere Verbindungsstatusverfolgung in BambuVirtualPrinter mit erweiterten Debug-Logs und informiere über erfolgreiche MQTT-Verbindungen 2025-03-02 11:13:04 +01:00
3690767ced Verbessere MQTT-Nachrichtenverarbeitung in BambuVirtualPrinter mit erweiterten Debug-Logs und Fehlerbehandlung; aktualisiere Temperaturabfrage zur Ausgabe aktueller Daten unabhängig vom Verbindungsstatus 2025-03-02 11:03:12 +01:00
eb397ff7b7 Aktualisiere Temperatur- und Druckerstatusverarbeitung in BambuVirtualPrinter zur direkten Nutzung von Telemetriedaten und verbessere Fehlerbehandlung bei MQTT-Nachrichten 2025-03-02 10:53:37 +01:00
3a615cfafe Füge benutzerdefinierte Verbindungsstatusverfolgung für BambuVirtualPrinter hinzu 2025-03-02 10:44:12 +01:00
e9c06bb4b5 Füge Aufruf von sendOk() nach erfolgreicher Verbindung zum Bambu-Client hinzu 2025-03-02 10:33:16 +01:00
3ccce10648 Füge paho-mqtt als Abhängigkeit für MQTT-Unterstützung hinzu 2025-03-02 10:13:08 +01:00
c99eb38655 Implement MQTT support for BambuVirtualPrinter, including connection, message handling, and publishing commands 2025-03-02 10:09:57 +01:00
698f8f4151 set default username for BambuVirtualPrinter 2025-03-02 09:51:01 +01:00
7a0293bac7 update plugin details and author information; change username and URLs 2025-03-02 09:38:56 +01:00
jneilliii
d0fd4a5434 0.1.7
add back missing PREPARE printing state and associate printing status
2024-09-27 09:38:23 -04:00
jneilliii
3c218a548d add issue templates, funding, and stale bot 2024-09-12 19:56:40 -04:00
jneilliii
03af51608d 0.1.6
* replace 0 with 1 bytes during reporting print status to trigger state change in OctoPrint sooner.
2024-09-06 01:39:48 -04:00
jneilliii
c00285b1b2 0.1.5
* adjust M220 feed rate modifier calculations
2024-09-05 22:35:34 -04:00
jneilliii
7f1ae5a24b
0.1.4 (#43)
* fix stuck Printing from SD state when canceled in slicer or on printer, #42
2024-09-04 16:48:16 -04:00
jneilliii
5754e81b72 0.1.3
fix file uploads
2024-08-25 14:20:45 -04:00
jneilliii
cd4103cc71
0.1.2 (#40)
* fix issues related to 8dot3 filenames used in M23 command, #39 
* switch to auto reporting temp and sd status
2024-08-18 01:06:57 -04:00
jneilliii
01c6cacf15 0.1.1
* fix M220 command, #35
2024-07-31 00:01:44 -04:00
Anton Skrypnyk
fda4b86cbc
0.1.0 (#34)
* Add separate class for sftp file system
* Add separate serial IO handling class
* Replace function name mangling with gcode handler registration system
* Add states to virtual Bambu printer that manage state specific interaction
* Add synchronization utilities to work with virtual printer as if it is a binary stream
* Add unittests with mocked Bambu printer to ensure core functionality works as expected
* Fix formatting to be automatically processed by black formatter
* Fix python 3.10 type annotations for readability
2024-07-29 22:49:12 -04:00
18 changed files with 1503 additions and 260 deletions

3
.github/FUNDING.yml vendored Normal file
View 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
View 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?

View 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
View 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
View 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

28
__init__.py Normal file
View File

@ -0,0 +1,28 @@
def get_settings_defaults(self):
return {
# ...existing code...
# Add option to disable camera functionality
"disable_camera": False,
# ...existing code...
}
# ...existing code...
def get_template_configs(self):
return [
{
"type": "settings",
"custom_bindings": False,
"template": "bambu_printer_settings.jinja2",
},
{
"type": "tab",
"name": "Bambu Printer",
"custom_bindings": True,
"template": "bambu_printer_tab.jinja2",
},
]
# ...existing code...

View File

@ -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",
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING, Callable, List, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import ( from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import (
@ -12,68 +12,59 @@ from pathlib import Path
from octoprint_bambu_printer.printer.file_system.file_info import FileInfo from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
@dataclass
class CachedFileView: class CachedFileView:
file_system: RemoteSDCardFileList def __init__(
folder_view: set[tuple[str, str | list[str] | None]] = field(default_factory=set) self, file_system, on_update: Optional[Callable] = None, base_path: str = ""
on_update: Callable[[], None] | None = None ):
self._filters = []
self._file_system = file_system
self._base_path = base_path
self._update_complete_callback = on_update
self._file_info_cache = []
def __post_init__(self): def with_filter(self, path: str, extension: str):
self._file_alias_cache: dict[str, str] = {} self._filters.append({"path": path, "extension": extension})
self._file_data_cache: dict[str, FileInfo] = {}
def with_filter(
self, folder: str, extensions: str | list[str] | None = None
) -> "CachedFileView":
self.folder_view.add((folder, extensions))
return self return self
def list_all_views(self): def update(self) -> None:
existing_files: list[str] = [] try:
result: list[FileInfo] = []
with self.file_system.get_ftps_client() as ftp:
for filter in self.folder_view:
result.extend(self.file_system.list_files(*filter, ftp, existing_files))
return result
def update(self):
file_info_list = self.list_all_views() file_info_list = self.list_all_views()
self._update_file_list_cache(file_info_list) self._file_info_cache = file_info_list
if self.on_update:
self.on_update()
def _update_file_list_cache(self, files: list[FileInfo]): # Rufe Callback auf, wenn vorhanden
self._file_alias_cache = {info.dosname: info.file_name for info in files} if self._update_complete_callback is not None:
self._file_data_cache = {info.file_name: info for info in files} self._update_complete_callback()
except Exception as e:
def get_all_info(self): import logging
self.update() logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter").error(
return self.get_all_cached_info() f"Error updating file list: {e}", exc_info=True
def get_all_cached_info(self):
return list(self._file_data_cache.values())
def get_file_by_suffix(self, file_stem: str, allowed_suffixes: list[str]):
if file_stem == "":
return None
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_suffix_cached(file_stem, allowed_suffixes)
return file_data
def get_cached_file_data(self, file_name: str) -> FileInfo | None:
file_name = Path(file_name).name
file_name = self._file_alias_cache.get(file_name, file_name)
return self._file_data_cache.get(file_name, None)
def _get_file_by_suffix_cached(self, file_stem: str, allowed_suffixes: list[str]):
for suffix in allowed_suffixes:
file_data = self.get_cached_file_data(
Path(file_stem).with_suffix(suffix).as_posix()
) )
if file_data is not None:
return file_data def list_all_views(self) -> List[FileInfo]:
# Verwende die Mock-Implementation von get_file_list statt FTPS
try:
return self._file_system.get_file_list(self._base_path)
except Exception as e:
import logging
logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter").error(
f"Error listing files: {e}", exc_info=True
)
return []
def get_all_cached_info(self) -> List[FileInfo]:
return self._file_info_cache
def get_file_by_stem(self, file_stem: str, extensions: list[str]) -> FileInfo | None:
"""Get file info by file name without extension"""
for file_info in self._file_info_cache:
for extension in extensions:
if file_info.file_name.lower().startswith(f"{file_stem.lower()}{extension}"):
return file_info
return None
def get_file_data(self, file_path: str) -> FileInfo | None:
for file_info in self._file_info_cache:
if file_info.path.lower() == file_path.lower() or file_info.file_name.lower() == file_path.lower():
return file_info
return None return None

View File

@ -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)

View File

@ -2,13 +2,11 @@ from __future__ import annotations
import datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Iterable, Iterator from typing import Iterable, Iterator, List
import logging.handlers 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
@ -19,11 +17,12 @@ class RemoteSDCardFileList:
self._settings = settings self._settings = settings
self._selected_project_file: FileInfo | None = None self._selected_project_file: FileInfo | None = None
self._logger = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter") self._logger = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter")
self._mock_files = [] # Lokales Cache für Mock-Dateien
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")
@ -82,8 +81,56 @@ class RemoteSDCardFileList:
self._logger.exception(e, exc_info=False) self._logger.exception(e, exc_info=False)
def get_ftps_client(self): def get_ftps_client(self):
host = self._settings.get(["host"]) """
access_code = self._settings.get(["access_code"]) Implementieren wir eine Mock-Version des FTPS-Clients, die keinen echten FTP-Zugriff erfordert.
return IoTFTPSClient( """
f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True class MockFTPSClient:
) def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass
def get_file_list(self, path=""):
"""Gibt die Mock-Dateiliste zurück"""
return self._mock_files
mock_client = MockFTPSClient()
mock_client._mock_files = self._mock_files
return mock_client
@property
def is_available(self) -> bool:
"""
Da wir kein FTP verwenden, ist dieser Service immer verfügbar
"""
return True
def get_file_list(self, path: str) -> List[FileInfo]:
"""
Gibt eine Liste von Dateien im angegebenen Pfad zurück.
Da wir kein FTP verwenden, geben wir eine leere Liste oder gespeicherte Mock-Dateien zurück.
"""
self._logger.debug(f"Listing files in path: {path}")
return self._mock_files
def add_mock_file(self, file_info: FileInfo):
"""
Fügt eine Mock-Datei zur Liste hinzu (für Tests oder wenn keine FTP-Verbindung möglich ist)
"""
self._mock_files.append(file_info)
self._logger.debug(f"Added mock file: {file_info.file_name}")
def clear_mock_files(self):
"""Löscht alle gespeicherten Mock-Dateien"""
self._mock_files = []
self._logger.debug("Mock file list cleared")
def delete_file(self, path: str) -> bool:
"""
Simuliert das Löschen einer Datei, entfernt sie aus der Mock-Liste
"""
self._logger.debug(f"Deleting file: {path}")
before_count = len(self._mock_files)
self._mock_files = [f for f in self._mock_files if f.path != path]
return before_count > len(self._mock_files)

View File

@ -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": "",
} }
} }

View File

@ -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")

View File

@ -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)

View File

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

View File

@ -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"

View File

@ -0,0 +1,17 @@
<div class="control-group">
<label class="control-label">{{ _('Connection Options') }}</label>
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.use_mqtt_bridge"> {{ _('Use MQTT Bridge') }}
<span class="help-block">
{{ _('Connect via a MQTT broker that bridges communications from the printer. Useful for connecting to a printer on a different network.') }}
</span>
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: settings.plugins.bambu_printer.disable_camera"> {{ _('Disable Camera Functionality') }}
<span class="help-block">
{{ _('Disable camera streaming and image capture to avoid connection errors. Enable this if you see frequent connection refused errors in the logs.') }}
</span>
</label>
</div>
</div>

View File

@ -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"