#!/usr/bin/env python # -*- coding: utf-8 -*- """ Created on Sun Jul 5 17:16:22 2015 @author: madengr """ import locale locale.setlocale(locale.LC_ALL, '') import curses import time import numpy as np class SpectrumWindow(object): """Curses spectrum display window Args: screen (object): a curses screen object Attributes: max_db (float): Top of window in dB min_db (float): Bottom of window in dB threshold_db (float): Threshold horizontal line """ def __init__(self, screen): self.screen = screen # Set default values self.max_db = 50.0 self.min_db = -20.0 self.threshold_db = 20.0 # Create a window object in top half of the screen, within the border screen_dims = screen.getmaxyx() height = int(screen_dims[0]/2.0) width = screen_dims[1]-2 self.win = curses.newwin(height, width, 1, 1) self.dims = self.win.getmaxyx() # Right end of window resreved for string of N charachters self.chars = 7 def draw_spectrum(self, data): """Scales input spectral data to window dimensions and draws bar graph Args: data (numpy.ndarray): FFT power spectrum data in linear, not dB Test cases for data with min_db=-100 and max_db=0 on 80x24 terminal: 1.0E-10 draws nothing since it is not above -100 dB 1.1E-10 draws one row 1.0E-05 draws 5 rows 1.0E+00 draws 10 rows 1.0E+01 draws 10 rows """ # Keep min_db to 10 dB below max_db if self.min_db > (self.max_db - 10): self.min_db = self.max_db - 10 # Split the data into N window bins # N is window width between border (i.e. self.dims[1]-2 ) # Data must be at least as long as the window width or crash # Use the maximum value from each input data bin for the window bin win_bins = np.array_split(data, self.dims[1]-self.chars) win_bin_max = [] for win_bin in win_bins: win_bin_max.append(np.max(win_bin)) # Convert to dB win_bin_max_db = 10*np.log10(win_bin_max) # The plot windows starts from max_db at the top # and draws DOWNWARD to min_db (remember this is a curses window). # Thus linear scaling goes from min_y=1 at the top # and draws DOWNWARD to max_y=dims[0]-1 at the bottom # The "1" and "-1" is to account for the border at top and bottom min_y = 1 max_y = self.dims[0]-1 # Scaling factor for plot scale = (min_y-max_y)/(self.max_db-self.min_db) # Generate y position, clip to window, and convert to int pos_y = (win_bin_max_db - self.max_db) * scale pos_y = np.clip(pos_y, min_y, max_y) pos_y = pos_y.astype(int) # Clear previous contents, draw border, and title self.win.clear() self.win.border(0) self.win.addnstr(0, self.dims[1]/2-4, "SPECTRUM", 8, curses.color_pair(4)) # Draw the bars for pos_x in range(len(pos_y)): # Invert the y fill since we want bars # Offset x (column) by 1 so it does not start on the border self.win.vline(pos_y[pos_x], pos_x+1, "*", max_y-pos_y[pos_x]) # Draw the max_db and min_db strings string = ">" + "%+03d" % self.max_db self.win.addnstr(0, 1 + self.dims[1] - self.chars, string, self.chars, curses.color_pair(1)) string = ">" + "%+03d" % self.min_db self.win.addnstr(max_y, 1 + self.dims[1] - self.chars, string, self.chars, curses.color_pair(3)) # Generate threshold line, clip to window, and convert to int pos_yt = (self.threshold_db - self.max_db) * scale pos_yt = np.clip(pos_yt, min_y, max_y-1) pos_yt = pos_yt.astype(int) # Draw the theshold line # x=1 start to account for left border self.win.hline(pos_yt, 1, "-", len(pos_y)) # Draw the theshold string string = ">" + "%+03d" % self.threshold_db self.win.addnstr(pos_yt, (1 + self.dims[1] - self.chars), string, self.chars, curses.color_pair(2)) # Hide cursor self.win.leaveok(1) # Update virtual window self.win.noutrefresh() def proc_keyb(self, keyb): """Process keystrokes Args: keyb (int): keystroke in ASCII Returns: bool: True if receiver needs tuning, False if not """ if keyb == ord('t'): self.threshold_db += 5 return True elif keyb == ord('r'): self.threshold_db -= 5 return True elif keyb == ord('T'): self.threshold_db += 1 return True elif keyb == ord('R'): self.threshold_db -= 1 return True elif keyb == ord('p'): self.max_db += 10 elif keyb == ord('o'): self.max_db -= 10 elif keyb == ord('w'): self.min_db += 10 elif keyb == ord('q'): self.min_db -= 10 else: pass return False class ChannelWindow(object): """Curses channel display window Args: screen (object): a curses screen object """ # pylint: disable=too-few-public-methods def __init__(self, screen): self.screen = screen # Create a window object in the bottom half of the screen # Make it about 1/3 the screen width # Place on left side and to the right of the border screen_dims = screen.getmaxyx() height = int(screen_dims[0]/2.0)-2 width = int(screen_dims[1]/3.0)-1 self.win = curses.newwin(height, width, height + 3, 1) self.dims = self.win.getmaxyx() def draw_channels(self, gui_tuned_channels): """Draws tuned channels list Args: rf_channels [string]: List of strings in MHz """ # Clear previous contents, draw border, and title self.win.clear() self.win.border(0) self.win.addnstr(0, self.dims[1]/2-4, "CHANNELS", 8, curses.color_pair(4)) # Limit the displayed channels to no more than two rows max_length = 2*(self.dims[0]-2) if len(gui_tuned_channels) > max_length: gui_tuned_channels = gui_tuned_channels[:max_length] else: pass # Draw the tuned channels prefixed by index in list (demodulator index) for idx, gui_tuned_channel in enumerate(gui_tuned_channels): text = str(idx) + ": " + gui_tuned_channel if idx < self.dims[0]-2: # Display in first column self.win.addnstr(idx+1, 1, text, 11) else: # Display in second column self.win.addnstr(idx-self.dims[0]+3, 13, text, 11) # Hide cursor self.win.leaveok(1) # Update virtual window self.win.noutrefresh() class LockoutWindow(object): """Curses lockout channel display window Args: screen (object): a curses screen object """ # pylint: disable=too-few-public-methods def __init__(self, screen): self.screen = screen # Create a window object in the bottom half of the screen # Make it about 1/4 the screen width # Place on left side and to the right of the border screen_dims = screen.getmaxyx() height = int(screen_dims[0]/2.0)-2 width = int(screen_dims[1]/4.0)-5 self.win = curses.newwin(height, width, height + 3, 26) self.dims = self.win.getmaxyx() def draw_channels(self, gui_lockout_channels): """Draws tuned channels list Args: rf_channels [string]: List of strings in MHz """ # Clear previous contents, draw border, and title self.win.clear() self.win.border(0) self.win.addnstr(0, self.dims[1]/2-3, "LOCKOUT", 7, curses.color_pair(4)) # Draw the lockout channels for idx, gui_lockout_channel in enumerate(gui_lockout_channels): # Don't draw past height of window if idx <= self.dims[0]-3: text = " " + gui_lockout_channel self.win.addnstr(idx+1, 1, text, 11) else: pass # Hide cursor self.win.leaveok(1) # Update virtual window self.win.noutrefresh() def proc_keyb_set_lockout(self, keyb): """Process keystrokes to lock out channels 0 - 9 Args: keyb (int): keystroke in ASCII Returns: bool: True if scanner needs adjusting, False if not """ # pylint: disable=no-self-use # Check if keys 0 - 9 pressed if keyb - 48 in range(10): return True else: return False def proc_keyb_clear_lockout(self, keyb): """Process keystrokes to clear lockout with "l" Args: keyb (int): keystroke in ASCII Returns: bool: True if scanner needs adjusting, False if not """ # pylint: disable=no-self-use # Check if 'l' is pressed if keyb == ord('l'): return True else: return False class RxWindow(object): """Curses receiver paramater window Args: screen (object): a curses screen object Attributes: center_freq (float): Hardware RF center frequency in Hz samp_rate (float): Hardware sample rate in sps (1E6 min) gain_db (int): Hardware RF gain in dB if_gain_db (int): Hardware IF gain in dB bb_gain_db (int): Hardware BB gain in dB squelch_db (int): Squelch in dB volume_dB (int): Volume in dB record (bool): Record audio to file if True lockout_file_name (string): Name of file with channels to lockout priority_file_name (string): Name of file with channels for priority """ # pylint: disable=too-many-instance-attributes def __init__(self, screen): self.screen = screen # Set default values self.center_freq = 146E6 self.samp_rate = 2E6 self.freq_entry = 'None' self.gain_db = 0 self.if_gain_db = 16 self.bb_gain_db = 16 self.squelch_db = -60 self.volume_db = 0 self.type_demod = 0 self.record = True self.lockout_file_name = "" self.priority_file_name = "" # Create a window object in the bottom half of the screen # Make it about 1/3 the screen width # Place on right side and to the left of the border screen_dims = screen.getmaxyx() height = int(screen_dims[0]/2.0)-2 width = int(screen_dims[1]/2.0)-2 self.win = curses.newwin(height, width, height + 3, int(screen_dims[1]-width-1)) self.dims = self.win.getmaxyx() def draw_rx(self): """Draws receiver paramaters """ # Clear previous contents, draw border, and title self.win.clear() self.win.border(0) self.win.addnstr(0, self.dims[1]/2-4, "RECEIVER", 8, curses.color_pair(4)) # Draw the receiver info prefix fields text = "RF Freq (MHz) : " self.win.addnstr(1, 1, text, 15) text = "RF Gain (dB) : " self.win.addnstr(2, 1, text, 15) text = "IF Gain (dB) : " self.win.addnstr(3, 1, text, 15) text = "BB Gain (dB) : " self.win.addnstr(4, 1, text, 15) text = "BB Rate (Msps): " self.win.addnstr(5, 1, text, 15) text = "BB Sql (dB) : " self.win.addnstr(6, 1, text, 15) text = "AF Vol (dB) : " self.win.addnstr(7, 1, text, 15) text = "Record : " self.win.addnstr(8, 1, text, 15) text = "Demod Type : " self.win.addnstr(9, 1, text, 15) text = "Files : " self.win.addnstr(10, 1, text, 15) # Draw the receiver info suffix fields if self.freq_entry <> 'None': text = self.freq_entry else: text = '{:.3f}'.format((self.center_freq)/1E6) self.win.addnstr(1, 17, text, 8, curses.color_pair(5)) text = str(self.gain_db) self.win.addnstr(2, 17, text, 8, curses.color_pair(5)) text = str(self.if_gain_db) self.win.addnstr(3, 17, text, 8, curses.color_pair(5)) text = str(self.bb_gain_db) self.win.addnstr(4, 17, text, 8, curses.color_pair(5)) text = str(self.samp_rate/1E6) self.win.addnstr(5, 17, text, 8) text = str(self.squelch_db) self.win.addnstr(6, 17, text, 8, curses.color_pair(5)) text = str(self.volume_db) self.win.addnstr(7, 17, text, 8, curses.color_pair(5)) text = str(self.record) self.win.addnstr(8, 17, text, 8) text = str(self.type_demod) self.win.addnstr(9, 17, text, 8) text = str(self.lockout_file_name) + " " + str(self.priority_file_name) self.win.addnstr(10, 17, text, 20) # Hide cursor self.win.leaveok(1) # Update virtual window self.win.noutrefresh() def proc_keyb_hard(self, keyb): """Process keystrokes to adjust hard receiver settings Tune center_freq in 100 MHz steps with 'x' and 'c' Tune center_freq in 10 MHz steps with 'v' and 'c' Tune center_freq in 1 MHz steps with 'm' and 'n' Tune center_freq in 100 kHz steps with 'k' and 'j' Args: keyb (int): keystroke in ASCII Returns: bool: True if receiver needs adjusting, False if not """ # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches # Tune self.center_freq in 100 MHz steps with 'x' and 'c' if keyb == ord('x'): self.center_freq += 1E8 return True elif keyb == ord('z'): self.center_freq -= 1E8 return True # Tune self.center_freq in 10 MHz steps with 'v' and 'c' elif keyb == ord('v'): self.center_freq += 1E7 return True elif keyb == ord('c'): self.center_freq -= 1E7 return True # Tune self.center_freq in 1 MHz steps with 'm' and 'n' elif keyb == ord('m'): self.center_freq += 1E6 return True elif keyb == ord('n'): self.center_freq -= 1E6 return True # Tune self.center_freq in 100 kHz steps with 'k' and 'j' elif keyb == ord('k'): self.center_freq += 1E5 return True elif keyb == ord('j'): self.center_freq -= 1E5 return True elif keyb == ord('/'): # set mode to frequency entry self.freq_entry = '' return False elif keyb == 27: # ESC # end frequncy entry mode without seting the frequency self.freq_entry = 'None' return False elif keyb == ord('\n'): # set the frequency from what was entered try: self.center_freq = float(self.freq_entry) * 1E6 except: pass self.freq_entry = 'None' return True elif self.freq_entry <> 'None' and (keyb - 48 in range (10) or keyb == ord('.')): # build up frequency from 1-9 and '.' self.freq_entry = self.freq_entry + chr(keyb) return False elif keyb == 127: # BKSP self.freq_entry = self.freq_entry[:-1] return False else: return False def proc_keyb_soft(self, keyb): """Process keystrokes to adjust soft receiver settings Tune gain_db in 10 dB steps with 'g' and 'f' Tune squelch_db in 1 dB steps with 's' and 'a' Tune volume_db in 1 dB steps with '.' and ',' Args: keyb (int): keystroke in ASCII Returns: bool: True if receiver needs tuning, False if not """ # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches # Tune self.gain_db in 10 dB steps with 'g' and 'f' if keyb == ord('g'): self.gain_db += 10 return True elif keyb == ord('f'): self.gain_db -= 10 return True # Tune self.gain_db in 1 dB steps with 'G' and 'F' if keyb == ord('G'): self.gain_db += 1 return True elif keyb == ord('F'): self.gain_db -= 1 return True # Tune self.if_gain_db in 10 dB steps with 'u' and 'y' if keyb == ord('u'): self.if_gain_db += 10 return True elif keyb == ord('y'): self.if_gain_db -= 10 return True # Tune self.if_gain_db in 1 dB steps with 'U' and 'Y' if keyb == ord('U'): self.if_gain_db += 1 return True elif keyb == ord('Y'): self.if_gain_db -= 1 return True # Tune self.bb_gain_db in 10 dB steps with ']' and '[' if keyb == ord(']'): self.bb_gain_db += 10 return True elif keyb == ord('['): self.bb_gain_db -= 10 return True # Tune self.bb_gain_db in 1 dB steps with '}' and '{' if keyb == ord('}'): self.bb_gain_db += 1 return True elif keyb == ord('{'): self.bb_gain_db -= 1 return True # Tune self.squelch_db in 1 dB steps with 's' and 'a' elif keyb == ord('s'): self.squelch_db += 1 return True elif keyb == ord('a'): self.squelch_db -= 1 return True # Tune self.volume_db in 1 dB steps with '.' and ',' elif keyb == ord('.'): self.volume_db += 1 return True elif keyb == ord(','): self.volume_db -= 1 return True else:# pylint: disable=too-many-return-statements return False def setup_screen(screen): """Sets up screen """ # Set screen to getch() is non-blocking screen.nodelay(1) # Define some colors curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLACK) curses.init_pair(4, curses.COLOR_MAGENTA, curses.COLOR_BLACK) curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Add border screen.border(0) def main(): """Test most of the GUI (except lockout processing) Initialize and set up screen Create windows Generate dummy spectrum data Update windows with dummy values Process keyboard strokes """ # Use the curses.wrapper() to crash cleanly # Note the screen object is passed from the wrapper() # http://stackoverflow.com/questions/9854511/ppos_ython-curses-dilemma # The issue is the debuuger won't work with the wrapper() # So enable the next 2 lines and don't pass screen to main() screen = curses.initscr() curses.start_color() # Setup the screen setup_screen(screen) # Create windows specwin = SpectrumWindow(screen) chanwin = ChannelWindow(screen) lockoutwin = LockoutWindow(screen) rxwin = RxWindow(screen) while 1: # Generate some random spectrum data from -dyanmic_range to 0 dB # then offset_db length = 128 dynamic_range_db = 100 offset_db = 50 data = np.power(10, (-dynamic_range_db*np.random.rand(length)/10)\ + offset_db/10) #data = 1E-5*np.ones(length) specwin.draw_spectrum(data) # Put some dummy values in the channel, lockout, and receiver windows chanwin.draw_channels(['144.100', '142.40', '145.00', '144.10',\ '142.40', '145.00', '144.10', '142.40', '145.00', '144.10', '142.40',\ '145.00', '142.40', '145.00', '144.10', '142.400', '145.00', '145.00']) lockoutwin.draw_channels(['144.100', '142.40', '145.00']) rxwin.draw_rx() # Update physical screen curses.doupdate() # Check for keystrokes and process keyb = screen.getch() specwin.proc_keyb(keyb) rxwin.proc_keyb_hard(keyb) rxwin.proc_keyb_soft(keyb) # Sleep to get about a 10 Hz refresh time.sleep(0.1) if __name__ == '__main__': try: #curses.wrapper(main) main() except KeyboardInterrupt: pass