SDR Scanner(aka Threaded wxPerl Example)

Background: The July 2015 issue of Nuts and Volts magazine had an article on using an inexpensive DVB-T(Digital Video Broadcast-Terrestrial-EU Standard) USB dongle along with a 24MHz upconverter as the basis for an HF and higher Software Defined Receiver(SDR). The software used in the article was SDR# an MS-Windows application. As I am an Ubuntu user, I looked for a similar Linux application. I found gqrx. While waiting on parts to build the upconverter, I installed gqrx-sdr from the Ubuntu Software Center. It ran and pulled in various signals from 24MHz up, but was unstable. Turns out this was a pretty old version and after uninstalling and then installing via sudo add-apt-repository ppa:gqrx/snapshots, a current stable version was found. The output of gqrx can be routed through pulseaudio to other software(like fldigi) that can decode various digital modes.

gqrx has a Remote Control feature that can accept commands(a subset of the Amateur Radio Rigctrl protocol) over a Telnet connection and more digging turned up a perl script(gqrx-scan) that uses this interface, through Net::Telnet, to implement a scanner feature. gqrx-scan has many bells and whistles that I wouldn't ever use so I wrote a lite/crude version(lite.pl) that just scans from F1 to F2 repeatedly. For more flexibility(and practice) I decided to expand lite.pl into a wxPerl application(threadedgqrxLite.pl), mostly a bunch of text controls and sizers. A future enhancement might include one or more lists of frequencies of interest, i.e. local repeaters, satellites, etc.

For a hands-on introduction to SDR, take a look at websdr.com, a world-wide collection of SDR servers.

What resulted is also an example of a wxPerl GUI operating in parallel with Perl Threads. YMMV. TMTOWTDI.

