Create a Device with an EPICS Interface#
It is possible to make a device accessible over channel access with EPICS by using an EPICS adapter with the device.
Note
For information on EPICS, see here.
This adapter creates and runs a python soft IOC within the simulator process. The
PVs on this IOC can be accessed with the normal methods, eg caget
, caput
etc.
If you have multiple devices in your simulation that require an epics adapter this is possible, a large singleton IOC is created as a composite IOC of all the individual ones.
Using an EPICS adapter#
When you create an IOC you often will have a db file, depicting the PVs that should be present on it. The epics adapter allows for loading of these PVs from the DB file so that you do not have to populate them by hand. This file is loaded into the adapter when the component is initialised. For example the Femto component:
@dataclass
class Femto(ComponentConfig):
"""Femto simulation with EPICS IOC."""
initial_gain: float = 2.5
initial_current: float = 0.0
db_file: str = "path/to/record.db"
ioc_name: str = "FEMTO"
def __call__(self) -> Component: # noqa: D102
device=FemtoDevice(
initial_gain=self.initial_gain,
initial_current=self.initial_current,
)
adapters = [
AdapterContainer(
FemtoAdapter(device),
EpicsIo(self.ioc_name),
),
]
return DeviceComponent(
name=self.name,
device=device,
adapters=adapters,
)
However, any PV you wish to directly link to a device attribute you must override and provide the necessary methods to get and set that attribute.
See again the femto device. It is a signal amplifier that takes an input and outputs a current.
class FemtoDevice(Device):
"""Electronic signal amplifier."""
class Inputs(TypedDict):
input: float
class Outputs(TypedDict):
current: float
def __init__(
self,
initial_gain: float,
initial_current: float,
) -> None:
self.gain: float = initial_gain
self._current: float = initial_current
def set_gain(self, gain: float) -> None:
self.gain = gain
def get_gain(self) -> float:
return self.gain
def set_current(self, input_current: float) -> None:
self._output_current = input_current * self.gain
def get_current(self) -> float:
return self._output_current
def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]:
current_value = inputs["input"]
if current_value is not None:
self.set_current(current_value)
return DeviceUpdate(self.Outputs(current=self.get_current()), None)
The femto device has a PV GAIN as an ao
record (analogue out).
record(ao, "$(device):GAIN") {
field(DTYP, "Hy8001")
field(OMSL, "supervisory")
field(OUT, "#C1 S0 @")
field(DESC, "Gain value")
field(EGU, "A")
}
This means it is settable by the user, you should be able to caput
a gain
value to change the PV. In order to get the device to update its attribute gain
to reflect that, we must override the epics adapter function on_db_load
.
class FemtoAdapter(EpicsAdapter):
"""The adapter for the Femto device."""
device: FemtoDevice
def __init__(self, device: AmplifierDevice) -> None:
super().__init__()
self.device = device
async def callback(self, value) -> None:
"""Device callback function.
Args:
value (float): The value to set the gain to.
"""
self.device.set_gain(value)
await self.interrupt()
def on_db_load(self) -> None:
"""Customises records that have been loaded in to suit the simulation."""
builder.aOut(
"GAIN", initial_value=self.device.get_gain(), on_update=self.callback
)
self.link_input_on_interrupt(builder.aIn("GAIN_RBV"), self.device.get_gain)
self.link_input_on_interrupt(builder.aIn("CURRENT"), self.device.get_current)
We provide a callback function to set the device gain to the new value then
raise an interrupt, causing the device to update. This callback function is
assigned to the epics record GAIN
so that a change in that changes the device.
A similar linking process occurs for readable records, eg aIn
, however these
are just supplied with getter methods to the device attributes.
As a result the Femto device is accessible via EPICS. It gain can be set, and its gain and current read via the IOC.