#! /usr/bin/perl # Name: threadedgqrxLite.pl - Simple gqrx SDR Scanner - wxPerl 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, Sizers and Event Handlers # - Scan and Listen Timers, Button Label Change as Status Indicator # 9/20/2015 - Additional Comments, add additional error checking/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 flags # - Add error message timer/event # 9/29/2015 - Set error message timer to 1 sec # - Redesign threads, collapse to 1 Telnet Server thread # - Object sharing not easily supported by Thread implementation # 10/19/2015- Fixed scanning loop in Telnet Thread # - Scaled {pause} and {listen} to be in msecs # - Scaled frequency values to KHz from Hz # - Added Modulation Mode setting # Description: "Simple" interface to gqrx to implement a Software 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 provide 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-scan # # Net::Telnet is not in the perl core and was installed from cpan # sudo cpanm Net::Telnet # # Start gqrx and the gqrx remote control option before running 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 Remote Control enabled, # or if the dongle is not plugged in, when a Telnet connection request is made. # # Notes: To change parameters: Stop Scanning, Change Parameters, Start Scanning. The # previous frequency range will complete scanning before the 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 Wx 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 counter) $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 signal - 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 LNA and Audio gains are set.\n"; while(1) { if($common{stopthreads}) {print "\nTelnet Server Thread Terminated\n"; return}; if($common{connect}) { # Process Connect Command if(!$common{connected}) { print "Open Telnet Connection to gqrx\n"; $telnetsession = Net::Telnet->new(Timeout => 2, port => $common{port}, Errmode => sub {$common{tnerror} = 1;}); $telnetsession->open($common{ip}); $telnetsession->print("M $common{mode}"); # Set the demodulator type $telnetsession->waitfor(Match=> '/RPRT', Timeout=>5, Errmode=>"return"); $common{connected} = 1; $common{connect} = 0; } } if($common{disconnect}) { # Process Disconnect 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 Command $common{scanstarted} = 1; $telnetsession->print("F $common{nextf}"); # Update 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 => "return"); 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 threads 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", wxDefaultPosition, 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", wxDefaultPosition, wxDefaultSize, $modulators, 3, wxRA_SPECIFY_COLS); # Create Buttons $self->{startbutton} = Wx::Button->new($self, -1, "Start Scanning", wxDefaultPosition, wxDefaultSize); $self->{stopbutton} = Wx::Button->new($self, -1, "Stop Scanning", 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", wxDefaultPosition, wxDefaultSize); # Create Data Entry Prompts and Boxes $self->{bflabel} = Wx::StaticText->new($self, -1, "Beginning Frequency, KHz", wxDefaultPosition, wxDefaultSize); $self->{bftext} = Wx::TextCtrl->new($self, -1, "144000", wxDefaultPosition, wxDefaultSize); $self->{eflabel} = Wx::StaticText->new($self, -1, "Ending Frequency, KHz", wxDefaultPosition, wxDefaultSize); $self->{eftext} = Wx::TextCtrl->new($self, -1, "144100", wxDefaultPosition, wxDefaultSize); $self->{fslabel} = Wx::StaticText->new($self, -1, "Frequency Step, Hz", wxDefaultPosition, wxDefaultSize); $self->{fstext} = Wx::TextCtrl->new($self, -1, "1000", wxDefaultPosition, wxDefaultSize); $self->{sllabel} = Wx::StaticText->new($self, -1, "Squelch Level", wxDefaultPosition, wxDefaultSize); $self->{sltext} = Wx::TextCtrl->new($self, -1, "-60.0", wxDefaultPosition, wxDefaultSize); $self->{splabel} = Wx::StaticText->new($self, -1, "Scan Pause, ms", wxDefaultPosition, wxDefaultSize); $self->{sptext} = Wx::TextCtrl->new($self, -1, "20", wxDefaultPosition, wxDefaultSize); $self->{lplabel} = Wx::StaticText->new($self, -1, "Listen Pause, ms", wxDefaultPosition, wxDefaultSize); $self->{lptext} = Wx::TextCtrl->new($self, -1, "1000", wxDefaultPosition, wxDefaultSize); $self->{rssilabel} = Wx::StaticText->new($self, -1, "RSSI", wxDefaultPosition, wxDefaultSize); $self->{rssitext} = Wx::TextCtrl->new($self, -1, "0", wxDefaultPosition, 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 errors 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 scaning if not connected Wx::MessageBox("Telnet is not connected\nCannot start 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 scanning 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 disconnect if not connected Wx::MessageBox("Telnet is not connected\nCannot Disconnect", "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 Scanning"); # 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"); # Restore 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;