Architecture Overview

Schematic

Overview

The erlang message switcher is logically part of the message bus as shown. It is transparent to all the processes. The bus spans all machines involved in the system, which can be one to many. Processes are not aware of the location of other processes they communicate with.

The system is started by the Erlang Application bottom right. This uses the erlang OTP (Open Telecom Platform) for much of its work.It will start a distributed system from a single command at the client side driven by a configuration file. In order to start remote processes it requires a small daemon to be available on all of the other machines. These processes communicate in order to start and stop the system as a whole.

A Little More Detail

The general scheme is that control signals and parameters pass between nodes using the Erlang Switcher.  Sampled data however resides in shared memory which is mapped by nodes that need to manipulate sampled data. Should those nodes be distributed then parts of the memory mapped data is also distributed and managed by a synchronisation protocol. The shared memory approach provides the minimum possible latency when the providers and consumers of data are on the same machine. When on different machines the additional overhead of synchronising the shared memory as opposed to sending the data directly is minimal and therefore generally speaking it gives the best of both worlds. In order that input and output streams can be on different machines the stream managers are split. A typical remote operating scenario would have input data from the radio on a sever and audio output on the client. Other benefits of the shared memory approach are:

  • The acquisition can be separated such that it can deal with sound cards or USB interfaces without having a performance hit of a extra hop. 
  • Multiple displays can use the data without it needing to be explicitly sent to each. 
  • Multiple staged data can be used such that data in all parts of the pipeline can be made visible for various purposes. 
  • Data can be shipped in/out to virtual sound cards or other programs  by a separate process without overhead.
  • There is potential to orchestrate the DSP in various ways, dynamically assembling the pipeline or running multiple DSP processes for different purposes.

In order to cater for virtually any SDR configuration the system is highly componentised and loosely coupled. Components may be Erlang nodes or Erlang processes as appropriate. Loose coupling is achieved through the messaging bus. The shared memory is somewhat more tightly coupled across those processes that map it. Access to shared memory is protected and synchronised.

One further consideration is the physical device access for samples and control signals. In some cases these will be on separate I/O devices; parallel, USB or sound cards (real or simulated). In other cases they may be shared on the same device either in separate channels (end points) or using in-band-signalling. The logical architecture will retain separation of different functions in all these cases.

Stream Management

There will be different stream management components, two are envisaged at present, for audio cards (with variants for the audio subsystem) and USB devices. The audio managers are much like those in the system at present with the functionality moved from the DSP processing chain to a separate process. A USB stream manager will initially be written for the HPSDR OZY protocol and SDR5000 . The stream manager will be responsible for separating/combining the control and data streams and for correct routing of the signals and data. Data will be acquired or emitted to/from shared memory directly. Control signals will be routed to the hardware controller. Signals from the hardware controller will be sent to the stream manager for transmission.

Components

Component

Description

User Presentation The Ui is a presentation engine and responsible for several classes of interaction:
  • Radio control signals.
  • Options and Configuration.
  • Connection management of the DSP chain (a future planned graphical interface to assemble the signal blocks).
  • Graphic and metering displays.
The way the UI is designed means it can cope with any number of different radio configurations using a system of profiles and capabilities.
Application The application is the heart of the system. It's implemented as a state machine using the Erlang OTP state machine behaviour. Again the way this is implemented means a different state/event matrix can be used for each radio varient and this will drive the UI profiile to deliver the correct interface elements. 
In and Out Stream Manager Described above.
DSP The signal processing core has two implementations, the first derived from the great DttSp work  and executes as a single process at present.  The second is the Java jDsp which is just starting out. There is room for other implementations such as support for  GNURadio blocks on a Python node.
Hardware Control This is the area where most differences will be apparent. There will be a common API as most radios after all need a similar set of functions. Underlying the interface the code will be very specific to the hardware interface. In some cases the hardware interface management is delegated to a stream manager.
Synchronization Where the shared memory needs to be distributed a synchronisation node is included on each side. On the sending side the node will monitor changes on the data structures and will send those structures (which are in the list of remote structures) to the receiving synchroniser node.
 

Implementation Notes

