Skip to content

Instantly share code, notes, and snippets.

@scivision
Last active June 11, 2025 16:45
Show Gist options
  • Save scivision/f7e8da430e812e1b2fe40b27f9c1e853 to your computer and use it in GitHub Desktop.
Save scivision/f7e8da430e812e1b2fe40b27f9c1e853 to your computer and use it in GitHub Desktop.
Quickly upload WSJT-X default log to LoTW

Upload recent WSJT-X contacts to LoTW using ARRL TSQL command line interface without needing a separate logging program.

WSJT-X can connect to a logging program daemon, but for those to wish to simply upload new QSOs to LoTW, this Python script does it all by itself.

Requirements

  • Python >= 3.10
  • TQSL program (from ARRL) already setup with certificates

Examples

Upload all new contacts from today to LoTW, local time zone:

python tqsl_upload.py
#!/usr/bin/env python3
"""
use "tqsl" command line utility to upload default WSJT-X log file to LoTW
MIT License
by W2NRL
"""
import shutil
import subprocess
import os
import functools
from pathlib import Path
from dateutil.parser import parse
import argparse
import platform
from datetime import datetime
import re
@functools.cache
def find_tqsl():
path = None
if platform.system() == "Darwin":
p = Path("/Applications/TrustedQSL/tqsl.app/Contents/MacOS")
if p.is_dir():
path = p
if tqsl := shutil.which("tqsl", path=path):
return tqsl
raise FileNotFoundError("TQSL command line utility not found on PATH.")
@functools.cache
def find_log_dir() -> Path:
system = platform.system()
match system:
case "Linux":
d = Path.home() / ".local/share/WSJT-X"
case "Darwin":
d = Path.home() / "Library/Application Support/WSJT-X"
case "Windows":
d = Path(os.environ["LOCALAPPDATA"]) / "WSJT-X"
case _:
raise ValueError(f"Unsupported OS: {system}")
if not d.is_dir():
raise FileNotFoundError(f"Log directory not found: {d}")
return d
def upload_new_log(log_file: Path, t0: datetime) -> int:
tqsl_exe = find_tqsl()
opts = ["-a", "compliant", f"-b {t0:%Y-%m-%d}", "-d", "-u", "-x", str(log_file)]
cmd = [tqsl_exe, *opts]
print(" ".join(cmd))
ret = subprocess.run(cmd, capture_output=True, text=True)
# the -x batch option also makes tqsl output on stderr only
# this output is lines ending with \n
if ret.returncode == 0:
lines = ret.stderr.split('\n')
upload_line = next((line for line in lines if line.startswith("Attempting to upload")), None)
if upload_line:
if "Attempting to upload one QSO" in upload_line:
return 1
match = re.search(r"Attempting to upload (\d+) QSOs", upload_line)
if match:
return int(match.group(1))
else:
if "Final Status: No QSOs to upload" in ret.stderr:
return 0
return -1
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Upload WSJT-X log file to LoTW using TQSL.")
parser.add_argument("-f", "--log_file", type=Path, help="Path to the WSJT-X log file to upload.")
parser.add_argument(
"-d",
"--date",
type=str,
help="Date to use for the upload (YYYY-MM-DD). Defaults to today.",
default=datetime.now(),
)
args = parser.parse_args()
if args.log_file is None:
log_file = find_log_dir() / "wsjtx_log.adi"
else:
log_file = Path(args.log_file).expanduser()
if not log_file.exists():
raise FileNotFoundError(f"Log file not found: {log_file}")
if isinstance(args.date, str):
t0 = parse(args.date)
else:
t0 = args.date
N = upload_new_log(log_file, t0)
if N > 0:
print(f"{N} QSOs uploaded to LoTW.")
elif N == 0:
print("No new QSOs to upload.")
else:
raise SystemExit("Failed to upload log file.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment