Skip to content

Commit

Permalink
reorganize scan_cycle function
Browse files Browse the repository at this point in the history
  • Loading branch information
john committed Mar 1, 2024
1 parent b33a013 commit c45daeb
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 150 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ The demodulator blocks are put into a hierarchical GR block so multiple can be i

The scanner.py contains the control code, and may be run on on it's own non-interactively. It instantiates the receiver.py with N demodulators and probes the average spectrum at ~10 Hz. The spectrum is processed with estimate.py, which takes a weighted average of the spectrum bins that are above a threshold. This weighted average does a fair job of estimating the modulated channel center to sub-kHz resolution given the RBW is several kHz. The estimate.py returns a list of baseband channels that are rounded to the nearest 5 kHz (for NBFM band plan ambiguity).

The lockout channels are removed from the list and the list used to tune the demodulators. The demodulators are only tuned if the channel has ceased activity from the last probe or if a higher priority channel has activity. Otherwise, the demodulator is held on the channel. The demodulators are parked at 0 Hz baseband when not tuned, as this provides a constant, low amplitude signal due to FM demod of LO leakage.
The list used to tune the demodulators (lockout channels are skipped). The demodulators are only tuned if the channel has ceased activity from the last probe or if a higher priority channel has activity. Otherwise, the demodulator is held on the channel. The demodulators are parked at 0 Hz baseband when not tuned, as this provides a constant, low amplitude signal due to FM demod of LO leakage.

The ham2mon.py interfaces the scanner.py with the curses.py GUI. The GUI provides a spectral display with adjustable scaling and detector threshold line. The center frequency, gain, squelch, and volume can be adjusted in real time, as well as adding channel lockouts. The hardware arguments, sample rate, number of demodulators, recording status, and lockout file are set via switches at run time.

Expand All @@ -227,7 +227,7 @@ The next iteration of this program will probably use gr-dsd to decode P25 public
## Priority File
The Priority file contains a frequency (in Hz) in each line. The frequencies are to be arranged in descending priority order. Therefore, the highest priority frequenncy will be the one at the top.

When the scanner detects a priority frequency it will demodulate that frequency over any one that is lower priority. Without a priority file the scanner will only demodulate a frequency if there is a demodulator that is inactive.
When the scanner detects a priority frequency it will demodulate that frequency over any one that is lower priority. Priority channels with be flagged with a 'P' in the CHANNELS section. Without a priority file the scanner will only demodulate a frequency if there is a demodulator that is inactive.

A use case for this is as follows: You want to hear something, anything, but want to hear certain things over other things if they're actually happening. In this case there would be one demodulator being fed to the speakers. To do this, place local repeaters (in priority order) into the priority file. If all of the repeaters are idle, other frequencies will be heard. However, if a channel more important becomes active, those of lessor importance will be dropped in favor of the channel with more priority.

Expand Down
61 changes: 31 additions & 30 deletions apps/cursesgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
@author: madengr
"""
import locale
locale.setlocale(locale.LC_ALL, '')
import curses
import time
import numpy as np
import logging
from h2h_types import Channel

locale.setlocale(locale.LC_ALL, '')
class SpectrumWindow(object):
"""Curses spectrum display window
Expand Down Expand Up @@ -192,7 +194,7 @@ def __init__(self, screen):
self.dims = self.win.getmaxyx()


