Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable controller initialisation on main event loop #49

Merged
merged 4 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 111 additions & 83 deletions src/fastcs/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,68 @@
from collections import defaultdict
from collections.abc import Callable
from types import MethodType
from typing import Any

from softioc.asyncio_dispatcher import AsyncioDispatcher

from .attributes import AttrR, AttrW, Sender, Updater
from .controller import Controller
from .exceptions import FastCSException
from .mapping import Mapping, SingleMapping


def _get_initial_tasks(mapping: Mapping) -> list[Callable]:
initial_tasks: list[Callable] = []
initial_tasks.append(mapping.controller.connect)
return initial_tasks


def _create_periodic_scan_task(period, methods: list[Callable]) -> Callable:
async def scan_task() -> None:
while True:
await asyncio.gather(*[method() for method in methods])
await asyncio.sleep(period)

return scan_task


def _get_periodic_scan_tasks(scan_dict: dict[float, list[Callable]]) -> list[Callable]:
periodic_scan_tasks: list[Callable] = []
for period, methods in scan_dict.items():
periodic_scan_tasks.append(_create_periodic_scan_task(period, methods))

return periodic_scan_tasks


def _add_scan_method_tasks(
scan_dict: dict[float, list[Callable]], single_mapping: SingleMapping
):
for method in single_mapping.scan_methods.values():
scan_dict[method.period].append(
MethodType(method.fn, single_mapping.controller)
class Backend:
_initial_tasks: list[Callable] = []
_context: dict[str, Any] = {}

def __init__(
self, controller: Controller, loop: asyncio.AbstractEventLoop | None = None
):
self._dispatcher = AsyncioDispatcher(loop)
self._loop = self._dispatcher.loop
self._controller = controller

self._initial_tasks.append(controller.connect)

asyncio.run_coroutine_threadsafe(
self._controller.initialise(), self._loop
).result()

self._mapping = Mapping(self._controller)
self._link_process_tasks()

self._context.update(
{
"dispatcher": self._dispatcher,
"controller": self._controller,
"mapping": self._mapping,
}
)

def _link_process_tasks(self):
for single_mapping in self._mapping.get_controller_mappings():
_link_single_controller_put_tasks(single_mapping)
_link_attribute_sender_class(single_mapping)

def _create_updater_callback(attribute, controller):
async def callback():
try:
await attribute.updater.update(controller, attribute)
except Exception as e:
print(
f"Update loop in {attribute.updater} stopped:\n"
f"{e.__class__.__name__}: {e}"
)
raise

return callback

def run(self):
self._run_initial_tasks()
self._start_scan_tasks()

def _add_attribute_updater_tasks(
scan_dict: dict[float, list[Callable]], single_mapping: SingleMapping
):
for attribute in single_mapping.attributes.values():
match attribute:
case AttrR(updater=Updater(update_period=update_period)) as attribute:
callback = _create_updater_callback(
attribute, single_mapping.controller
)
scan_dict[update_period].append(callback)
self._run()

def _run_initial_tasks(self):
for task in self._initial_tasks:
future = asyncio.run_coroutine_threadsafe(task(), self._loop)
future.result()

def _get_scan_tasks(mapping: Mapping) -> list[Callable]:
scan_dict: dict[float, list[Callable]] = defaultdict(list)
def _start_scan_tasks(self):
scan_tasks = _get_scan_tasks(self._mapping)

for single_mapping in mapping.get_controller_mappings():
_add_scan_method_tasks(scan_dict, single_mapping)
_add_attribute_updater_tasks(scan_dict, single_mapping)
for task in scan_tasks:
asyncio.run_coroutine_threadsafe(task(), self._loop)

scan_tasks = _get_periodic_scan_tasks(scan_dict)
return scan_tasks
def _run(self):
raise NotImplementedError("Specific Backend must implement _run")

Check warning on line 66 in src/fastcs/backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backend.py#L66

Added line #L66 was not covered by tests


def _link_single_controller_put_tasks(single_mapping: SingleMapping) -> None:
Expand All @@ -94,13 +83,6 @@
)


def _create_sender_callback(attribute, controller):
async def callback(value):
await attribute.sender.put(controller, attribute, value)

return callback


def _link_attribute_sender_class(single_mapping: SingleMapping) -> None:
for attr_name, attribute in single_mapping.attributes.items():
match attribute:
Expand All @@ -113,25 +95,71 @@
attribute.set_process_callback(callback)


class Backend:
def __init__(self, mapping: Mapping, loop: asyncio.AbstractEventLoop):
self._mapping = mapping
self._loop = loop
def _create_sender_callback(attribute, controller):
async def callback(value):
await attribute.sender.put(controller, attribute, value)

Check warning on line 100 in src/fastcs/backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backend.py#L100

Added line #L100 was not covered by tests

def link_process_tasks(self):
for single_mapping in self._mapping.get_controller_mappings():
_link_single_controller_put_tasks(single_mapping)
_link_attribute_sender_class(single_mapping)
return callback

def run_initial_tasks(self):
initial_tasks = _get_initial_tasks(self._mapping)

for task in initial_tasks:
future = asyncio.run_coroutine_threadsafe(task(), self._loop)
future.result()
def _get_scan_tasks(mapping: Mapping) -> list[Callable]:
scan_dict: dict[float, list[Callable]] = defaultdict(list)

def start_scan_tasks(self):
scan_tasks = _get_scan_tasks(self._mapping)
for single_mapping in mapping.get_controller_mappings():
_add_scan_method_tasks(scan_dict, single_mapping)
_add_attribute_updater_tasks(scan_dict, single_mapping)

