From 7a2b564829f1a1996d368825ba8a3291e28b18b1 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 20 Nov 2023 16:37:13 +0000 Subject: [PATCH 1/6] 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/6] 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 From 4df69ab7aa8089222117ac1bed3e426d27f18fd3 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Wed, 29 Nov 2023 16:23:30 +0000 Subject: [PATCH 3/6] Add capacity for precison and RBV --- src/tickit/adapters/epics.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/tickit/adapters/epics.py b/src/tickit/adapters/epics.py index bac9609dd..bca2c20c5 100644 --- a/src/tickit/adapters/epics.py +++ b/src/tickit/adapters/epics.py @@ -112,16 +112,24 @@ def float_rbv( getter: Callable[[], float], setter: Callable[[float], None], rbv_name: Optional[str] = None, + precision: int = 2, ): rbv_name = rbv_name or f"{name}_RBV" builder.aOut( name, initial_value=getter(), on_update=self.interrupting_callback(setter), + PREC=precision, ) - rbv = builder.aIn(rbv_name, initial_value=getter()) + rbv = builder.aIn(rbv_name, initial_value=getter(), PREC=precision,) self.link_input_on_interrupt(rbv, getter) + def float_ro(self, name: str, getter: Callable[[], float], precision: int = 3,): + self.link_input_on_interrupt( + builder.aIn(name, PREC=precision), + getter, + ) + def int_rbv( self, name: str, @@ -154,22 +162,11 @@ def bool_rbv( 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), + def bool_ro(self, name: str, getter: Callable[[], float]): + self.link_input_on_interrupt( + builder.boolIn(name), + getter, ) - rbv = builder.boolIn(rbv_name, initial_value=getter()) - self.link_input_on_interrupt(rbv, getter) - def interrupting_callback( self, action: Callable[[Any], None] From 2625c583dbfc265fe9effc4d0b030c4768d7d39b Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Wed, 29 Nov 2023 16:26:43 +0000 Subject: [PATCH 4/6] Delete signal generator and move it to tickit devices --- examples/configs/signal-generator.yaml | 15 - src/tickit/devices/signal_generator.edl | 595 ------------------------ src/tickit/devices/signal_generator.py | 204 -------- 3 files changed, 814 deletions(-) delete mode 100644 examples/configs/signal-generator.yaml delete mode 100644 src/tickit/devices/signal_generator.edl delete mode 100644 src/tickit/devices/signal_generator.py diff --git a/examples/configs/signal-generator.yaml b/examples/configs/signal-generator.yaml deleted file mode 100644 index f7faeccfa..000000000 --- a/examples/configs/signal-generator.yaml +++ /dev/null @@ -1,15 +0,0 @@ -- 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 deleted file mode 100644 index c3a3cf3d5..000000000 --- a/src/tickit/devices/signal_generator.edl +++ /dev/null @@ -1,595 +0,0 @@ -4 0 1 -beginScreenProperties -major 4 -minor 0 -release 1 -x 2660 -y 386 -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 "Sine Generator" -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 deleted file mode 100644 index feaeff69e..000000000 --- a/src/tickit/devices/signal_generator.py +++ /dev/null @@ -1,204 +0,0 @@ -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 51ce1ba73ab5003229e1393967f3145ca8e1ef25 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 1 Dec 2023 13:54:10 +0000 Subject: [PATCH 5/6] Correctly initialise epics adapter interrupt records --- src/tickit/adapters/epics.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tickit/adapters/epics.py b/src/tickit/adapters/epics.py index bca2c20c5..149dcc904 100644 --- a/src/tickit/adapters/epics.py +++ b/src/tickit/adapters/epics.py @@ -4,7 +4,7 @@ from abc import abstractmethod from dataclasses import dataclass from enum import Enum -from typing import Any, Awaitable, Callable, Dict, Optional, Set, TypeVar +from typing import Any, Awaitable, Callable, Dict, Generic, Optional, Set, TypeVar from softioc import asyncio_dispatcher, builder, softioc @@ -83,7 +83,6 @@ async def run_background_tasks() -> None: softioc.dbl() # type: ignore LOGGER.debug("IOC started") - @dataclass(frozen=True) class InputRecord: """A data container representing an EPICS input record.""" @@ -103,9 +102,12 @@ class OutputRecord: class EpicsAdapter: """An adapter interface for the EpicsIo.""" - interrupt_records: Dict[InputRecord, Callable[[], Any]] = {} + interrupt_records: Dict[InputRecord, Callable[[], Any]] interrupt: RaiseInterrupt + def __init__(self) -> None: + self.interrupt_records = {} + def float_rbv( self, name: str, @@ -124,7 +126,7 @@ def float_rbv( rbv = builder.aIn(rbv_name, initial_value=getter(), PREC=precision,) self.link_input_on_interrupt(rbv, getter) - def float_ro(self, name: str, getter: Callable[[], float], precision: int = 3,): + def float_ro(self, name: str, getter: Callable[[], float], precision: int = 2,): self.link_input_on_interrupt( builder.aIn(name, PREC=precision), getter, From f4495540eb8cf449976909297e2d21863561ca50 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 1 Dec 2023 15:58:58 +0000 Subject: [PATCH 6/6] Fix imports --- src/tickit/adapters/epics.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/tickit/adapters/epics.py b/src/tickit/adapters/epics.py index 149dcc904..62e9180fd 100644 --- a/src/tickit/adapters/epics.py +++ b/src/tickit/adapters/epics.py @@ -3,8 +3,7 @@ import logging from abc import abstractmethod from dataclasses import dataclass -from enum import Enum -from typing import Any, Awaitable, Callable, Dict, Generic, Optional, Set, TypeVar +from typing import Any, Awaitable, Callable, Dict, Optional, Set from softioc import asyncio_dispatcher, builder, softioc @@ -42,6 +41,7 @@ def register_adapter() -> int: 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. @@ -83,6 +83,7 @@ async def run_background_tasks() -> None: softioc.dbl() # type: ignore LOGGER.debug("IOC started") + @dataclass(frozen=True) class InputRecord: """A data container representing an EPICS input record.""" @@ -123,10 +124,19 @@ def float_rbv( on_update=self.interrupting_callback(setter), PREC=precision, ) - rbv = builder.aIn(rbv_name, initial_value=getter(), PREC=precision,) + rbv = builder.aIn( + rbv_name, + initial_value=getter(), + PREC=precision, + ) self.link_input_on_interrupt(rbv, getter) - def float_ro(self, name: str, getter: Callable[[], float], precision: int = 2,): + def float_ro( + self, + name: str, + getter: Callable[[], float], + precision: int = 2, + ): self.link_input_on_interrupt( builder.aIn(name, PREC=precision), getter, @@ -207,5 +217,5 @@ async def polling_task() -> None: while True: await asyncio.sleep(interval) await self.interrupt() - + register_background_task(polling_task())