diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67f0ea0..b4d15df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
# Changelog
+- [24-12-3-1] Adding ARRL 160
- [24-12-3] Add button to bandmap to delete marked spots.
- [24-11-27] Added CAT poll interval.
- [24-11-26-1] Changed ESC to stop CW, CTRL-W to wipe input fields.
diff --git a/README.md b/README.md
index da37ddb..e1b4ab3 100644
--- a/README.md
+++ b/README.md
@@ -207,6 +207,7 @@ generated, 'cause I'm lazy, list of those who've submitted PR's.
## Recent Changes (Polishing the Turd)
+- [24-12-3-1] Adding ARRL 160
- [24-12-3] Add button to bandmap to delete marked spots.
See [CHANGELOG.md](CHANGELOG.md) for prior changes.
diff --git a/not1mm/data/new_contest.ui b/not1mm/data/new_contest.ui
index 8dee6e3..28ff222 100644
--- a/not1mm/data/new_contest.ui
+++ b/not1mm/data/new_contest.ui
@@ -217,6 +217,11 @@
ARRL 10M
+ -
+
+ ARRL 160M
+
+
-
ARRL DX CW
diff --git a/not1mm/lib/version.py b/not1mm/lib/version.py
index c69cae8..8701a69 100644
--- a/not1mm/lib/version.py
+++ b/not1mm/lib/version.py
@@ -1,3 +1,3 @@
"""It's the version"""
-__version__ = "24.12.3"
+__version__ = "24.12.3.1"
diff --git a/not1mm/plugins/arrl_160m.py b/not1mm/plugins/arrl_160m.py
new file mode 100644
index 0000000..82dd8a7
--- /dev/null
+++ b/not1mm/plugins/arrl_160m.py
@@ -0,0 +1,543 @@
+"""ARRL 160 CW plugin"""
+
+# pylint: disable=invalid-name, c-extension-no-member, unused-import, line-too-long
+
+import datetime
+import logging
+import platform
+
+from pathlib import Path
+
+from PyQt6 import QtWidgets
+
+from not1mm.lib.plugin_common import gen_adif
+from not1mm.lib.version import __version__
+
+logger = logging.getLogger(__name__)
+
+EXCHANGE_HINT = "ST/Prov or DX CQ Zone"
+
+name = "ARRL 160-Meter"
+cabrillo_name = "ARRL-160"
+mode = "CW" # CW SSB BOTH RTTY
+
+columns = [
+ "YYYY-MM-DD HH:MM:SS",
+ "Call",
+ "Freq",
+ "Snt",
+ "Rcv",
+ "PFX",
+ "Exchange1",
+ "PTS",
+]
+
+advance_on_space = [True, True, True, True, True]
+
+# 1 once per contest, 2 work each band, 3 each band/mode, 4 no dupe checking
+dupe_type = 1
+
+
+def init_contest(self):
+ """setup plugin"""
+ set_tab_next(self)
+ set_tab_prev(self)
+ interface(self)
+ self.next_field = self.other_2
+
+
+def interface(self):
+ """Setup user interface"""
+ self.field1.show()
+ self.field2.show()
+ self.field3.hide()
+ self.field4.show()
+ self.snt_label.setText("SNT")
+ self.field1.setAccessibleName("RST Sent")
+ self.exch_label.setText("ARRL/RAC Section")
+ self.field4.setAccessibleName("Received Exchange")
+
+
+def reset_label(self): # pylint: disable=unused-argument
+ """reset label after field cleared"""
+
+
+def set_tab_next(self):
+ """Set TAB Advances"""
+ self.tab_next = {
+ self.callsign: self.sent,
+ self.sent: self.receive,
+ self.receive: self.other_2,
+ self.other_1: self.other_2,
+ self.other_2: self.callsign,
+ }
+
+
+def set_tab_prev(self):
+ """Set TAB Advances"""
+ self.tab_prev = {
+ self.callsign: self.other_2,
+ self.sent: self.callsign,
+ self.receive: self.sent,
+ self.other_1: self.receive,
+ self.other_2: self.receive,
+ }
+
+
+def set_contact_vars(self):
+ """Contest Specific"""
+ self.contact["SNT"] = self.sent.text()
+ self.contact["RCV"] = self.receive.text()
+ self.contact["SentNr"] = self.contest_settings.get("SentExchange", 0)
+ self.contact["Exchange1"] = self.other_2.text()
+
+
+def predupe(self): # pylint: disable=unused-argument
+ """called after callsign entered"""
+
+
+def prefill(self):
+ """Fill SentNR"""
+
+
+def points(self):
+ """Calc point"""
+ # Each contact between W/VE stations counts for two (2) QSO points. Each contact with a DX station counts five (5) QSO points
+ call = self.contact.get("Call", "")
+ dupe_check = self.database.check_dupe(call)
+ if dupe_check.get("isdupe", 0) > 0:
+ return 0
+ result = self.cty_lookup(self.station.get("Call", ""))
+ if result:
+ for item in result.items():
+ mypfx = item[1].get("primary_pfx", "")
+ # mycountry = item[1].get("entity", "")
+ # mycontinent = item[1].get("continent", "")
+
+ result = self.cty_lookup(self.contact.get("Call", ""))
+ if result:
+ for item in result.items():
+ pfx = item[1].get("primary_pfx", "")
+ # entity = item[1].get("entity", "")
+ # continent = item[1].get("continent", "")
+
+ # Both in same country
+
+ if mypfx in ["K", "VE"] and pfx in ["K", "VE"]:
+ return 2
+
+ if mypfx.upper() != pfx.upper():
+ return 5
+
+ return 0
+
+
+def show_mults(self):
+ """Return display string for mults"""
+ result = self.database.fetch_country_count()
+ mults = int(result.get("dxcc_count", 0))
+
+ result = self.database.fetch_exchange1_unique_count()
+ mults2 = int(result.get("exch1_count", 0))
+
+ return mults + mults2
+
+
+def show_qso(self):
+ """Return qso count"""
+ result = self.database.fetch_qso_count()
+ if result:
+ return int(result.get("qsos", 0))
+ return 0
+
+
+def calc_score(self):
+ """Return calculated score"""
+ result = self.database.fetch_points()
+ if result is not None:
+ score = result.get("Points", "0")
+ if score is None:
+ score = "0"
+ contest_points = int(score)
+
+ result = self.database.fetch_country_count()
+ mults = int(result.get("dxcc_count", 0))
+
+ result = self.database.fetch_exchange1_unique_count()
+ mults2 = int(result.get("exch1_count", 0))
+ return contest_points * (mults + mults2)
+ return 0
+
+
+def adif(self):
+ """Call the generate ADIF function"""
+ gen_adif(self, cabrillo_name, "ARRL 160-Meter")
+
+
+def output_cabrillo_line(line_to_output, ending, file_descriptor, file_encoding):
+ """"""
+ print(
+ line_to_output.encode(file_encoding, errors="ignore").decode(),
+ end=ending,
+ file=file_descriptor,
+ )
+
+
+def cabrillo(self, file_encoding):
+ """Generates Cabrillo file. Maybe."""
+ # https://www.cw160.com/cabrillo.htm
+ logger.debug("******Cabrillo*****")
+ logger.debug("Station: %s", f"{self.station}")
+ logger.debug("Contest: %s", f"{self.contest_settings}")
+ now = datetime.datetime.now()
+ date_time = now.strftime("%Y-%m-%d_%H-%M-%S")
+ filename = (
+ str(Path.home())
+ + "/"
+ + f"{self.station.get('Call', '').upper()}_{cabrillo_name}_{date_time}.log"
+ )
+ logger.debug("%s", filename)
+ log = self.database.fetch_all_contacts_asc()
+ try:
+ with open(filename, "w", encoding=file_encoding) as file_descriptor:
+ output_cabrillo_line(
+ "START-OF-LOG: 3.0",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"CREATED-BY: Not1MM v{__version__}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"CONTEST: {cabrillo_name}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ if self.station.get("Club", ""):
+ output_cabrillo_line(
+ f"CLUB: {self.station.get('Club', '').upper()}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"CALLSIGN: {self.station.get('Call','')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"LOCATION: {self.station.get('ARRLSection', '')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"CATEGORY-OPERATOR: {self.contest_settings.get('OperatorCategory','')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"CATEGORY-ASSISTED: {self.contest_settings.get('AssistedCategory','')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"CATEGORY-BAND: {self.contest_settings.get('BandCategory','')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"CATEGORY-MODE: {self.contest_settings.get('ModeCategory','')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"CATEGORY-TRANSMITTER: {self.contest_settings.get('TransmitterCategory','')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ if self.contest_settings.get("OverlayCategory", "") != "N/A":
+ output_cabrillo_line(
+ f"CATEGORY-OVERLAY: {self.contest_settings.get('OverlayCategory','')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"GRID-LOCATOR: {self.station.get('GridSquare','')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"CATEGORY-POWER: {self.contest_settings.get('PowerCategory','')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+
+ output_cabrillo_line(
+ f"CLAIMED-SCORE: {calc_score(self)}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ ops = f"@{self.station.get('Call','')}"
+ list_of_ops = self.database.get_ops()
+ for op in list_of_ops:
+ ops += f", {op.get('Operator', '')}"
+ output_cabrillo_line(
+ f"OPERATORS: {ops}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"NAME: {self.station.get('Name', '')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"ADDRESS: {self.station.get('Street1', '')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"ADDRESS-CITY: {self.station.get('City', '')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"ADDRESS-STATE-PROVINCE: {self.station.get('State', '')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"ADDRESS-POSTALCODE: {self.station.get('Zip', '')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"ADDRESS-COUNTRY: {self.station.get('Country', '')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line(
+ f"EMAIL: {self.station.get('Email', '')}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ for contact in log:
+ the_date_and_time = contact.get("TS", "")
+ themode = contact.get("Mode", "")
+ if themode == "LSB" or themode == "USB":
+ themode = "PH"
+ frequency = str(int(contact.get("Freq", "0"))).rjust(5)
+
+ loggeddate = the_date_and_time[:10]
+ loggedtime = the_date_and_time[11:13] + the_date_and_time[14:16]
+ thesentnr = contact.get("SentNr", "---")
+ if thesentnr == "":
+ thesentnr = "---"
+ theexch = contact.get("Exchange1", "---")
+ if theexch == "":
+ theexch = "---"
+
+ output_cabrillo_line(
+ f"QSO: {frequency} {themode} {loggeddate} {loggedtime} "
+ f"{contact.get('StationPrefix', '').ljust(13)} "
+ f"{str(contact.get('SNT', '')).ljust(3)} "
+ f"{str(thesentnr).ljust(6)} "
+ f"{contact.get('Call', '').ljust(13)} "
+ f"{str(contact.get('RCV', '')).ljust(3)} "
+ f"{str(theexch).ljust(6)}",
+ "\r\n",
+ file_descriptor,
+ file_encoding,
+ )
+ output_cabrillo_line("END-OF-LOG:", "\r\n", file_descriptor, file_encoding)
+ self.show_message_box(f"Cabrillo saved to: {filename}")
+ except IOError as exception:
+ logger.critical("cabrillo: IO error: %s, writing to %s", exception, filename)
+ self.show_message_box(f"Error saving Cabrillo: {exception} {filename}")
+ return
+
+
+# def trigger_update(self):
+# """Triggers the log window to update."""
+# cmd = {}
+# cmd["cmd"] = "UPDATELOG"
+# cmd["station"] = platform.node()
+# self.multicast_interface.send_as_json(cmd)
+
+
+def recalculate_mults(self):
+ """Recalculates multipliers after change in logged qso."""
+ # all_contacts = self.database.fetch_all_contacts_asc()
+ # for contact in all_contacts:
+ # time_stamp = contact.get("TS", "")
+ # if contact.get("CountryPrefix", "") == "K":
+ # query = f"select count(*) as count from dxlog where TS < '{time_stamp}' and CountryPrefix = 'K' and Exchange1 = '{contact.get('Exchange1', '')}' and ContestNR = '{self.pref.get('contest', '0')}'"
+ # result = self.database.exec_sql(query)
+ # if result.get("count", 0) == 0:
+ # contact["IsMultiplier1"] = 1
+ # else:
+ # contact["IsMultiplier1"] = 0
+ # self.database.change_contact(contact)
+ # continue
+ # if contact.get("CountryPrefix", "") == "VE":
+ # query = f"select count(*) as count from dxlog where TS < '{time_stamp}' and CountryPrefix = 'VE' and Exchange1 = '{contact.get('Exchange1', '')}' and ContestNR = '{self.pref.get('contest', '0')}'"
+ # result = self.database.exec_sql(query)
+ # if result.get("count", 0) == 0:
+ # contact["IsMultiplier1"] = 1
+ # else:
+ # contact["IsMultiplier1"] = 0
+ # self.database.change_contact(contact)
+ # continue
+ # query = f"select count(*) as count from dxlog where TS < '{time_stamp}' and CountryPrefix = '{contact.get('CountryPrefix', '')}' and ContestNR = '{self.pref.get('contest', '0')}'"
+ # result = self.database.exec_sql(query)
+ # if result.get("count", 0) == 0:
+ # contact["IsMultiplier1"] = 1
+ # else:
+ # contact["IsMultiplier1"] = 0
+ # self.database.change_contact(contact)
+ # trigger_update(self)
+
+
+def process_esm(self, new_focused_widget=None, with_enter=False):
+ """ESM State Machine"""
+
+ # self.pref["run_state"]
+
+ # -----===== Assigned F-Keys =====-----
+ # self.esm_dict["CQ"]
+ # self.esm_dict["EXCH"]
+ # self.esm_dict["QRZ"]
+ # self.esm_dict["AGN"]
+ # self.esm_dict["HISCALL"]
+ # self.esm_dict["MYCALL"]
+ # self.esm_dict["QSOB4"]
+
+ # ----==== text fields ====----
+ # self.callsign
+ # self.sent
+ # self.receive
+ # self.other_1
+ # self.other_2
+
+ if new_focused_widget is not None:
+ self.current_widget = self.inputs_dict.get(new_focused_widget)
+
+ # print(f"checking esm {self.current_widget=} {with_enter=} {self.pref.get("run_state")=}")
+
+ for a_button in [
+ self.esm_dict["CQ"],
+ self.esm_dict["EXCH"],
+ self.esm_dict["QRZ"],
+ self.esm_dict["AGN"],
+ self.esm_dict["HISCALL"],
+ self.esm_dict["MYCALL"],
+ self.esm_dict["QSOB4"],
+ ]:
+ if a_button is not None:
+ self.restore_button_color(a_button)
+
+ buttons_to_send = []
+
+ if self.pref.get("run_state"):
+ if self.current_widget == "callsign":
+ if len(self.callsign.text()) < 3:
+ self.make_button_green(self.esm_dict["CQ"])
+ buttons_to_send.append(self.esm_dict["CQ"])
+ elif len(self.callsign.text()) > 2:
+ self.make_button_green(self.esm_dict["HISCALL"])
+ self.make_button_green(self.esm_dict["EXCH"])
+ buttons_to_send.append(self.esm_dict["HISCALL"])
+ buttons_to_send.append(self.esm_dict["EXCH"])
+
+ elif self.current_widget in ["other_2"]:
+ if self.contact.get("CountryPrefix", "") in ["K", "VE"]:
+ if self.other_2.text() == "":
+ self.make_button_green(self.esm_dict["AGN"])
+ buttons_to_send.append(self.esm_dict["AGN"])
+ else:
+ self.make_button_green(self.esm_dict["QRZ"])
+ buttons_to_send.append(self.esm_dict["QRZ"])
+ buttons_to_send.append("LOGIT")
+ else:
+ self.make_button_green(self.esm_dict["QRZ"])
+ buttons_to_send.append(self.esm_dict["QRZ"])
+ buttons_to_send.append("LOGIT")
+
+ if with_enter is True and bool(len(buttons_to_send)):
+ for button in buttons_to_send:
+ if button:
+ if button == "LOGIT":
+ self.save_contact()
+ continue
+ self.process_function_key(button)
+ else:
+ if self.current_widget == "callsign":
+ if len(self.callsign.text()) > 2:
+ self.make_button_green(self.esm_dict["MYCALL"])
+ buttons_to_send.append(self.esm_dict["MYCALL"])
+
+ elif self.current_widget in ["other_2"]:
+ if self.contact.get("CountryPrefix", "") in ["K", "VE"]:
+ if self.other_2.text() == "":
+ self.make_button_green(self.esm_dict["AGN"])
+ buttons_to_send.append(self.esm_dict["AGN"])
+ else:
+ self.make_button_green(self.esm_dict["EXCH"])
+ buttons_to_send.append(self.esm_dict["EXCH"])
+ buttons_to_send.append("LOGIT")
+
+ else:
+ self.make_button_green(self.esm_dict["EXCH"])
+ buttons_to_send.append(self.esm_dict["EXCH"])
+ buttons_to_send.append("LOGIT")
+
+ if with_enter is True and bool(len(buttons_to_send)):
+ for button in buttons_to_send:
+ if button:
+ if button == "LOGIT":
+ self.save_contact()
+ continue
+ self.process_function_key(button)
+
+
+def populate_history_info_line(self):
+ result = self.database.fetch_call_history(self.callsign.text())
+ if result:
+ self.history_info.setText(
+ f"{result.get('Call', '')}, {result.get('Name', '')}, {result.get('Exch1', '')}, {result.get('UserText','...')}"
+ )
+ else:
+ self.history_info.setText("")
+
+
+def check_call_history(self):
+ """"""
+ result = self.database.fetch_call_history(self.callsign.text())
+ if result:
+ self.history_info.setText(f"{result.get('UserText','')}")
+ if self.other_2.text() == "":
+ self.other_2.setText(f"{result.get('Exch1', '')}")
diff --git a/pyproject.toml b/pyproject.toml
index 63ee677..1adec3c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "not1mm"
-version = "24.12.3"
+version = "24.12.3.1"
description = "NOT1MM Logger"
readme = "README.md"
requires-python = ">=3.9"