Purpose


The Radio API is a central piece of the design philosophy. This is not a closed box that requires fiddling in the engine room to make any changes. The application node is there for a very good reason, to separate out the logic of the application from the logic of the presentation . However, it has a very important second role.  The application naturally presents an API to the rest of the system. The primary user of this API is the UI but that is not  to be the only user. I have started to define and implement a connector that will allow any Java enabled node to access the application and therefore effectively be a co-driver of the system. Any mesages sent to the application cause the state machine to execute and implement the function, including updating any UI . Normally you see this as say changing frequency by scrolling the VFO digits and seeing the digits change in response. However it is not the UI that changes the display, it's the application sending a message that changes the displayed frequency. It's therefore easy to see that the frequency can be changed by any other node that can send a message to the application. 

The user API to this system will be Java. I have taken that decision because it's big and I don't have time to do all that for C as well. It's also because it fits with the cross-platform philosophy and in fact sits much better on erlang. There will be a C API but it will be internal to help develop the C parts of tyhe system.

The API is a services provider not simply a means to control the radio. This means it provides e.g. logging and persistence services and will provide others as I think of them. It is already quite comprehensive. The Java code is about 80% completed with place holders where it's not finished. During testing I am sure it will change a little. Once it's all up and running I will build some much more interesting scripts, some with GUI components. The next step will then be to retrofit  to the Jython GUI and in the process a lot of code will go from the GUI because that's where most of it has come from (somewhat modified and translated to Java of course).

This is the JavaDoc for the API. It looks a lot, but it will be very easy to use. Start at the  ErlinkConnector as thats the way in . Then follow the documentation for the Command, Event and Data interfaces. The other services at the moment are persistence and logging. Capability provides info on the current system.  This is preliminary info, it has mistakes and some formattting issues which will get resolved in due course.

JavaDoc API Documentation.



User Scripting

The ability to produce plug-in parts with the minimum of effort is a major aim.  As the system is dynamic, parts such as these can be run completely independently from the command line and will attach to a running system. They can also be fully integrated by adding a few lines to the configuration file to start a user scripted part or two when the system is started.

Simple Filter Switcher

This is about the most straight forward thing I could think of that actually does something useful.


This is the code in Jython to produce a working filter switcher.

from co.uk.g3ukb.connectorJ import ErlinkConnector
from co.uk.g3ukb.connectorJ import DttSpEventInterface

from javax.swing import JPanel as Panel
from javax.swing import JFrame as Frame
from java.awt import FlowLayout as FL
from javax.swing import JButton as Button
       
def start():
    global cmd
    conn = ErlinkConnector()
    evt = EventReceiver()
    conn.do_connect('mynode', 'LT-BOBC-01', 'ui', 'switcher@LT-BOBC-01', evt)
    cmd = conn.get_cmd_interface()
       
def refresh():
    global cmd
    cmd.refresh()
           
# The main panel
class FilterPanel:
   
    def __init__ (self, filters):  # constructor
        # Create the outer frame
        myframe = Frame("ERLINK-SR - Filter Switcher")
        # Create the panel
        p = Panel(FL())
        # Add the filter buttons
        f = 0
        for filter in filters:
            b = Button(filter[2])
            b.preferredSize=(75, 25)
            b.setName(str(f))
            b.actionPerformed = self.change_filter
            p.add(b)
            f += 1
        myframe.add(p)          
        myframe.size = (len(filters)*75)/2, 150
        myframe.show()
          
    def change_filter(self, event):
        global cmd
        cmd.update_filter(0, int(event.getSource().getName()))
           
class EventReceiver(DttSpEventInterface):

    def __init__(self):
        DttSpEventInterface.__init__(self)
       
    def e_filters_col(self, filters):
        FilterPanel(filters)


First we import the Java API classes then a few Swing classes we will need to build the UI. Run up Jython and type a few commands.

> import  Scripter

This imports the file above .

> Scripter.start()

