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 ComposedAdapter 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.

We will be using a composed adapter with a TCP server and the command interpreter. For more information on adapters see here.

Initialise Adapter#

We shall begin using the same amp.py file from the Amplifier device. In that file add a new AmplifierAdapter class which inherits ComposedAdapter. Within AmplifierAdapter we need to assign the AmplifierDevice as a class member, and initialise the server and interpreter like so:

from tickit.adapters.composed import ComposedAdapter
from tickit.adapters.interpreters.command.command_interpreter import CommandInterpreter
from tickit.adapters.servers.tcp import TcpServer
from tickit.utils.byte_format import ByteFormat


class AmplifierAdapter(ComposedAdapter):
    device: AmplifierDevice

    def __init__(
        self,
        host: str = "localhost",
        port: int = 25565,
    ) -> None:
        super().__init__(
            TcpServer(host, port, ByteFormat(b"%b\r\n")),
            CommandInterpreter(),
        )

Adapter Commands#

Now we have an adapter for our device, we need to tell it how to identify commands. When using the CommandInterpreter, commands may be 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.interpreters.command.regex_command import RegexCommand

class AmplifierAdapter(ComposedAdapter):

     ...

    @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(ComposedAdapter):

    ...

    @RegexCommand(r"A=(\d+\.?\d*)", True, "utf-8")
    async def set_amplification(self, amplification: float) -> None:
        self.device.amplification = amplification

In its entirety your adapter should look as below.

from tickit.adapters.composed import ComposedAdapter
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.utils.byte_format import ByteFormat


class AmplifierAdapter(ComposedAdapter):
    device: AmplifierDevice

    def __init__(
        self,
        host: str = "localhost",
        port: int = 25565,
    ) -> None:
        super().__init__(
            TcpServer(host, port, ByteFormat(b"%b\r\n")),
            CommandInterpreter(),
        )

    @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 simply add it to the arguments of DeviceSimulation.

@pydantic.v1.dataclasses.dataclass
class Amplifier(ComponentConfig):
    initial_amplification: int

    def __call__(self) -> Component:
        return DeviceSimulation(
            name=self.name,
            device=AmplifierDevice(
                initial_amplification=self.initial_amplification,
            ),
            adapters=[AmplifierAdapter()],
        )

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 amp_conf.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.