klippy_connection: add is_printing() and is_ready() methods

Several components throughout Moonraker determine whether or not
Klipper is printing or is ready before taking action.  This centralizes
queries in one area.  The checks do not query Klipper directly but
rather rely on subscriptions to push state to Moonraker.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2023-01-06 08:09:42 -05:00
parent 019c5fc416
commit ecf7fb9267
No known key found for this signature in database
GPG Key ID: 5A1EB336DFB4C71B
4 changed files with 38 additions and 43 deletions

View File

@ -38,10 +38,12 @@ if TYPE_CHECKING:
from inotify_simple import Event as InotifyEvent from inotify_simple import Event as InotifyEvent
from confighelper import ConfigHelper from confighelper import ConfigHelper
from websockets import WebRequest from websockets import WebRequest
from klippy_connection import KlippyConnection
from components import database from components import database
from components import klippy_apis from components import klippy_apis
from components import shell_command from components import shell_command
from components.job_queue import JobQueue from components.job_queue import JobQueue
from components.job_state import JobState
StrOrPath = Union[str, pathlib.Path] StrOrPath = Union[str, pathlib.Path]
DBComp = database.MoonrakerDatabase DBComp = database.MoonrakerDatabase
APIComp = klippy_apis.KlippyAPI APIComp = klippy_apis.KlippyAPI
@ -403,7 +405,7 @@ class FileManager:
if force: if force:
# Make sure that the directory does not contain a file # Make sure that the directory does not contain a file
# loaded by the virtual_sdcard # loaded by the virtual_sdcard
await self._handle_operation_check(dir_path) self._handle_operation_check(dir_path)
self.notify_sync_lock = NotifySyncLock(dir_path) self.notify_sync_lock = NotifySyncLock(dir_path)
try: try:
await self.event_loop.run_in_thread( await self.event_loop.run_in_thread(
@ -423,20 +425,19 @@ class FileManager:
raise self.server.error("Operation Not Supported", 405) raise self.server.error("Operation Not Supported", 405)
return result return result
async def _handle_operation_check(self, requested_path: str) -> bool: def _handle_operation_check(self, requested_path: str) -> bool:
if not self.get_relative_path("gcodes", requested_path): if not self.get_relative_path("gcodes", requested_path):
# Path not in the gcodes path # Path not in the gcodes path
return True return True
# Get virtual_sdcard status kconn: KlippyConnection
kapis: APIComp = self.server.lookup_component('klippy_apis') kconn = self.server.lookup_component("klippy_connection")
result: Dict[str, Any] job_state: JobState = self.server.lookup_component("job_state")
result = await kapis.query_objects({'print_stats': None}, {}) last_stats = job_state.get_last_stats()
pstats = result.get('print_stats', {}) loaded_file: str = last_stats.get('filename', "")
loaded_file: str = pstats.get('filename', "") state: str = last_stats.get('state', "")
state: str = pstats.get('state', "")
gc_path = self.file_paths.get('gcodes', "") gc_path = self.file_paths.get('gcodes', "")
full_path = os.path.join(gc_path, loaded_file) full_path = os.path.join(gc_path, loaded_file)
is_printing = state in ["printing", "paused"] is_printing = kconn.is_ready() and state in ["printing", "paused"]
if loaded_file and is_printing: if loaded_file and is_printing:
if os.path.isdir(requested_path): if os.path.isdir(requested_path):
# Check to see of the loaded file is in the request # Check to see of the loaded file is in the request
@ -482,13 +483,13 @@ class FileManager:
raise self.server.error(f"File {source_path} does not exist") raise self.server.error(f"File {source_path} does not exist")
# make sure the destination is not in use # make sure the destination is not in use
if os.path.exists(dest_path): if os.path.exists(dest_path):
await self._handle_operation_check(dest_path) self._handle_operation_check(dest_path)
if ep == "/server/files/move": if ep == "/server/files/move":
if source_root not in self.full_access_roots: if source_root not in self.full_access_roots:
raise self.server.error( raise self.server.error(
f"Source path is read-only, cannot move: {source_root}") f"Source path is read-only, cannot move: {source_root}")
# if moving the file, make sure the source is not in use # if moving the file, make sure the source is not in use
await self._handle_operation_check(source_path) self._handle_operation_check(source_path)
op_func: Callable[..., str] = shutil.move op_func: Callable[..., str] = shutil.move
result['source_item'] = { result['source_item'] = {
'path': source, 'path': source,
@ -665,7 +666,7 @@ class FileManager:
can_start: bool = False can_start: bool = False
try: try:
check_path: str = upload_info['dest_path'] check_path: str = upload_info['dest_path']
can_start = await self._handle_operation_check(check_path) can_start = self._handle_operation_check(check_path)
except self.server.error as e: except self.server.error as e:
if e.status_code == 403: if e.status_code == 403:
raise self.server.error( raise self.server.error(
@ -857,7 +858,7 @@ class FileManager:
if not os.path.isfile(full_path): if not os.path.isfile(full_path):
raise self.server.error(f"Invalid file path: {path}") raise self.server.error(f"Invalid file path: {path}")
try: try:
await self._handle_operation_check(full_path) self._handle_operation_check(full_path)
except self.server.error as e: except self.server.error as e:
if e.status_code == 403: if e.status_code == 403:
raise raise
@ -1683,7 +1684,7 @@ class MetadataStorage:
def get(self, def get(self,
key: str, key: str,
default: _T = None default: Optional[_T] = None
) -> Union[_T, Dict[str, Any]]: ) -> Union[_T, Dict[str, Any]]:
return deepcopy(self.metadata.get(key, default)) return deepcopy(self.metadata.get(key, default))

View File

@ -33,6 +33,7 @@ if TYPE_CHECKING:
from .mqtt import MQTTClient from .mqtt import MQTTClient
from .template import JinjaTemplate from .template import JinjaTemplate
from .http_client import HttpClient from .http_client import HttpClient
from klippy_connection import KlippyConnection
APIComp = klippy_apis.KlippyAPI APIComp = klippy_apis.KlippyAPI
class PrinterPower: class PrinterPower:
@ -94,13 +95,6 @@ class PrinterPower:
"job_queue:job_queue_changed", self._handle_job_queued) "job_queue:job_queue_changed", self._handle_job_queued)
self.server.register_notification("power:power_changed") self.server.register_notification("power:power_changed")
async def _check_klippy_printing(self) -> bool:
kapis: APIComp = self.server.lookup_component('klippy_apis')
result: Dict[str, Any] = await kapis.query_objects(
{'print_stats': None}, default={})
pstate = result.get('print_stats', {}).get('state', "").lower()
return pstate == "printing"
async def component_init(self) -> None: async def component_init(self) -> None:
for dev in self.devices.values(): for dev in self.devices.values():
if not dev.initialize(): if not dev.initialize():
@ -261,13 +255,6 @@ class PowerDevice:
'initial_state', None 'initial_state', None
) )
async def _check_klippy_printing(self) -> bool:
kapis: APIComp = self.server.lookup_component('klippy_apis')
result: Dict[str, Any] = await kapis.query_objects(
{'print_stats': None}, default={})
pstate = result.get('print_stats', {}).get('state', "").lower()
return pstate == "printing"
def _schedule_firmware_restart(self, state: str = "") -> None: def _schedule_firmware_restart(self, state: str = "") -> None:
if not self.need_scheduled_restart: if not self.need_scheduled_restart:
return return
@ -399,8 +386,9 @@ class PowerDevice:
self.notify_power_changed() self.notify_power_changed()
return cur_state return cur_state
if not force: if not force:
printing = await self._check_klippy_printing() kconn: KlippyConnection
if self.locked_while_printing and printing: kconn = self.server.lookup_component("klippy_connection")
if self.locked_while_printing and kconn.is_printing():
raise self.server.error( raise self.server.error(
f"Unable to change power for {self.name} " f"Unable to change power for {self.name} "
"while printing") "while printing")

View File

@ -40,7 +40,7 @@ if TYPE_CHECKING:
from moonraker import Server from moonraker import Server
from confighelper import ConfigHelper from confighelper import ConfigHelper
from websockets import WebRequest from websockets import WebRequest
from components.klippy_apis import KlippyAPI as APIComp from klippy_connection import KlippyConnection
from components.shell_command import ShellCommandFactory as SCMDComp from components.shell_command import ShellCommandFactory as SCMDComp
from components.database import MoonrakerDatabase as DBComp from components.database import MoonrakerDatabase as DBComp
from components.database import NamespaceWrapper from components.database import NamespaceWrapper
@ -68,6 +68,8 @@ class UpdateManager:
def __init__(self, config: ConfigHelper) -> None: def __init__(self, config: ConfigHelper) -> None:
self.server = config.get_server() self.server = config.get_server()
self.event_loop = self.server.get_event_loop() self.event_loop = self.server.get_event_loop()
self.kconn: KlippyConnection
self.kconn = self.server.lookup_component("klippy_connection")
self.channel = config.get('channel', "dev") self.channel = config.get('channel', "dev")
if self.channel not in ["dev", "beta"]: if self.channel not in ["dev", "beta"]:
raise config.error( raise config.error(
@ -224,20 +226,13 @@ class UpdateManager:
if notify: if notify:
self.cmd_helper.notify_update_refreshed() self.cmd_helper.notify_update_refreshed()
async def _check_klippy_printing(self) -> bool:
kapi: APIComp = self.server.lookup_component('klippy_apis')
result: Dict[str, Any] = await kapi.query_objects(
{'print_stats': None}, default={})
pstate: str = result.get('print_stats', {}).get('state', "")
return pstate.lower() == "printing"
async def _handle_auto_refresh(self, eventtime: float) -> float: async def _handle_auto_refresh(self, eventtime: float) -> float:
cur_hour = time.localtime(time.time()).tm_hour cur_hour = time.localtime(time.time()).tm_hour
if self.initial_refresh_complete: if self.initial_refresh_complete:
# Update when the local time is between 12AM and 5AM # Update when the local time is between 12AM and 5AM
if cur_hour >= MAX_UPDATE_HOUR: if cur_hour >= MAX_UPDATE_HOUR:
return eventtime + UPDATE_REFRESH_INTERVAL return eventtime + UPDATE_REFRESH_INTERVAL
if await self._check_klippy_printing(): if self.kconn.is_printing():
# Don't Refresh during a print # Don't Refresh during a print
logging.info("Klippy is printing, auto refresh aborted") logging.info("Klippy is printing, auto refresh aborted")
return eventtime + UPDATE_REFRESH_INTERVAL return eventtime + UPDATE_REFRESH_INTERVAL
@ -268,7 +263,7 @@ class UpdateManager:
async def _handle_update_request(self, async def _handle_update_request(self,
web_request: WebRequest web_request: WebRequest
) -> str: ) -> str:
if await self._check_klippy_printing(): if self.kconn.is_printing():
raise self.server.error("Update Refused: Klippy is printing") raise self.server.error("Update Refused: Klippy is printing")
app: str = web_request.get_endpoint().split("/")[-1] app: str = web_request.get_endpoint().split("/")[-1]
if app == "client": if app == "client":
@ -391,7 +386,7 @@ class UpdateManager:
if ( if (
machine.validation_enabled() or machine.validation_enabled() or
self.cmd_helper.is_update_busy() or self.cmd_helper.is_update_busy() or
await self._check_klippy_printing() or self.kconn.is_printing() or
not self.initial_refresh_complete not self.initial_refresh_complete
): ):
if check_refresh: if check_refresh:
@ -433,7 +428,7 @@ class UpdateManager:
async def _handle_repo_recovery(self, async def _handle_repo_recovery(self,
web_request: WebRequest web_request: WebRequest
) -> str: ) -> str:
if await self._check_klippy_printing(): if self.kconn.is_printing():
raise self.server.error( raise self.server.error(
"Recovery Attempt Refused: Klippy is printing") "Recovery Attempt Refused: Klippy is printing")
app: str = web_request.get_str('name') app: str = web_request.get_str('name')

View File

@ -35,6 +35,7 @@ if TYPE_CHECKING:
from components.klippy_apis import KlippyAPI from components.klippy_apis import KlippyAPI
from components.file_manager.file_manager import FileManager from components.file_manager.file_manager import FileManager
from components.machine import Machine from components.machine import Machine
from components.job_state import JobState
FlexCallback = Callable[..., Optional[Coroutine]] FlexCallback = Callable[..., Optional[Coroutine]]
# These endpoints are reserved for klippy/moonraker communication only and are # These endpoints are reserved for klippy/moonraker communication only and are
@ -550,6 +551,16 @@ class KlippyConnection:
def is_connected(self) -> bool: def is_connected(self) -> bool:
return self.writer is not None and not self.closing return self.writer is not None and not self.closing
def is_ready(self) -> bool:
return self.state == "ready"
def is_printing(self) -> bool:
if not self.is_ready():
return False
job_state: JobState = self.server.lookup_component("job_state")
stats = job_state.get_last_stats()
return stats.get("state", "") == "printing"
async def _on_connection_closed(self) -> None: async def _on_connection_closed(self) -> None:
self.init_list = [] self.init_list = []
self._state = "disconnected" self._state = "disconnected"