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.