The system implementation is a mix of Erlang, Java/Jython and 'C'. This may seem a strange and perhaps unecessary mix. However, all these languages fit naturally together thanks to the Erlang C and Java libraries and bindings. The philosophy is to use the best technology for the function required with also a mind to developer productivity and OS portability. Thus cross-platform dynamic languages are preferred to statically  typed compiled languages. However, when operations are time critical or require system calls C is the preferred language. The implementation of components is as follows. 

Component

Implementation

User Presentation Jython using the Java Swing libraries and the Erlang jInterface library.
Application Erlang
In and Out Stream Managers Currently C Nodes but planned migration to an Erlang Port Driver model which allows Erlang to manage the communications. 
DSP DttSp is a C Node but again planned move to a port driver model which will allow a separate shared library for each platform. jDsp is a Java node.
Hardware Control Erlang Port Driver with dynamic loading of the appropriate C library for the hardware control.
Synchronization Erlang Port Driver.
Erlang application and daemon. Erlang nodes.
Switcher, register, gateways and other utility erlang processes. Erlang node.

 

Terminology

C Node - this is a 100% C executable. It uses the erlang C libraries for communication. To anything else on the network it is indistinguishable from an Erlang node. There are limitations with this approach. It is not possible to have a node which is both a client and a sever. For this reason alone this is not a preferred implementation. 

Port Driver - this is an Erlang node that loads a dynamic library (which can be in any language that can compile as a dynamic library). The full capabilities of erlang communications are available and each erlang process can load its own library. 

Java Node - as for a 'C' node but pure Java using the jInterface library. The Java library more closely follows the pattern of an Erlang communication and it can be a client and server concurrently.

Erlang Node - obviously a pure Erlang node.


GUI Architecture

This section shows the high level design of the GUI and its interaction with the application node.

GUI Schematic

All panels are Jython classes at the top level although they are usually composed of many classes of user defined controls, simple panels may be implemented as one class.  Starting at a panel, when a user interacts the panel is responsible for hooking events generated by the user, this is normal event based GUI programming. Some events are contained entirely within the panel and have no external effect but most will require something to happen outside of the panel. In this case the panel calls an update function. The update function it calls knows what to do with the event. Any data contained in the event may be used to update the UI model and drive the logic of the function. In most cases there is more to do than simply update the model.

After making any updates to the model the update function will usually fire events, internal events (those that do not need to go outside the UI) are fired at the local dispatcher.  The local dispatcher also contains a register. When each panel is created (panels are created dynamically by selecting a menu item) it registers for the events it is interested in (such as a mode, filter or frequency change). Most panels register for multiple events and many panels can register for the same events (the frequency for example may be displayed in several places on different panels). A panel registers simply by providing the event name and a callback function to be called when that event fires. Thus the update function may raise several internal events causing all the panel to update appropriately. The panel itself when receiving the callback is expected to source any data it needs from the model. It is therefore important that the Update Function updates the model before raising events. The final task of the Update function is to fire external events which will go to the application node for processing.

Continuing the journey, when the application node receives an event message it decodes it and dispatches it to an action (just a function really but the state machine implementation calls them actions). An action will typically update the application model and generate further messages to other parts of the system including back to the UI as some updates to the UI are not done locally because they rely on application not presentation logic. Back in the UI any update messages are decoded by the Erlang incoming interface and typically will update the UI model and then fire events at the local dispatcher which will cause the appropriate UI panels to update.

This design allows the GUI to be updated when it did not originate any events, such external events could come from some other GUI , CAT or automated scripting node.

DSP Architecture

A new DSP architecture is underway using Java. This may eventually become the prime DSP for erlink-sr. However it also continue to support DttSp and I would hope GNURadio blocks at  some point. 

In a Nutshell

The framework is up and running and its as sweet as it is simple.

DSP Sequence.

Starting at the Jython Node. This is the interface  that bridges the outside world to the DSP. It takes control messages from other nodes and actions them. These are the normal start and stop signals and parameter changes but in addition can be a description of the DSP workflow. Right now this definition is simply a dictionary of lists, each list being a sequence and is hard coded at the node. In the future this will be designed by a graphical designer and shipped to this node.