This first creates an instance of the ErlinkConnector and an instance of  our EventReceiver (more on ths later).  The do_connect method is called to connect to erlink-sr (see the API for details of the parameters). Note that our EventReceiver is passed into the do_connect(). Finally we get the command interface instance from  the ErlinkConnector. As Jython is dynamically typed we don't care what actual interface is returned.

> Scripter.refresh()

This will cause the model to be sent to all class 'ui' recipients (of which we are one as we registered with class ui). Now the interesting bit. The class EventReceiver at the bottom inherits from DttSpEventInterface as described in the API docs. We override one method which will receive the filters array. This array will be sent as a response to the refresh(). When we receive it we create and initialise the little GUI which is done in the class FilterPanel. This simply  iterates the response and creates a button for each filter, finally adding the new panel to a frame and showing the frame. We use the default flow layout which just adds components one following the other. It also creates one event  function that will be called every time a button is pressed. The event proc simply sends an update filter command  which will both set the filter and send an event  to say the filter has been changed. If we overrode the rx_filter() method as well we would see that event fire.

The Erlang wxWidgets way forward (Filter Switcher revisited)



It's not a thing of beauty, but it does the job.

This is the first piece of  the new UI strategy. It is a proof-of-concept piece of work  and although far short of the complex requirements of the full UI it looks very promising. The code is here in full and may be copy pasted into a file filter_block.erl and compiled with 'erl> c(filter_block)'. and then used as in the instructions in the code.

Note that a straight compare between this and the above code (which are functionally equivalent) is not a fair comparison. This code is completely stand-alone whereas the jython script  requires a sizable java library behind it to work. When I have decided the architecture for the erlang UI  there will be an equivalent (but much smaller) library that will reduce the bulk and complexity of the actual UI code.

%% Author: bobc
%% Created: 21 Dec 2007
%% Description:
%%    UI fragment - Filter Block
%%
-module(filter_block).

%%
%% Include files
%%

%%
%% Exported Functions
%%
-export([start/3]).

%%
%% API Functions
%%

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Run the filter block
%%
%% Simply run this from the command line in
%% an erl session as this is a demonstration it
%% is not integrated with the system yet.
%%
%% The wxerlang install is available from  http://www.erlang.org/~dgud/wxerlang/
%% The Windows version contain all required wx libraries.
%%
%% To start:
%%    cd to the directory containing this beam file
%%    werl -smp -sname eui -setcookie sdrnet
%% Then in werl enter:
%%    filter_block:start(ui, 'switcher@vm-xp-dev', sm).
%%    substitute your machine name for vm-xp-dev
%%
%% NOTE: the wx bindings have an issue with erl
%% under Windows (use werl). The SMP extensions
%% are also required (this needs Erlang R12B).
%% NOTE: Start the erlink-sr system first. When running
%% and before pressing power-on, start up the filter-block
%% as above. Then power-on. The filter-block should be active.
%%
%% @Class        The class of this client
%%                         (the user interface client (nominally 'ui')to get all ui messages)
%% @Registrar   The location of the registrar (nominally 'switcher@nodename')
%% @Target        The target for messages from this client (usually all messages
%%                         go to the state machine (nominally 'sm'))
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
start(Class, Registrar, Target) ->
   
    %%%
    %% register ourselves with the registrar
    %% returns the gateway through which all communications are routed
    Gateway = register_node(Class, Registrar),
   
    %%%
    %% construct the main frame
    WX = wx:new(),
    Frame = wxFrame:new(WX, 1, "Filter Selector", []),
    wxFrame:center(Frame),
    wxFrame:setSize(Frame, {220,150}),
    %% add the frame to a window
    wxWindow:new(Frame, -1),
    %% add a grid sizer to the frame with 3 columns
    MainSz = wxGridSizer:new(3),
    wxWindow:setSizer(Frame,MainSz),
   
    %%%   
    %% request static data
    %% this clumsy message will go, it's a hang over from using C nodes.
    %% In fact this whole arrangement will change as data will be sourced
    %% from mnesia not callback messages from the state machine.
    Gateway ! {accept, Target, {"REFRESH_STATIC_DATA", {{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}}}},
    %% and wait for the filters, discarding all other static data messages
    Filters = filters(),
   
    %%%
    %% create the set of filter buttons
    Add = fun(E) ->
        %%io:format("erlink-console ~w: Element is ~p~n", [self(), E]),
        %% extract the ID and Label
        {ID, Label} = E,
        %% generate a button
        B = wxButton:new(Frame, ID, [{label,Label}]),
        %% add the button to the sizer
        wxSizer:add(MainSz, B),
        %% connect the button events
        wxButton:connect(B, ID, command_button_clicked)
        %%io:format("erlink-console ~w: Button is ~p~n", [self(), B])
    end,
    %% add an incrementing ID to the filter list as in [{N, {low, high, label}}, ...]
    Merged = lists:zip(lists:seq(0, length(Filters)-1), Filters),
    %% transform into [{ID, Label},...]
    E = [{X,Y} || {X,{_,_,Y}} <- Merged],
    %% process the filter list
    lists:foreach(Add, E),
   
    %%%
    %% finally show the window
    wxWindow:show(Frame, []),
   
    %% start up the event loop
    events(Gateway, Target).
 
