From 7a2b564829f1a1996d368825ba8a3291e28b18b1 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 20 Nov 2023 16:37:13 +0000 Subject: [PATCH 1/2] Add fake signal generator with IOC --- examples/configs/signal-generator.yaml | 3 + src/tickit/adapters/epics.py | 97 +++- src/tickit/devices/signal_generator.edl | 595 ++++++++++++++++++++++++ src/tickit/devices/signal_generator.py | 204 ++++++++ 4 files changed, 898 insertions(+), 1 deletion(-) create mode 100644 examples/configs/signal-generator.yaml create mode 100644 src/tickit/devices/signal_generator.edl create mode 100644 src/tickit/devices/signal_generator.py diff --git a/examples/configs/signal-generator.yaml b/examples/configs/signal-generator.yaml new file mode 100644 index 000000000..64572ed52 --- /dev/null +++ b/examples/configs/signal-generator.yaml @@ -0,0 +1,3 @@ +- type: tickit.devices.signal_generator.EpicsSignalGenerator + name: gen + inputs: {} diff --git a/src/tickit/adapters/epics.py b/src/tickit/adapters/epics.py index 2846be684..bac9609dd 100644 --- a/src/tickit/adapters/epics.py +++ b/src/tickit/adapters/epics.py @@ -3,7 +3,8 @@ import logging from abc import abstractmethod from dataclasses import dataclass -from typing import Any, Callable, Dict, Set +from enum import Enum +from typing import Any, Awaitable, Callable, Dict, Optional, Set, TypeVar from softioc import asyncio_dispatcher, builder, softioc @@ -17,6 +18,8 @@ #: Ids of all adapters currently registered but not ready. _REGISTERED_ADAPTER_IDS: Set[int] = set() +_REGISTERED_IOC_BACKGROUND_TASKS: Set[Awaitable[None]] = set() + #: Iterator of unique IDs for new adapters _ID_COUNTER: itertools.count = itertools.count() @@ -36,6 +39,9 @@ def register_adapter() -> int: return adapter_id +def register_background_task(task: Awaitable[None]) -> None: + _REGISTERED_IOC_BACKGROUND_TASKS.add(task) + def notify_adapter_ready(adapter_id: int) -> None: """Notify the builder that a particular adapter has made all the records it needs. @@ -64,6 +70,13 @@ def _build_and_run_ioc() -> None: event_loop = asyncio.get_event_loop() dispatcher = asyncio_dispatcher.AsyncioDispatcher(event_loop) softioc.iocInit(dispatcher) + + async def run_background_tasks() -> None: + if len(_REGISTERED_IOC_BACKGROUND_TASKS) > 0: + await asyncio.wait(_REGISTERED_IOC_BACKGROUND_TASKS) + + dispatcher(run_background_tasks) + # dbl directly prints out all record names, so we have to check # the log level in order to only do it in DEBUG. if LOGGER.level <= logging.DEBUG: @@ -93,6 +106,80 @@ class EpicsAdapter: interrupt_records: Dict[InputRecord, Callable[[], Any]] = {} interrupt: RaiseInterrupt + def float_rbv( + self, + name: str, + getter: Callable[[], float], + setter: Callable[[float], None], + rbv_name: Optional[str] = None, + ): + rbv_name = rbv_name or f"{name}_RBV" + builder.aOut( + name, + initial_value=getter(), + on_update=self.interrupting_callback(setter), + ) + rbv = builder.aIn(rbv_name, initial_value=getter()) + self.link_input_on_interrupt(rbv, getter) + + def int_rbv( + self, + name: str, + getter: Callable[[], int], + setter: Callable[[int], None], + rbv_name: Optional[str] = None, + ): + rbv_name = rbv_name or f"{name}_RBV" + builder.mbbOut( + name, + initial_value=getter(), + on_update=self.interrupting_callback(setter), + ) + rbv = builder.mbbIn(rbv_name, initial_value=getter()) + self.link_input_on_interrupt(rbv, getter) + + def bool_rbv( + self, + name: str, + getter: Callable[[], bool], + setter: Callable[[bool], None], + rbv_name: Optional[str] = None, + ): + rbv_name = rbv_name or f"{name}_RBV" + builder.boolOut( + name, + initial_value=getter(), + on_update=self.interrupting_callback(setter), + ) + rbv = builder.boolIn(rbv_name, initial_value=getter()) + self.link_input_on_interrupt(rbv, getter) + + def bool_rbv( + self, + name: str, + getter: Callable[[], bool], + setter: Callable[[bool], None], + rbv_name: Optional[str] = None, + ): + rbv_name = rbv_name or f"{name}_RBV" + builder.boolOut( + name, + initial_value=getter(), + on_update=self.interrupting_callback(setter), + ) + rbv = builder.boolIn(rbv_name, initial_value=getter()) + self.link_input_on_interrupt(rbv, getter) + + + def interrupting_callback( + self, action: Callable[[Any], None] + ) -> Callable[[Any], Awaitable[None]]: + async def callback(value: Any) -> None: + action(value) + await self.interrupt() + + return callback + def link_input_on_interrupt( self, record: InputRecord, getter: Callable[[], Any] ) -> None: @@ -115,3 +202,11 @@ def after_update(self) -> None: current_value = getter() record.set(current_value) print(f"Record {record.name} updated to : {current_value}") + + def polling_interrupt(self, interval: float) -> None: + async def polling_task() -> None: + while True: + await asyncio.sleep(interval) + await self.interrupt() + + register_background_task(polling_task()) diff --git a/src/tickit/devices/signal_generator.edl b/src/tickit/devices/signal_generator.edl new file mode 100644 index 000000000..38540ac15 --- /dev/null +++ b/src/tickit/devices/signal_generator.edl @@ -0,0 +1,595 @@ +4 0 1 +beginScreenProperties +major 4 +minor 0 +release 1 +x 2067 +y 454 +w 620 +h 556 +font "arial-bold-r-14.0" +ctlFont "arial-bold-r-14.0" +btnFont "arial-bold-r-14.0" +fgColor index 14 +bgColor index 3 +textColor index 14 +ctlFgColor1 index 14 +ctlFgColor2 index 14 +ctlBgColor1 index 3 +ctlBgColor2 index 3 +topShadowColor index 1 +botShadowColor index 11 +title "Linkam" +showGrid +endScreenProperties + +# (Rectangle) +object activeRectangleClass +beginObjectProperties +major 4 +minor 0 +release 0 +x 317 +y 57 +w 140 +h 160 +lineColor index 14 +fill +fillColor index 5 +endObjectProperties + +# (X-Y Graph) +object xyGraphClass +beginObjectProperties +major 4 +minor 8 +release 0 +# Geometry +x 3 +y 222 +w 457 +h 227 +# Appearance +plotAreaBorder +autoScaleUpdateMs 5000 +autoScaleThreshPct 80 +fgColor index 13 +bgColor index 7 +gridColor index 33 +font "arial-bold-r-14.0" +# Operating Modes +plotMode "plotLastNPts" +nPts 3000 +updateTimerMs 100 +# X axis properties +showXAxis +xAxisStyle "time" +xAxisSrc "AutoScale" +xMax 1 +xShowLabelGrid +# Y axis properties +showYAxis +yAxisSrc "AutoScale" +yMin -5 +yMax 5 +yShowLabelGrid +yShowMajorGrid +# Y2 axis properties +y2AxisSrc "AutoScale" +y2Max 1 +# Trace Properties +numTraces 1 +yPv { + 0 "$(P):Signal_RBV" +} +plotColor { + 0 index 36 +} +endObjectProperties + +# (Rectangle) +object activeRectangleClass +beginObjectProperties +major 4 +minor 0 +release 0 +x 7 +y 57 +w 304 +h 160 +lineColor index 14 +fill +fillColor index 5 +endObjectProperties + +# (Rectangle) +object activeRectangleClass +beginObjectProperties +major 4 +minor 0 +release 0 +x 0 +y 0 +w 624 +h 40 +lineColor index 61 +fill +fillColor index 61 +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 16 +y 11 +w 204 +h 21 +font "arial-bold-r-18.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "$(P) - Signal Generator" +} +autoSize +endObjectProperties + +# (Exit Button) +object activeExitButtonClass +beginObjectProperties +major 4 +minor 1 +release 0 +x 367 +y 457 +w 90 +h 30 +fgColor index 46 +bgColor index 3 +topShadowColor index 1 +botShadowColor index 11 +label "EXIT" +font "arial-medium-r-18.0" +3d +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 12 +y 47 +w 57 +h 16 +font "arial-bold-r-14.0" +fgColor index 14 +bgColor index 5 +value { + "Settings" +} +autoSize +border +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 337 +y 127 +w 45 +h 16 +font "arial-bold-r-14.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Signal" +} +autoSize +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 321 +y 46 +w 45 +h 16 +font "arial-bold-r-14.0" +fgColor index 14 +bgColor index 5 +value { + "Status" +} +autoSize +border +endObjectProperties + +# (Text Monitor) +object activeXTextDspClass:noedit +beginObjectProperties +major 4 +minor 6 +release 0 +x 337 +y 157 +w 110 +h 20 +controlPv "$(P):Signal_RBV" +format "float" +font "arial-bold-r-14.0" +fontAlign "center" +fgColor index 16 +fgAlarm +bgColor index 10 +precision 3 +nullColor index 14 +fastUpdate +useHexPrefix +showUnits +newPos +objType "monitors" +noExecuteClipMask +endObjectProperties + +# (Text Monitor) +object activeXTextDspClass:noedit +beginObjectProperties +major 4 +minor 6 +release 0 +x 225 +y 64 +w 70 +h 20 +controlPv "$(P):Amplitude_RBV" +format "float" +font "arial-bold-r-14.0" +fontAlign "center" +fgColor index 16 +fgAlarm +bgColor index 10 +precision 3 +nullColor index 14 +fastUpdate +useHexPrefix +showUnits +newPos +objType "monitors" +noExecuteClipMask +endObjectProperties + +# (Textentry) +object TextentryClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 126 +y 62 +w 95 +h 23 +controlPv "$(P):Amplitude" +displayMode "decimal" +precision 3 +fgColor index 25 +fgAlarm +bgColor index 3 +fill +font "arial-bold-r-14.0" +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 27 +y 67 +w 72 +h 16 +font "arial-bold-r-14.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Amplitude" +} +autoSize +endObjectProperties + +# (Text Monitor) +object activeXTextDspClass:noedit +beginObjectProperties +major 4 +minor 6 +release 0 +x 225 +y 94 +w 70 +h 20 +controlPv "$(P):Offset_RBV" +format "float" +font "arial-bold-r-14.0" +fontAlign "center" +fgColor index 16 +fgAlarm +bgColor index 10 +precision 3 +nullColor index 14 +fastUpdate +useHexPrefix +showUnits +newPos +objType "monitors" +noExecuteClipMask +endObjectProperties + +# (Textentry) +object TextentryClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 126 +y 92 +w 95 +h 23 +controlPv "$(P):Offset" +displayMode "decimal" +precision 3 +fgColor index 25 +fgAlarm +bgColor index 3 +fill +font "arial-bold-r-14.0" +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 57 +y 97 +w 43 +h 16 +font "arial-bold-r-14.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Offset" +} +autoSize +endObjectProperties + +# (Text Monitor) +object activeXTextDspClass:noedit +beginObjectProperties +major 4 +minor 6 +release 0 +x 225 +y 124 +w 70 +h 20 +controlPv "$(P):Frequency_RBV" +format "float" +font "arial-bold-r-14.0" +fontAlign "center" +fgColor index 16 +fgAlarm +bgColor index 10 +precision 3 +nullColor index 14 +fastUpdate +useHexPrefix +showUnits +newPos +objType "monitors" +noExecuteClipMask +endObjectProperties + +# (Textentry) +object TextentryClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 126 +y 122 +w 95 +h 23 +controlPv "$(P):Frequency" +displayMode "decimal" +precision 3 +fgColor index 25 +fgAlarm +bgColor index 3 +fill +font "arial-bold-r-14.0" +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 27 +y 127 +w 75 +h 16 +font "arial-bold-r-14.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Frequency" +} +autoSize +endObjectProperties + +# (Text Monitor) +object activeXTextDspClass:noedit +beginObjectProperties +major 4 +minor 6 +release 0 +x 225 +y 154 +w 70 +h 20 +controlPv "$(P):GateThreshold_RBV" +format "float" +font "arial-bold-r-14.0" +fontAlign "center" +fgColor index 16 +fgAlarm +bgColor index 10 +precision 3 +nullColor index 14 +fastUpdate +useHexPrefix +showUnits +newPos +objType "monitors" +noExecuteClipMask +endObjectProperties + +# (Textentry) +object TextentryClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 126 +y 152 +w 95 +h 23 +controlPv "$(P):GateThreshold" +displayMode "decimal" +precision 3 +fgColor index 25 +fgAlarm +bgColor index 3 +fill +font "arial-bold-r-14.0" +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 27 +y 157 +w 72 +h 16 +font "arial-bold-r-14.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Threshold" +} +autoSize +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 347 +y 87 +w 33 +h 16 +font "arial-bold-r-14.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Gate" +} +autoSize +endObjectProperties + +# (Byte) +object ByteClass +beginObjectProperties +major 4 +minor 0 +release 0 +x 407 +y 87 +w 40 +h 20 +controlPv "$(P):Gate_RBV" +lineColor index 14 +onColor index 15 +offColor index 19 +endian "little" +numBits 1 +endObjectProperties + +# (Byte) +object ByteClass +beginObjectProperties +major 4 +minor 0 +release 0 +x 407 +y 67 +w 40 +h 20 +controlPv "$(P):Enabled_RBV" +lineColor index 14 +onColor index 15 +offColor index 19 +endian "little" +numBits 1 +endObjectProperties + +# (Button) +object activeButtonClass +beginObjectProperties +major 4 +minor 1 +release 0 +x 327 +y 67 +w 70 +h 20 +fgColor index 14 +onColor index 3 +offColor index 3 +inconsistentColor index 3 +topShadowColor index 1 +botShadowColor index 11 +controlPv "$(P):Enabled" +indicatorPv "$(P):Enabled" +onLabel "Disable" +offLabel "Enable" +labelType "literal" +3d +font "arial-bold-r-14.0" +objType "controls" +endObjectProperties + diff --git a/src/tickit/devices/signal_generator.py b/src/tickit/devices/signal_generator.py new file mode 100644 index 000000000..feaeff69e --- /dev/null +++ b/src/tickit/devices/signal_generator.py @@ -0,0 +1,204 @@ +import logging +import math +from enum import Enum +from typing import Any, TypedDict + +import pydantic.v1.dataclasses +from pydantic.v1 import Field +from softioc import builder + +from tickit.adapters.epics import EpicsAdapter +from tickit.adapters.io import EpicsIo +from tickit.core.adapter import AdapterContainer +from tickit.core.components.component import Component, ComponentConfig +from tickit.core.components.device_component import DeviceComponent +from tickit.core.device import Device, DeviceUpdate +from tickit.core.typedefs import SimTime + + +@pydantic.v1.dataclasses.dataclass +class WaveConfig: + amplitude: float = 1.0 + amplitude_offset: float = 1.0 + frequency: float = 1.0 + enabled: bool = True + + +class SignalGeneratorDevice(Device): + """A simple device which produces a pre-configured value.""" + + #: An empty typed mapping of device inputs + class Inputs(TypedDict): + ... + + #: A typed mapping containing the 'value' output value + class Outputs(TypedDict): + value: float + gate: bool + + _wave: WaveConfig + _gate_threshold: float + + _value: float + _gate: bool + + def __init__(self, wave: WaveConfig, gate_threshold: float = 0.5) -> None: + """A constructor of the source, which takes the pre-configured output value. + + Args: + value (Any): A pre-configured output value. + """ + self._wave = wave + self._gate_threshold = gate_threshold + self._value = 0.0 + self._gate = False + + def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: + """The update method which produces the pre-configured output value. + + Args: + time (SimTime): The current simulation time (in nanoseconds). + inputs (State): A mapping of inputs to the device and their values. + + Returns: + DeviceUpdate[Outputs]: + The produced update event which contains the pre-configured value, and + never requests a callback. + """ + self._value = self._compute_wave(time) + self._gate = self._value > self._gate_threshold + return DeviceUpdate( + SignalGeneratorDevice.Outputs( + value=self._value, + gate=self._gate, + ), + None, + ) + + def get_amplitude(self) -> float: + return self._wave.amplitude + + def set_amplitude(self, amplitude: float) -> None: + self._wave.amplitude = amplitude + + def get_amplitude_offset(self) -> float: + return self._wave.amplitude + + def set_amplitude_offset(self, amplitude_offset: float) -> None: + self._wave.amplitude_offset = amplitude_offset + + def get_frequency(self) -> float: + return self._wave.amplitude + + def set_frequency(self, frequency: float) -> None: + self._wave.frequency = frequency + + def get_gate_threshold(self) -> float: + return self._gate_threshold + + def set_gate_threshold(self, gate_threshold: float) -> None: + self._gate_threshold = gate_threshold + + def is_enabled(self) -> bool: + return self._wave.enabled + + def set_enabled(self, enabled: bool) -> None: + self._wave.enabled = enabled + + def get_value(self) -> float: + return self._value + + def is_gate_open(self) -> bool: + return self._gate + + def _compute_wave(self, time: SimTime) -> float: + if self._wave.enabled: + return self._sine(time) + else: + return 0.0 + + def _sine(self, time: SimTime) -> float: + return self._wave.amplitude_offset + ( + self._wave.amplitude * self._sinosoid(time) + ) + + def _sinosoid(self, time: SimTime) -> float: + time_seconds = time * 1e-9 + return math.sin(2 * math.pi * self._wave.frequency * time_seconds) + + +class SignalGeneratorAdapter(EpicsAdapter): + """The adapter for the Femto device.""" + + device: SignalGeneratorDevice + + def __init__(self, device: SignalGeneratorDevice) -> None: + super().__init__() + self.device = device + + def on_db_load(self) -> None: + """Customises records that have been loaded in to suit the simulation.""" + self.float_rbv( + "Amplitude", + self.device.get_amplitude, + self.device.set_amplitude, + ) + self.float_rbv( + "Offset", + self.device.get_amplitude_offset, + self.device.set_amplitude_offset, + ) + self.float_rbv( + "Frequency", + self.device.get_frequency, + self.device.set_frequency, + ) + self.float_rbv( + "GateThreshold", + self.device.get_gate_threshold, + self.device.set_gate_threshold, + ) + self.bool_rbv( + "Enabled", + self.device.is_enabled, + self.device.set_enabled, + ) + + self.link_input_on_interrupt(builder.aIn("Signal_RBV"), self.device.get_value) + self.link_input_on_interrupt(builder.aIn("Gate_RBV"), self.device.is_gate_open) + + self.polling_interrupt(0.1) + + +@pydantic.v1.dataclasses.dataclass +class SignalGenerator(ComponentConfig): + """Source of a fixed value.""" + + wave: WaveConfig = Field(default_factory=WaveConfig) + + def __call__(self) -> Component: # noqa: D102 + return DeviceComponent( + name=self.name, device=SignalGeneratorDevice(wave=self.wave) + ) + + +@pydantic.v1.dataclasses.dataclass +class EpicsSignalGenerator(ComponentConfig): + """Source of a fixed value.""" + + wave: WaveConfig = Field(default_factory=WaveConfig) + ioc_name: str = "SIGNALGEN" + + def __call__(self) -> Component: # noqa: D102 + device = SignalGeneratorDevice(wave=self.wave) + adapters = [ + AdapterContainer( + SignalGeneratorAdapter(device), + EpicsIo(self.ioc_name), + ) + ] + return DeviceComponent( + name=self.name, + device=device, + adapters=adapters, + ) From 94fc01d37abfae72320f7f3a849a863d6af11631 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Wed, 22 Nov 2023 13:56:05 +0000 Subject: [PATCH 2/2] Add sink to config --- examples/configs/signal-generator.yaml | 12 ++++++++++++ src/tickit/devices/signal_generator.edl | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/examples/configs/signal-generator.yaml b/examples/configs/signal-generator.yaml index 64572ed52..f7faeccfa 100644 --- a/examples/configs/signal-generator.yaml +++ b/examples/configs/signal-generator.yaml @@ -1,3 +1,15 @@ - type: tickit.devices.signal_generator.EpicsSignalGenerator name: gen inputs: {} +- type: tickit.devices.sink.Sink + name: sink + inputs: + input: + component: gen + port: value +- type: tickit.devices.sink.Sink + name: sink + inputs: + input: + component: gen + port: gate diff --git a/src/tickit/devices/signal_generator.edl b/src/tickit/devices/signal_generator.edl index 38540ac15..c3a3cf3d5 100644 --- a/src/tickit/devices/signal_generator.edl +++ b/src/tickit/devices/signal_generator.edl @@ -3,8 +3,8 @@ beginScreenProperties major 4 minor 0 release 1 -x 2067 -y 454 +x 2660 +y 386 w 620 h 556 font "arial-bold-r-14.0" @@ -19,7 +19,7 @@ ctlBgColor1 index 3 ctlBgColor2 index 3 topShadowColor index 1 botShadowColor index 11 -title "Linkam" +title "Sine Generator" showGrid endScreenProperties