#!/usr/bin/env python3 # # The MIT License (MIT) # # Copyright (c) 2023 Matthias P. Braendli # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import socket import serial from tkinter import * from tkinter import ttk from functools import partial import sys NUM_MACROS=5 DEFAULT_MACROS=["CQ WCA de HB9HI/P HB9HI/P HB9HI/P pse K", "THIS IS A CASTLE ACTIVATION", "REF WCA HB-00631 LOC JN36GR", "", ""] DEFAULT_CQ_TIMER = 30 class GUI(Frame): def __init__(self, root): super().__init__(root) self.root = root self.timer_interval = DEFAULT_CQ_TIMER self.create_widgets() self.last_tx_freq = None self.last_serial_message = bytes() try: self.serial = serial.Serial("/dev/ttyACM0", 115200, timeout=0) print(f"Opened serial port {self.serial.name}") except Exception as e: self.serial = None self.set_status(f"Serial port failed {e}") self.root.after(1000, self.fetch_gqrx_freq) self.root.after(1000, self.read_serial) self.root.after(1000, self.handle_timer) def read_serial(self): if self.serial is not None: try: dat = self.serial.read(1) if dat: self.last_serial_message += dat if self.last_serial_message.endswith(b"\n"): print(f"< {self.last_serial_message.decode().strip()}") self.set_status(f"Received: {self.last_serial_message.decode().strip()}") if self.last_serial_message == b"Switch RX\n" and self.enable_mute.get(): os.system("pactl set-sink-mute alsa_output.pci-0000_00_1f.3.analog-stereo false") elif self.last_serial_message == b"Switch TX\n" and self.enable_mute.get(): os.system("pactl set-sink-mute alsa_output.pci-0000_00_1f.3.analog-stereo true") elif self.last_serial_message.startswith(b"FH sent"): if self.tx_messages: del self.tx_messages[0] self.tx_messages_var.set(self.tx_messages) if self.tx_messages: msg = self.tx_messages[0] print(f"Send '{msg}'") self.send_serial('m {} \n'.format(msg).encode("ascii")) else: self.send_serial(b'rx\n') self.last_serial_message = bytes() except serial.SerialException as e: self.set_status(f"Serial read failed: {e:!r}") except Exception as e: self.set_status(f"Serial read failed: {e:!r}") self.root.after(10, self.read_serial) else: self.root.after(1000, self.read_serial) # For GQRX remote control commands see https://github.com/csete/gqrx/blob/master/resources/remote-control.txt def fetch_gqrx_freq(self): gqrx_sock = socket.socket() gqrx_sock.settimeout(100) try: gqrx_sock.connect(("127.0.0.1", 7356)) gqrx_sock.send(b"f\n") buf = gqrx_sock.recv(1024) freq = int(buf.decode()) if self.enable_freq_sync.get(): self.gqrx_freq_var.set(freq) tx_freq = freq + self.gqrx_offset_var.get() if self.last_tx_freq != tx_freq: self.last_tx_freq = tx_freq self.send_serial('f{}\n'.format(tx_freq).encode("ascii")) self.root.after(100, self.fetch_gqrx_freq) except Exception as e: self.set_status(f"GQRX socket error {e}") self.root.after(1000, self.fetch_gqrx_freq) gqrx_sock.close() def handle_timer(self): try: if self.timer_enabled.get(): pending = self.timer_pending.get() if pending == 0: msg = self.macro_widgets[0]["var"].get() print(f"TX and Send '{msg}'") self.send_serial('tx\nm {} \n'.format(msg).encode("ascii")) if self.timer_interval > 0: self.timer_pending.set(self.timer_interval) else: self.timer_enabled.set(0) # safety else: self.timer_pending.set(pending - 1) else: self.timer_interval = self.timer_pending.get() except Exception as e: print(f"Exception {e}") self.root.after(1000, self.handle_timer) def terminate(self): print("Terminate: send RX") self.send_serial(b'rx\n') def toggle_to_rx(self): self.root.after(1000, self.send_serial, b'rx\n') def send_wspr(self): self.root.after(1000, self.send_serial, b'wspr\n') def load_macro(self, i): self.tx_messages.append(self.macro_widgets[i]["var"].get()) self.tx_messages_var.set(self.tx_messages) def send_serial(self, message): if self.serial is not None: self.serial.write(message) def set_font_narrow(self): print("Set font narrow") self.send_serial(b'fontn\n') def set_font_wide(self): print("Set font wide") self.send_serial(b'fontw\n') def send_tx_message(self): if self.tx_messages: msg = self.tx_messages[0] print(f"TX and Send '{msg}'") self.send_serial('tx\nm {} \n'.format(msg).encode("ascii")) def append_message_edit(self, event=None): self.tx_messages.append(self.message_edit_var.get()) self.tx_messages_var.set(self.tx_messages) self.message_edit_var.set("") def clear_tx_messages(self): self.tx_messages.clear() self.tx_messages_var.set(self.tx_messages) def create_widgets(self): self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) self.top_frame = Frame(self.root, borderwidth=5, relief="ridge", width=800, height=600) self.top_frame.grid(column=0, row=0, sticky=(N, S, E, W)) self.top_frame.columnconfigure(0, weight=1) self.top_frame.columnconfigure(1, weight=1) self.top_frame.rowconfigure(0, weight=0) self.top_frame.rowconfigure(1, weight=1) self.macros_frame = Frame(self.top_frame, borderwidth=1, relief="ridge") self.macros_frame.grid(column=0, row=0, sticky=(N, S, E, W)) self.macro_widgets = [] for i in range(NUM_MACROS): tv = StringVar() tv.set(DEFAULT_MACROS[i]) e = Entry(self.macros_frame, textvariable=tv, width=80) e.grid(column=0, row=i) b = Button(self.macros_frame, text=f"Load {i+1}", command=partial(self.load_macro, i)) b.grid(column=1, row=i) def handle_f1_event(i, event=None): self.load_macro(i) self.root.bind("".format(i+1), partial(handle_f1_event, i)) self.macro_widgets.append({'entry': e, 'button': b, 'var': tv}) self.control_frame = Frame(self.top_frame, borderwidth=1, relief="ridge") self.control_frame.grid(column=1, row=0, sticky=(N, S, E, W)) self.font_frame = Frame(self.control_frame, borderwidth=1, relief="ridge") self.font_frame.grid(column=0, row=0) self.font_label = Label(self.font_frame, text="Font selection") self.font_label.grid(column=0, row=0) self.button_font_narrow = Button(self.font_frame, text="Narrow", command=self.set_font_narrow) self.button_font_narrow.grid(column=0, row=1) self.button_font_wide = Button(self.font_frame, text="Wide", command=self.set_font_wide) self.button_font_wide.grid(column=1, row=1) self.gqrx_frame = Frame(self.control_frame, borderwidth=1, relief="ridge") self.gqrx_frame.grid(column=0, row=1, sticky=(N, S, E, W)) self.enable_freq_sync = IntVar() self.enable_mute = IntVar() self.gqrx_checkbutton = Checkbutton(self.gqrx_frame, text="Sync", variable=self.enable_freq_sync) self.gqrx_checkbutton.grid(column=0, row=0, columnspan=1) self.mute_on_tx_checkbutton = Checkbutton(self.gqrx_frame, text="Mute on TX", variable=self.enable_mute) self.mute_on_tx_checkbutton.grid(column=1, row=0, columnspan=1) self.gqrx_freq_label = Label(self.gqrx_frame, text="Frequency") self.gqrx_freq_label.grid(column=0, row=1) self.gqrx_freq_var = IntVar() self.gqrx_freq = Label(self.gqrx_frame, width=10, textvariable=self.gqrx_freq_var) self.gqrx_freq.grid(column=1, row=1, columnspan=1, sticky=(N, S, E, W)) self.gqrx_offset_label = Label(self.gqrx_frame, text="Offset") self.gqrx_offset_label.grid(column=0, row=2) self.gqrx_offset_var = IntVar() self.gqrx_offset_var.set(800) self.gqrx_offset = Entry(self.gqrx_frame, width=10, textvariable=self.gqrx_offset_var) self.gqrx_offset.grid(column=1, row=2, columnspan=1, sticky=(N, S, E, W)) self.timer_frame = Frame(self.control_frame, borderwidth=1, relief="ridge") self.timer_frame.grid(column=0, row=2, sticky=(N, S, E, W)) self.timer_label = Label(self.timer_frame, text="CQ Timer") self.timer_label.grid(column=0, row=1) self.timer_pending = IntVar() self.timer_pending.set(self.timer_interval) self.timer_entry = Entry(self.timer_frame, textvariable=self.timer_pending) self.timer_entry.grid(column=0, row=2, columnspan=1, sticky=(N, S, E, W)) self.timer_enabled = IntVar() self.timer_enable_checkbox = Checkbutton(self.timer_frame, text="CQ Timer", variable=self.timer_enabled) self.timer_enable_checkbox.grid(column=0, row=3, columnspan=1) self.trx_frame = Frame(self.control_frame, borderwidth=1, relief="ridge") self.trx_frame.grid(column=0, row=3, sticky=(N, S, E, W)) self.rx_button = Button(self.trx_frame, text="Receive!", command=self.toggle_to_rx) self.rx_button.grid(column=0, row=0, columnspan=2) self.tx_button = Button(self.trx_frame, text="WSPR!", command=self.send_wspr) self.tx_button.grid(column=0, row=2, columnspan=1) self.transmit_frame = Frame(self.top_frame, borderwidth=1, relief="ridge") self.transmit_frame.grid(column=0, row=1, columnspan=2) self.transmit_frame.columnconfigure(0, weight=1) self.transmit_frame.columnconfigure(1, weight=1) self.message_edit_var = StringVar() self.message_edit = Entry(self.transmit_frame, width=90, textvariable=self.message_edit_var) self.message_edit.bind("", self.append_message_edit) self.message_edit.grid(column=0, row=0, sticky=(N, S, E, W)) self.tx_messages = [] self.tx_messages_var = StringVar(value=self.tx_messages) self.tx_message_entry = Listbox(self.transmit_frame, width=90, listvariable=self.tx_messages_var) self.tx_message_entry.grid(column=0, columnspan=2, row=1, sticky=(N, S, E, W)) self.tx_message_entry.columnconfigure(0, weight=1) self.tx_message_entry.columnconfigure(1, weight=1) self.tx_message_button = Button(self.transmit_frame, text="Send", command=self.send_tx_message) self.tx_message_button.grid(column=0, row=2, sticky=(N, S, E, W), columnspan=1) self.clear_tx_message_button = Button(self.transmit_frame, text="Clear", command=self.clear_tx_messages) self.clear_tx_message_button.grid(column=1, row=2, sticky=(N, S, E, W), columnspan=1) self.status_var = StringVar() self.status_label = Label(self.top_frame, bd=1, relief=SUNKEN, anchor=W, textvariable=self.status_var) self.status_label.grid(column=0, row=2, columnspan=2, sticky=(N, S, E, W)) self.set_status("") def set_status(self, message): self.status_var.set(f"Status: {message}") root = Tk() app = GUI(root) try: root.mainloop() finally: app.terminate()