10. ccfGui Design Decisions

This chapter documents the core design decisions and architectural patterns used in ccfGui, the reference application built with the IFW Widget Library. ccfGui demonstrates how all the library components – communication facades, widgets, utilities, and styles – are wired together into a complete CCF based system control panel.

../_images/ccfGui_full.png

ccfGui panel using CCF ifw-widgets

ccfGui is implemented across two files with a clear separation of concerns:

  • ccfGui.py – the CLI entry point that parses arguments, discovers the service URI, and launches the application

  • ccfguimainwindow.py – the CcfGuiMainWindow class that handles all UI logic, facade management, and dynamic widget composition

The following diagram shows the launch flow between the two files:

CLI arguments
    |
    v
+-----------------+
|  ccfGui.py      |
| (entry point)   |
+-----------------+
    |
    |  --name given?  -->  Consul.resolve(name) --> URI
    |  --uri given?   -->  use directly
    v
Build remote_ddt_dic from paired --publisher-ddt / --remote-ddt-uri
    |
    v
TaurusApplication(app_name="ccfGui", ...)
    |
    v
CcfGuiMainWindow(uri, log_level, remote_ddt_dic)
+-------------------------------+
|  ccfguimainwindow.py           |
| (QMainWindow + Logger)         |
|                                |
|  - WdglibLogger init           |
|  - UI setup (.ui file)         |
|  - ConnectionManager + facades |
|  - _get_db_prefix()            |
|  - OLDB validation             |
|  - Dynamic widget composition  |
|  - Error / connection handlers |
|  - State machine OLDB dpts     |
|  - Status bar setup            |
+-------------------------------+
    |
    v
menu_set_stylesheet(style_resource)
show()
app.exec_()  -->  Qt event loop

The entry point knows nothing about widgets, facades, or OLDB. It only handles launch concerns and delegates all application logic to the main window class.

10.1. ccfGui.py – Application Entry Point

The ccfGui.py file is a lightweight CLI launcher built with click. Its responsibilities are:

  1. Parse command-line arguments

  2. Resolve the CCF service URI (either direct or via Consul)

  3. Build the remote DDT broker dictionary from paired arguments

  4. Create the TaurusApplication instance

  5. Instantiate CcfGuiMainWindow, apply the theme, and start the event loop

10.1.1. Command-Line Arguments

The following table summarises the click options accepted by the entry point:

Option

Description

Default

-n, --name

Registered CCF name in Consul

ccf-req

-u, --uri

