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