Building a repeater app for Linux, part 5: Subprocess for listening to the radio

With the number of cheap RTLSDR devices that let you listen to radio or ham radio, it’s only natural to want to check out each of the repeaters and their use when you go to a new area. In fact, once you have a device set up correctly it is easy to integrate that (or any other command line features) into your project.

To start with I set up a class based on what I added for Hearham uploader – this will make the command run in a separate thread, continuing the process until it is killed. An ongoing process must not be on the same thread as the GUI (in any interface, Java, Android, or GTK…) This is going to use subprocess module as it can make it easier to use an existing utility (rlt_fm command in this case), rather than doing the whole signal processing in Python.

class RTLSDRRun(Thread):
    def __init__(self, cmd):
        Thread.__init__(self)
        self.cmd = cmd
        
    def run(self):
        #cmd = 'rtl_fm -M fm -f '+self.freq+'M -l 202 | play -r 24k -t raw -e s -b 16 -c 1 -V1 -'
        cmds = self.cmd.split('|')
        self.proc = subprocess.Popen(cmds[0].split(),
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)
        subprocess.check_output(cmds[1].split(),stdin=self.proc.stdout)
        for line in iter(self.proc.stdout.readline, b''):
            line = line.decode('utf-8')
            #print(line)

One of these will be run at a time, so the window needs a class variable to hold a RTLSDRRun thread, and a current playing frequency to see which one is playing:

        self.rtllistener = None
        self.playingfreq = None
        self.PLAYSIZE = Gtk.IconSize.BUTTON

I add the play button under the distance by revising that part, making a VBox to hold them both, and connect a clicked event to start up rtl_fm, kill it if clicked again (stopping audio), and switching the icon of the button as applicable:

        playbtn = Gtk.Button(stock=Gtk.STOCK_MEDIA_PLAY)
        playbtn.set_label('')
        playbtn.selFrequency = repeater.freq
        if repeater.freq == self.playingfreq:
            playbtn.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_STOP, self.PLAYSIZE))
        else:
            playbtn.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY, self.PLAYSIZE))
        playbtn.connect('clicked', self.playpause)
        rightbox = Gtk.VBox()
        rightbox.pack_start(distlbl, False, True, 10)
        rightbox.pack_start(playbtn, True, True, 0)
        hbox.pack_start(rightbox, False, True, 0)
         
        #These two arrays should correspond!
         self.GTKListRows.append(row)
        self.playBtns.append(playbtn)
         self.listbox.add(row)
        
    def playpause(self, btn):
        if btn.selFrequency != self.playingfreq:
            self.playRTLSDR(btn.selFrequency)
            self.playingfreq = btn.selFrequency
            for b in self.playBtns:
                b.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY, self.PLAYSIZE))
            #All others are stopped.
            btn.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_STOP, self.PLAYSIZE))
        else:
            if self.rtllistener:
                self.rtllistener.proc.kill()
            self.playingfreq = None
            btn.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY, self.PLAYSIZE))

To keep track of all the play buttons and not have multiple showing stop to stop while they are in fact not playing, I added an array for the play buttons, to set play not stop when another frequency is played.

self.GTKListRows = []
self.playBtns = [] #added
...
    def on_map_change(self, event):
        if self.renderedLat != self.osm.props.latitude or self.renderedLon != self.osm.props.longitude:
            #Center changed.
            self.renderedLat = self.osm.props.latitude
            self.renderedLon = self.osm.props.longitude

            t = time.time()
            text = 'Map Center: latitude %s longitude %s' if self.mainScreen.get_width() > 800 else 'lat: %s lon: %s'
            self.latlon_entry.set_text(
                text % (
                    round(self.osm.props.latitude, 4),
                    round(self.osm.props.longitude, 4)
                )
            )
            # cursor lat,lon = self.osm.get_event_location(event).get_degrees()
            lat, lon = self.osm.props.latitude, self.osm.props.longitude
            maxkm = 500
            self.irlps = sorted(self.irlps, key = lambda repeater : repeater.distance(lat,lon))
            for r in self.GTKListRows:
                r.destroy()
            self.playBtns = []
            ...

and append to that list whenever self.GTKListRows is appended, remove whenever self.GTKListRows is removed/cleared. It’s odd that when I tried destroy() on the buttons it caused visual issues and GTK Critical errors in the console. It would appear you should not destroy() when the container of it has been destroyed, also not keep a list of references to those button widgets!

You can see the full code change here and check it out from github and give it a try.

Leave a Reply

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

two × 5 =