Service URI (e.g., zpb.rr://127.0.0.1:12091)

none

-l, --log-level

Debugging level

ERROR

-r, --style_resource

Built-in QSS stylesheet resource

Default

-d, --remote-ddt-uri

Remote DDT broker URI (repeatable)

(none)

-p, --publisher-ddt

DDT publisher name to pair with --remote-ddt-uri (repeatable)

(none)

10.1.2. Consul Service Discovery

When --uri is not provided (i.e., remains its default value of "none"), the entry point resolves the service URI from Consul:

if uri == "none":
    cons = consul_utils.ConsulClient()
    uri = cons.get_uri(name)

This allows operators to launch ccfGui by CCF task name rather than hardcoding a URI, supporting deployments where service endpoints vary.

10.1.3. Remote DDT Dictionary

The --remote-ddt-uri and --publisher-ddt options are paired to build a dictionary mapping publisher names to remote broker URIs:

remote_ddt_dic = {}
for remote_uri, pub_name in zip(remote_ddt_uri, publisher_ddt):
    remote_ddt_dic[pub_name] = remote_uri

This dictionary is passed to CcfGuiMainWindow and forwarded to the pipeline widgets, which use it to configure publishers that connect to DDT brokers running on different machines.

10.1.4. Application Launch

Finally, the entry point creates the Qt application, instantiates the main window, applies the theme, and enters the event loop:

app = TaurusApplication(
    app_name="ccfGui",
    app_version="1.0.0",
    org_name="ESO",
    org_domain="eso.org"
)

window = CcfGuiMainWindow(uri, log_level, remote_ddt_dic)
window.menu_set_stylesheet(style_resource)
window.setWindowTitle("CCF GUI")
window.show()
sys.exit(app.exec_())

All of the substance of the application – facade management, widget creation, error handling – lives in CcfGuiMainWindow.

10.2. CcfGuiMainWindow – Main Window

The CcfGuiMainWindow class inherits from QMainWindow and Logger. It owns all communication facades, manages connections, composes widgets dynamically, and provides global error handling and connection monitoring.

10.2.1. Initialization Sequence

The __init__ method follows a strict sequence:

  1. Base class initializationQMainWindow and Logger

  2. Logging setupWdglibLogger.init() loads the logging configuration resource

  3. UI setup – the Qt Designer .ui file is loaded via Ui_CcfGuiMainWindow.setupUi()

  4. Facade creationStdCmds, DcsCmds, and RecCmds facades are obtained from the singleton ConnectionManager

  5. OLDB prefix query_get_db_prefix() retrieves the OLDB path from the running CCF

  6. OLDB structure validation – verifies that sm, status, properties, and recording exist under the prefix

  7. Camera and pipeline discovery – queries OLDB for available cameras and pipelines

  8. Dynamic widget composition – creates state, acquisition, setup, recording, and pipeline widgets, adding them to layouts

  9. Post-init GUI setup_init_gui() connects menu actions and facade signals

  10. OLDB datapoint setup_state_dp_setup() creates datapoints for state machine reads

  11. Status bar setup_init_gui_statusbar() adds connection LED and labels

def __init__(self, uri, log_level, remote_ddt_dic) -> None:
    QMainWindow.__init__(self)
    WdglibLogger.init("config/ifw/ccf/gui/ccfGui_logging.json")
    Logger.__init__(self, qApp.applicationName())
    self.setLogLevel(getattr(taurus, log_level.title()))

    self.ui = Ui_CcfGuiMainWindow()
    self.ui.setupUi(self)

    self._base_uri = uri.rstrip('/')

    self.connection_manager = ConnectionManager()
    self.connection_manager.auto_connect = True

    self.std_cmds_facade = self.connection_manager.get_stdcmds(
        f"{self._base_uri}/StdCmds"
    )
    self.dcs_cmds_facade = self.connection_manager.get_dcscmds(
        f"{self._base_uri}/DcsCmds"
    )
    self.rec_cmds_facade = self.connection_manager.get_reccmds(
        f"{self._base_uri}/RecCmds"
    )

    if not self._get_db_prefix():
        sys.exit(1)
    # ... validate OLDB structure, discover cameras/pipelines ...
    # ... create widgets dynamically ...
    self._init_gui()
    self._state_dp_setup()
    self._init_gui_statusbar()

The .ui file defines the static skeleton of the window (menu bar, dock widgets, placeholder layouts). Widgets are added programmatically into those layouts at runtime, making the panel adaptable to any CCF configuration.

10.2.2. Determining the OLDB Prefix

At startup, the main window makes a synchronous MAL call to the DCS IF to retrieve the OLDB prefix:

from ModDcsif.Dcsif.DcsCmds import DcsCmdsSync
import elt.pymal as mal

def _get_db_prefix(self) -> bool:
    ciiFactory = mal.CiiFactory.getInstance()
    qos = mal.rr.qos.ReplyTime(timedelta(seconds=3))

    zpbmal = mal.loadMal("zpb", {})
    ciiFactory.registerMal("zpb.rr", zpbmal)

    with ciiFactory.getClient(
        f"{self._base_uri}/DcsCmds", DcsCmdsSync, qos, {}
    ) as client:
        try:
            self._dcs_db_prefix = client.GetConfig(
                "sys::oldb_prefix"
            ).split("=")[1].strip().lower()
            return True
        except Exception:
            return False

The connection used for this call is discarded after the prefix is retrieved. All subsequent communication goes through the library facades, which manage their own persistent connections.

After retrieving the prefix, the main window validates that the OLDB structure matches what it expects for a CCF:

dcs_childrens = get_children_from_ui_path(self._dcs_db_prefix)
if not (
    "sm" in dcs_childrens
    and "status" in dcs_childrens
    and "properties" in dcs_childrens
    and "recording" in dcs_childrens
):
    self.error("DCS db-prefix does not have a CCF camera structure")
    sys.exit(1)

10.2.3. Dynamic Widget Composition

The number of cameras and pipelines is not known at compile time. CcfGuiMainWindow queries OLDB to discover them:

cameras_path = f"{self._dcs_db_prefix}/status/acquisition"
cameras_list = get_children_from_ui_path(cameras_path)

pipelines_path = f"{self._dcs_db_prefix}/status/pipelines"
pipelines_list = get_children_from_ui_path(pipelines_path)

State, acquisition, setup, and recording widgets are created once:

self.ui.ccfStateWidget = CcfStateWidget(
    self, self._base_uri, self._dcs_db_prefix
)
self.ui.dynStdHorizontalLayout.addWidget(self.ui.ccfStateWidget)

self.ui.ccfAcquisitionWidget = CcfAcquisitionWidget(
    self, self._base_uri, self._dcs_db_prefix, cameras_list[0],
)
self.ui.dynDcsHorizontalLayout.addWidget(self.ui.ccfAcquisitionWidget)

self.ui.ccfSetupWidget = CcfSetupWidget(
    self, self._base_uri, self._dcs_db_prefix,
)
self.ui.dynDcsHorizontalLayout.addWidget(self.ui.ccfSetupWidget)

self.ui.ccfRecordingWidget = CcfRecordingWidget(
    self, self._base_uri, self._dcs_db_prefix, cameras_list[0],
)
self.ui.dynRecHorizontalLayout.addWidget(self.ui.ccfRecordingWidget)

Pipeline widgets are created in a loop, one per tab:

for pipeline in pipelines_list:
    ccfPipelineWidget = CcfPipelineWidget(
        self, self._base_uri, self._dcs_db_prefix,
        pipeline, remote_ddt_dic,
    )
    self.ui.pipelinesTabWidget.addTab(ccfPipelineWidget, pipeline)

The pipeline and log dock widgets are tabified so they share the same dock area:

self.tabifyDockWidget(self.ui.pipelineDockWidget, self.ui.logDockWidget)

This means the panel adapts to whatever CCF configuration is running.

Note

Each pipeline widget internally discovers its own recipes and publishers from OLDB and creates sub-widgets accordingly, using the adapter type read via get_datapoint_value() to select the correct widget class.

10.2.4. Shared Facade Connections

All widgets retrieve facades from the singleton ConnectionManager. This is a critical design decision that ensures there is exactly one MAL connection per facade URI across the entire application:

self.connection_manager = ConnectionManager()
self.connection_manager.auto_connect = True

self.std_cmds_facade = self.connection_manager.get_stdcmds(
    f"{self._base_uri}/StdCmds"
)
self.dcs_cmds_facade = self.connection_manager.get_dcscmds(
    f"{self._base_uri}/DcsCmds"
)
self.rec_cmds_facade = self.connection_manager.get_reccmds(
    f"{self._base_uri}/RecCmds"
)

Because ConnectionManager is a singleton, widgets instantiated later receive the same facade instances. This means:

  • Connection state changes are seen by all widgets

  • Error signals from any facade can be handled globally

  • There is no risk of having two different connections to the same endpoint

10.2.5. Global Error Handling

Error signals from all facades are connected to a single error handler that shows a modal ErrorDialog:

self.std_cmds_facade.command_error.connect(self._error_slot)
self.dcs_cmds_facade.command_error.connect(self._error_slot)
self.rec_cmds_facade.command_error.connect(self._error_slot)

@Slot(tuple)
def _error_slot(self, exception_err):
    dlg = ErrorDialog(
        f"{exception_err[0]} {exception_err[1].getDesc()}",
        f"{exception_err[0]}\n{exception_err[1]}\n{exception_err[2]}",
    )
    dlg.exec_()

This pattern provides immediate operator feedback: any MAL command failure pops up a dialog with a human-readable summary and expandable traceback.

10.2.6. Connection Monitoring

Connection state from all facades is monitored centrally. A status bar LED shows green when all connections are up, yellow when some are connected, and red when all are down:

self.std_cmds_facade.connectionChanged.connect(
    self.on_cmds_connection_changed
)
self.dcs_cmds_facade.connectionChanged.connect(
    self.on_cmds_connection_changed
)
self.rec_cmds_facade.connectionChanged.connect(
    self.on_cmds_connection_changed
)

@Slot(bool)
def on_cmds_connection_changed(self, state):
    self._update_connection()

def _update_connection(self):
    connections = self.connection_manager.get_all_connections()
    conn_states = [conn.is_connected() for conn in connections.values()]

    led_color = "red"
    if all(conn_states):
        led_color = "green"
    elif any(conn_states):
        led_color = "yellow"

    self.ui.connectionLed.setLedColor(led_color)
    self.ui.connectionLed.setToolTip(str(self.connection_manager))

The status bar also displays the base URI and the OLDB prefix:

self.ui.statusbar.addPermanentWidget(self.ui.connectionLabel)
self.ui.statusbar.addPermanentWidget(self.ui.connectionLed)
self.ui.statusbar.addPermanentWidget(self.ui.dbLabel)

Individual widgets also handle connection state internally to enable/disable themselves, so both the status bar and the widgets provide visual feedback.

10.2.7. State Machine Awareness

The main window handles state machine transitions through menu actions rather than delegating them to individual widgets. This is because transitions like Init->Enable require orchestration across the entire application.

10.2.7.1. OLDB Datapoint Setup

State and substate are read synchronously via OLDB datapoints, set up in _state_dp_setup():

self._oldb_client = elt.oldb.CiiOldbFactory.get_instance()
self.state_dp_uri = elt.config.Uri(
    f"{self._dcs_db_prefix}/sm/status/state"
)
self.substate_dp_uri = elt.config.Uri(
    f"{self._dcs_db_prefix}/sm/status/substate"
)
self.state_dp = self._oldb_client.get_data_point(self.state_dp_uri)
self.substate_dp = self._oldb_client.get_data_point(self.substate_dp_uri)

These datapoints are used by the Enable menu action to check the current state before deciding which command to send.

10.2.7.2. Enable with State Check

The Enable menu action checks whether the system is in NotOperational::NotReady. If so, it calls Init first and auto-enables once Init succeeds:

@Slot()
def _on_actionEnable_triggered(self):
    state = self.state_dp.read_value().get_value()
    substate = self.substate_dp.read_value().get_value()
    if state == "NotOperational" and substate == "NotReady":
        self.std_cmds_facade.init(result_slot=self._auto_enable_on_init)
    else:
        self.std_cmds_facade.enable()

@Slot(str)
def _auto_enable_on_init(self, reply):
    if reply == "OK":
        QTimer.singleShot(50, self.std_cmds_facade.enable)

The QTimer.singleShot ensures the Enable call is posted after the Init result has been fully processed, avoiding race conditions.

10.2.7.3. Other State Machine Commands

The remaining menu actions delegate directly to the facade:

@Slot()
def _on_actionInit_triggered(self):
    self.std_cmds_facade.init()

@Slot()
def _on_actionDisable_triggered(self):
    self.std_cmds_facade.disable()

@Slot()
def _on_actionReset_triggered(self):
    self.std_cmds_facade.reset()

@Slot()
def _on_actionRecover_triggered(self):
    self.dcs_cmds_facade.recover()

The Recover action is disabled by default in _init_gui() and intended for future use.

10.2.8. Theming and Style Switching

The application exposes four style themes in the menu. Switching themes is purely a UI-level operation that does not require widget recreation:

@Slot()
def _on_actionCombinear_triggered(self) -> None:
    self.menu_set_stylesheet("Combinear")

@Slot()
def _on_actionDefault_triggered(self) -> None:
    self.menu_set_stylesheet("Default")

The menu_set_stylesheet() method loads the QSS file using find_file(), applies it globally via qApp.setStyleSheet(), and handles theme-specific styling for the CiiLogsWidget:

def menu_set_stylesheet(self, style_resource: str) -> None:
    style = find_file(
        f"config/ifw/wdglib/styles/{style_resource}/{style_resource}.qss"
    )
    with open(style, "r") as fh:
        qApp.setStyleSheet(fh.read())
        QIcon.setThemeName("breeze")

        if style_resource == "Combinear":
            self.ui.ciiLogsWidget.setStyleSheet(
                "alternate-background-color: #3a3a3a; background-color: #000;"
            )
        # ... other themes ...
        else:
            self.ui.ciiLogsWidget.setStyleSheet(self.log_style)

See the Styles chapter for details on how QSS themes are structured and applied.

10.3. Summary of Key Patterns

The following table summarizes the core design decisions in ccfGui:

Decision

Rationale

Two-file entry point / main window split

Separates CLI launch concerns from application logic

Consul URI resolution

Operators launch by task name, not hardcoded URI

Remote DDT broker pairing

Pipeline publishers can connect to brokers on different machines

Query dcs_db_prefix at startup

Supports any CCF instance, any OLDB location

Discover cameras/pipelines from OLDB

Panels adapt to runtime configuration

Singleton ConnectionManager

One connection per endpoint, shared state

Global ErrorDialog for facade errors

Immediate operator feedback on command failures

Status bar LED for connection state

At-a-glance visibility of MAL connection health

remove_bgrole() on all widgets

QSS theme overrides Taurus default colors

State-aware Init before Enable

Handles NotOperational::NotReady transitions smoothly

Sync OLDB reads for state checks

State queries are occasional; synchronous reads are acceptable