diff --git a/CHANGELOG.md b/CHANGELOG.md index 582f1170..2ca038a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- [25-1-1] Added ARRL RTTY RU. - [24-12-29] Add {LOGIT} macro. - [24-12-15] Fixed Button focus policy in the bandmap window. - [24-12-14] Changed method of detecting fldigi QSOs. See docs. @@ -11,7 +12,7 @@ - [24-12-8-1] Changed cabrillo names for Weekly RTTY, CW Ops CWT and K1USN SST. - [24-12-8] Fix: Weekly RTTY mults. Add RTC to Weekly RTTY. - [24-12-6] Add RTC to K1USN. --[24-12-5-1] ARRL 160 gets rtc. +- [24-12-5-1] ARRL 160 gets rtc. - [24-12-5] Add 'real time' score posting to external sites. - [24-12-4] Merged PR from @alduhoo Add STATION_CALLSIGN field to ADIF output - [24-12-3-1] Adding ARRL 160 diff --git a/README.md b/README.md index f648a330..65af68b0 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ generated, 'cause I'm lazy, list of those who've submitted PR's. - ARRL 160M - ARRL DX CW, SSB - ARRL Field Day +- ARRL RTTY Roundup - ARRL Sweepstakes CW, SSB - ARRL VHF January, June, September - CQ 160 CW, SSB @@ -208,22 +209,7 @@ generated, 'cause I'm lazy, list of those who've submitted PR's. ## Recent Changes (Polishing the Turd) -- [24-12-29] Add {LOGIT} macro. -- [24-12-15] Fixed Button focus policy in the bandmap window. -- [24-12-14] Changed method of detecting fldigi QSOs. See docs. -- [24-12-12] Add a try exception for a unicode decode error. -- [24-12-11-1] Add RTC to RAC Canada Day, ARRL VHF, ARRL Field Day, ARRL SS, ARRL DX, 10 10 -- [24-12-11] Add RTC to IARU HF, IARU Field Day, DARC XMAS, CQ WW, CQ WPX -- [24-12-9] Add RTC to Winter Field Day, Stew Perry, REF, RAEM, NAQP, LZ-DX, JIDX -- [24-12-8-2] Add RTC to ARRL 10M, Tweaked cabrillo file output. -- [24-12-8-1] Changed cabrillo names for Weekly RTTY, CW Ops CWT and K1USN SST. -- [24-12-8] Fix: Weekly RTTY mults. Add RTC to Weekly RTTY. -- [24-12-6] Add RTC to K1USN. --[24-12-5-1] ARRL 160 gets rtc. -- [24-12-5] Add 'real time' score posting to external sites. -- [24-12-4] Merged PR from @alduhoo Add STATION_CALLSIGN field to ADIF output -- [24-12-3-1] Adding ARRL 160 -- [24-12-3] Add button to bandmap to delete marked spots. +- [25-1-1] Added ARRL RTTY RU. See [CHANGELOG.md](CHANGELOG.md) for prior changes. diff --git a/not1mm/data/new_contest.ui b/not1mm/data/new_contest.ui index 28ff222a..4c71c0db 100644 --- a/not1mm/data/new_contest.ui +++ b/not1mm/data/new_contest.ui @@ -237,6 +237,11 @@ ARRL FIELD DAY + + + ARRL RTTY RU + + ARRL SS CW diff --git a/not1mm/lib/version.py b/not1mm/lib/version.py index c735bd26..787dd525 100644 --- a/not1mm/lib/version.py +++ b/not1mm/lib/version.py @@ -1,3 +1,3 @@ """It's the version""" -__version__ = "24.12.29" +__version__ = "25.1.1" diff --git a/not1mm/plugins/arrl_rtty_ru.py b/not1mm/plugins/arrl_rtty_ru.py index 6db61b2a..6955127e 100644 --- a/not1mm/plugins/arrl_rtty_ru.py +++ b/not1mm/plugins/arrl_rtty_ru.py @@ -1,19 +1,55 @@ -"""ARRL plugin""" +""" +Mode: RTTY + Bands: 80, 40, 20, 15, 10m + Classes: Single Op (QRP/Low/High) +Single Op Unlimited (QRP/Low/High) +Single Op Overlay: (Limited Antennas) +Multi-Single (Low/High) +Multi-Two +Multi-Multi + Max operating hours: 24 hours + Max power: HP: 1500 watts +LP: 100 watts + Exchange: W/VE: RST + (state/province) +non-W/VE: RST + Serial No. + Work stations: Once per band + QSO Points: 1 point per QSO + Multipliers: Each US state+DC (except KH6/KL7) once only +Each VE province/territory once only +Each DXCC country (including KH6/KL7) once only + Score Calculation: Total score = total QSO points x total mults + Submit logs by: 2359Z January 12, 2025 + E-mail logs to: (none) + Upload log at: https://contest-log-submission.arrl.org/ + Mail logs to: RTTY Roundup +ARRL +225 Main St. +Newington, CT 06111 +USA + Find rules at: https://www.arrl.org/rtty-roundup + + +""" # pylint: disable=invalid-name, unused-argument, unused-variable, c-extension-no-member import datetime -from decimal import Decimal -from pathlib import Path +import logging +from pathlib import Path from PyQt6 import QtWidgets -from not1mm.lib.plugin_common import online_score_xml -EXCHANGE_HINT = "" +from not1mm.lib.plugin_common import gen_adif, get_points, online_score_xml +from not1mm.lib.version import __version__ + +logger = logging.getLogger(__name__) -name = "ARRL RTTY Round Up" +EXCHANGE_HINT = "State/Province or #" + +name = "ARRL RTTY Roundup" +mode = "RTTY" # CW SSB BOTH RTTY cabrillo_name = "ARRL-RTTY" -mode = "BOTH" # CW SSB BOTH RTTY + columns = [ "YYYY-MM-DD HH:MM:SS", "Call", @@ -22,23 +58,13 @@ "Rcv", "SentNr", "RcvNr", - "Exchange1", - "CK", - "Prec", - "Sect", - "WPX", - "Power", - "M1", - "ZN", - "M2", - "PFX", "PTS", - "Name", - "Comment", ] +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 = 4 +dupe_type = 2 def init_contest(self): @@ -46,10 +72,23 @@ def init_contest(self): 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.show() + self.field4.show() + self.snt_label.setText("SNT") + self.field1.setAccessibleName("RST Sent") + # label = self.field3.findChild(QtWidgets.QLabel) + self.other_label.setText("SentNR") + self.field3.setAccessibleName("Sent Number") + # label = self.field4.findChild(QtWidgets.QLabel) + self.exch_label.setText("State|Prov|SN") + self.field4.setAccessibleName("State Province or Serial Number") def reset_label(self): @@ -78,8 +117,22 @@ def set_tab_prev(self): } +def validate(self): + """doc""" + # exchange = self.other_2.text().upper().split() + # if len(exchange) == 3: + # if exchange[0].isalpha() and exchange[1].isdigit() and exchange[2].isalpha(): + # return True + # return False + return True + + def set_contact_vars(self): """Contest Specific""" + self.contact["SNT"] = self.sent.text() + self.contact["RCV"] = self.receive.text() + self.contact["NR"] = self.other_2.text().upper() + self.contact["SentNr"] = self.other_1.text() def predupe(self): @@ -87,15 +140,51 @@ def predupe(self): def prefill(self): - """Fill SentNR""" + """Fill sentnr""" + result = self.database.get_serial() + serial_nr = str(result.get("serial_nr", "1")).zfill(3) + if serial_nr == "None": + serial_nr = "001" + + exchange = self.contest_settings.get("SentExchange", "").replace("#", serial_nr) + if len(self.other_1.text()) == 0: + self.other_1.setText(exchange) def points(self): """Calc point""" + return 1 def show_mults(self, rtc=None): """Return display string for mults""" + # CountryPrefix, integer + + us_ve = 0 + dx = 0 + + sql = ( + "select count(DISTINCT(NR || ':' || Mode)) as mult_count " + f"from dxlog where ContestNR = {self.database.current_contest} and typeof(NR) = 'text';" + ) + result = self.database.exec_sql(sql) + if result: + us_ve = result.get("mult_count", 0) + + # DX + sql = ( + "select count(DISTINCT(CountryPrefix || ':' || Mode)) as mult_count " + f"from dxlog where ContestNR = {self.database.current_contest} " + "and typeof(NR) = 'integer';" + ) + result = self.database.exec_sql(sql) + if result: + dx = result.get("mult_count", 0) + + if rtc is not None: + return dx, us_ve + + return us_ve + dx def show_qso(self): @@ -106,183 +195,346 @@ def show_qso(self): return 0 -def get_points(self): - """Return raw points before mults""" - result = self.database.fetch_points() - if result: - return int(result.get("Points", 0)) - return 0 - - def calc_score(self): """Return calculated score""" - result = self.database.fetch_points() + # Multipliers: Each US State + DC once per mode + _points = get_points(self) + _mults = show_mults(self) + _power_mult = 1 + # if self.contest_settings.get("PowerCategory", "") == "QRP": + # _power_mult = 2 + return _points * _power_mult * _mults def adif(self): - """ - Creates an ADIF file of the contacts made. - """ + """Call the generate ADIF function""" + gen_adif(self, cabrillo_name) + + +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.cqwpx.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}.adi" + + 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="utf-8") as file_descriptor: - print("2.2.0", end="\r\n", file=file_descriptor) - print("", end="\r\n", file=file_descriptor) + 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', '')}", + "\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, + ) + mode = self.contest_settings.get("ModeCategory", "") + if mode in ["SSB+CW", "SSB+CW+DIGITAL"]: + mode = "MIXED" + output_cabrillo_line( + f"CATEGORY-MODE: {mode}", + "\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: - hiscall = contact.get("Call", "") - hisname = contact.get("Name", "") - the_date_and_time = contact.get("TS") - # band = contact.get("Band") - themode = contact.get("Mode") - frequency = str(Decimal(str(contact.get("Freq", 0))) / 1000) - sentrst = contact.get("SNT", "") - rcvrst = contact.get("RCV", "") - sentnr = str(contact.get("SentNr", "0")) - rcvnr = str(contact.get("NR", "0")) - grid = contact.get("GridSquare", "") - comment = contact.get("Comment", "") + 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] - print( - f"" - f"{''.join(loggeddate.split('-'))}", - end="\r\n", - file=file_descriptor, + output_cabrillo_line( + f"QSO: {frequency} {themode} {loggeddate} {loggedtime} " + f"{contact.get('StationPrefix', '').ljust(13)} " + f"{str(contact.get('SNT', '')).ljust(3)} " + f"{str(contact.get('SentNr', '')).ljust(6)} " + f"{contact.get('Call', '').ljust(13)} " + f"{str(contact.get('RCV', '')).ljust(3)} " + f"{str(contact.get('NR', '')).ljust(6)}", + "\r\n", + file_descriptor, + file_encoding, ) - - try: - print( - f"{loggedtime}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - print( - f"{hiscall.upper()}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - if len(hisname): - print( - f"{hisname.title()}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - print( - f"{themode}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - print( - f"{frequency}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - print( - f"{sentrst}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - print( - f"{rcvrst}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - if sentnr != "0": - print( - f"{sentnr}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - if rcvnr != "0": - print( - f"{rcvnr}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - if len(grid) > 1: - print( - f"{grid}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - try: - if len(comment): - print( - f"{comment}", - end="\r\n", - file=file_descriptor, - ) - except TypeError: - ... - - print("", end="\r\n", file=file_descriptor) - print("", end="\r\n", file=file_descriptor) - except IOError: - ... - - -def cabrillo(self): - """Generates Cabrillo file. Maybe.""" + 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 recalculate_mults(self): """Recalculates multipliers after change in logged qso.""" -# States/Prov. DXCC state country +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.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") + + 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 + if button == self.esm_dict["HISCALL"]: + self.process_function_key(button, rttysendrx=False) + 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.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") + + 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('State', '')}, {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('State', '')}") def get_mults(self): """""" - mults = {} - mults["state"], mults["country"] = show_mults(self, rtc=True) + mults["country"], mults["state"] = show_mults(self, rtc=True) return mults diff --git a/pyproject.toml b/pyproject.toml index 15a48eda..56f33975 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "not1mm" -version = "24.12.29" +version = "25.1.1" description = "NOT1MM Logger" readme = "README.md" requires-python = ">=3.9"