diff --git a/moonraker/components/file_manager/metadata.py b/moonraker/components/file_manager/metadata.py index 63c909b..4970cd0 100644 --- a/moonraker/components/file_manager/metadata.py +++ b/moonraker/components/file_manager/metadata.py @@ -33,7 +33,7 @@ if TYPE_CHECKING: pass UFP_MODEL_PATH = "/3D/model.gcode" -UFP_THUMB_PATH = "/Metadata/thumbnail.png" +UFP_THUMB_PATH = "/Metadata/thumbnail.jpg" def log_to_stderr(msg: str) -> None: sys.stderr.write(f"{msg}\n") @@ -217,11 +217,18 @@ class BaseSlicer(object): return None def parse_thumbnails(self) -> Optional[List[Dict[str, Any]]]: + is_jpg = False for data in [self.header_data, self.footer_data]: thumb_matches: List[str] = re.findall( r"; thumbnail begin[;/\+=\w\s]+?; thumbnail end", data) if thumb_matches: break + else: + thumb_matches: List[str] = re.findall( + r"; thumbnail_JPG begin[;/\+=\w\s]+?; thumbnail_JPG end", data) + if thumb_matches: + is_jpg = True + break else: return None thumb_dir = os.path.join(os.path.dirname(self.path), ".thumbs") @@ -233,7 +240,7 @@ class BaseSlicer(object): return None thumb_base = os.path.splitext(os.path.basename(self.path))[0] parsed_matches: List[Dict[str, Any]] = [] - has_miniature: bool = False + #has_miniature: bool = False for match in thumb_matches: lines = re.split(r"\r?\n", match.replace('; ', '')) info = _regex_find_ints(r".*", lines[0]) @@ -248,7 +255,10 @@ class BaseSlicer(object): f"MetadataError: Thumbnail Size Mismatch: " f"detected {info[2]}, actual {len(data)}") continue - thumb_name = f"{thumb_base}-{info[0]}x{info[1]}.png" + if not is_jpg: + thumb_name = f"{thumb_base}-{info[0]}x{info[1]}.png" + else: + thumb_name = f"{thumb_base}-{info[0]}x{info[1]}.jpg" thumb_path = os.path.join(thumb_dir, thumb_name) rel_thumb_path = os.path.join(".thumbs", thumb_name) with open(thumb_path, "wb") as f: @@ -257,33 +267,71 @@ class BaseSlicer(object): 'width': info[0], 'height': info[1], 'size': os.path.getsize(thumb_path), 'relative_path': rel_thumb_path}) - if info[0] == 32 and info[1] == 32: - has_miniature = True - if len(parsed_matches) > 0 and not has_miniature: - # find the largest thumb index - largest_match = parsed_matches[0] - for item in parsed_matches: - if item['size'] > largest_match['size']: - largest_match = item - # Create miniature thumbnail if one does not exist - thumb_full_name = largest_match['relative_path'].split("/")[-1] - thumb_path = os.path.join(thumb_dir, f"{thumb_full_name}") - rel_path_small = os.path.join(".thumbs", f"{thumb_base}-32x32.png") - thumb_path_small = os.path.join( - thumb_dir, f"{thumb_base}-32x32.png") - # read file - try: - with Image.open(thumb_path) as im: - # Create 32x32 thumbnail - im.thumbnail((32, 32)) - im.save(thumb_path_small, format="PNG") - parsed_matches.insert(0, { - 'width': im.width, 'height': im.height, - 'size': os.path.getsize(thumb_path_small), - 'relative_path': rel_path_small - }) - except Exception as e: - log_to_stderr(str(e)) + # find the smallest thumb index + smallest_match = parsed_matches[0] + max_size = min_size = smallest_match['size'] + for item in parsed_matches: + if item['size'] < smallest_match['size']: + smallest_match = item + if item["size"] < min_size: + min_size = item["size"] + if item["size"] > max_size: + max_size = item["size"] + # Create thumbnail for screen + thumb_full_name = smallest_match['relative_path'].split("/")[-1] + thumb_path = os.path.join(thumb_dir, f"{thumb_full_name}") + thumb_QD_full_name = f"{thumb_base}-{smallest_match['width']}x{smallest_match['height']}_QD.jpg" + thumb_QD_path = os.path.join(thumb_dir, f"{thumb_QD_full_name}") + rel_path_QD = os.path.join(".thumbs", thumb_QD_full_name) + try: + with Image.open(thumb_path) as img: + img = img.convert("RGB") + img = img.resize((smallest_match['width'], smallest_match['height'])) + img = img.rotate(90, expand=True) + img.save(thumb_QD_path, "JPEG", quality=90) + except Exception as e: + log_to_stderr(f"convert failed: {e}") + parsed_matches.append({ + 'width': smallest_match['width'], 'height': smallest_match['height'], + 'size': (max_size + min_size) // 2, + 'relative_path': rel_path_QD}) + # if info[0] == 32 and info[1] == 32: + # has_miniature = True + # if len(parsed_matches) > 0 and not has_miniature: + # # find the largest thumb index + # largest_match = parsed_matches[0] + # for item in parsed_matches: + # if item['size'] > largest_match['size']: + # largest_match = item + # # Create miniature thumbnail if one does not exist + # thumb_full_name = largest_match['relative_path'].split("/")[-1] + # thumb_path = os.path.join(thumb_dir, f"{thumb_full_name}") + # rel_path_small = os.path.join(".thumbs", f"{thumb_base}-32x32.jpg") + # thumb_path_small = os.path.join( + # thumb_dir, f"{thumb_base}-32x32.jpg") + # rel_path_small_png = os.path.join(".thumbs", f"{thumb_base}-32x32.png") + # thumb_path_small_png = os.path.join( + # thumb_dir, f"{thumb_base}-32x32.png") + # # read file + # try: + # with Image.open(thumb_path) as im: + # # Create 32x32 thumbnail + # im.thumbnail((32, 32)) + # im.save(thumb_path_small_png) + # im = im.convert('RGB') + # im.save(thumb_path_small) + # parsed_matches.insert(0, { + # 'width': im.width, 'height': im.height, + # 'size': os.path.getsize(thumb_path_small), + # 'relative_path': rel_path_small + # }) + # parsed_matches.insert(0, { + # 'width': im.width, 'height': im.height, + # 'size': os.path.getsize(thumb_path_small_png), + # 'relative_path': rel_path_small_png + # }) + # except Exception as e: + # log_to_stderr(str(e)) return parsed_matches def parse_layer_count(self) -> Optional[int]: @@ -383,7 +431,7 @@ class PrusaSlicer(BaseSlicer): return _regex_find_string( r";\sfilament_type\s=\s(.*)", self.footer_data) - def parse_filament_name(self) -> Optional[str]: + def parse_filament_name(self) -> Optional[str]: return _regex_find_string( r";\sfilament_settings_id\s=\s(.*)", self.footer_data) @@ -549,10 +597,10 @@ class Cura(BaseSlicer): # Check for thumbnails extracted from the ufp thumb_dir = os.path.join(os.path.dirname(self.path), ".thumbs") thumb_base = os.path.splitext(os.path.basename(self.path))[0] - thumb_path = os.path.join(thumb_dir, f"{thumb_base}.png") - rel_path_full = os.path.join(".thumbs", f"{thumb_base}.png") - rel_path_small = os.path.join(".thumbs", f"{thumb_base}-32x32.png") - thumb_path_small = os.path.join(thumb_dir, f"{thumb_base}-32x32.png") + thumb_path = os.path.join(thumb_dir, f"{thumb_base}.jpg") + rel_path_full = os.path.join(".thumbs", f"{thumb_base}.jpg") + rel_path_small = os.path.join(".thumbs", f"{thumb_base}-32x32.jpg") + thumb_path_small = os.path.join(thumb_dir, f"{thumb_base}-32x32.jpg") if not os.path.isfile(thumb_path): return None # read file @@ -566,7 +614,7 @@ class Cura(BaseSlicer): }) # Create 32x32 thumbnail im.thumbnail((32, 32), Image.ANTIALIAS) - im.save(thumb_path_small, format="PNG") + im.save(thumb_path_small, format="JPEG") thumbs.insert(0, { 'width': im.width, 'height': im.height, 'size': os.path.getsize(thumb_path_small), @@ -1087,7 +1135,7 @@ def extract_ufp(ufp_path: str, dest_path: str) -> None: log_to_stderr(f"UFP file Not Found: {ufp_path}") sys.exit(-1) thumb_name = os.path.splitext( - os.path.basename(dest_path))[0] + ".png" + os.path.basename(dest_path))[0] + ".jpg" dest_thumb_dir = os.path.join(os.path.dirname(dest_path), ".thumbs") dest_thumb_path = os.path.join(dest_thumb_dir, thumb_name) try: diff --git a/moonraker/components/klippy_apis.py b/moonraker/components/klippy_apis.py index 59314ba..89faf70 100644 --- a/moonraker/components/klippy_apis.py +++ b/moonraker/components/klippy_apis.py @@ -7,6 +7,9 @@ from __future__ import annotations from utils import SentinelClass from websockets import WebRequest, Subscribable +import os +import shutil +import json # Annotation imports from typing import ( @@ -23,6 +26,7 @@ if TYPE_CHECKING: from confighelper import ConfigHelper from websockets import WebRequest from klippy_connection import KlippyConnection as Klippy + from .file_manager.file_manager import FileManager Subscription = Dict[str, Optional[List[Any]]] _T = TypeVar("_T") @@ -41,6 +45,7 @@ class KlippyAPI(Subscribable): def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.klippy: Klippy = self.server.lookup_component("klippy_connection") + self.fm: FileManager = self.server.lookup_component("file_manager") app_args = self.server.get_app_args() self.version = app_args.get('software_version') # Maintain a subscription for all moonraker requests, as @@ -60,7 +65,12 @@ class KlippyAPI(Subscribable): "/printer/restart", ['POST'], self._gcode_restart) self.server.register_endpoint( "/printer/firmware_restart", ['POST'], self._gcode_firmware_restart) - + self.server.register_endpoint( + "/printer/breakheater", ['POST'], self.breakheater) + self.server.register_endpoint( + "/printer/breakmacro", ['POST'], self.breakmacro) + self.server.register_endpoint( + "/printer/modifybabystep", ["POST"], self.modifybabystep) async def _gcode_pause(self, web_request: WebRequest) -> str: return await self.pause_print() @@ -79,6 +89,11 @@ class KlippyAPI(Subscribable): async def _gcode_firmware_restart(self, web_request: WebRequest) -> str: return await self.do_restart("FIRMWARE_RESTART") + + async def modifybabystep(self, web_request: WebRequest) -> str: + adjust: float = web_request.get_float('ADJUST', 0) + move: int = web_request.get_int('MOVE', 1) + return await self.modify_babystep(adjust, move) async def _send_klippy_request(self, method: str, @@ -110,10 +125,24 @@ class KlippyAPI(Subscribable): # Doing so will result in "wait_started" blocking for the specifed # timeout (default 20s) and returning False. # XXX - validate that file is on disk + homedir = os.path.expanduser("~") if filename[0] == '/': filename = filename[1:] # Escape existing double quotes in the file name filename = filename.replace("\"", "\\\"") + if os.path.split(filename)[0].split(os.path.sep)[0] != ".cache": + base_path = os.path.join(homedir, "gcode_files") + target = os.path.join(".cache", os.path.basename(filename)) + cache_path = os.path.join(base_path, ".cache") + if not os.path.exists(cache_path): + os.makedirs(cache_path) + shutil.rmtree(cache_path) + os.makedirs(cache_path) + metadata = self.fm.gcode_metadata.metadata.get(filename, None) + self.copy_file_to_cache(os.path.join(base_path, filename), os.path.join(base_path, target)) + msg = "// metadata=" + json.dumps(metadata) + self.server.send_event("server:gcode_response", msg) + filename = target script = f'SDCARD_PRINT_FILE FILENAME="{filename}"' await self.klippy.wait_started() return await self.run_gcode(script) @@ -136,9 +165,33 @@ class KlippyAPI(Subscribable): self, default: Union[SentinelClass, _T] = SENTINEL ) -> Union[_T, str]: self.server.send_event("klippy_apis:cancel_requested") + await self._send_klippy_request( + "breakmacro", {}, default) + await self._send_klippy_request( + "breakheater", {}, default) return await self._send_klippy_request( "pause_resume/cancel", {}, default) + async def breakheater( + self, default: Union[SentinelClass, _T] = SENTINEL + ) -> Union[_T, str]: + return await self._send_klippy_request( + "breakheater", {}, default) + + async def breakmacro( + self, default: Union[SentinelClass, _T] = SENTINEL + ) -> Union[_T, str]: + return await self._send_klippy_request( + "breakmacro", {}, default) + + async def modify_babystep(self, + babystep: float = 0, + move: int = 1, + default: Union[SentinelClass, _T] = SENTINEL + ) -> Union[_T, str]: + return await self._send_klippy_request( + "modifybabystep", {"ADJUST": babystep, "MOVE": move} , default) + async def do_restart(self, gc: str) -> str: # WARNING: Do not call this method from within the following # event handlers: @@ -232,5 +285,16 @@ class KlippyAPI(Subscribable): ) -> None: self.server.send_event("server:status_update", status) + def copy_file_to_cache(self, origin, target): + stat = os.statvfs("/") + free_space = stat.f_frsize * stat.f_bfree + filesize = os.path.getsize(os.path.join(origin)) + if (filesize < free_space): + shutil.copy(origin, target) + else: + msg = "!! Insufficient disk space, unable to read the file." + self.server.send_event("server:gcode_response", msg) + raise self.server.error("Insufficient disk space, unable to read the file.", 500) + def load_component(config: ConfigHelper) -> KlippyAPI: return KlippyAPI(config) diff --git a/moonraker/components/machine.py b/moonraker/components/machine.py index 977e3d6..724ac50 100644 --- a/moonraker/components/machine.py +++ b/moonraker/components/machine.py @@ -105,10 +105,6 @@ class Machine: self.server.register_endpoint( "/machine/system_info", ['POST'], self._handle_sysinfo_request) - # self.server.register_endpoint( - # "/machine/dev_name", ['GET'], - # self._handle_devname_request) - self.server.register_notification("machine:service_state_changed")