Source code for pyrogue.pydm

#-----------------------------------------------------------------------------
from __future__ import annotations

#-----------------------------------------------------------------------------
# Company    : SLAC National Accelerator Laboratory
#-----------------------------------------------------------------------------
#  Description:
#       PyRogue PyDM Package, Function to start default Rogue PyDM GUI
#-----------------------------------------------------------------------------
# This file is part of the rogue software platform. It is subject to
# the license terms in the LICENSE.txt file found in the top-level directory
# of this distribution and at:
#    https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html.
# No part of the rogue software platform, including this file, may be
# copied, modified, propagated, or distributed except according to the terms
# contained in the LICENSE.txt file.
#-----------------------------------------------------------------------------
import os
import signal
import inspect
from collections.abc import Callable
from types import FrameType
from typing import TYPE_CHECKING

from pyrogue.interfaces import VirtualClient

if TYPE_CHECKING:
    from pydm import Display


def _constructDisplay(
    *,
    display: type[Display] | None,
    display_factory: Callable[..., Display] | None,
    args: list[str],
) -> Display | None:
    """Construct a PyDM ``Display`` from a class or factory."""
    from pydm import Display

    target = display_factory if display_factory is not None else display
    if target is None:
        return None

    if display is not None:
        return display(parent=None, args=args, macros=None)

    sig = inspect.signature(target)
    kwargs = {}
    for name in sig.parameters:
        if name == 'parent':
            kwargs[name] = None
        elif name == 'args':
            kwargs[name] = args
        elif name == 'macros':
            kwargs[name] = None

    disp = target(**kwargs)

    if not isinstance(disp, Display):
        raise TypeError("display_factory must return a pydm.Display instance")

    return disp

# Define a signal handler to ensure the application quits gracefully
def pydmSignalHandler(sig: int, frame: FrameType | None) -> None:
    """Handle process termination signals by closing all PyDM windows.

    Parameters
    ----------
    sig : int
        Received signal number.
    frame : types.FrameType | None
        Current stack frame provided by :mod:`signal`.
    """
    import pydm

    app = pydm.PyDMApplication.instance()
    if app is not None:
        app.closeAllWindows()


def _configureVirtualClients(
    serverList: str,
    *,
    linkTimeout: float,
    requestStallTimeout: float | None,
) -> None:
    """Preconfigure cached VirtualClient instances for each GUI server.

    The GUI shares one cached VirtualClient per ``host:port`` endpoint. This
    helper applies timeout settings before PyDM widgets create channels so the
    whole session uses consistent link-state behavior.
    """
    for server in serverList.split(","):
        server = server.strip()
        if server == "":
            continue

        host, port = server.rsplit(":", 1)
        VirtualClient(
            addr=host,
            port=int(port),
            linkTimeout=linkTimeout,
            requestStallTimeout=requestStallTimeout,
        )

# Function to run the PyDM application with specified parameters
[docs] def runPyDM( serverList: str = 'localhost:9090', ui: str | None = None, display: type[Display] | None = None, display_factory: Callable[..., Display] | None = None, title: str | None = None, sizeX: int = 800, sizeY: int = 1000, maxListExpand: int = 5, maxListSize: int = 100, linkTimeout: float = 10.0, requestStallTimeout: float | None = None, ) -> None: """Launch the default Rogue PyDM application. Parameters ---------- serverList : str, optional Comma-separated list of ``host:port`` Rogue servers. ui : str | None, optional Optional UI file path. Defaults to ``pydmTop.py`` in this package if neither ``display`` nor ``display_factory`` is supplied. display : type[pydm.Display] | None, optional Optional top-level ``pydm.Display`` subclass to instantiate directly. display_factory : callable | None, optional Optional factory that returns a ``pydm.Display`` instance. title : str | None, optional Optional window title. Defaults to ``"Rogue Server: <servers>"``. sizeX : int, optional Initial window width in pixels. sizeY : int, optional Initial window height in pixels. maxListExpand : int, optional Debug-tree auto-expand depth argument forwarded to the UI. maxListSize : int, optional Debug-tree list-size cap argument forwarded to the UI. linkTimeout : float, optional Idle timeout in seconds for VirtualClient link-state detection. This is the normal tuning knob for long-running hardware or simulation transactions and defaults to 10 seconds. requestStallTimeout : float | None, optional In-flight request age in seconds before the VirtualClient declares the server stalled. ``None`` disables stalled-request detection, which is usually the right default unless the application has a strict upper bound for valid request duration. Returns ------- None This function runs the Qt event loop until the application exits. """ import pydm import pydm.data_plugins from pydm.utilities import establish_widget_connections from pydm.widgets.rules import register_widget_rules from pyrogue.pydm.data_plugins.rogue_plugin import RoguePlugin if sum(v is not None for v in (ui, display, display_factory)) > 1: raise ValueError("ui, display, and display_factory are mutually exclusive") # Set the ROGUE_SERVERS environment variable os.environ['ROGUE_SERVERS'] = serverList _configureVirtualClients( serverList, linkTimeout=linkTimeout, requestStallTimeout=requestStallTimeout, ) # Set the UI file to a default value only for the file-based launch path if (ui is None or ui == '') and display is None and display_factory is None: ui = os.path.dirname(os.path.abspath(__file__)) + '/pydmTop.py' # Set the title to a default value if not provided if title is None: title = "Rogue Server: {}".format(os.getenv('ROGUE_SERVERS')) # Prepare command line arguments args = [] args.append(f"sizeX={sizeX}") args.append(f"sizeY={sizeY}") args.append(f"title='{title}'") args.append(f"maxListExpand={maxListExpand}") args.append(f"maxListSize={maxListSize}") pydm.data_plugins.initialize_plugins_if_needed() # Add Rogue plugin manually, if it hasn't already been added based on $PYDM_DATA_PLUGINS_PATH if 'rogue' not in pydm.data_plugins.plugin_modules: pydm.data_plugins.add_plugin(RoguePlugin) # Initialize the PyDM application with specified parameters app = pydm.PyDMApplication(ui_file=ui, command_line_args=args, hide_nav_bar=True, hide_menu_bar=True, hide_status_bar=True) custom_display = _constructDisplay(display=display, display_factory=display_factory, args=args) if custom_display is not None: establish_widget_connections(custom_display) register_widget_rules(custom_display) if app.main_window.home_widget is None: app.main_window.home_widget = custom_display app.main_window.set_display_widget(custom_display) # Setup signal handling for CTRL+C and SIGTERM for handling termination signal signal.signal(signal.SIGINT, pydmSignalHandler) signal.signal(signal.SIGTERM, pydmSignalHandler) # Print message indicating the GUI is running and how to exit print(f"Running GUI. Close window, hit cntrl-c or send SIGTERM to {os.getpid()} to exit.") # Run the PyDM application app.exec()