Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Qidi Plus 4 #947

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions moonraker/components/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ def __init__(self, config: ConfigHelper) -> None:
"/access/user/password", RequestType.POST, self._handle_password_reset,
transports=TransportType.HTTP | TransportType.WEBSOCKET
)
# Custom endpoint: find a user by username and reset password (only suitable for ordinary user)
self.server.register_endpoint(
"/access/user/password_by_name", RequestType.POST, self._handle_password_reset_by_name,
transports=TransportType.HTTP | TransportType.WEBSOCKET
)
self.server.register_endpoint(
"/access/api_key", RequestType.GET | RequestType.POST,
self._handle_apikey_request,
Expand Down Expand Up @@ -500,6 +505,29 @@ async def _handle_password_reset(self,
'username': username,
'action': "user_password_reset"
}

async def _handle_password_reset_by_name(self,
web_request: WebRequest
) -> Dict[str, str]:
username: str = web_request.get_str('username')
new_pass: str = web_request.get_str('new_password')

user_info = self.users[username]
if user_info.source == "ldap":
raise self.server.error(
f"Can´t Reset password for ldap user {username}")
if username in RESERVED_USERS:
raise self.server.error(
f"Invalid Reset Request for user {username}")
salt = bytes.fromhex(user_info.salt)
new_hashed_pass = hashlib.pbkdf2_hmac(
'sha256', new_pass.encode(), salt, HASH_ITER).hex()
self.users[username].password = new_hashed_pass
await self._sync_user(username)
return {
'username': username,
'action': "user_password_reset_by_name"
}

async def _login_jwt_user(
self, web_request: WebRequest, create: bool = False
Expand Down Expand Up @@ -852,19 +880,23 @@ async def authenticate_request(
) -> Optional[UserInfo]:
if request.method == "OPTIONS":
return None

# Check JSON Web Token
jwt_user = self._check_json_web_token(request, auth_required)
if jwt_user is not None:
return jwt_user


# Allow local request
try:
ip = ipaddress.ip_address(request.remote_ip) # type: ignore
# logging.info(f"request.remote_ip: {request.remote_ip}, is_loopback: {ipaddress.ip_address(request.remote_ip).is_loopback}") # type: ignore
ip = ipaddress.ip_address(request.remote_ip) # type: ignore
if ip.is_loopback:
return None
except ValueError:
logging.exception(
f"Unable to Create IP Address {request.remote_ip}")
ip = None

# Check JSON Web Token
jwt_user = self._check_json_web_token(request, auth_required)
if jwt_user is not None:
return jwt_user

# Check oneshot access token
ost: Optional[List[bytes]] = request.arguments.get('token', None)
if ost is not None:
Expand Down
73 changes: 46 additions & 27 deletions moonraker/components/file_manager/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def parse_thumbnails(self) -> Optional[List[Dict[str, Any]]]:
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"(%D)", lines[0])
Expand All @@ -246,33 +246,33 @@ def parse_thumbnails(self) -> Optional[List[Dict[str, Any]]]:
'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:
# 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.save(thumb_QD_path, "JPEG", quality=90)
except Exception as e:
logger.info(str(e))
parsed_matches.append({
'width': smallest_match['width'], 'height': smallest_match['height'],
'size': (max_size + min_size) // 2,
'relative_path': rel_path_QD})
return parsed_matches

def parse_layer_count(self) -> Optional[int]:
Expand Down Expand Up @@ -306,6 +306,7 @@ def parse_thumbnails(self) -> Optional[List[Dict[str, Any]]]:
class PrusaSlicer(BaseSlicer):
def check_identity(self, data: str) -> Optional[Dict[str, str]]:
aliases = {
'QIDISlicer': r"QIDISlicer\s(.*)\son",
'PrusaSlicer': r"PrusaSlicer\s(.*)\son",
'SuperSlicer': r"SuperSlicer\s(.*)\son",
'OrcaSlicer': r"OrcaSlicer\s(.*)\son",
Expand Down Expand Up @@ -422,6 +423,14 @@ def parse_nozzle_diameter(self) -> Optional[float]:
def parse_layer_count(self) -> Optional[int]:
return regex_find_int(r"; total layers count = (%D)", self.footer_data)

def parse_gimage(self) -> Optional[str]:
return regex_find_string(
r";gimage:(.*)", self.footer_data)

def parse_simage(self) -> Optional[str]:
return regex_find_string(
r";simage:(.*)", self.footer_data)

class Slic3rPE(PrusaSlicer):
def check_identity(self, data: str) -> Optional[Dict[str, str]]:
match = re.search(r"Slic3r\sPrusa\sEdition\s(.*)\son", data)
Expand Down Expand Up @@ -556,6 +565,14 @@ def parse_thumbnails(self) -> Optional[List[Dict[str, Any]]]:
return None
return thumbs

def parse_gimage(self) -> Optional[str]:
return regex_find_string(
r";gimage:(.*)", self.header_data)

def parse_simage(self) -> Optional[str]:
return regex_find_string(
r";simage:(.*)", self.header_data)

class Simplify3D(BaseSlicer):
def check_identity(self, data: str) -> Optional[Dict[str, str]]:
match = re.search(r"Simplify3D\(R\)\sVersion\s(.*)", data)
Expand Down Expand Up @@ -927,6 +944,8 @@ def parse_first_layer_bed_temp(self) -> Optional[float]:
KISSlicer, IdeaMaker, IceSL, KiriMoto
]
SUPPORTED_DATA = [
'gimage',
'simage',
'gcode_start_byte',
'gcode_end_byte',
'layer_count',
Expand Down
55 changes: 55 additions & 0 deletions moonraker/components/klippy_apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import logging
from ..utils import Sentinel
from ..common import WebRequest, APITransport, RequestType
import os
import shutil
import json

# Annotation imports
from typing import (
Expand All @@ -26,6 +29,7 @@
from ..confighelper import ConfigHelper
from ..common import UserInfo
from .klippy_connection import KlippyConnection as Klippy
from .file_manager.file_manager import FileManager
Subscription = Dict[str, Optional[List[Any]]]
SubCallback = Callable[[Dict[str, Dict[str, Any]], float], Optional[Coroutine]]
_T = TypeVar("_T")
Expand All @@ -44,6 +48,7 @@ class KlippyAPI(APITransport):
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")
self.eventloop = self.server.get_event_loop()
app_args = self.server.get_app_args()
self.version = app_args.get('software_version')
Expand Down Expand Up @@ -74,6 +79,15 @@ def __init__(self, config: ConfigHelper) -> None:
self.server.register_event_handler(
"server:klippy_disconnect", self._on_klippy_disconnect
)
self.server.register_endpoint(
"/printer/list_endpoints", RequestType.GET, self.list_endpoints
)
self.server.register_endpoint(
"/printer/breakheater", RequestType.POST, self.breakheater
)
self.server.register_endpoint(
"/printer/breakmacro", RequestType.POST, self.breakmacro
)

def _on_klippy_disconnect(self) -> None:
self.host_subscription.clear()
Expand Down Expand Up @@ -140,6 +154,20 @@ async def start_print(
filename = filename[1:]
# Escape existing double quotes in the file name
filename = filename.replace("\"", "\\\"")
homedir = os.path.expanduser("~")
if os.path.split(filename)[0].split(os.path.sep)[0] != ".cache":
base_path = os.path.join(homedir, "printer_data/gcodes")
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}"'
if wait_klippy_started:
await self.klippy.wait_started()
Expand Down Expand Up @@ -169,8 +197,24 @@ async def cancel_print(
) -> Union[_T, str]:
self.server.send_event("klippy_apis:cancel_requested")
logging.info("Requesting job cancel...")
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[Sentinel, _T] = Sentinel.MISSING
) -> Union[_T, str]:
return await self._send_klippy_request(
"breakheater", {}, default)

async def breakmacro(
self, default: Union[Sentinel, _T] = Sentinel.MISSING
) -> Union[_T, str]:
return await self._send_klippy_request(
"breakmacro", {}, default)

async def do_restart(
self, gc: str, wait_klippy_started: bool = False
Expand Down Expand Up @@ -295,5 +339,16 @@ def send_status(
self.eventloop.register_callback(cb, status, eventtime)
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)
10 changes: 10 additions & 0 deletions moonraker/components/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,16 @@ async def _handle_sysinfo_request(self,
"moonraker": self.unit_name,
"klipper": kconn.unit_name
}
# Used for Qidi Slicer searching device
dev_name = web_request.get_str('dev_name',default=None)
if dev_name !=None:
Note=open('/dev_info.txt',mode='w')
Note.write(dev_name)
Note.close()
with open('/dev_info.txt', 'r') as f:
content = f.read()
f.close()
self.system_info["machine_name"] = content
return {"system_info": sys_info}

async def _set_sudo_password(
Expand Down
Loading