Use Command Interpreter Wrappers#

In Using an Adapter we saw how to use a composed adapter with the amplifier device so that it could receive commands of the form A? and A=4.4 to get and set values to the device. These commands had to be sent exactly as specified and individually. However, when talking to a simulated device this may not be the case. Our simulated device may receive more complex messages and as a result our command interpreter needs to be able to handle them.

This can be achieved by wrapping the CommandInterpreter in other helper interpreters. This allows us to do some pre/post processing on all commands before/after they are handled.

There are three such wrappers:

  1. Beheading Interpreter

  2. Splitting Interpreter

  3. Joining Interpreter

The use of such wrappers will be demonstrated with the example Shutter device.

Beheading Interpreter#

Suppose that when communicating with a real shutter device, all messages are prepended with a fixed-length header that can be ignored. For example each message being prepended with a header of 2 bytes: \x00\x02P?, \x00\x04T=1.0 etc.

Our simulated device needs to handle messages of the same form.

We could use regex pattern matching to avoid this header but in some cases this is not desirable. In such cases we can cutoff the first two characters of each message before passing it on to the CommandInterpreter by wrapping the CommandInterpreter with a BeheadingInterpreter in the adapter’s __init__ as follows:

from typing import Awaitable, Callable
from tickit.adapters.interpreters.command import CommandInterpreter
from tickit.adapters.interpreters.wrappers import BeheadingInterpreter
from tickit.adapters.servers.tcp import TcpServer
from tickit.core.adapter import Adapter
class ShutterAdapter(Adapter):
    def __init__(
        self,
        device: Shutter,
        raise_interrupt: Callable[[], Awaitable[None]],
        host: str = "localhost",
        port: int = 25565,
    ) -> None:
        super().__init__(
            device,
            raise_interrupt,
            TcpServer(host, port, ByteFormat(b"%b\r\n")),
            BeheadingInterpreter(CommandInterpreter(), header_size=2),
        )

With the rest of the adapter unchanged. The adapter will then correctly interpret commands in messages with the header attached:

\x00\x02P?
2.0

Splitting Interpreter#

Now suppose that we would like to be able to send multiple commands in a single message, separated by some delimiter. For example, though P? and T? are interpreted as commands by the ShutterAdapter, P? T? is not:

P?
0.2
T?
0.2
P? T?
Request does not match any known command

This is the use-case of SplittingInterpreter. This splits a message into multiple sub-messages and then passes them on to another interpreter that it wraps. Wrapping the adapter’s CommandInterprerter as

SplittingInterpreter(CommandInterpreter(), delimiter=b" ")

with the rest of the adapter unchanged, allows for interpreting commands separated by a space:

P?
0.2
T?
0.2
P? T?
0.2
0.2

A wrapped adapter can be wrapped by another adapter wrapper. Combining the two above examples as

BeheadingInterpreter(
    SplittingInterpreter(
        CommandInterpreter(), delimiter=b" "
    ),
    header_size=2
)

allows for the adapter to deal with messages with a fixed-length header containing multiple commands separated by a space:

\x00\x02P?
0.2
\x00\x05P? T?
0.2
0.2

So far we have only seen interpreter wrappers altering the received message before passing it on to another interpreter. There are also wrappers that alter the response received from the wrapped interpreter before it is sent back.

Joining Interpreter#

Above, by wrapping the ShutterAdapter’s CommandInterpreter with a SplittingInterpreter we were able to execute multiple commands from a single message. Each executed command sent its own response, i.e. one message resulted in multiple responses. We may instead want each message to have its own response containing multiple command responses. This is the use-case for JoiningInterpreter. Wrapping the SplittingInterpreter with this will join each of the responses into a single message.

JoiningInterpreter(
    BeheadingInterpreter(
        SplittingInterpreter(
            CommandInterpreter(), delimiter=b" "
        ),
        header_size=2
    ),
    response_delimiter=b" "
)

Results in

P?
0.2
T?
0.2
P? T?
0.2 0.2