JOAN Overview
In this section, we try to explain the overall high-level structure of the JOAN framework. Please note that if you are interested in adding your own modules you need a more thorough understanding than just this section, if that is the case, please also consult the Advanced Steps Section for more information.
High-Level JOAN Structure
In the image above you'll notice that JOAN consists mainly of 2 groups, namely the core
and the modules
. For now
we'll leave the core be, however, we will go into the modules here.
Module
A module executes a certain function, like recording data or communicating with CARLA. The modules are set up to run in their own process (using the multiprocessing
toolbox) to distribute the computational load. For more info on how we set up multiprocessing in JOAN, check out below.
Modules can be anything you want because as it says in the name already JOAN has a modular structure. It is important to know what exactly is contained in a JOAN Module, this is summarized in the image below:
The descriptions in the above image are of course a very short summary, for a deeper understanding of what each element does, and a bit more context please refer to the following explanations:
Data flow & Communication
Because of multiprocessing and the modular structure of JOAN the data flow can be a bit difficult to grasp. In this section, we try to shed some light on these topics. A graphic overview is given in the image below, this image is an arbitrary example where we use 2 separate modules, module 1
and module 2
.
Please keep the above image in mind when going through the text below, there we will explain the flow of data and JOAN in more detail.
States
The first thing you'll notice is that we have 4 columns of almost the same thing, these represent the different states
JOAN can be in during expected operation; STOPPED
, IDLE
, READY
and RUNNING
. This is important because it will give you more insight
into when, how, and why certain communication elements are used.
Settings
As mentioned earlier every module has its own settings. These settings are made whenever a module is loaded and stored in the Settings()
class.
Loading a module means nothing more than including the module in the main.py of the program via the headquarters. Whenever you have a module loaded it will
have a settings object associated with it, this settings object can get filled with different other setting objects, for example, the
hardware manager module has a base settings object for the module: HardwareManagerSettings()
, but inside this object, we have a dictionary
called inputs
which contains the setting objects of different types of inputs. For example, it can contain a
KeyboardSettings()
object and 2 JoystickSettings()
objects. These specific settings objects are dynamically created and destroyed depending on the state of the module. The way
it goes is as depicted below:
Shared Variables
The Shared Variables
is one of the most essential classes of JOAN. As with the Settings()
the creation and removal of these classes
depend on the States as shown below:
Important
Shared Variables have to be picklable (serializable)!!
Signals
The Signals class in the Default JOAN is a bit obscure, so far it is only used in the activate condition
function in the
experiment manager. This triggers signals so that whenever a condition is activated it updates all dialogs of all included modules.
The Signals class uses the built-in signals & slots functionality from PyQt, so for a deeper understanding please refer to this
tutorial on signals and slots
News
The News()
class is used throughout all modules and serves as a sort of message pipe to all modules. It mainly contains the separate shared_variables
objects
of all the modules. This is needed because then we can access our shared variables from all modules. For example, if I'd like to calculate something in a separate
calculator
module, and for this, I need the input values of a keyboard
from the hardwaremanager
I can easily access this info in my new module!
Modules multiprocessing
JOAN modules are set up to run in their own process using the multiprocessing
toolbox. In short, this means that each module will create its own process and communicates with that process once the user hits the "Get ready" and "Run" buttons. The module's functionality (everything that happens in do_while_running(self)
) is then executed in a separate process. Communicating with a different process is not trivial, and we encourage you to check out the multiprocessing documentation.
We use shared variable objects to enable communication between the module processes. These shared variables objects only allow you to specify simple data types (ints, floats, a byte array). If you need to add parameters in a module's shared variables object, check out the existing SharedVariables classes of the existing modules.
If you want to use an object both in the module manager class and in the module process class, the object needs to be picklable. Check out how we convert a module’s settings class to a dictionary and back to a settings object such that we can also use it in the process itself.