.. _framework:
*************
The Framework
*************
.. only:: html
.. topic:: Abstract
This chapter will give the reader an overview on how the framework works,
from which classes it is composed, and how they interact with one
another, in order to simulate a subsystem of a radio telescope.
The framework is written in
`Python 3.9.4 `__. This `Python`
version was chosen to maintain compatibility with the `ACS` default `Python`
version. By matching the two versions, all the simulators can be executed on
the same machine where the `DISCOS` control software is running, no matter if
it is a production, a development, a physical or a virtual machine.
The framework architecture is composed of two layers. The topmost layer is
in charge of handling network communications, it behaves as a server, listening
for incoming connections from clients and relaying every received byte to the
other layer of the framework, the simulation layer. This layer is where
received commands are parsed and executed, it can faithfully replicate the
behavior of the hardware it is supposed to simulate, or it can simply answer
back to clients with a simple response, whether the latter is the correct one
or not, to simulate an error condition and thoroughly test the `DISCOS` control
software.
.. _layers:
.. figure:: images/layers.png
Framework layers and communication behavior.
.. raw:: latex
\clearpage
Networking layer
================
The `Simulator` class
---------------------
The `Simulator` class represents the topmost layer of the framework. Each
simulator system can have multiple instances or servers to be launched
(i.e. the Active Surface simulator has 96 different instances), this class has
the task to start a server process for each of the instances needed for the
given simulator.
.. module:: simulators.server
.. autoclass:: Simulator
:members:
:inherited-members:
The `Server` class
------------------
As previously mentioned, every simulator exposes one or multiple server
instances via network connection. Each server instance is an object of the
`Server` class, its purpose it to listen on a given port for incoming
communications from any client. This class inherits from the
`SocketServer.ThreadingMixIn` class and, depending on which type of socket it
will use, it also inherits either from the `SocketServer.ThreadingTCPServer`
class or from the `SocketServer.ThreadingUDPServer` class. The reader can find
more information about the `system` parameter in the :ref:`system` section.
.. autoclass:: Server
:members:
The `Server` class is capable of behaving in three different ways.
The first one is to act as a listening server, a server that awaits for
incoming commands and sends back the answers to the client.
The second possible behavior is to act as a sending server, sending its status
message periodically to its clients after they start any communication.
Finally, a server can act as a combined version of the aforementioned two.
It is therefore able to behave as both listening and sending server on two
different ports but relaying incoming commands to and sending the status of a
single simulator object to its connected clients.
Depending on how a server is configured to behave, it will use different
handler classes to manage communications with its clients.
Handler classes of a `Server`
-----------------------------
The handler class of a server object, is the endpoint class, in charge of
handling any communication between the server object and its underlying
simulator. Depending on the type of handler a server object has underneath,
its behavior when receiving a message will be different.
The `BaseHandler` class
~~~~~~~~~~~~~~~~~~~~~~~
The main handler class in the framework is the `BaseHandler` class. It inherits
from the `BaseRequestHandler` class.
.. autoclass:: BaseHandler
:members:
:inherited-members:
.. autoattribute:: custom_header
.. autoattribute:: custom_tail
.. automethod:: _execute_custom_command
The `BaseHandler` class alone is not able to handle any incoming message, its
only purpose in fact is to act as a base class for the following two classes,
providing its children classes the `_execute_custom_command` method.
The `ListenHandler` class
~~~~~~~~~~~~~~~~~~~~~~~~~
The `ListenHandler` class, as its name suggests, listens for incoming
messages from any client, and relays these messages to the underlying
simulator. If the simulator answers with a response message, the message is
then relayed back to the client.
.. autoclass:: ListenHandler
:members:
The `SendHandler` class
~~~~~~~~~~~~~~~~~~~~~~~
The `SendHandler` class, as soon as a client opens a communication channel,
starts retrieving and sending periodically the status message of its underlying
simulator class to its connected clients.
.. autoclass:: SendHandler
:members:
.. raw:: latex
\clearpage
Simulation layer
================
.. _system:
The `System` class
------------------
The `System` class is the main class of any hardware simulator. It is the class
in charge of parsing any incoming command received from the `Handler` object of
the `Server`, and/or periodically provide the status of the simulator it is
supposed to be mimicking.
The `BaseSystem` class
~~~~~~~~~~~~~~~~~~~~~~
The `BaseSystem` class is simply bare implementation of a full `System` class.
This class is the right place where to implement any custom method that can be
helpful to handle some behavior that is common to all simulators. As it can be
seen from the API below, it is the case of `system_greet()` and `system_stop()`
methods, which have to be defined for every simulator. They can be overridden
in case a `System` object has to behave differently than the default
implementation.
.. module:: simulators.common
.. autoclass:: BaseSystem
:members:
In order for a system object to be able to either parse commands or send its
status to any connected client, writing a `System` class that inherits from
`BaseSystem` is not enough. The `System` class of a simulator in fact has to
inherit from one (or both) of the two classes described below. It the `System`
class inherits from both the classes, it will have to implement all the
required methods and define the required attributes.
The `ListeningSystem` class and the `parse` method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order for any `System` class to be able to parse any command received by the
server, a `parse` method has to be defined.
This method takes a string composed by a single character as argument and returns:
* `False` when the character is not recognized as a message header and the system
is still waiting for the correct header
* `True` when the system has already received the header and it is waiting to
receive the rest of the message
* a response to the client, a non empty string, built according to the protocol
definition. The syntax of the response thus is different between different
simulators.
If the system has nothing to send to the client, as in the case of broadcast
requests, `System.parse()` must return `True`.
When the simulator is brought to behave unexpectedly, a `ValueError` has to be
raised, it will be captured and logged by the parent server process.
.. autoclass:: ListeningSystem
:members:
The `SendingSystem` class and the `subscribe` and `unsubscribe` methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the `System` class inherits from `common.SendingSystem`, it has to define
and implement the `System.subscribe()` and `System.unsubscribe()` methods,
along with the `sampling_time` attribute.
Both the `System.subscribe()` and `System.unsubscribe()` methods interfaces are
described in `issue #175 `__
on GitHub.
The `subscribe` method takes a queue object as argument and adds it to the list
of the connected clients. For each client in this list the system will then be
able to send the required message by putting it into each of the clients queues.
The `unsubscribe` method receives once again the same queue object received by
the `subscribe` method, letting the system know that that queue object,
relative to a disconnecting client, has to be removed from the clients queues.
The `sampling_time` attribute defines the time period (in milliseconds)
that elapses between two consecutive messages that the system have to send
to its clients. It is internally defined in the `SendingSystem` base class,
and its default value is equal to 10ms. If a different sampling time is needed,
it is sufficient to override this variable in the inheriting `System` class.
.. autoclass:: SendingSystem
:members:
.. autoattribute:: sampling_time
.. _classes:
.. figure:: images/classes.png
System class inheritance.
The `MultiTypeSystem` class
---------------------------
Some simulators might have multiple different implementations, having therefore
multiple `System` classes that behave differently from one another. For
example, among the already developed simulators, there are two different
implementations of the `IF Distributor` simulator. In order to keep multiple
different `System` classes under the same simulator name, another class called
`MultiTypeSystem` was written, it acts as a `class factory`. This means that it
works by receiving the name of the configuration of the system we want to
launch as `system_type` keyword argument.
.. autoclass:: MultiTypeSystem
:members:
.. automethod:: __new__
The main `System` class, just like a regular `System` class, should be
defined in the `__init__.py` file, inside the module main directory. It must
inherit from the `MultiTypeSystem` class and override the `__new__` method
as shown below::
systems = get_multitype_systems(__file__)
class System(MultiTypeSystem):
def __new__(cls, **kwargs):
cls.systems = systems
cls.system_type = kwargs.pop('system_type')
return MultiTypeSystem.__new__(cls, **kwargs)
As you can see from the code above, before defining the class, it is necessary
to retrieve the list of the available configurations for the given simulator.
This can be done by calling the `get_multitype_systems` function, defined in
the :ref:`utils` library. The said function will recursively search for any
`System` class in the given path. Generally speaking, the passed `__file__`
value will ensure that only the `System` classes defined in the module's
directory and sub-directories will end up inside the `systems` list. For more
information, take a look at the :ref:`function` in the
:ref:`utils` section. The default system configuration can be defined as
`system_type` inside the `kwargs` dictionary.
To know how to launch a simulator of this kind, please, take a look at
:ref:`this paragraph`.