Skip to content

Commit

Permalink
Added a new menu item for updating the ADIF modes from the ADIF websi…
Browse files Browse the repository at this point in the history
…te. Addresses issue #69.
  • Loading branch information
Christian Jacobs committed Apr 14, 2019
1 parent 091da72 commit 2487424
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 134 deletions.
1 change: 1 addition & 0 deletions bin/pyqso
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class PyQSO:
# Kills the application if the close button is clicked on the main window itself.
self.window.connect("delete-event", Gtk.main_quit)

# Status bar.
self.statusbar = self.builder.get_object("statusbar")
context_id = self.statusbar.get_context_id("Status")
self.statusbar.push(context_id, "No logbook is currently open.")
Expand Down
99 changes: 5 additions & 94 deletions pyqso/adif.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import ConfigParser as configparser
from os.path import expanduser

from pyqso.modes import Modes

# ADIF field names and their associated data types available in PyQSO.
AVAILABLE_FIELD_NAMES_TYPES = {"CALL": "S",
"QSO_DATE": "D",
Expand Down Expand Up @@ -98,98 +100,6 @@
# E: Enumerated
DATA_TYPES = ["A", "B", "N", "S", "I", "D", "T", "M", "G", "L", "E"]

# All the valid modes listed in the ADIF specification. This is a dictionary with the key-value pairs holding the MODE and SUBMODE(s) respectively.
MODES = {"": ("",),
"AM": ("",),
"ATV": ("",),
"CHIP": ("", "CHIP64", "CHIP128"),
"CLO": ("",),
"CONTESTI": ("",),
"CW": ("", "PCW"),
"DIGITALVOICE": ("",),
"DOMINO": ("", "DOMINOEX", "DOMINOF"),
"DSTAR": ("",),
"FAX": ("",),
"FM": ("",),
"FSK441": ("",),
"FT8": ("",),
"HELL": ("", "FMHELL", "FSKHELL", "HELL80", "HFSK", "PSKHELL"),
"ISCAT": ("", "ISCAT-A", "ISCAT-B"),
"JT4": ("", "JT4A", "JT4B", "JT4C", "JT4D", "JT4E", "JT4F", "JT4G"),
"JT6M": ("",),
"JT9": ("",),
"JT44": ("",),
"JT65": ("", "JT65A", "JT65B", "JT65B2", "JT65C", "JT65C2"),
"MFSK": ("", "MFSK4", "MFSK8", "MFSK11", "MFSK16", "MFSK22", "MFSK31", "MFSK32", "MFSK64", "MFSK128"),
"MT63": ("",),
"OLIVIA": ("", "OLIVIA 4/125", "OLIVIA 4/250", "OLIVIA 8/250", "OLIVIA 8/500", "OLIVIA 16/500", "OLIVIA 16/1000", "OLIVIA 32/1000"),
"OPERA": ("", "OPERA-BEACON", "OPERA-QSO"),
"PAC": ("", "PAC2", "PAC3", "PAC4"),
"PAX": ("", "PAX2"),
"PKT": ("",),
"PSK": ("", "FSK31", "PSK10", "PSK31", "PSK63", "PSK63F", "PSK125", "PSK250", "PSK500", "PSK1000", "PSKAM10", "PSKAM31", "PSKAM50", "PSKFEC31", "QPSK31", "QPSK63", "QPSK125", "QPSK250", "QPSK500"),
"PSK2K": ("",),
"Q15": ("",),
"ROS": ("", "ROS-EME", "ROS-HF", "ROS-MF"),
"RTTY": ("", "ASCI"),
"RTTYM": ("",),
"SSB": ("", "LSB", "USB"),
"SSTV": ("",),
"THOR": ("",),
"THRB": ("", "THRBX"),
"TOR": ("", "AMTORFEC", "GTOR"),
"V4": ("",),
"VOI": ("",),
"WINMOR": ("",),
"WSPR": ("",)
}

# A dictionary of all the deprecated MODE values.
MODES_DEPRECATED = {"AMTORFEC": ("",),
"ASCI": ("",),
"CHIP64": ("",),
"CHIP128": ("",),
"DOMINOF": ("",),
"FMHELL": ("",),
"FSK31": ("",),
"GTOR": ("",),
"HELL80": ("",),
"HFSK": ("",),
"JT4A": ("",),
"JT4B": ("",),
"JT4C": ("",),
"JT4D": ("",),
"JT4E": ("",),
"JT4F": ("",),
"JT4G": ("",),
"JT65A": ("",),
"JT65B": ("",),
"JT65C": ("",),
"MFSK8": ("",),
"MFSK16": ("",),
"PAC2": ("",),
"PAC3": ("",),
"PAX2": ("",),
"PCW": ("",),
"PSK10": ("",),
"PSK31": ("",),
"PSK63": ("",),
"PSK63F": ("",),
"PSK125": ("",),
"PSKAM10": ("",),
"PSKAM31": ("",),
"PSKAM50": ("",),
"PSKFEC31": ("",),
"PSKHELL": ("",),
"QPSK31": ("",),
"QPSK63": ("",),
"QPSK125": ("",),
"THRBX": ("",)
}

# Include all deprecated modes.
MODES.update(MODES_DEPRECATED)

# All the bands listed in the ADIF specification.
BANDS = ["", "2190m", "630m", "560m", "160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m", "4m", "2m", "1.25m", "70cm", "33cm", "23cm", "13cm", "9cm", "6cm", "3cm", "1.25cm", "6mm", "4mm", "2.5mm", "2mm", "1mm"]
# The lower and upper frequency bounds (in MHz) for each band in BANDS.
Expand All @@ -207,6 +117,7 @@ class ADIF:

def __init__(self):
""" Initialise class for I/O of files using the Amateur Data Interchange Format (ADIF). """
self.modes = Modes()
return

def read(self, path):
Expand Down Expand Up @@ -521,9 +432,9 @@ def is_valid(self, field_name, data, data_type):
elif(data_type == "E" or data_type == "A"):
# Enumeration, AwardList.
if(field_name == "MODE"):
return (data in list(MODES.keys()))
return (data in list(self.modes.all.keys()))
elif(field_name == "SUBMODE"):
submodes = [submode for mode in list(MODES.keys()) for submode in MODES[mode]]
submodes = [submode for mode in list(self.modes.all.keys()) for submode in self.modes.all[mode]]
return (data in submodes)
elif(field_name == "BAND"):
return (data in BANDS)
Expand Down
13 changes: 12 additions & 1 deletion pyqso/logbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from pyqso.blank import Blank
from pyqso.printer import Printer
from pyqso.compare import compare_date_and_time, compare_default
from pyqso.update_modes_dialog import UpdateModesDialog


class Logbook:
Expand Down Expand Up @@ -907,7 +908,7 @@ def delete_record_callback(self, widget):
return
log = self.logs[log_index]

(sort_model, path) = self.treeselection[log_index].get_selected_rows() # Get the selected row in the log
(sort_model, path) = self.treeselection[log_index].get_selected_rows() # Get the selected row in the log.
try:
sort_iter = sort_model.get_iter(path[0])
filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter)
Expand Down Expand Up @@ -1113,6 +1114,16 @@ def paste_callback(self, widget=None, path=None):

