Building an amateur radio app for Linux, part 1

There are several amateur radio apps like Repeaterbook, but oddly enough no native Linux apps for offline map viewing? Why are all the radio repeater apps for iOS or Android? With the Librem 5 phone coming up, this is going to be an important app to make for Amateur radio enthusiasts! So let’s make the most feature-packed and easy to use repeater app using Python 3 and the osm-gps-map library:

As you can see from the documentation, this is a full featured map with offline support and a full C or Python API. Rather than use the install instructions to build the source, you can quickly install it through your package manager. For me on Ubuntu this was

sudo apt install gir1.2-osmgpsmap-1.0

Now you should be able to run their example code and see a map with various buttons and tools. Now the first obvious step is to find a way to draw repeater locations on the map: After some experimenting I found that do_render and do_draw within DummyLayer class seemed to cause a 100% cpu loop, but adding an icon to render outside of that, after the add layer, seems to work properly and renders it if you call image_add after the code:

#Add right after:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale('signaltower.svg',width=20,height=20,preserve_aspect_ratio=True)
                    self.osm.image_add(lat, lon, pixbuf)

Note you need that “signaltower” in the same directory here for that “signaltower.svg” file to get picked up. Now I have permission to mirror and download the IRLP current node listing, which I am mirroring on every 10 minutes. To start out I saved the to local file and read it into the program using a class I created to represent an individual node:

class IRLPNode:
    def __init__(self, line):
        """ Unpack the line to the properties of this class: """
        [self.node, self.callsign,, self.state,, self.status, self.record, self.install,, self.lon, self.lastupdate, self.freq, self.offset,, self.owner, self.url, self.lastchange, self.avrsstatus] = line.split('\t') = float(
        self.lon = float(self.lon)
    def distance(self, lat,lon):
        """ Distance in km """
        earthR = 6373
        dlat = radians(
        dlon = radians(lon-self.lon)
        a = sin(dlat/2)**2 + cos(radians( * cos(radians(lat)) * sin(dlon/2)**2
        c = 2*atan2(sqrt(a), sqrt(1-a))
        return earthR*c

Pulling Data

Note the handy unpacking [variable, variable, variable] = [array] to unpack from the tab separated values. Handy. I read from a file I created to load those after the add DummyLayer() line:

with open('nohtmlstatus.txt') as repfile:
            self.irlps = []
            for line in repfile:
                    values = IRLPNode(line)
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale('signaltower.svg',width=20,height=20,preserve_aspect_ratio=True)
                    self.osm.image_add(, values.lon, pixbuf)
                except ValueError:

and it displays! Proof of concept working! Note that there is one ValueError where the initial line is the labels and not exactly the format of all the other rows.

Now, ideally we want these to be able to display all these IRLP repeaters, and select ones that are nearby.

We also aught to update from the site and pull in the latest data whenever the computer is online, so let’s do that: This should be on a separate thread to not mess the main GTK UI thread…

class BackgroundDownload(Thread):
    def __init__(self, url, filename):
        #Thread init, as this is a thread:
        self.url = url
        self.filename = filename
        self.finished = False
    def run(self):
            tmpfile = 'output'+str(int(time.time()))
            urllib.request.urlretrieve(self.url, tmpfile)
            shutil.move( tmpfile, self.filename )
            self.finished = True
        except urllib.error.URLError:
            self.finished = True
        except urllib.error.HTTPError:
            print("Failed to fetch.")
            self.finished = True

This thread can be used to download that file, so let’s call that shortly after the program starts up – in the window __init__ function I add:

GObject.timeout_add(10000, self.downloadBackground)

and call creation of a download thread, and update all the repeaters in the internal array and display (“displayNodes”, a function with the nohtmlstatus.txt loading code above):

def downloadBackground(self):
        if self.bgdl == None:
            self.bgdl = BackgroundDownload('', 'nohtmlstatus.txt')
            #Call again 10m later
            GObject.timeout_add(600000, self.downloadBackground)
            if self.bgdl.finished:
                self.bgdl = None #and do create thread again:

Continued in part 2, see the listing code and link to the full Github source!

Leave a Reply

Your email address will not be published. Required fields are marked *

÷ three = 2