#! /usr/bin/perl # Name: threadedgqrxLite.pl - Simple gqrx SDR Scanner - wxP +erl Version # Author: James M. Lynes, Jr. # Created: September 17, 2015 # Modified By: James M. Lynes, Jr. # Last Modified: October 19, 2015 # Environment: Ubuntu 14.04LTS / perl v5.18.2 / wxPerl 3.0.1 / +HP 15 Quad Core # Change Log: 9/17/2015 - Program Created # 9/19/2015 - Added Title Text, Connection Error Popup, Siz +ers and Event Handlers # - Scan and Listen Timers, Button Label Change as St +atus Indicator # 9/20/2015 - Additional Comments, add additional error che +cking/processing # 9/26/2015 - Restructure to a threaded implementation # 9/28/2015 - Stubbed threaded structure working, flesh out + thread code, # - modify event code # - Test thread commands and button interlocking flag +s # - Add error message timer/event # 9/29/2015 - Set error message timer to 1 sec # - Redesign threads, collapse to 1 Telnet Server thr +ead # - Object sharing not easily supported by Thread imp +lementation # 10/19/2015- Fixed scanning loop in Telnet Thread # - Scaled {pause} and {listen} to be in msecs # - Scaled frequency values to KHz fro +m Hz # - Added Modulation Mode setting # Description: "Simple" interface to gqrx to implement a Softwa +re Defined Radio(SDR) # scanner function using the remote control feature of gqrx # (a small subset of the amateur radio rigctrl protocol) # # gqrx is a software defined receiver powered by GNU-Radio + and QT # Developed by Alex Csete - gqrx.dk # The latest version is at sudo add-apt-repository ppa:gqrx +/snapshots # # gqrx uses inexpensive($12 USD) DVB-T USB Dongles to provi +de I and Q signals for DSP # DVB-T is the EU standard for Digital Video Broadcast # See Alex's website for a list of supported dongles # # This code is inspired by "Controlling gqrx from a Remote + Host" by Alex Csete # and gqrx-scan by Khaytsus - github.com/khaytsus/gqrx-sca +n # # Net::Telnet is not in the perl core and was installed fro +m cpan # sudo cpanm Net::Telnet # # Start gqrx and the gqrx remote control option before run +ning this perl code. # Also, check that the gqrx LNA and audio gains are set. # An error window will popup if gqrx is not running with Re +mote Control enabled, # or if the dongle is not plugged in, when a Telnet connect +ion request is made. # # Notes: To change parameters: Stop Scanning, Change Parameters +, Start Scanning. The # previous frequency range will complete scanning before th +e new range takes effect. # Modulation Mode change requires disconnect/reconnect. # package main; use strict; use warnings; use Net::Telnet; use Time::HiRes qw(sleep); use threads; use threads::shared; use Data::Dumper; # ----------------------------------- Thread setup must occur before W +x GUI setup --------------------------- # Define the Thread shared data area my %common : shared; $common{ip} = "127.0.0.1"; # Localhost $common{port} = "7356"; # Local Port as defined in gqrx $common{tnerror} = 0; # Status, Telnet Error $common{connect} = 0; # Command $common{connected} = 0; # Status $common{disconnect} = 0; # Command $common{scanstart} = 0; # Command $common{scanstarted} = 0; # Status $common{beginf} = 0; # Scan - Beginning Frequency $common{endf} = 0; # Scan - Ending Frequency $common{nextf} = 0; # Scan - Variable Frequency(loop co +unter) $common{step} = 0; # Scan - Frequency Step $common{squelch} = 0; # Scan - Minimum RSSI(Recieved Signal + Strength Indicator) $common{rssi} = 0; # Scan - Latest RSSI $common{rssiupdate} = 0; # Scan - RSSI Update Command $common{pause} = 0; # Scan - Time between scan cycles - + msec $common{listen} = 0; # Scan - Time to Listen to a strong si +gnal - msec $common{mode} = 0; # Scan - Demodulator Type $common{stopthreads} = 0; # Command # Create Threads and Detach my $thconnect = threads->create(\&TelnetServer); $thconnect->detach(); # Define Telnet Server Thread Processing sub TelnetServer { my $telnetsession; print "\nTelnet Server Thread Started\n"; print " Check that gqrx Remote Control is enabled\n and that L +NA and Audio gains are set.\n"; while(1) { if($common{stopthreads}) {print "\nTelnet Server Thread Termin +ated\n"; return}; if($common{connect}) { # Process Connec +t Command if(!$common{connected}) { print "Open Telnet Connection to gqrx\n"; $telnetsession = Net::Telnet->new(Timeout => 2, port => + $common{port}, Errmode => sub {$common{tnerro +r} = 1;}); $telnetsession->open($common{ip}); $telnetsession->print("M $common{mode}"); # Set +the demodulator type $telnetsession->waitfor(Match=> '/RPRT', Timeout=>5, Er +rmode=>"return"); $common{connected} = 1; $common{connect} = 0; } } if($common{disconnect}) { # Process Disconn +ect Command if($common{connected}) { print "Close Telnet Connection to gqrx\n"; $telnetsession->print("c"); $common{disconnect} = 0; $common{connected} = 0; } } if($common{scanstart}) { # Process Scan Com +mand $common{scanstarted} = 1; $telnetsession->print("F $common{nextf}"); # Up +date frequency $telnetsession->waitfor(Match => 'RPRT', Timeout => 5, + Errmode => "return"); $telnetsession->print("l"); # Get RSSI my ($prematch, $rssi) = $telnetsession->waitfor(Match +=> '/-{0,1}\d+\.\d/', Timeout => 5, Errmode => "retu +rn"); if(defined($rssi)) { if($rssi >= $common{squelch}) { # Found + a strong signal $common{rssi} = $rssi; $common{rssiupdate} = 1; Time::HiRes::sleep($common{listen}); # +Pause and listen awhile } } $common{nextf} = $common{nextf} + $common{step}; # +Loop for next frequency if($common{nextf} >= $common{endf}) {$common{nextf} = +$common{beginf}}; Time::HiRes::sleep($common{pause}); } threads->yield(); } } # ------------ Start up the Wx GUI Processing, must happen after the t +hreads are started ------------ my $app = App->new(); $app->MainLoop; package App; use strict; use warnings; use base 'Wx::App'; sub OnInit { my $frame = Frame->new(); $frame->Show(1); } package Frame; use strict; use warnings; use Wx qw(:everything); use base qw(Wx::Frame); sub new { my ($class, $parent) = @_; # Create top level frame my $self = $class->SUPER::new($parent, -1, "gqrx Lite Scanner", wx +DefaultPosition, wxDefaultSize); # Create Title Text $self->{titletext} = Wx::StaticText->new($self, -1, "Threaded gqrx + Lite Scanner", wxDefaultPosition, wxDefaultSize); # Create Modulation Radio Box - First entry is the default my $modulators = ["FM", "AM", "WFM_ST", "WFM", "LSB", "USB", "CW", + "CWL", "CWU"]; $self->{modbox} = Wx::RadioBox->new($self, -1, "Modulation", wxDef +aultPosition, wxDefaultSize, $modulators, 3, wxRA_SPECIFY_COLS); # Create Buttons $self->{startbutton} = Wx::Button->new($self, -1, "Start Scan +ning", wxDefaultPosition, wxDefaultSize); $self->{stopbutton} = Wx::Button->new($self, -1, "Stop Scann +ing", wxDefaultPosition, wxDefaultSize); $self->{connectbutton} = Wx::Button->new($self, -1, "Connect", +wxDefaultPosition, wxDefaultSize); $self->{disconnectbutton} = Wx::Button->new($self, -1, "Disconnect +", wxDefaultPosition, wxDefaultSize); $self->{quitbutton} = Wx::Button->new($self, -1, "Quit", wxD +efaultPosition, wxDefaultSize); # Create Data Entry Prompts and Boxes $self->{bflabel} = Wx::StaticText->new($self, -1, "Beginning Frequ +ency, KHz", wxDefaultPosition, wxDefaultSize); $self->{bftext} = Wx::TextCtrl->new($self, -1, "144000", wxDefault +Position, wxDefaultSize); $self->{eflabel} = Wx::StaticText->new($self, -1, "Ending Frequenc +y, KHz", wxDefaultPosition, wxDefaultSize); $self->{eftext} = Wx::TextCtrl->new($self, -1, "144100", wxDefault +Position, wxDefaultSize); $self->{fslabel} = Wx::StaticText->new($self, -1, "Frequency Step, + Hz", wxDefaultPosition, wxDefaultSize); $self->{fstext} = Wx::TextCtrl->new($self, -1, "1000", wxDefaultPo +sition, wxDefaultSize); $self->{sllabel} = Wx::StaticText->new($self, -1, "Squelch Level", + wxDefaultPosition, wxDefaultSize); $self->{sltext} = Wx::TextCtrl->new($self, -1, "-60.0", wxDefaultP +osition, wxDefaultSize); $self->{splabel} = Wx::StaticText->new($self, -1, "Scan Pause, ms" +, wxDefaultPosition, wxDefaultSize); $self->{sptext} = Wx::TextCtrl->new($self, -1, "20", wxDefaultPosi +tion, wxDefaultSize); $self->{lplabel} = Wx::StaticText->new($self, -1, "Listen Pause, m +s", wxDefaultPosition, wxDefaultSize); $self->{lptext} = Wx::TextCtrl->new($self, -1, "1000", wxDefaultPo +sition, wxDefaultSize); $self->{rssilabel} = Wx::StaticText->new($self, -1, "RSSI", wxDefa +ultPosition, wxDefaultSize); $self->{rssitext} = Wx::TextCtrl->new($self, -1, "0", wxDefaultPos +ition, wxDefaultSize); # Define Sizer Structure - My "Standard" Layout # Assumes: One Main Sizer(Horizontal) # One Header Sizer(Horizontal) # One Body Sizer(Horizontal) containing # Left Body Sizer(Vertical) # Right Body Sizer(Vertical) # Three Footer Sizers(horizontal) # # Create Sizers my $mainSizer = Wx::BoxSizer->new(wxVERTICAL); $self->SetSizer($mainSizer); my $headerSizer = Wx::BoxSizer->new(wxHORIZONTAL); my $bodySizer = Wx::BoxSizer->new(wxHORIZONTAL); my $leftbodySizer = Wx::BoxSizer->new(wxVERTICAL); my $rightbodySizer = Wx::BoxSizer->new(wxVERTICAL); my $footer1Sizer = Wx::BoxSizer->new(wxHORIZONTAL); my $footer2Sizer = Wx::BoxSizer->new(wxHORIZONTAL); my $footer3Sizer = Wx::BoxSizer->new(wxHORIZONTAL); # Layout Main Sizer $mainSizer->Add($headerSizer,0,0,0); $mainSizer->AddSpacer(20); $mainSizer->Add($bodySizer,0,0,0); $mainSizer->AddSpacer(30); $mainSizer->Add($footer1Sizer,0,0,0); $mainSizer->AddSpacer(10); $mainSizer->Add($footer2Sizer,0,0,0); $mainSizer->AddSpacer(10); $mainSizer->Add($footer3Sizer,0,0,0); # Layout Header Sizer $headerSizer->AddSpacer(150); $headerSizer->Add($self->{titletext},0,0,0); # Layout Body Sizer $bodySizer->Add($leftbodySizer,0,0,0); $bodySizer->AddSpacer(50); $bodySizer->Add($rightbodySizer,0,0,0); # Layout Right and Left Body Sizers $leftbodySizer->Add($self->{bflabel},0,0,0); $leftbodySizer->Add($self->{bftext},0,0,0); $leftbodySizer->Add($self->{eflabel},0,0,0); $leftbodySizer->Add($self->{eftext},0,0,0); $leftbodySizer->Add($self->{fslabel},0,0,0); $leftbodySizer->Add($self->{fstext},0,0,0); $leftbodySizer->Add($self->{sllabel},0,0,0); $leftbodySizer->Add($self->{sltext},0,0,0); $leftbodySizer->Add($self->{splabel},0,0,0); $leftbodySizer->Add($self->{sptext},0,0,0); $leftbodySizer->Add($self->{lplabel},0,0,0); $leftbodySizer->Add($self->{lptext},0,0,0); $rightbodySizer->Add($self->{modbox},0,0,0); $rightbodySizer->AddSpacer(10); $rightbodySizer->Add($self->{rssilabel},0,0,0); $rightbodySizer->AddSpacer(10); $rightbodySizer->Add($self->{rssitext},0,0,0); # Layout Footer Sizers $footer1Sizer->Add($self->{startbutton},0,0,0); $footer1Sizer->AddSpacer(10); $footer1Sizer->Add($self->{stopbutton},0,0,0); $footer2Sizer->Add($self->{connectbutton},0,0,0); $footer2Sizer->AddSpacer(10); $footer2Sizer->Add($self->{disconnectbutton},0,0,0); $footer3Sizer->Add($self->{quitbutton},0,0,0); # Define Messaging Timer to schedule checking flags and displaying err +ors from the threads $self->{msgtimer} = Wx::Timer->new($self); # Define Event Handlers Wx::Event::EVT_BUTTON($self, $self->{startbutton}, sub { my ($self, $event) = @_; if(!$common{connected}) { # Can't start s +caning if not connected Wx::MessageBox("Telnet is not connected\nCannot star +t scanning", "Telnet Connection Error", wxICON_ERROR, $self); } else { $common{beginf} = $self->{bftext}->GetValue*1000; + # Scale KHz to Hz $common{endf} = $self->{eftext}->GetValue*1000; + # Scale KHz to Hz $common{nextf} = $common{beginf}; $common{step} = $self->{fstext}->GetValue; $common{squelch} = $self->{sltext}->GetValue; $common{pause} = $self->{sptext}->GetValue/1000; + # Scale to msec $common{listen} = $self->{lptext}->GetValue/1000; + # Scale to msec $common{scanstart} = 1; $self->{startbutton}->SetLabel("Scanning"); # + Change button label to indicate status }}); Wx::Event::EVT_BUTTON($self, $self->{stopbutton}, sub { my ($self, $event) = @_; if(!$common{connected}) { # Can't stop sc +anning if not connected Wx::MessageBox("Telnet is not connected\nCannot stop + scanning", "Telnet Connection Error", wxICON_ERROR, $self); } else { $common{scanstart} = 0; $common{scanstarted} = 0; $self->{startbutton}->SetLabel("Start Scanning"); + # Restore button label }}); Wx::Event::EVT_BUTTON($self, $self->{connectbutton}, sub { my ($self, $event) = @_; if(!$common{connected}) { $common{mode} = $self->{modbox}->GetStringSelection; $common{connect} = 1; $self->{connectbutton}->SetLabel("Connected"); # +Change button label to indicate status } }); Wx::Event::EVT_BUTTON($self, $self->{disconnectbutton}, sub { my ($self, $event) = @_; if(!$common{connected}) { # Can't dis +connect if not connected Wx::MessageBox("Telnet is not connected\nCannot Disc +onnect", "Telnet Connection Error", wxICON_ERROR, $self);} else { if($common{connected}) { $common{disconnect} = 1; $self->{connectbutton}->SetLabel("Connect"); + # Restore button label } if($common{scanstarted}) { $common{scanstart} = 0; $common{scanstarted} = 0; $self->{startbutton}->SetLabel("Start Scannin +g"); # Restore button label } }}); Wx::Event::EVT_BUTTON($self, $self->{quitbutton}, sub { my ($self, $event) = @_; $common{stopthreads} = 1; $self->Close; }); Wx::Event::EVT_TEXT($self, $self->{bftext}, sub { my ($self, $event) = @_; $self->{beginf} = $self->{bftext}->GetValue; }); Wx::Event::EVT_TEXT($self, $self->{eftext}, sub { my ($self, $event) = @_; $self->{endf} = $self->{eftext}->GetValue; }); Wx::Event::EVT_TEXT($self, $self->{fstext}, sub { my ($self, $event) = @_; $self->{step} = $self->{fstext}->GetValue; }); Wx::Event::EVT_TEXT($self, $self->{sltext}, sub { my ($self, $event) = @_; $self->{squelch} = $self->{sltext}->GetValue; }); Wx::Event::EVT_TEXT($self, $self->{sptext}, sub { my ($self, $event) = @_; $self->{pause} = $self->{sptext}->GetValue; }); Wx::Event::EVT_TEXT($self, $self->{lptext}, sub { my ($self, $event) = @_; $self->{listen} = $self->{lptext}->GetValue; }); Wx::Event::EVT_TEXT($self, $self->{rssitext}, sub { my ($self, $event) = @_; $self->{rssi} = $self->{rssitext}->GetValue; }); Wx::Event::EVT_RADIOBOX($self, $self->{modbox}, sub { my ($self, $event) = @_; $self->{mode} = $self->{modbox}->GetStringSelection; }); Wx::Event::EVT_TIMER($self, $self->{msgtimer}, sub { # +Display Error messages if($common{tnerror}) { # Telnet Error Wx::MessageBox("Telnet Connection Failed", "gqrx Lite +Scanner Error", wxICON_ERROR, $self); $self->{connectbutton}->SetLabel("Connect"); # Rest +ore button label $common{tnerror} = 0; } if($common{rssiupdate}) { $self->{rssitext}->SetValue($common{rssi}) +; $common{rssiupdate} = 0; } }); # Start Error Message Timer $self->{msgtimer}->Start(1000); # 1 second +period # Assign mainSizer to the Frame and trigger layout $mainSizer->Fit($self); $mainSizer->Layout(); return $self; } 1;

James

There's never enough time to do it right, but always enough time to do it over...


In reply to SDR Scanner(aka Threaded wxPerl Example) by jmlynesjr

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post, it's "PerlMonks-approved HTML":



  • Posts are HTML formatted. Put <p> </p> tags around your paragraphs. Put <code> </code> tags around your code and data!
  • Titles consisting of a single word are discouraged, and in most cases are disallowed outright.
  • Read Where should I post X? if you're not absolutely sure you're posting in the right place.
  • Please read these before you post! —
  • Posts may use any of the Perl Monks Approved HTML tags:
    a, abbr, b, big, blockquote, br, caption, center, col, colgroup, dd, del, details, div, dl, dt, em, font, h1, h2, h3, h4, h5, h6, hr, i, ins, li, ol, p, pre, readmore, small, span, spoiler, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, tr, tt, u, ul, wbr
  • You may need to use entities for some characters, as follows. (Exception: Within code tags, you can put the characters literally.)
            For:     Use:
    & &amp;
    < &lt;
    > &gt;
    [ &#91;
    ] &#93;
  • Link using PerlMonks shortcuts! What shortcuts can I use for linking?
  • See Writeup Formatting Tips and other pages linked from there for more info.