return

def update_modes_callback(self, widget=None, path=None):
umd = UpdateModesDialog(self.application)
response = umd.dialog.run()
if(response == Gtk.ResponseType.OK):
modes = Modes()
modes.update(url=umd.url)
umd.dialog.destroy()
return


@property
def log_count(self):
""" Return the total number of logs in the logbook.
Expand Down
4 changes: 4 additions & 0 deletions pyqso/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def __init__(self, application):
self.items["RECORD_COUNT"] = self.builder.get_object("mitem_record_count")
self.items["RECORD_COUNT"].connect("activate", self.application.logbook.record_count_callback)

# Record count
self.items["UPDATE_MODES"] = self.builder.get_object("mitem_update_modes")
self.items["UPDATE_MODES"].connect("activate", self.application.logbook.update_modes_callback)

# View toolbox
self.items["TOOLBOX"] = self.builder.get_object("mitem_toolbox")
config = configparser.ConfigParser()
Expand Down
198 changes: 183 additions & 15 deletions pyqso/modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,192 @@
# You should have received a copy of the GNU General Public License
# along with PyQSO. If not, see <http://www.gnu.org/licenses/>.

import sqlite3
import os
from urllib.request import urlopen
from bs4 import BeautifulSoup
import logging

page = urlopen('http://www.adif.org/307/ADIF_307.htm').read()
soup = BeautifulSoup(page, "html.parser")
MODES_FILE = os.path.expanduser("~/.config/pyqso/modes.db")

# Remove the <span> tags but keep the tags' contents.
for match in soup.findAll('span'):
match.unwrap()

# Find the MODES table.
rows = soup.find(id="Enumeration_Mode").find_all('tr')
class Modes:

def __init__(self):

try:
connection = sqlite3.connect(MODES_FILE)
c = connection.cursor()
c.execute("""CREATE TABLE IF NOT EXISTS modes (
mode TEXT NOT NULL,
submode TEXT NOT NULL,
UNIQUE(mode, submode)
); """)

# Fill the new table with the basic list of modes and submodes.
for mode in self.basic:
for submode in self.basic[mode]:
c.execute("""REPLACE INTO modes(mode, submode) VALUES(?, ?)""", (mode, submode))
connection.commit()
connection.close()
except sqlite3.Error as e:
logging.exception(e)

#self.update("http://www.adif.org/309/ADIF_309.htm")
return

@property
def basic(self):
""" A basic list of valid modes listed in the ADIF specification.
This is a dictionary with the key-value pairs holding the MODE and SUBMODE(s) respectively. """

modes = {"": ("",),
"AM": ("",),
"ATV": ("",),
"CHIP": ("", "CHIP64", "CHIP128"),
"CLO": ("",),
"CONTESTI": ("",),
"CW": ("", "PCW"),
"DIGITALVOICE": ("",),
"DOMINO": ("", "DOMINOEX", "DOMINOF"),
"DSTAR": ("",),
"FAX": ("",),
"FM": ("",),
"FSK441": ("",),
"FT8": ("",),
"HELL": ("", "FMHELL", "FSKHELL", "HELL80", "HFSK", "PSKHELL"),
"ISCAT": ("", "ISCAT-A", "ISCAT-B"),
"JT4": ("", "JT4A", "JT4B", "JT4C", "JT4D", "JT4E", "JT4F", "JT4G"),
"JT6M": ("",),
"JT9": ("",),
"JT44": ("",),
"JT65": ("", "JT65A", "JT65B", "JT65B2", "JT65C", "JT65C2"),
"MFSK": ("", "MFSK4", "MFSK8", "MFSK11", "MFSK16", "MFSK22", "MFSK31", "MFSK32", "MFSK64", "MFSK128"),
"MT63": ("",),
"OLIVIA": ("", "OLIVIA 4/125", "OLIVIA 4/250", "OLIVIA 8/250", "OLIVIA 8/500", "OLIVIA 16/500", "OLIVIA 16/1000", "OLIVIA 32/1000"),
"OPERA": ("", "OPERA-BEACON", "OPERA-QSO"),
"PAC": ("", "PAC2", "PAC3", "PAC4"),
"PAX": ("", "PAX2"),
"PKT": ("",),
"PSK": ("", "FSK31", "PSK10", "PSK31", "PSK63", "PSK63F", "PSK125", "PSK250", "PSK500", "PSK1000", "PSKAM10", "PSKAM31", "PSKAM50", "PSKFEC31", "QPSK31", "QPSK63", "QPSK125", "QPSK250", "QPSK500"),
"PSK2K": ("",),
"Q15": ("",),
"ROS": ("", "ROS-EME", "ROS-HF", "ROS-MF"),
"RTTY": ("", "ASCI"),
"RTTYM": ("",),
"SSB": ("", "LSB", "USB"),
"SSTV": ("",),
"THOR": ("",),
"THRB": ("", "THRBX"),
"TOR": ("", "AMTORFEC", "GTOR"),
"V4": ("",),
"VOI": ("",),
"WINMOR": ("",),
"WSPR": ("",)
}

# A dictionary of all the deprecated MODE values.
deprecated = {"AMTORFEC": ("",),
"ASCI": ("",),
"CHIP64": ("",),
"CHIP128": ("",),
"DOMINOF": ("",),
"FMHELL": ("",),
"FSK31": ("",),
"GTOR": ("",),
"HELL80": ("",),
"HFSK": ("",),
"JT4A": ("",),
"JT4B": ("",),
"JT4C": ("",),
"JT4D": ("",),
"JT4E": ("",),
"JT4F": ("",),
"JT4G": ("",),
"JT65A": ("",),
"JT65B": ("",),
"JT65C": ("",),
"MFSK8": ("",),
"MFSK16": ("",),
"PAC2": ("",),
"PAC3": ("",),
"PAX2": ("",),
"PCW": ("",),
"PSK10": ("",),
"PSK31": ("",),
"PSK63": ("",),
"PSK63F": ("",),
"PSK125": ("",),
"PSKAM10": ("",),
"PSKAM31": ("",),
"PSKAM50": ("",),
"PSKFEC31": ("",),
"PSKHELL": ("",),
"QPSK31": ("",),
"QPSK63": ("",),
"QPSK125": ("",),
"THRBX": ("",)
}

# Include all deprecated modes.
modes.update(deprecated)
return modes

@property
def all(self):
try:
connection = sqlite3.connect(MODES_FILE)
c = connection.cursor()
result = c.execute("""SELECT * FROM modes""")
rows = result.fetchall()

modes = {}
for row in rows:
mode = row[0]
submode = row[1]
if(mode in modes.keys()):
modes[mode].append(submode)
else:
modes[mode] = [submode]
connection.close()
except sqlite3.Error as e:
logging.exception(e)
return modes

def update(self, url):
modes = self.parse(url)
try:
connection = sqlite3.connect(MODES_FILE)
c = connection.cursor()
for mode in modes:
for submode in modes[mode]:
c.execute("REPLACE INTO modes(mode, submode) VALUES(?,?)", (mode, submode))
connection.commit()
connection.close()
except sqlite3.Error as e:
logging.exception(e)

return

def parse(self, url):
page = urlopen(url).read()
soup = BeautifulSoup(page, "html.parser")

# Remove the <span> tags but keep the tags' contents.
for match in soup.findAll("span"):
match.unwrap()

# Find the MODES table.
rows = soup.find(id="Enumeration_Mode").find_all("tr")

# Extract modes and submodes.
modes = {}
for row in rows[1:]: # Ignores the header row.
mode, submode = row.find_all("td")[0:2]
mode = mode.text.split(" (import-only)")[0].strip()
submode = tuple(submode.text.strip().split(", "))
if(mode not in modes):
modes[mode] = submode

return modes

# Extract modes and submodes.
modes = {}
for row in rows[1:]:
mode, submode, description = row.find_all('td')
mode = mode.text.split(" (import-only)")[0].strip()
submode = tuple(submode.text.strip().split(", "))
modes[mode] = submode
print(modes)
Loading

0 comments on commit 2487424

Please sign in to comment.