%%
%% Local Functions
%%

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Register our node for supervisor functions
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
register_node(Class, Registrar) ->
    %% register with a well known name
    {registration, Registrar} ! {'new_reg', self(), Class, "Mode", "direct", "Format"},
    io:format("erlink-console ~w: Waiting for registration response message for sysmon~n", [self()]),
    receive
        {GatewayPid}->
            io:format("erlink-console ~w: Received my gateway ~w~n", [self(), GatewayPid])
    end,
    GatewayPid.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Process messages received from
%% the system via the switcher.
%% Return when the filters message is received.
%% This should have timeout processing added.
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
filters() ->
    receive
        {"e_filters_col",Filters} ->
            %%io:format("erlink-console ~w: Received filters [~p]~n", [self(), Filters]),
            tuple_to_list(Filters);
        Any ->
            %%io:format("erlink-console ~w: Received [~p]~n", [self(), Any]),
            filters() 
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Process messages received from
%% the system via the switcher.
%% These are call back messages from our
%% connected wx components
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
events(Gateway, Target) ->
    receive
        {wx,FilterID,{wx_ref,_,wxButton},{wxCommand,command_button_clicked}} ->
            %%io:format("erlink-console ~w: Received event for [~p]~n", [self(), FilterID]),
            Gateway ! {accept, Target, {"RX_FILTER", {{0,0},{0,FilterID},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}}}},
            events(Gateway, Target);
        Any ->
            %%io:format("erlink-console ~w: Received event [~p]~n", [self(), Any]),
            events(Gateway, Target) 
    end.
.

Active Scanner

From the very simple to reasonably complex. It's still pretty short and most of the code is to build the UI. It does demonstrate a few more features.

  • Use of the command and event interfaces as before and now the data interface to get the average 'S' meter reading.
  • Using the events to track current frequency.
  • Dispatch of the events back to the UI thread.


The UI part is quite simple. You type in a start, end and step to control the scan.  In this example we go from 3.7 to 3.8 MHz in 100Hz steps. The next entry  is the dBM trigger level. Any signals found above this threshold will be added to the list on the right with the frequency and the level. The scan can be stopped at any time by pressing the stop button. Clicking on an entry in the list will set the receiver to that frequency. There is a lot of room for improvement in this but the purpose is to demonstrate features of the API.

This is the code:

# Java erlink-sr API
from co.uk.g3ukb.connectorJ import ErlinkConnector
from co.uk.g3ukb.connectorJ import DttSpEventInterface
# Jython imports
import thread, synchronize
import time
import sys
# Swing imports
from javax.swing import JFrame as Frame
from javax.swing import JPanel as Panel
from javax.swing import JButton as Button
from javax.swing import JFormattedTextField as Field
from javax.swing.text import MaskFormatter as Formatter
from javax.swing import JLabel as Label
from javax.swing import JScrollPane as ScrollPane
from javax.swing import JList as List
from javax.swing import DefaultListModel as Model
from javax.swing.event import ListSelectionListener
from javax.swing import BorderFactory as Borders
from javax.swing.SwingUtilities import invokeLater
from java.awt import GridBagLayout as GBL
from java.awt import GridBagConstraints as Constraints
from java.awt import Insets
from java.awt import Color
from java.awt import Font

from java.lang import Thread, Runnable

#==================================================================
# Entry point
# Connect and create the UI      
def start():
    global cmd
    global data
    global sp
    conn = ErlinkConnector()
    evt = EventReceiver()
    conn.do_connect('mynode', 'LT-BOBC-01', 'ui', 'switcher@LT-BOBC-01', evt)
    # get references to the command and data interfaces
    cmd = conn.get_cmd_interface()
    data = conn.get_data_interface()
    # create the UI
    sp = ScanPanel()

#==================================================================
# The scanner UI              
class ScanPanel:
   
    def __init__ (self):  # constructor
        global hit_list
        # Create the outer frame
        myframe = Frame("ERLINK-SR - Scanner")
        # Create the panel with grid bag layout
        p = Panel(GBL())
        # create the scan controls
        # a couple of formattedtext fields for the start and end frequency
        self.hit_list = Model()
        hit_list = self.hit_list
        freq_list = List(self.hit_list)
        freq_list.setForeground(Color.red)
        scroll_pane = ScrollPane(freq_list)
        freq_list.addListSelectionListener(Monitor())
        l_start_freq = Label('Start')
        self.start_freq = Field(Formatter('##.###_###'))
        self.start_freq.preferredSize=(130, 25)
        l_end_freq = Label('End')
        self.end_freq  = Field(Formatter('##.###_###'))
        self.end_freq.preferredSize=(130, 25)
        l_step_freq = Label('Step')
        self.step_freq  = Field(Formatter('##.###_###'))
        self.step_freq.preferredSize=(130, 25)
        l_trigger = Label('Trigger (dBM)')
        self.trigger = Field(Formatter('-##.##'))
        self.trigger.preferredSize=(130, 25)
        l_scan_freq = Label('Scanning')
        self.scan_freq = Label('00.000.000')
        self.scan_freq.setForeground(Color.red)
        self.scan_freq.setBorder(Borders.createLineBorder(Color.black))
        self.scan_freq.setFont(Font("Serif", Font.ITALIC, 18))
        scan = Button('Scan')
        scan.preferredSize=(75, 25)
        scan.actionPerformed = self.start_scan
        stop = Button('Stop')
        stop.preferredSize=(75, 25)
        stop.actionPerformed = self.stop_scan
        close = Button('Close')
        close.preferredSize=(75, 25)
        close.actionPerformed = self.close
       
        # Add the components to the panel.
        # The panel has a grid bag layout which is basically a grid into which
        # components can be placed. Each component can occupy one or more
        # cells horizontally or vertically.
        C = Constraints()
        comps =  (#    comp,        gridy,     gridx,     weightx,    weighty,    fill,            gridwidth, gridheight    insets(t,l,b,r) anchor
                 (scroll_pane,      0,         2,         1.0,        1.0,         C.BOTH,          4,        5,            (2,5,2,2),    C.LINE_START),
                 (l_start_freq,     0,         0,         1.0,        1.0,         C.NONE,          1,        1,            (0,0,0,0),    C.CENTER),
                 (l_end_freq,       1,         0,         1.0,        1.0,         C.NONE,          1,        1,            (0,0,0,0),    C.CENTER),
                 (l_step_freq,      2,         0,         1.0,        1.0,         C.NONE,          1,        1,            (0,0,0,0),    C.CENTER),
                 (l_trigger,        3,         0,         1.0,        1.0,         C.NONE,          1,        1,            (0,0,0,0),    C.CENTER),
                 (l_scan_freq,      4,         0,         1.0,        1.0,         C.NONE,          1,        1,            (0,0,0,0),    C.CENTER),
                 (self.start_freq,  0,         1,         0.5,        1.0,         C.HORIZONTAL,    1,        1,            (0,0,0,0),    C.LINE_START),
                 (self.end_freq,    1,         1,         0.5,        1.0,         C.HORIZONTAL,    1,        1,            (0,0,0,0),    C.LINE_START),
                 (self.step_freq,   2,         1,         0.5,        1.0,         C.HORIZONTAL,    1,        1,            (0,0,0,0),    C.LINE_START),
                 (self.trigger,     3,         1,         0.5,        1.0,         C.HORIZONTAL,    1,        1,            (0,0,0,0),    C.LINE_START),
                 (self.scan_freq,   4,         1,         0.5,        1.0,         C.HORIZONTAL,    1,        1,            (0,0,0,0),    C.LINE_START),
                 (scan,             5,         2,         1.0,        1.0,         C.HORIZONTAL,    1,        1,            (0,0,0,0),    C.LINE_START),
                 (stop,             5,         3,         1.0,        1.0,         C.HORIZONTAL,    1,        1,            (0,0,0,0),    C.LINE_START),
                 (close,            5,         5,         1.0,        1.0,         C.HORIZONTAL,    1,        1,            (0,0,0,0),    C.LINE_START)
                 )
        # compose the components into the panel
        compose(p, comps, C)
        # add the panel to the frame
        #p.size=(300,250)
        myframe.add(p)       
        # show the frame 
        myframe.size=(400,200)
        myframe.show()      
   
    # Button event handlers     
    def start_scan(self, event):
        global scanning
        start = self.start_freq.getValue()
        start = float(start[:2]+'.'+start[3:6]+start[7:])
        end = self.end_freq.getValue()
        end = float(end[:2]+'.'+end[3:6]+end[7:])
        step = self.step_freq.getValue()
        step = float(step[:2]+'.'+step[3:6]+step[7:])
        trigger = self.trigger.getValue()
        trigger = float(trigger[1:3]+'.'+trigger[4:6])
        thread.start_new_thread(scanner, (start, end, step, trigger))
        scanning = 1
   
    def stop_scan(self, event):
        global scanning
        scanning = 0
   
    def close(self, event):
        sys.exit()
       
    # API functions for the UI   
    def set_freq(self, freq):
        self.scan_freq.setText(freq)
       
    def add_event(self, freq, level):
        self.hit_list.addElement(freq + ' [' + level + ']')

