Skip to content

Commit 606686f

Browse files
authored
Adapter rework for Zebra (#174)
This changes the way adapters work within tickit. This attempts unifies the adapters in a composed way by seperating out the IO and the device specific handling logic. You now have an `AdapterContainer` which has both an `adapter`, the device specific part that handles the message, and the `io` which does the actual io logic. The `AdapterContainer` acts as the previous adapters did and contains the `run_forever` method. Now though, this calls a `setup` method in the `io` in which the `io` takes the specific `adapter` that has been provided. The `adapter` and `io` are failry coupled given the io needs a specific adapter type to work, eg. `class HttpIo(AdapterIo[HttpAdapter]): `. However hopefully this change provides a clearer divide in developer and user implemented code and simplifies the life of the users. As before users would need to write specific adapters for their devices but this should simplifiy their lives a bit as all they now need is the device and device specific methods. Most importantly, and the motivation for this change, is there is no longer a limitation on what an adapter may apply to. This allows us to write an adapter which inspect the components in a system simulation There is an example which uses a tcpio and commandadapter.
1 parent bd673a8 commit 606686f

70 files changed

Lines changed: 1673 additions & 1776 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/user/explanations/adapters.rst

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,28 @@ Adapters allow us to influence the device from outside the simulation, such as
77
using a TCP client to alter a parameter on a device. A device may have multiple
88
adapters simultaneously.
99

10-
There are four adapters included in the framework:
10+
Adapters are implemented within `AdapterContainer` and these containers are added
11+
to the device or system simulations. An AdapterContainer simply takes an adapter and
12+
an appropriate `AdapterIo`. When the container is run, it sets up the io with the
13+
specific adapter.
1114

12-
#. `Composed adapter`_
15+
- The Adapter contains the device specific interface for a given io type.
16+
- The `AdapterIo` contains IO logic which is agnositc to the details of the device.
17+
18+
There are four adapter types included in the framework:
19+
20+
#. `Command adapter`_
1321
#. `ZMQ adapter`_
1422
#. `HTTP adapter`_
1523
#. `EPICS adapter`_
1624

17-
Composed adapter
18-
----------------
19-
The composed adapter acts to implement a server and an interpreter. It delegates
20-
the hosting of an external messaging protocol to a server and message handling
21-
to an interpreter.
22-
23-
Tickit currently includes one server implementation, a TCP server, and one
24-
interpreter, the command interpreter.
2525

26-
The command interpreter is a generic interpreter which identifies commands from
27-
incoming messages. Commands are defined via decoration of adapter methods and
28-
the only such command type currently is a `RegexCommand`. This matches incoming
29-
messages to regex patterns and processes the command appropriately.
30-
31-
Tickit also includes three interpreter wrappers for the command interpreter.
32-
These wrap the command interpreter to allow for more complex message handling
33-
and can be used with the composed adapter in the same way.
34-
35-
Users may implement their own servers and interpreters and then use the composed
36-
adapter to utilise them for the device.
26+
Command adapter
27+
----------------
28+
The command adapter identifies and handles commands from incoming messages and is
29+
utilised with `TcpIo`. Commands are defined via decoration of adapter methods and the
30+
only such command type currently is a `RegexCommand`. This matches incoming messages to
31+
regex patterns and processes the command appropriately.
3732

3833

3934
ZMQ adapter
@@ -43,7 +38,7 @@ An adapter for use on a ZeroMQ data stream.
4338

4439
HTTP adapter
4540
------------
46-
An adapter that hosts an HTTP server, e.g. for devices with REST APIs.
41+
An adapter that utilises HTTP endpoints for the `HttpIo`.
4742

4843

4944
EPICS adapter
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
System Simulation Adapters
2+
==========================
3+
4+
Adapters may also be used with system simulations (`SystemSimulationComponent`),
5+
as well as with devices (`DeviceSimulation`). This would allow you, for example, to
6+
query what device components are inside the system simulation component, and how they
7+
are wired together.
8+
9+
Currently system adapters need both wiring and components. There is a helper base class
10+
for implementing this.
11+
12+
.. code-block:: python
13+
14+
class BaseSystemSimulationAdapter:
15+
"""A base for a SystemSimulationComponent adapter."""
16+
17+
_components: Dict[ComponentID, Component]
18+
_wiring: Union[Wiring, InverseWiring]
19+
20+
def setup_adapter(
21+
self,
22+
components: Dict[ComponentID, Component],
23+
wiring: Union[Wiring, InverseWiring],
24+
) -> None:
25+
"""Provides the components and wiring of a SystemSimulationComponent."""
26+
self._components = components
27+
self._wiring = wiring
28+
29+
30+
31+
Nested Amplifier Example
32+
------------------------
33+
34+
The following system adapter is a simple `CommandAdapter`. This Can be used to query the
35+
SystemSimulationComponent for a list of ID's of the components it contains; and to
36+
return the wiring map of the components.
37+
38+
.. code-block:: python
39+
40+
class SystemSimulationAdapter(BaseSystemSimulationAdapter, CommandAdapter):
41+
42+
_byte_format: ByteFormat = ByteFormat(b"%b\r\n")
43+
44+
@RegexCommand(r"ids", False, "utf-8")
45+
async def get_component_ids(self) -> bytes:
46+
"""Returns a list of ids for all the components in the system simulation."""
47+
return str(self._components.keys()).encode("utf-8")
48+
49+
@RegexCommand(r"wiring", False, "utf-8")
50+
async def get_wiring(self) -> bytes:
51+
"""Returns the wiring object used by the nested scheduler."""
52+
return str(self._wiring).encode("utf-8")
53+
54+
To use this adapter we would need to put it in an adapter container with TcpIo and
55+
assign it as an argument in the SystemSimulationComponent.
56+
57+
.. code-block:: python
58+
59+
@pydantic.v1.dataclasses.dataclass
60+
class NestedAmplifierWithAdapter(ComponentConfig):
61+
"""Simulation of a nested amplifier with a CommandAdapter."""
62+
63+
name: ComponentID
64+
inputs: Dict[PortID, ComponentPort]
65+
components: List[ComponentConfig]
66+
expose: Dict[PortID, ComponentPort]
67+
68+
def __call__(self) -> Component: # noqa: D102
69+
return SystemSimulationComponent(
70+
name=self.name,
71+
components=self.components,
72+
expose=self.expose,
73+
adapter=AdapterContainer(
74+
SystemSimulationAdapter(),
75+
TcpIo(host="localhost", port=25560),
76+
),
77+
)
78+
79+
80+
81+
This ComponentConfig ``NestedAmplifierWithAdapter`` can be used as any other component.
82+
83+
Below is a very simple simulation with a system simulation component. It is a
84+
source, which is connected to a `SystemSimulationComponent` which only contains a
85+
single amplifier. The output of this amplfier is fed to the output of the system
86+
simulation component, which is wired to a sink.
87+
88+
89+
.. code-block:: yaml
90+
91+
- type: tickit.devices.source.Source
92+
name: source
93+
inputs: {}
94+
value: 10.0
95+
- type: examples.adapters.system_simulation_adapter_config.NestedAmplifierWithAdapter
96+
name: nested-amp
97+
inputs:
98+
input_1:
99+
component: source
100+
port: value
101+
components:
102+
- type: examples.devices.amplifier.Amplifier
103+
name: amp
104+
inputs:
105+
initial_signal:
106+
component: external
107+
port: input_1
108+
initial_amplification: 2
109+
expose:
110+
output_1:
111+
component: amp
112+
port: amplified_signal
113+
- type: tickit.devices.sink.Sink
114+
name: external_sink
115+
inputs:
116+
sink_1:
117+
component: nested-amp
118+
port: output_1
119+
120+
121+
122+
Interacting with devices using a system simulation adapter
123+
----------------------------------------------------------
124+
125+
When using a system adapter you must be careful to achieve the behaviour you desire.
126+
127+
If you wish to write to and change the devices within the system simulation then any
128+
change you make must be followed by raising an interrupt in that specific device
129+
component. If you do not, the changes will not propagate correctly.
130+
131+
This is done below in the ``raise_component_interrupt`` method which takes a given
132+
component ID and does ``await component.raise_interrupt()`` for the specific component.
133+
134+
135+
.. code-block:: python
136+
137+
class SystemSimulationAdapter(BaseSystemSimulationAdapter, CommandAdapter):
138+
139+
_byte_format: ByteFormat = ByteFormat(b"%b\r\n")
140+
141+
@RegexCommand(r"interrupt=(\w+)", False, "utf-8")
142+
async def raise_component_interrupt(self, id: str) -> bytes:
143+
"""Raises an interrupt in the component of the given id."""
144+
component = self._components.get(ComponentID(id), None)
145+
146+
if isinstance(component, BaseComponent):
147+
await component.raise_interrupt()
148+
return str(f"Raised Interupt in {component.name}").encode("utf-8")
149+
else:
150+
return str("ComponentID not recognised, No interupt raised.").encode(
151+
"utf-8"
152+
)

docs/user/how-to/use-command-wrappers.rst

Lines changed: 0 additions & 170 deletions
This file was deleted.

0 commit comments

Comments
 (0)