Skip to content

Commit

Permalink
IOC singleton for EpicsAdapter (#66)
Browse files Browse the repository at this point in the history
Module for IOC management, used by all `EpicsAdapter`s.
Share a single channel access context
  • Loading branch information
callumforrester authored Jul 14, 2022
1 parent 3141025 commit 215582f
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 17 deletions.
23 changes: 23 additions & 0 deletions examples/configs/multi-ioc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
- tickit.devices.pneumatic.Pneumatic:
name: filter1
inputs: {}
initial_speed: 0.5
initial_state: False
ioc_name: PNEUMATIC
db_file: tickit/devices/pneumatic/db_files/filter1.db
- tickit.devices.sink.Sink:
name: contr_sink
inputs:
input: filter1:output
- tickit.devices.femto.Current:
name: current_device
inputs: {}
callback_period: 1000000000
- tickit.devices.femto.Femto:
name: femto
inputs:
input: current_device:output
initial_gain: 2.5
initial_current: 0.0
db_file: tickit/devices/femto/record.db
ioc_name: FEMTO
Empty file.
File renamed without changes.
28 changes: 28 additions & 0 deletions tests/devices/multi/test_multi_device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import asyncio

import pytest
from aioca import caget, caput


@pytest.mark.asyncio
@pytest.mark.parametrize(
"tickit_process", ["examples/configs/multi-ioc.yaml"], indirect=True
)
async def test_femto_system(tickit_process):
assert (await caget("FEMTO:GAIN_RBV")) == 2.5
current = await caget("FEMTO:CURRENT")
assert 100 * 2.5 <= current < 200 * 2.5
await caput("FEMTO:GAIN", 0.01)
await asyncio.sleep(0.5)
assert (await caget("FEMTO:GAIN_RBV")) == 0.01
current = await caget("FEMTO:CURRENT")
assert 100 * 0.01 <= current < 200 * 0.01

async def toggle(expected: bool):
assert (await caget("PNEUMATIC:FILTER_RBV")) != expected
await caput("PNEUMATIC:FILTER", expected)
await asyncio.sleep(0.8)
assert (await caget("PNEUMATIC:FILTER_RBV")) == expected

await toggle(True)
await toggle(False)
3 changes: 3 additions & 0 deletions tickit/adapters/epicsadapter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .adapter import EpicsAdapter, InputRecord, OutputRecord

__all__ = ["EpicsAdapter", "InputRecord", "OutputRecord"]
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import asyncio
import os
import re
from abc import abstractmethod
from dataclasses import dataclass
from tempfile import NamedTemporaryFile
from typing import Any, Callable, Dict

from softioc import asyncio_dispatcher, builder, softioc
from softioc import builder, softioc

from tickit.core.adapter import Adapter, RaiseInterrupt
from tickit.core.device import Device

from .ioc_manager import notify_adapter_ready, register_adapter


@dataclass(frozen=True)
class InputRecord:
Expand Down Expand Up @@ -41,6 +42,7 @@ def __init__(self, db_file: str, ioc_name: str) -> None:
self.db_file = db_file
self.ioc_name = ioc_name
self.interrupt_records: Dict[InputRecord, Callable[[], Any]] = {}
self.ioc_num = register_adapter()

def link_input_on_interrupt(
self, record: InputRecord, getter: Callable[[], Any]
Expand Down Expand Up @@ -76,23 +78,13 @@ def load_records_without_DTYP_fields(self):
softioc.dbLoadDatabase(out.name, substitutions=f"device={self.ioc_name}")
os.unlink(out.name)

def build_ioc(self) -> None:
"""Builds an EPICS python soft IOC for the adapter."""
builder.SetDeviceName(self.ioc_name)

self.load_records_without_DTYP_fields()
self.on_db_load()

softioc.devIocStats(self.ioc_name)

builder.LoadDatabase()
event_loop = asyncio.get_event_loop()
dispatcher = asyncio_dispatcher.AsyncioDispatcher(event_loop)
softioc.iocInit(dispatcher)

async def run_forever(
self, device: Device, raise_interrupt: RaiseInterrupt
) -> None:
"""Runs the server continously."""
await super().run_forever(device, raise_interrupt)
self.build_ioc()
builder.SetDeviceName(self.ioc_name)
self.load_records_without_DTYP_fields()
self.on_db_load()
builder.UnsetDevice()
notify_adapter_ready(self.ioc_num)
67 changes: 67 additions & 0 deletions tickit/adapters/epicsadapter/ioc_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import asyncio
import itertools
import logging
from typing import Set

from softioc import asyncio_dispatcher, builder, softioc

LOGGER = logging.getLogger(__name__)

#: Name for the global Tickit IOC, will prefix some meta PVs.
_TICKIT_IOC_NAME: str = "TICKIT_IOC"

#: Ids of all adapters currently registered but not ready.
_REGISTERED_ADAPTER_IDS: Set[int] = set()

#: Iterator of unique IDs for new adapters
_ID_COUNTER: itertools.count = itertools.count()


def register_adapter() -> int:
"""Register a new adapter that may be creating records for the process-wide IOC.
The IOC will not be initialized until all registered adapters have notified that
they are ready.
Returns:
int: A unique ID for this adapter to use when notifiying that it is ready.
"""
adapter_id = next(_ID_COUNTER)
LOGGER.debug(f"New IOC adapter registering with ID: {adapter_id}")
_REGISTERED_ADAPTER_IDS.add(adapter_id)
return adapter_id


def notify_adapter_ready(adapter_id: int) -> None:
"""Notify the builder that a particular adpater has made all the records it needs to.
Once all registered adapters have notified, the IOC will start.
Args:
adapter_id (int): Unique ID of the adapter
"""
_REGISTERED_ADAPTER_IDS.remove(adapter_id)
LOGGER.debug(f"IOC adapter #{adapter_id} reports ready")
if not _REGISTERED_ADAPTER_IDS:
LOGGER.debug("All registered adapters are ready, starting IOC")
_build_and_run_ioc()


def _build_and_run_ioc() -> None:
"""Build an EPICS python soft IOC for the adapter."""
LOGGER.debug("Initializing database")

# Records become immutable after this point
builder.SetDeviceName(_TICKIT_IOC_NAME)
softioc.devIocStats(_TICKIT_IOC_NAME)
builder.LoadDatabase()

LOGGER.debug("Starting IOC")
event_loop = asyncio.get_event_loop()
dispatcher = asyncio_dispatcher.AsyncioDispatcher(event_loop)
softioc.iocInit(dispatcher)
# 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:
softioc.dbl()
LOGGER.debug("IOC started")

0 comments on commit 215582f

Please sign in to comment.