Writing blocks will be relatively simple in terms of what needs to be done to interface the block. A block is a Java class, it's interface is trivial and its operation data and parameters are all available through the single parameter it is passed (see later). If something like Eclipse is used which compiles in the background it's simply a case of creating the new class and putting it somewhere on the class path. To add the block into the sequence just add one line to the Jython dictionary. Of course writing the algorithmic code is the same amount of effort whatever the architecture. But not having to figure out how to interface it, how to get it ito run in sequence and how to compile the whole caboodle (hey! how about Caboodle as a name, it kind of fits with the whole kit and caboodle thing) on your favorite OS is a big plus.

Creating the sequences -

In order to keep the interface simple there is an AddBlock() method on the DSP Interface which take a string name for the block. This string name is simply the unqualified class name that implements the block, such as 'NoiseBlanker_1'. When a block is added in this way it is stored Java side as a HashMap of Vectors (a HashMap is really just a dictionary and a Vector is an ordered list), with each Vector entry being an Object in which is stored both the string name and an instantiated object for the block. The block object is instantiated using the Java class loader.   Many named sequences can therefore be stored and run at will.

Running the blocks -

Rather than connecting blocks together via pseudo ports and pumping data between the ports (conceptually nice but unecessary and I think rather limiting in interface terms) there is an object (the data store)  that is passed into each block. This follows the workflow pattern where the object contains all the information a block needs and acts as a means of communication between blocks. This data store is populated with raw samples by the controller and may collect other sets and outputs as it progresses through the sequence, such as metering and display data as well as processed samples. When the block has finished a pass through the sequence any results are put back in shared memory and the next set of raw samples copied in.  It's important to note that no data coying goes on in this process except between the store and shared memory.

The blocks are run simply by iterating through the correct stored sequence. The instantiated block objects must implement the block interface which has a single method, run(), taking one argument which is the data store. This is necessary because the controller has no idea what the blocks are and requires them to have a common interface.

Parameter management -

It  is obviously important not to go changing parameters while the DSP sequence is running. Fortunately it is a simple matter to avoid this without using any mutual exclusion techniques.  The data store contains two HashMaps, one is the operational-parameters and the other the update-parameters. A HashMap is a convienient way to store parameters as they are then name/value pairs and changes are not required to the data store every time parameters are added/deleted. The operational parameters are the ones accessed and used by the running blocks. Any parameter changes that come into the DSPInterface update the update-parameters map and a dirty flag is also set. At the end of a pass the controller simple looks at the dirty flag and if it is set the update_parameters are copied over the corresponding operational-parameters which also clears the dirty flag. Thus the next pass will use the new parameters.

Options Management

This has been a point of consternation with me in every implementation. The idea of hard coding every option from the GUI to all the consumers of options fills me with horror. I would probably die of boredom before I ever finished it. I have played with various ways to do this and had something I quite liked in the Squeak implementation (if that means nothing don't worry). The scheme has eventually worked itself out and was forced on me by doing the DSP work, I had to have some means to parameterize the blocks as I was writing them.

The scheme is this.

Options


From a development perspective, using this scheme means that to add an option to any node requiring one is simply a case of adding one line to the appropriate options structure in the application. This then will create a route from UI to the class of node requiring the option setting. The basic mechanism is working but there will be a fair amount more work to support more complex options such as selections, arrays and matrices. However, many options are simple types and I will add code for other types as and when I need to.

The application node owns the options structures. These structures contain enough information on the naming and type of data, limits etc etc to allow the UI to dynamically render the panel in the form of a properties panel with names in the left column and value selection in the right column.  Of course more complex options will have an ellipse in the right column which will bring up another panel for the value settings. However, embedded controls can still include check boxes, text, number and formatted input as well as drop downs and spinners, so it covers quite a lot. When the GUI receives an options structure it stores it locally and creates a leaf on the options tree.

The application sends options structures when it receives an INIT from a UI. Further, when each node is initialised by the application, it will send the current values from the appropriate options structure to the node, so that it is initialised with the last saved value of the options.

In order for this to work options are implemented as name/value pairs so there is only a single options message that the UI, application and receiving nodes have to cope with. This is essential, otherwise changes would be required right through the path every time an option was added. How does the application know what option structures to send where? That's down to the profile that was was started and thus the state machine that implements that profile knows the right thing to do.

I think this scheme will serve well and stop me going stir crazy when the number of options balloons as it surely will as more hardware is supported and more nodes added that need parameterization.

This scheme has recently been extended to cover internal option that are required by the UI only. In this case the UI holds the data structure.