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

Include wiring for isolated components #120

Merged
merged 22 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b973347
Include wiring for isolated components
abbiemery Apr 14, 2023
4cf2494
Add place holder tests
abbiemery Apr 18, 2023
f96ede0
Fix isolated component set for event router
abbiemery Jun 8, 2023
0c9e76c
Include isolated component in event router test graph
abbiemery Jun 8, 2023
f578fd0
Add example IoBox device
abbiemery Jun 8, 2023
d869498
Remove erroneous print statement
abbiemery Jun 8, 2023
e5b26a5
Change misslabeled wiring
abbiemery Jun 8, 2023
5e54e7d
Include record.db file required for iobox
abbiemery Jun 8, 2023
2e304ab
Add new line
abbiemery Jun 8, 2023
ada0717
Include multiple graph fixtures
abbiemery Jun 8, 2023
82cceac
Paramaterise further tests
abbiemery Jun 9, 2023
0cb8c44
Paramaterise test_event_router_wiring_from_wiring
abbiemery Jun 9, 2023
6b14612
Paramaterise test_event_router_wiring_from_inverse
abbiemery Jun 9, 2023
534be17
Update isolated node naming
abbiemery Jun 9, 2023
1937091
Add component sets and paramaterise component test
abbiemery Jun 9, 2023
371087f
Add input component sets and paramaterise input component test
abbiemery Jun 9, 2023
66087bf
Add output component sets and paramaterise output component test
abbiemery Jun 9, 2023
244a44f
Add isolated component sets and paramaterise isolated component test
abbiemery Jun 9, 2023
df87a87
Include comments for graph descriptions
abbiemery Jun 9, 2023
ee7bfdd
Update examples/devices/iobox_record.db
abbiemery Jun 12, 2023
f967d60
Update src/tickit/core/management/event_router.py
abbiemery Jun 12, 2023
0488d5d
Rename Iobox for isolated box device
abbiemery Jun 12, 2023
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
6 changes: 6 additions & 0 deletions examples/configs/isolated-device.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- examples.devices.isolated_device.IsolatedBox:
name: MrBox
inputs: {}
initial_value: 2
ioc_name: ISOLATED_BOX
port: 25561
142 changes: 142 additions & 0 deletions examples/devices/isolated_device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from dataclasses import dataclass
from typing import TypedDict

from softioc import builder

from tickit.adapters.composed import ComposedAdapter
from tickit.adapters.epicsadapter.adapter import EpicsAdapter
from tickit.adapters.interpreters.command.command_interpreter import CommandInterpreter
from tickit.adapters.interpreters.command.regex_command import RegexCommand
from tickit.adapters.servers.tcp import TcpServer
from tickit.core.components.component import Component, ComponentConfig
from tickit.core.components.device_simulation import DeviceSimulation
from tickit.core.device import Device, DeviceUpdate
from tickit.core.typedefs import SimTime
from tickit.utils.byte_format import ByteFormat


class IsolatedBoxDevice(Device):
"""Isolated device which stores a float value.

The device has no inputs or outputs and interacts solely through adapters.
"""

Inputs: TypedDict = TypedDict("Inputs", {})
Outputs: TypedDict = TypedDict("Outputs", {})

def __init__(self, initial_value: float = 2) -> None:
"""Constructor which configures the initial value

Args:
initial_value (int): The inital value of the device. Defaults to 2.
"""
self.value = initial_value

def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]:
"""Update method that outputs nothing.

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 nothing.
"""
return DeviceUpdate(self.Outputs(), None)

def get_value(self):
"""Returns the value set for the device, required for the epics adapter."""
return self.value

def set_value(self, value: float):
"""Sets the value for the device, required for the epics adapter."""
self.value = value


class IsolatedBoxTCPAdapter(ComposedAdapter):
"""A composed adapter which allows getting and setting the value of the device."""

device: IsolatedBoxDevice

def __init__(
self,
host: str = "localhost",
port: int = 25565,
) -> None:
"""Instantiate a composed adapter with a configured TCP server.

Args:
host (Optional[str]): The host address of the TcpServer. Defaults to
"localhost".
port (Optional[int]): The bound port of the TcpServer. Defaults to 25565.

"""
super().__init__(
TcpServer(host, port, ByteFormat(b"%b\r\n")),
CommandInterpreter(),
)

@RegexCommand(r"v\?", False, "utf-8")
async def get_value(self) -> bytes:
"""Regex string command which returns the utf-8 encoded value.

Returns:
bytes: The utf-8 encoded value.
"""
return str(self.device.value).encode("utf-8")

@RegexCommand(r"v=(\d+\.?\d*)", True, "utf-8")
async def set_value(self, value: float) -> None:
"""Regex string command which sets the value

Args:
value (float): The desired new value.
"""
self.device.value = value


class IsolatedBoxEpicsAdapter(EpicsAdapter):
"""IsolatedBox adapter to allow interaction using an EPICS interface."""

device: IsolatedBoxDevice

async def callback(self, value) -> None:
"""Device callback function.

Args:
value (float): The value to set the device to.
"""
self.device.set_value(value)
await self.raise_interrupt()

def on_db_load(self) -> None:
"""Customises records that have been loaded in to suit the simulation."""
builder.aOut("VALUE", initial_value=self.device.value, on_update=self.callback)
self.link_input_on_interrupt(builder.aIn("VALUE_RBV"), self.device.get_value)


@dataclass
class IsolatedBox(ComponentConfig):
"""Isolated box device you can change the value of either over TCP or via EPICS."""

initial_value: float
port: int
ioc_name: str
host: str = "localhost"
db_file_path: str = "src/../examples/devices/isolated_record.db"

def __call__(self) -> Component: # noqa: D102
return DeviceSimulation(
name=self.name,
device=IsolatedBoxDevice(
initial_value=self.initial_value,
),
adapters=[
IsolatedBoxTCPAdapter(host=self.host, port=self.port),
IsolatedBoxEpicsAdapter(
db_file=self.db_file_path,
ioc_name=self.ioc_name,
),
],
)
1 change: 1 addition & 0 deletions examples/devices/isolated_record.db
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This file is required for the epics adapter to be instantiated, even though it is empty.
18 changes: 17 additions & 1 deletion src/tickit/core/management/event_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,11 @@ def components(self) -> Set[ComponentID]:
Returns:
Set[ComponentID]: A set of all components in the wiring.
"""
return set.union(self.input_components, self.output_components)
return set.union(
self.input_components,
self.output_components,
self.isolated_components,
DiamondJoseph marked this conversation as resolved.
Show resolved Hide resolved
abbiemery marked this conversation as resolved.
Show resolved Hide resolved
)

@cached_property
def output_components(self) -> Set[ComponentID]:
Expand All @@ -194,6 +198,18 @@ def input_components(self) -> Set[ComponentID]:
for dev, _ in port
)

@cached_property
def isolated_components(self) -> Set[ComponentID]:
"""A cached set of components without inputs or outputs.

Returns:
Set[ComponentID]: A set of components which are isolated
"""
return set.difference(
set(self.wiring.keys()),
set.union(self.input_components, self.output_components),
)

@cached_property
def component_tree(self) -> Dict[ComponentID, Set[ComponentID]]:
"""A cached mapping of first order component dependants.
Expand Down
Loading