for task in scan_tasks:
asyncio.run_coroutine_threadsafe(task(), self._loop)
scan_tasks = _get_periodic_scan_tasks(scan_dict)
return scan_tasks


def _add_scan_method_tasks(
scan_dict: dict[float, list[Callable]], single_mapping: SingleMapping
):
for method in single_mapping.scan_methods.values():
scan_dict[method.period].append(
MethodType(method.fn, single_mapping.controller)
)


def _add_attribute_updater_tasks(
scan_dict: dict[float, list[Callable]], single_mapping: SingleMapping
):
for attribute in single_mapping.attributes.values():
match attribute:
case AttrR(updater=Updater(update_period=update_period)) as attribute:
callback = _create_updater_callback(
attribute, single_mapping.controller
)
scan_dict[update_period].append(callback)


def _create_updater_callback(attribute, controller):
async def callback():
try:
await attribute.updater.update(controller, attribute)
except Exception as e:
print(

Check warning on line 142 in src/fastcs/backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backend.py#L141-L142

Added lines #L141 - L142 were not covered by tests
f"Update loop in {attribute.updater} stopped:\n"
f"{e.__class__.__name__}: {e}"
)
raise

Check warning on line 146 in src/fastcs/backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backend.py#L146

Added line #L146 was not covered by tests

return callback


def _get_periodic_scan_tasks(scan_dict: dict[float, list[Callable]]) -> list[Callable]:
periodic_scan_tasks: list[Callable] = []
for period, methods in scan_dict.items():
periodic_scan_tasks.append(_create_periodic_scan_task(period, methods))

return periodic_scan_tasks


def _create_periodic_scan_task(period, methods: list[Callable]) -> Callable:
async def scan_task() -> None:
while True:
await asyncio.gather(*[method() for method in methods])
await asyncio.sleep(period)

return scan_task
31 changes: 7 additions & 24 deletions src/fastcs/backends/asyncio_backend.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
from softioc import asyncio_dispatcher, softioc
from softioc import softioc

from fastcs.backend import Backend
from fastcs.mapping import Mapping
from fastcs.controller import Controller


class AsyncioBackend:
def __init__(self, mapping: Mapping):
self._mapping = mapping

def run_interactive_session(self):
# Create an asyncio dispatcher; the event loop is now running
dispatcher = asyncio_dispatcher.AsyncioDispatcher()

backend = Backend(self._mapping, dispatcher.loop)

backend.link_process_tasks()
backend.run_initial_tasks()
backend.start_scan_tasks()
class AsyncioBackend(Backend):
def __init__(self, controller: Controller): # noqa: F821
super().__init__(controller)

Check warning on line 9 in src/fastcs/backends/asyncio_backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/asyncio_backend.py#L9

Added line #L9 was not covered by tests

def _run(self):
# Run the interactive shell
global_variables = globals()
global_variables.update(
{
"dispatcher": dispatcher,
"mapping": self._mapping,
"controller": self._mapping.controller,
}
)
softioc.interactive_ioc(globals())
softioc.interactive_ioc(self._context)

Check warning on line 13 in src/fastcs/backends/asyncio_backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/asyncio_backend.py#L13

Added line #L13 was not covered by tests
23 changes: 12 additions & 11 deletions src/fastcs/backends/epics/backend.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
from fastcs.mapping import Mapping
from fastcs.backend import Backend
from fastcs.controller import Controller

from .docs import EpicsDocs, EpicsDocsOptions
from .gui import EpicsGUI, EpicsGUIOptions
from .ioc import EpicsIOC
from .ioc import EpicsIOC, EpicsIOCOptions


class EpicsBackend:
def __init__(self, mapping: Mapping, pv_prefix: str = "MY-DEVICE-PREFIX"):
self._mapping = mapping
class EpicsBackend(Backend):
def __init__(self, controller: Controller, pv_prefix: str = "MY-DEVICE-PREFIX"):
super().__init__(controller)

Check warning on line 11 in src/fastcs/backends/epics/backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/backend.py#L11

Added line #L11 was not covered by tests

self._pv_prefix = pv_prefix
self._ioc = EpicsIOC(pv_prefix, self._mapping)

Check warning on line 14 in src/fastcs/backends/epics/backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/backend.py#L14

Added line #L14 was not covered by tests

def create_docs(self, options: EpicsDocsOptions | None = None) -> None:
docs = EpicsDocs(self._mapping)
docs.create_docs(options)
EpicsDocs(self._mapping).create_docs(options)

Check warning on line 17 in src/fastcs/backends/epics/backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/backend.py#L17

Added line #L17 was not covered by tests

def create_gui(self, options: EpicsGUIOptions | None = None) -> None:
gui = EpicsGUI(self._mapping, self._pv_prefix)
gui.create_gui(options)
EpicsGUI(self._mapping, self._pv_prefix).create_gui(options)

Check warning on line 20 in src/fastcs/backends/epics/backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/backend.py#L20

Added line #L20 was not covered by tests

def get_ioc(self) -> EpicsIOC:
return EpicsIOC(self._mapping, self._pv_prefix)
def _run(self, options: EpicsIOCOptions | None = None):
self._ioc.run(self._dispatcher, self._context, options)

Check warning on line 23 in src/fastcs/backends/epics/backend.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/backend.py#L23

Added line #L23 was not covered by tests
Loading
Loading