Arrange EPICS Screens with Groups and Sub-Controllers#

This guide shows how to use group on attributes and commands to organise widgets into labelled boxes on a screen, and how splitting a device into sub-controllers creates navigable sub-screens for larger devices.

Both the CA and PVA EPICS transports generate screens from the same controller structure, so the techniques shown here apply to both.

Group Attributes and Commands into Boxes#

By default, all attributes and commands on a controller appear as a flat list of widgets on the generated screen. Assigning a group string places them together inside a labelled box.

from fastcs.attributes import AttrR, AttrRW
from fastcs.controllers import Controller
from fastcs.datatypes import Float, Int
from fastcs.methods import command


class PowerSupplyController(Controller):
    voltage = AttrRW(Float(), group="Output")
    current = AttrRW(Float(), group="Output")
    power = AttrR(Float(), group="Output")

    temperature = AttrR(Float(), group="Status")
    fault_code = AttrR(Int(), group="Status")

    @command(group="Actions")
    async def reset_faults(self) -> None:
        ...

    @command(group="Actions")
    async def enable_output(self) -> None:
        ...

The generated screen will show three boxes — Output, Status, and Actions — each containing only the widgets assigned to that group. Attributes and commands with no group are placed outside any box, directly on the screen.

Use Sub-Controllers to Create Sub-Screens#

For devices with many attributes, a single flat screen becomes unwieldy. Splitting functionality across multiple controllers, connected with add_sub_controller(), causes the transport to generate a top-level screen with navigation links to per-sub-controller sub-screens.

from fastcs.attributes import AttrR, AttrRW
from fastcs.controllers import Controller
from fastcs.datatypes import Float, Int
from fastcs.methods import command


class ChannelController(Controller):
    voltage = AttrRW(Float(), group="Output")
    current = AttrRW(Float(), group="Output")
    temperature = AttrR(Float(), group="Status")

    @command(group="Actions")
    async def enable(self) -> None:
        ...


class MultiChannelPSU(Controller):
    total_power = AttrR(Float())

    @command()
    async def disable_all(self) -> None:
        ...

    def __init__(self, num_channels: int) -> None:
        super().__init__()
        for i in range(1, num_channels + 1):
            self.add_sub_controller(f"Ch{i:02d}", ChannelController())

The top-level screen for MultiChannelPSU shows TotalPower and DisableAll alongside buttons labelled Ch01, Ch02, … that each open the sub-screen for that channel. Each channel sub-screen then shows the Output, Status, and Actions boxes defined on ChannelController.