Using an Adapter#
This tutorial shows how to use a simple adapter to externally interact with a device in the tickit framework.
We will use a CommandAdapter
which will act as a simple TCP interface to the
Amplifier
device we created in the previous how-to.
See also
See the Creating a Device how-to for a walk-through of creating the Amplifier device.
For more information on adapters see here.
Initialise Adapter#
We shall begin using the same amplifier.py
file from the Amplifier
device. In
that file add a new AmplifierAdapter
class which inherits CommandAdapter
.
from tickit.adapters.tcp import CommandAdapter
from tickit.utils.byte_format import ByteFormat
class AmplifierAdapter(CommandAdapter):
device: AmplifierDevice
def __init__(self, device: AmplifierDevice) -> None:
super().__init__()
self.device = device
Adapter Commands#
Now we have an adapter for our device, we need to tell it how to identify commands. Commands are registered by decorating an adapter method with a command register.
We shall create two methods, one which returns the current amplification of the device, and another which sets a new value for the amplification.
We shall begin by creating a method which returns the current value of the
amplification. To do this we register it as a RegexCommand
which is called by
sending A?
. Since reading a device value will not alter the device state we
shall set interrupt
to False
. We shall also specify that the byte encoded
message should be decoded to a string prior to matching using the utf-8
standard.
from tickit.adapters.specifications import RegexCommand
class AmplifierAdapter(CommandAdapter):
...
@RegexCommand(r"A\?", False, "utf-8")
async def get_amplification(self) -> bytes:
return str(self.device.amplification).encode("utf-8")
We shall now add a method which sets a new value for the amplification when
A=(\d+\.?\d*)
is received, where \d+\.?\d*
denotes a decimal number and the
parentheses form the capture group from which the argument is extracted.
class AmplifierAdapter(CommandAdapter):
...
@RegexCommand(r"A=(\d+\.?\d*)", True, "utf-8")
async def set_amplification(self, amplification: float) -> None:
self.device.amplification = amplification
We also optionally want to have messages formatted more nicely so can set the
_byte_format
to something slightly more readable. In its entirety your
adapter should look as below.
from tickit.adapters.tcp import CommandAdapter
from tickit.utils.byte_format import ByteFormat
from tickit.adapters.specifications import RegexCommand
class AmplifierAdapter(CommandAdapter):
device: AmplifierDevice
_byte_format: ByteFormat = ByteFormat(b"%b\r\n")
def __init__(self, device: AmplifierDevice) -> None:
super().__init__()
self.device = device
@RegexCommand(r"A\?", False, "utf-8")
async def get_amplification(self) -> bytes:
return str(self.device.amplification).encode("utf-8")
@RegexCommand(r"A=(\d+\.?\d*)", True, "utf-8")
async def set_amplification(self, amplification: float) -> None:
self.device.amplification = amplification
Include the Adapter#
In order to now use this adapter to control our device we need to include it in
our amplifier ComponentConfig
. To do this we first construct an AdapterContainer
with our AmplifierAdapter
and the appropriate AdapterIo
. In this case TcpIo
.
Once this is done we simply add it to the arguments of DeviceComponent
.
@pydantic.v1.dataclasses.dataclass
class Amplifier(ComponentConfig):
"""Amplifier you can set the amplification value of over TCP."""
initial_amplification: int
host: str = "localhost"
port: int = 25565
def __call__(self) -> Component: # noqa: D102
device = AmplifierDevice(
initial_amplification=self.initial_amplification,
)
adapters = [
AdapterContainer(
AmplifierAdapter(device),
TcpIo(
self.host,
self.port,
)
]
return DeviceComponent(
name=self.name,
device=device,
adapters=adapters,
)
It is possible to add many adapters to a device, for example a composed and an epics adapter. To do this simply list them.
Using the Adapter#
The simulation can be run the same as before using the yaml.
python -m tickit all amplifier.yaml
Additionally, we will start a telnet client which communicates with the TcpServer of the adapter, this may be performed by running the following command:
telnet localhost 25565
When run we expect a response akin to:
Trying ::1...
Connected to localhost.
Escape character is \'^]\'.
From this telnet client we can send various messages and receive responses from our
adapter. The only messages our adapter will recognise are A?
and A=
, so we
enquire for the current amplification.
A?
2.0
It tells us 2.
Finally, we may wish to set a new amplification with A=
. Here is an example of
setting a new amplification of 4.4 with accompanying tickit debug output:
A=4.4
DEBUG:asyncio:Using selector: EpollSelector
DEBUG:tickit.core.management.ticker:Doing tick @ 0
DEBUG:tickit.core.components.component:source got Input(target='source', time=0, changes=immutables.Map({}))
DEBUG:tickit.devices.source:Sourced 10.0
DEBUG:tickit.core.management.schedulers.base:Scheduler got Output(source='source', time=0, changes=immutables.Map({'value': 10.0}), call_at=None)
DEBUG:tickit.core.components.component:amp got Input(target='amp', time=0, changes=immutables.Map({'initial_signal': 10.0}))
DEBUG:tickit.core.management.schedulers.base:Scheduler got Output(source='amp', time=0, changes=immutables.Map({'amplified_signal': 20.0}), call_at=None)
DEBUG:tickit.core.components.component:sink got Input(target='sink', time=0, changes=immutables.Map({'input': 20.0}))
DEBUG:tickit.devices.sink:Sunk {'input': 20.0}
DEBUG:tickit.core.management.schedulers.base:Scheduler got Output(source='sink', time=0, changes=immutables.Map({}), call_at=None)
DEBUG:tickit.adapters.servers.tcp:Received b'A=4.4\r\n' from ('127.0.0.1', 56930)
DEBUG:tickit.core.management.schedulers.base:Scheduler got Interrupt(source='amp')
DEBUG:tickit.core.management.schedulers.base:Scheduling amp for wakeup at 17846862439
DEBUG:tickit.core.management.ticker:Doing tick @ 17846862439
DEBUG:tickit.core.components.component:amp got Input(target='amp', time=17846862439, changes=immutables.Map({}))
DEBUG:tickit.core.management.schedulers.base:Scheduler got Output(source='amp', time=17846862439, changes=immutables.Map({'amplified_signal': 44.0}), call_at=None)
DEBUG:tickit.core.components.component:sink got Input(target='sink', time=17846862439, changes=immutables.Map({'input': 44.0}))
DEBUG:tickit.devices.sink:Sunk {'input': 44.0}
DEBUG:tickit.core.management.schedulers.base:Scheduler got Output(source='sink', time=17846862439, changes=immutables.Map({}), call_at=None)
Here we see the initial tick at time=0 which initialises the system. We see the source providing a signal of 10, the amplifier getting the value of 10, amplifying it to 20 and outputting it to the sink, which takes it. You then see the adapter interrupting and changing the amplification to 4.4, continuing the same pattern but with the sink finally receiving a input signal of 44.