def draw_channels(self, gui_tuned_channels, gui_active_channels):
def draw_channels(self, channels: list[Channel]):
"""Draws tuned channels list
Args:
Expand All @@ -208,33 +210,32 @@ def draw_channels(self, gui_tuned_channels, gui_active_channels):

# 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

active_channels = set(gui_active_channels)

# Draw the tuned channels prefixed by index in list (demodulator index)
# Use color if tuned channel is in active channel list during this scan_cycle
for idx, gui_tuned_channel in enumerate(gui_tuned_channels):
text = str(idx)
text = text.zfill(2) + ": " + f'{gui_tuned_channel:.3f}'
# Use color if tuned channel is active during this scan_cycle
subset = channels[:max_length]
subset = [c for c in subset if c.active or c.hanging]
for idx, channel in enumerate(subset):
icon = 'P' if channel.priority else ''
text = f'{idx:02d}: {channel.frequency:.3f}'
if idx < self.dims[0]-2:
# Display in first column
# text color based on activity
# curses.color_pair(5)
if gui_tuned_channel in active_channels:
self.win.addnstr(idx+1, 1, text, 11, curses.color_pair(2) | curses.A_BOLD)
if channel.active:
self.win.addnstr(idx+1, 1, text, 12, curses.color_pair(2) | curses.A_BOLD)
self.win.addnstr(idx+1, 12, icon, 1, curses.color_pair(2))
else:
self.win.addnstr(idx+1, 1, text, 11, curses.color_pair(6))
self.win.addnstr(idx+1, 1, text, 12, curses.color_pair(6))
self.win.addnstr(idx+1, 12, icon, 1, curses.color_pair(6))
else:
# Display in second column
self.win.addnstr(idx-self.dims[0]+3, 13, text, 11)
if gui_tuned_channel in active_channels:
self.win.addnstr(idx-self.dims[0]+3, 13, text, 11, curses.color_pair(2))
if channel.active:
self.win.addnstr(idx-self.dims[0]+3, 13, text, 11, curses.color_pair(2) | curses.A_BOLD)
self.win.addnstr(idx-self.dims[0]+3, 24, icon, 1, curses.color_pair(2))
else:
self.win.addnstr(idx-self.dims[0]+3, 13, text, 11)
self.win.addnstr(idx-self.dims[0]+3, 13, text, 11, curses.color_pair(6))
self.win.addnstr(idx-self.dims[0]+3, 24, icon, 1, curses.color_pair(6))

# Hide cursor
self.win.leaveok(1)
Expand Down Expand Up @@ -264,7 +265,7 @@ def __init__(self, screen):
self.win = curses.newwin(height, width, screen_dims[0] - height - 1, width+1)
self.dims = self.win.getmaxyx()

def draw_channels(self, gui_lockout_channels, gui_active_channels):
def draw_channels(self, gui_lockout_channels, channels: list[Channel]):
"""Draws lockout channels list
Args:
Expand All @@ -277,23 +278,23 @@ def draw_channels(self, gui_lockout_channels, gui_active_channels):
self.win.addnstr(0, int(self.dims[1]/2-3), "LOCKOUT", 7,
curses.color_pair(6) | curses.A_DIM | curses.A_BOLD)

active_channels = set(gui_active_channels)

# Draw the lockout channels
# Use color if lockout channel is in active channel list during this scan_cycle
for idx, lockout_channel in enumerate(gui_lockout_channels):
locked_channels = [c for c in channels if c.locked]
for idx, lockout in enumerate(gui_lockout_channels):
# Don't draw past height of window
if idx <= self.dims[0]-3:
attr = curses.color_pair(6)
if isinstance(lockout_channel, dict): # handle this range
text = f"{lockout_channel['min']:.3f}-{lockout_channel['max']:.3f}"
for channel in active_channels:
if lockout_channel['min'] <= channel <= lockout_channel['max']:
if isinstance(lockout, dict): # handle this range
text = f"{lockout['min']:.3f}-{lockout['max']:.3f}"
for channel in locked_channels:
if lockout['min'] <= channel.frequency <= lockout['max']:
attr = curses.color_pair(5) | curses.A_BOLD
else: # handle this single frequency
text = f"{lockout_channel:.3f}"
if lockout_channel in active_channels:
attr = curses.color_pair(5) | curses.A_BOLD
text = f"{lockout:.3f}"
for channel in locked_channels:
if lockout == channel.frequency:
attr = curses.color_pair(5) | curses.A_BOLD
self.win.addnstr(idx+1, 1, text, 20, attr)
else:
pass
Expand Down
16 changes: 16 additions & 0 deletions apps/h2h_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Created on Thu Feb 29 09:00:22 2024
@author: john
"""

from dataclasses import dataclass

@dataclass(kw_only=True)
class Channel:
baseband: int
frequency: float
locked: bool
active: bool
priority: bool
hanging: bool
4 changes: 2 additions & 2 deletions apps/ham2mon.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ async def cycle(self):

# Update the spectrum, channel, and rx displays
self.specwin.draw_spectrum(self.scanner.spectrum)
self.chanwin.draw_channels(self.scanner.gui_tuned_channels, self.scanner.gui_active_channels)
self.lockoutwin.draw_channels(self.scanner.gui_lockout_channels, self.scanner.gui_active_channels)
self.chanwin.draw_channels(self.scanner.get_channels())
self.lockoutwin.draw_channels(self.scanner.gui_lockout_channels, self.scanner.get_channels())
self.rxwin.draw_rx()

# Update physical screen
Expand Down
Loading

0 comments on commit c45daeb

Please sign in to comment.