#==================================================================
# List event handler (can't do this the Jython way)
class Monitor(ListSelectionListener):
       
    def valueChanged(self, e):
        global cmd
        global hit_list
        value = hit_list.elementAt(e.getSource().getSelectedIndex())
        freq = float(value[:3]+'.'+value[4:7]+value[8:11])
        cmd.set_main_freq(freq)
       
#==================================================================
# The actuhal scanner function.
# Runs on a separate thread and exits when the scan is complete       
def scanner(start, end, step, trigger_level):
    global cmd
    global data
    global scanning
    global hit_list
 
    cmd.set_main_freq(start)
    hit_list.clear()
    current = start
    while 1:
        cmd.update_main_freq_inc(step)
        level = data.get_rx_metering(1) # 1 = Average-S
        if level >= -trigger_level:
            # add a hit to the list
            m,k,h = freqToString(current)
            sp.add_event(m+'.'+k+'_'+h, str(round(level,2)))
            # move out the way to prevent multiple triggers
            current += 0.003
            cmd.update_main_freq_inc(0.003)
        current += step
        time.sleep(0.05)
        if current >= end: return
        if not scanning: return       

#==================================================================
# Our Event Receiver             
class EventReceiver(DttSpEventInterface):

    def __init__(self):
        DttSpEventInterface.__init__(self)  
       
    def e_freq(self, f_main, f1, f2, f3):
        global sp
        m,k,h = freqToString(f_main)
        invokeLater(UpdaterRun(m+'.'+k+'_'+h))

