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;