The Framework

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.

../_images/layers.png

Fig. 3 Framework layers and communication behavior.

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.

class simulators.server.Simulator(system_module, **kwargs)[source]

This class represents the whole simulator, composed of one or more servers.

Parameters:

system_module (module that implements the System class, string) – the module that implements the System class.

start(daemon=False)[source]

Starts a simulator by instancing the servers listed in the given module.

Parameters:

daemon (bool) – if true, the server processes are created as daemons, meaning that when this simulator object is destroyed, they get destroyed as well. Default value is false, meaning that the server processes will continue to run even if the simulator object gets destroyed. To stop these processes, method stop must be called.

stop()[source]

Stops a simulator by sending the custom $system_stop%%%%% command to all servers of the given simulator.

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 The System class section.

class simulators.server.Server(system, server_type, kwargs, l_address=None, s_address=None)[source]

This class can instance a server for the given address(es). The server can either be a TCP or a UDP server, depending on which server_type argument is provided. Also, the server could be a listening server, if param l_address is provided, or a sending server, if param s_address is provided. If both addresses are provided, the server acts as both as a listening server and a sending server. Be aware that if the server both listens and sends to its clients, l_address and s_address must not share the same endpoint (IP address and/or port should be different).

Parameters:
  • system (System class that inherits from ListeningServer or/and SendingServer) – the desired simulator system module

  • server_type (ThreadingTCPServer or ThreadingUDPServer) – the type of threading server to be used

  • kwargs (dict) – the arguments to pass to the system instance constructor method

  • l_address ((ip, port)) – the address of the server that exposes the System.parse() method

  • s_address ((ip, port)) – the address of the server that exposes the System.subscribe() and System.unsubscribe() methods

serve_forever()[source]

This method starts the System and then cycle for incoming requests. It stops the cycle only when the stop_me variable gets set to True.

start()[source]

Starts a daemon thread which calls the serve_forever method. The server is therefore started as a daemon.

stop()[source]

Sets the stop_me value to True, stopping the server.

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.

class simulators.server.BaseHandler(request, client_address, server)[source]

This is the base handler class from which ListenHandler and SendHandler classes are inherited. It only defines the custom header and tail for accepting some commands not related to the system protocol, and the _execute_custom_command method to parse the received custom command.

Example:

A $system_stop%%%%% command will stop the server, a $error%%%%% command will configure the system in order to respond with errors, etc.

custom_header = '$'
custom_tail = '%%%%%'
_execute_custom_command(msg_body)[source]

This method accepts a custom command (without the custom header and tail) formatted as command_name:par1,par2,…,parN. It then parses the command and its parameters and tries to call the system’s equivalent method, also handling unexpected exceptions.

Parameters:

msg_body (string) – the custom command message without the custom header and tail ($ and %%%%% respectively)

setup()[source]

Method that gets called whenever a client connects to the server.

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.

class simulators.server.ListenHandler(request, client_address, server)[source]
handle()[source]

Method that gets called right after the setup method ends its execution. It handles incoming messages, whether they are received via a TCP or a UDP socket. It passes down the System class the received messages one byte at a time in order for the System.parse() method to work properly. It then returns the System response when received from the System class. It also constantly listens for custom commands that do not belong to a specific System class, but are useful additions to the framework with the purpose of reproducing a specific scenario (i.e. some error condition).

setup()[source]

Method that gets called whenever a client connects to the server.

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.

class simulators.server.SendHandler(request, client_address, server)[source]
handle()[source]

Method that gets called right after the setup method ends its execution. It handles messages that the server has to periodically send to its connected client(s). It also constantly listens for custom commands that do not belong to a specific System class, but are useful additions to the framework with the purpose of reproducing a specific scenario (i.e. some error condition).

Simulation layer

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.

class simulators.common.BaseSystem[source]

System class from which every other System class is inherited. If a custom command that can be useful for every kind of simulator has to be implemented, this class is the right place.

static system_greet()[source]

Override this method to define a greeting message to send to the clients as soon as they connect.

Returns:

the greeting message to sent to connected clients.

system_stop()[source]

Sends back to the server the message $server_shutdown%%%%% ordering it to stop accepting requests, to close its socket and to shut down.

Returns:

a message telling the server to proceed with its shutdown.

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.

class simulators.common.ListeningSystem[source]

Implements a server that waits for its client(s) to send a command, it can then answer back when required.

abstract parse(byte)[source]

Receives and parses the command to be sent to the System. Additional information here: https://github.com/discos/simulators/issues/1

Parameters:

byte (byte) – the received message byte.

Returns:

False when the given byte is not the header, but the header is expected. True when the given byte is the header or a following expected byte. The response (the string to be sent back to the client) when the message is completed.

Return type:

boolean, string

Raises:

ValueError – when the declared length of the message exceeds the maximum expected length, when the sent message carries a wrong checksum or when the client asks to execute an unknown command.

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.

class simulators.common.SendingSystem[source]

Implements a server that periodically sends some information data regarding the status of the system to every connected client. The time period is the one defined as sampling_time variable, which defaults to 10ms and can be overridden. The class also accepts simulator-related custom commands, but no regular commands are accepted (they are ignored and immediately discarded).

sampling_time = 0.01
abstract subscribe(q)[source]

Passes a queue object to the System instance in order for it to add it to its clients list. The System will therefore put any new status message into this queue, along with the queue of other clients, as soon as the status message is updated. Additional information here: https://github.com/discos/simulators/issues/175

Parameters:

q (Queue) – the queue object in which the System will put the last status message to be sent to the client.

abstract unsubscribe(q)[source]

Passes a queue object to the System instance in order for it to be removed from the clients list. The System will therefore release the handle to the queue object in order for the garbage collector to destroy it when the client has finally disconnected. Additional information here: https://github.com/discos/simulators/issues/175

Parameters:

q (Queue) – the queue object that contains the last status message to send to the connected client.

../_images/classes.png

Fig. 4 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.

class simulators.common.MultiTypeSystem(**kwargs)[source]

This class acts as a ‘class factory’, it means that given the attributes system_type and systems (that must be defined in child classes), creating an instance of MultiTypeSystem (or some other class that inherits from this one) will actually create an object of system_type type if it’s defined in the systems list. This class is meant to be used in systems that have multiple simulator types or configuration in order for the user to be able to choose the desired type when launching the simulator.

static __new__(cls, **kwargs)[source]

Checks if the desired configuration is available and returns its correspondent class type.

Returns:

the System class correspoding to the one selected via command line interface, or the default one.

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 The simulators.utils library 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 function in the The simulators.utils library 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 this paragraph.