#==================================================================
# Utility Functions
# compose a panel from the given component specs
def compose(panel, comps, C):
    for c in comps:   
        C.gridy     = c[1]
        C.gridx     = c[2]
        C.weightx   = c[3]
        C.weighty   = c[4]
        C.fill      = c[5]
        C.gridwidth = c[6]
        C.gridheight = c[7]
        C.insets    = Insets(c[8][0], c[8][1], c[8][2], c[8][3])
        C.anchor    = c[9]
        panel.add(c[0], C)

# Convert a float to the MHz, KHz, Hz string parts                     
def freqToString(freq):
    # convert whole frequency to a string
    sFreq = '%(freq)f' %vars()
    l = len(sFreq)                  # length of complete string
    o = sFreq.find('.')             # offset of decimal point
    if o == -1:
        return ("000", "000", "000")
       
    # zero pad to a full frequency length
    #   i.e. from 3.9997 to 003.999700
    if o<3:     # need to left pad
        sFreq = sFreq.zfill(3-o+l)      # number of zeros to add plus total length
    l = len(sFreq)                      # recalc length
    if l<10:    # need to right pad
        sFreq = sFreq.ljust(10)         # space fill to 10 characters
        sFreq = sFreq.replace(' ','0')  # change spaces to zeros
       
    # unpack into fields and return
    return (sFreq[:3], sFreq[4:7], sFreq[7:10])

#===============================================================================
# Puts us back on the right thread to call swing
class UpdaterRun(Runnable):

    def __init__(self, freq):
        self.freq = freq
       
    def run(self):
        global sp
        sp.set_freq(self.freq)

Ok, it's a bit longer but it's not too difficult  once broken down into parts. The imports section can be ignored, we just need quite a bit of stuff for Swing. The bulk of the code is in the class ScanPanel which creates the UI. If interested in this bit there are lots of tutorials on Swing on the web. I will just pick out the salient features. It uses the Gridbag layout which is the one I find most useful. I always create panels the same way. First create all the required controls then pack the information required to add the controls to a panel into a data structure (comps) and call a method to add the components. 

The start_scan() event handler is the interesting one. It simply gets hold of the user entered data and does a bit of string manipulation to get the float values (the input fields are masked fields to make entry easy but we have to pick the bits out from the full string). It then starts a new thread to run the scan (this is so we can abort the scan with the stop button, otherwise our UI would be locked until the scan finished). The scanner() function is the code that runs in the new thread and does the actual scan. This simply steps the frequency from start to end and at each step if the signal level is above the threshold adds it to the list. The class 'Monitor' is the event handler for the list and this sets the frequency when you click on an item in the list.

The EventReceiver in this case simply listens for frequency events, makes the frequency into a string and calls something called invokeLater(). This method is part of Swing and you give it a class derived from Runnable. In effect it calls the method we would have called directly, in this case to set the frequency display but calls it on the Swing main thread rather than the event thread we are on. If you don't do this things may appear to work ok most of the time but painting can go strange with bits not painting or painting in the wrong place.  The utility functions are listed to show what they are, in reality these would be in a utility module (in fact I copied them straight out of my utility module).

Just to make things slightly less boring here's a screen video of the thing working. Due to the size of these AVI files it's VERY short. Note the synchronisation of the frequency display on the main radio and the active scanner UI. Sorry it's a bit jerky but the update is slow and the capture program also takes a whopping 30% of my CPU.

ScannerRecording.zip

What's Next

Not planning any more snippets at the moment. The next task is to make the real UI use the API. While doing this I am going to create a UI library of radio components and utility functions. I did wonder if I should do this in Java but have decided to just wrap the Jython components I already have up properly. So essentially I will pull out as much as makes sense and properly document it  as the Radio Component Library (RCL). The library will of course use the API. I will then rebuild my UI using vthe RCL.