diff --git a/710.py b/710.py index 08967e6..6d3d75a 100755 --- a/710.py +++ b/710.py @@ -11,6 +11,7 @@ import tkinter as tk from common710 import stamp from common710 import XMLRPC_PORT +from common710 import GPIO_PTT_DICT from controller710 import Controller __title__ = "710.py" @@ -18,7 +19,7 @@ __copyright__ = "Copyright 2022, Steve Magnuson" __credits__ = ["Steve Magnuson"] __license__ = "GPL v3.0" -__version__ = "2.1.3" +__version__ = "2.1.4" __maintainer__ = "Steve Magnuson" __email__ = "ag7gn@arrl.net" __status__ = "Production" @@ -105,9 +106,10 @@ def get_ports(): "XML-RPC rig control calls from " "clients such as Fldigi or Hamlib") parser.add_argument("-r", "--rig", type=str, - choices=('none', 'left', 'right'), + choices=list(GPIO_PTT_DICT.keys()), default='none', - help="Nexus DR-X Users: Select left or right " + help="BCM GPIO pin number for PTT control. " + "Nexus DR-X Users: Select left or right " "radio if you want to control GPIO PTT via " "an XML-RPC 'rig.set_ptt' call. This will " "map to GPIO pin 12 for the left radio and " diff --git a/README.md b/README.md index 4be833b..a4b4a77 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Kenwood Files related to Kenwood radios -VERSION 20220129 +VERSION 20220819 ## 710.py and 710.sh @@ -12,7 +12,46 @@ Two scripts that provide CAT control for Kenwood TM-D710G or TM-V71A radios on a The `710.sh` script requires `710.py` to talk to the radio. It does not start the `710.py` GUI in this case. `710.py` uses the Python serial library to communicate with the radio. -## Most recent significant changes +## Recent significant changes + +1. Ability to set shift (simplex, negative or positive). +1. Changing the frequency, modulation, step, tone, tone frequency, reverse or shift in the GUI while in memory mode will now prompt the user to modify the memory or copy the memory contents to VFO and then make the modifications. If the user attempts to change the frequency to one that is not in the band currently set to that side of the radio, the user will be prompted to either modify the memory or abort the change. +1. User can select any Raspberry Pi GPIO pin for PTT, not just the 'left' and 'right' radios for the Nexus DR-X. 'left' and 'right' are still available and will map to GPIO 12 and GPIO23 respectively. +1. Clicking 'Up' or 'Down' when in VFO mode now more closely mimics turning the tuning knob on the radio in terms of what parameters are kept from frequency to frequency. For example, the shift setting will automatically change as frequencies traverse the ranges described in the TM-D710GA manual: + + - VHF + + Under 145.100 MHz: No offset (Simplex operation) + + 145.100 ~ 145.499 MHz: – 600 kHz offset + + 145.500 ~ 145.999 MHz: No offset (Simplex operation) + + 146.000 ~ 146.399 MHz: + 600 kHz offset + + 146.400 ~ 146.599 MHz: No offset (Simplex operation) + + 146.600 ~ 146.999 MHz: – 600 kHz offset + + 147.000 ~ 147.399 MHz: + 600 kHz offset + + 147.400 ~ 147.599 MHz: No offset (Simplex operation) + + 147.600 ~ 147.999 MHz: – 600 kHz offset + + 148.000 MHz and higher: No offset (Simplex operation) + + - UHF + + Under 442.000 MHz: No offset (Simplex operation) + + 442.000 ~ 444.999 MHz: + 5 MHz offset + + 445.000 ~ 446.999 MHz: No offset (Simplex operation) + + 447.000 ~ 449.999 MHz: – 5 MHz offset + + 450.000 MHz and higher: No offset (Simplex operation) 1. `710.py` now has a multithreaded XML-RPC server. This allows Fldigi to communicate with `710.py` as if `710.py` were Flrig. Apps using Hamlib can also use `710.py` via hamlib's 'FLRig' setting. Details below. @@ -26,7 +65,7 @@ The `710.sh` script requires `710.py` to talk to the radio. It does not start th 1. When in VFO mode, you cannot select a frequency outside the frequency band set for that side. - 1. If you modify the frequency, tone type, tone frequency, step, reverse or modulation using the GUI (`710.py`), the change will take place __*in the current mode for that side (VFO, CALL or MR)*__. If the change to one of these parameters is requested when in MR mode, __*that memory location will be modified*__! A warning message will appear in the message queue window telling the user that the memory location has been modified. + 1. Changing the frequency, modulation, step, tone, tone frequency, reverse or shift in the GUI while in memory mode will prompt the user to modify the memory or copy the memory contents to VFO and then make the modifications. If the user attempts to change the frequency to one that is not in the band currently set to that side of the radio, the user will be prompted to either modify the memory or abort the change. 1. If you use the shell script (`710.sh`) to change the frequency, the script will first change the mode to VFO (if it's not already in that mode) and attempt to set the desired frequency. If the desired frequency is not in the currently set frequency band for that side, the frequency will not be changed. @@ -218,36 +257,34 @@ If you did the optional `udev` rule setup in the previous step, then you already - Running `710.py -h` my Pi for example shows: - pi@nexuspi4b-ag7gn:~ $ 710.py -h + pi@nexuspi4b-ag7gn:/usr/local/bin $ 710.py -h usage: 710.py [-h] [-v] - [-p {/dev/kenwoodTM-V71A,/dev/gps1,/dev/ttyUSB1,/dev/serial0,/dev/serial1,/dev/ttyS0,/dev/ttyAMA0,/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0}] - [-b {300,1200,2400,4800,9600,19200,38400,57600}] - [-s] - [-l x:y] - [-x [1024-65535]] - [-r {none,left,right}] - [-c COMMAND] + [-p {/dev/kenwoodTM-D710G,/dev/gps2,/dev/ttyUSB2,/dev/gps0,/dev/gps1,/dev/serial1,/dev/serial0,/dev/ttyUSB1,/dev/ttyUSB0,/dev/ttyS0,/dev/ttyAMA0,/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0,/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_IC-7100_02007519_A-if00-port0,/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_IC-7100_02007519_B-if00-port0}] + [-b {300,1200,2400,4800,9600,19200,38400,57600}] [-s] [-l x:y] [-x [1024-65535]] + [-r {none,left,right,17,18,27,22,23,24,25,4,5,6,13,19,26,12,16,20,21}] [-c COMMAND] CAT control for Kenwood TM-D710G/TM-V71A optional arguments: - -h, --help show this help message and exit - -v, --version show program's version number and exit - -p {/dev/kenwoodTM-V71A,/dev/gps1,/dev/ttyUSB1,/dev/serial0,/dev/serial1,/dev/ttyS0,/dev/ttyAMA0,/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0}, --port {/dev/kenwoodTM-V71A,/dev/gps1,/dev/ttyUSB1,/dev/serial0,/dev/serial1,/dev/ttyS0,/dev/ttyAMA0,/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0} - Serial port connected to radio (default: /dev/gps1) + -h, --help show this help message and exit + -v, --version show program's version number and exit + -p {/dev/kenwoodTM-D710G,/dev/gps2,/dev/ttyUSB2,/dev/gps0,/dev/gps1,/dev/serial1,/dev/serial0,/dev/ttyUSB1,/dev/ttyUSB0,/dev/ttyS0,/dev/ttyAMA0,/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0,/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_IC-7100_02007519_A-if00-port0,/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_IC-7100_02007519_B-if00-port0}, --port {/dev/kenwoodTM-D710G,/dev/gps2,/dev/ttyUSB2,/dev/gps0,/dev/gps1,/dev/serial1,/dev/serial0,/dev/ttyUSB1,/dev/ttyUSB0,/dev/ttyS0,/dev/ttyAMA0,/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0,/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_IC-7100_02007519_A-if00-port0,/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_IC-7100_02007519_B-if00-port0} + Serial port connected to radio (default: /dev/ttyUSB0) -b {300,1200,2400,4800,9600,19200,38400,57600}, --baudrate {300,1200,2400,4800,9600,19200,38400,57600} - Serial port speed (must match radio) (default: 57600) - -s, --small Smaller GUI window (default: False) + Serial port speed (must match radio) (default: 57600) + -s, --small Smaller GUI window (default: False) -l x:y, --location x:y - x:y: Initial x and y position (in pixels) of upper left corner of GUI. (default: None) + x:y: Initial x and y position (in pixels) of upper left corner of GUI. (default: None) -x [1024-65535], --xmlport [1024-65535] - TCP port on which to listen for XML-RPC rig control calls from clients such as Fldigi or Hamlib (default: 12345) - -r {none,left,right}, --rig {none,left,right} - Nexus DR-X Users: Select left or right radio if you want to control GPIO PTT via an XML-RPC 'rig.set_ptt' call. This will map to GPIO - pin 12 for the left radio and pin 23 for the right. 'none' means that 'rig.set_ptt' calls will be ignored. In any case, PTT - activation via CAT command is never used. (default: none) + TCP port on which to listen for XML-RPC rig control calls from clients such as Fldigi or Hamlib (default: + 12345) + -r {none,left,right,17,18,27,22,23,24,25,4,5,6,13,19,26,12,16,20,21}, --rig {none,left,right,17,18,27,22,23,24,25,4,5,6,13,19,26,12,16,20,21} + BCM GPIO pin number for PTT control. Nexus DR-X Users: Select left or right radio if you want to control GPIO + PTT via an XML-RPC 'rig.set_ptt' call. This will map to GPIO pin 12 for the left radio and pin 23 for the + right. 'none' means that 'rig.set_ptt' calls will be ignored. In any case, PTT activation via CAT command is + never used. (default: none) -c COMMAND, --command COMMAND - CAT command to send to radio (no GUI) (default: None) + CAT command to send to radio (no GUI) (default: None) ### Make a Hamradio menu selection for `710.py` diff --git a/cat710.py b/cat710.py index 4ddad7f..810a3e2 100644 --- a/cat710.py +++ b/cat710.py @@ -1,6 +1,8 @@ import io import re import sys +from os import environ + from common710 import * from queue import Queue from gpiozero import OutputDevice @@ -9,7 +11,7 @@ __copyright__ = "Copyright 2022, Steve Magnuson" __credits__ = ["Steve Magnuson"] __license__ = "GPL v3.0" -__version__ = "2.0.3" +__version__ = "2.0.4" __maintainer__ = "Steve Magnuson" __email__ = "ag7gn@arrl.net" __status__ = "Production" @@ -25,7 +27,7 @@ class Cat(object): are documented at https://github.com/LA3QMA/TM-V71_TM-D710-Kenwood """ - def __init__(self, serial_port: object, nexus_side: str): + def __init__(self, serial_port: object, ptt_pin: str): """ Initializes a BufferedRWPair object that wraps a serial object. Wrapping the serial port allows customization of the end-of-line @@ -62,12 +64,12 @@ def __init__(self, serial_port: object, nexus_side: str): 'gpio': None, 'id': '' } - self.nexus_side = nexus_side - if self.nexus_side == 'none': + self.ptt_pin = ptt_pin + if self.ptt_pin == 'none': self.state['gpio'] = None self.ptt = None else: - self.state['gpio'] = GPIO_PTT_DICT[self.nexus_side] + self.state['gpio'] = GPIO_PTT_DICT[self.ptt_pin] self.ptt = OutputDevice(self.state['gpio'], active_high=True, initial_value=False) @@ -157,6 +159,43 @@ def handle_query(self, cmd: str) -> list: else: return result + @staticmethod + def ask(ask_type: str, ask_msg: str): + """ + If X Windows environment detected, pop up a window with + the warning message, otherwise print message to stderr. + :param ask_type: str of 'yesnocancel' or 'okcancel'. Used to determine + messagebox type. Default is okcancel. + :param ask_msg: String containing warning message + :return: True|False if user clicks Yes|No respectively, None if + user clicks Cancel + """ + if ask_type not in ('okcancel', 'yesnocancel'): + return False + if environ.get('DISPLAY', '') == '': + # X not running. Print ask_msg to stderr and don't prompt + # for answer + print(f"{stamp()}: {ask_msg}: YES", file=sys.stderr) + return True + else: + # X running. Use existing tkinter root if set + from tkinter import messagebox + import tkinter as tk + ask_root = tk.Tk() + ask_root.withdraw() + if ask_type == 'okcancel': + result = messagebox.askokcancel(title=f"Confirm", + message=ask_msg, + parent=ask_root) + elif ask_type == 'yesnocancel': + result = messagebox.askyesnocancel(title=f"Confirm", + message=ask_msg, + parent=ask_root) + else: + result = False + ask_root.destroy() + return result + def set_id(self, radio_id: str): self.state['id'] = radio_id @@ -358,7 +397,7 @@ def common_elements(_mode_str: str): def get_dictionary(self) -> dict: """ - Returns state of radio state as a dictionary + Returns state of radio as a dictionary :return: state dictionary """ return self.state @@ -409,16 +448,37 @@ def get_arg_list() -> list: else: return [] + def get_ptt_ctrl() -> tuple: + """ + Retrieves current PTT and CTRL state of radio + :return: Tuple (CTRL_state, PTT_state) where CTRL_state and + PTT_state are 0 or 1. Returns empty tuple if unable to + retrieve data + """ + _answer = self.handle_query("BC") + if not _answer: + return () + return _answer[1], _answer[2] + if job[0] in ('mode',): # 'VM' command - mode change requested + # Save current CTRL state because radio will move CTRL to the + # side of the radio that's changing modes. Will restore + # state later. + ptt_ctrl_state = get_ptt_ctrl() + if not ptt_ctrl_state: + return [] arg = f"VM {SIDE_DICT['inv'][job[1]]},{job[2]}" if not self.handle_query(arg): return [] + # Restore original PTT, CTRL state + _ctrl, _ptt = ptt_ctrl_state + if not self.handle_query(f"BC {_ctrl},{_ptt}"): + return [] elif job[0] in ('ptt', 'ctrl'): # 'BC' command - answer = self.handle_query("BC") + answer = get_ptt_ctrl() if not answer: return [] - ctrl = answer[1] - ptt = answer[2] + ctrl, ptt = answer if job[0] == 'ptt': arg = f"BC {ctrl},{SIDE_DICT['inv'][job[1]]}" else: # Setting ctrl @@ -437,7 +497,7 @@ def get_arg_list() -> list: if not self.handle_query(arg): return [] elif job[0] in ('frequency', 'modulation', 'step', - 'tone', 'tone_frequency', 'rev'): + 'tone', 'tone_frequency', 'rev', 'shift'): arg_list = get_arg_list() if not arg_list or arg_list[0] == 'N': return [] @@ -479,10 +539,14 @@ def get_arg_list() -> list: TONE_FREQUENCY_DICT[current_type]['inv'][job[2]] if job[0] == 'frequency': arg_list[2] = f"{int(job[2] * 1000000):010d}" + arg_list[4] = frequency_shifts(int(job[2] * 1000000))[0] + arg_list[12] = frequency_shifts(int(job[2] * 1000000))[1] if job[0] == 'modulation': arg_list[13] = job[2] if job[0] == 'step': arg_list[3] = job[2] + if job[0] == 'shift': + arg_list[4] = job[2] # if job[0] == 'rev' and arg_list[4] != '0': if job[0] == 'rev': _freq = int(arg_list[2]) @@ -508,13 +572,57 @@ def get_arg_list() -> list: else: pass if arg_list[0] == 'ME': - # MR mode - # Kenwood provides no CAT command for changing the - # frequency band on a given side. To work around - # this shortcoming, we must modify the memory channel. - msg_queue.put(['WARNING', - f"{stamp()}: WARNING: Modifying " - f"memory {int(arg_list[1])}!"]) + # MR mode - If GUI, ask user to confirm modification of + # memory location + # First, determine whether memory contains a frequency + # that's allowed as a VFO on this side of the radio + # Toggle to VFO mode and get the VFO for this side + if not self.handle_query(f"VM {SIDE_DICT['inv'][job[1]]},0"): + return [] + result = self.handle_query(f"FO {SIDE_DICT['inv'][job[1]]}") + if not result: + return [] + # Toggle back to Memory mode + if not self.handle_query(f"VM {SIDE_DICT['inv'][job[1]]},1"): + return [] + # Is the VFO frequency in the same band as the memory freq? + if same_frequency_band(int(result[2]), int(arg_list[2])): + answer = self.ask('yesnocancel', + f"You are about to modify memory " + f"{int(arg_list[1])}. Proceed?\n\n" + f"Yes: Modify mem {int(arg_list[1])}\n" + f"No: Copy mem {int(arg_list[1])} to VFO,\n\t" + f"then modify VFO\n" + f"Cancel: Do nothing") + else: + # Copying memory contents to VFO is not possible + # because mem frequency is out of band for VFO on this + # side of the radio. + answer = self.ask('okcancel', + f"You are about to modify memory " + f"{int(arg_list[1])}. Continue?") + if not answer: + answer = None + if answer: + # User clicked Yes/OK, so modify memory location + msg_queue.put(['WARNING', + f"{stamp()}: WARNING: Modifying " + f"memory {int(arg_list[1])}!"]) + elif answer is None: + # User cancelled + job[0] = None + else: + # User clicked No + # Change to VFO mode and set VFO to data from memory location + msg_queue.put(['INFO', + f"{stamp()}: Copying memory " + f"{int(arg_list[1])} contents to VFO"]) + + if not self.handle_query(f"VM {SIDE_DICT['inv'][job[1]]},0"): + return [] + arg_list[0] = 'FO' + arg_list[1] = SIDE_DICT['inv'][job[1]] + del arg_list[14:] if job[0] is not None: if not self.handle_query(f"{arg_list[0]} {','.join(arg_list[1:])}"): return [] @@ -571,7 +679,7 @@ def get_arg_list() -> list: arg_list = get_arg_list() # Get the channel data for current mode if not arg_list or arg_list[0] == 'N': return [] - if arg_list[0] == 'FO': + if arg_list[0] in ('FO', ): frequency = int(arg_list[2]) step = int(STEP_DICT['map'][arg_list[3]]) * 1000 if job[0] == 'down': @@ -582,6 +690,16 @@ def get_arg_list() -> list: # print(f"min = {_min}, max = {_min}") if _min <= frequency <= _max: arg_list[2] = f"{frequency:010d}" + arg_list[4] = frequency_shifts(frequency)[0] + arg_list[5] = '0' # Disable reverse + # arg_list[6] = '0' # Set tone status to no tone + # arg_list[7] = '0' # Set CTCSS status to no CTCSS + # arg_list[8] = '0' # Set DCS status to no DCS + # arg_list[9] = '08' # Set tone frequency to default + # arg_list[10] = '08' # Set CTCSS frequency to default + # arg_list[11] = '000' # Set DCS frequency to default + arg_list[12] = frequency_shifts(frequency)[1] + # arg_list[13] = '0' # Set mode to FM arg = f"{arg_list[0]} {','.join(arg_list[1:])}" _ans = self.handle_query(arg) if not _ans: @@ -591,7 +709,7 @@ def get_arg_list() -> list: f"{stamp()}: Frequency " f"must be between {float(FREQUENCY_LIMITS[job[1]]['min']):.3f} " f"and {float(FREQUENCY_LIMITS[job[1]]['max']):.3f} MHz"]) - elif arg_list[0] == 'ME': + elif arg_list[0] in ('ME', ): ctrl_moved_temporarily = False if self.state[job[1]]['ctrl'] != 'CTRL': ctrl_moved_temporarily = True diff --git a/common710.py b/common710.py index 319879c..5769eeb 100644 --- a/common710.py +++ b/common710.py @@ -31,10 +31,14 @@ 'CTRL_DICT', 'MENU_DICT', 'GPIO_PTT_DICT', + 'DEFAULT_STEP_VHF', + 'DEFAULT_STEP_UHF', 'stamp', 'within_frequency_limits', + 'same_frequency_band', 'QueryException', - 'UpdateDisplayException' + 'UpdateDisplayException', + 'frequency_shifts' ] XMLRPC_PORT = 12345 @@ -85,7 +89,7 @@ def same_frequency_band(freq1: int, freq2: int) -> bool: False otherwise """ same_band = False - for band, freq_range in FREQUENCY_BAND_LIMITS.items(): + for freq_range in FREQUENCY_BAND_LIMITS.values(): if freq1 in range(freq_range['min'], freq_range['max']) \ and freq2 in range(freq_range['min'], freq_range['max']): @@ -94,7 +98,36 @@ def same_frequency_band(freq1: int, freq2: int) -> bool: return same_band -GPIO_PTT_DICT = {'none': None, 'left': 12, 'right': 23} +def frequency_shifts(frequency: int) -> tuple: + """ + Given a frequency, it returns a value that translates to whether + the frequency is simplex (0), + (1) or - (2) + :param frequency: Integer value of frequency in Hz + :return: Tuple containing 0, 1 or 2 (simplex, up or down + respectively) and shift frequency in Hz + """ + if 145100000 <= frequency <= 145499900: + return '2', '00600000' + elif 146000000 <= frequency <= 146399000: + return '1', '00600000' + elif 146600000 <= frequency <= 146999000: + return '2', '00600000' + elif 147000000 <= frequency <= 147399000: + return '1', '00600000' + elif 147600000 <= frequency <= 147999000: + return '2', '00600000' + elif 442000000 <= frequency <= 444999000: + return '1', '05000000' + elif 447000000 <= frequency <= 449999000: + return '2', '05000000' + else: + return '0', '00000000' + + +GPIO_PTT_DICT = {'none': None, 'left': 12, 'right': 23, '17': 17, + '18': 18, '27': 27, '22': 22, '23': 23, '24': 24, + '25': 25, '4': 4, '5': 5, '6': 6, '13': 13, '19': 19, + '26': 26, '12': 12, '16': 16, '20': 20, '21': 21} FREQUENCY_LIMITS = {'A': {'min': 118.0, 'max': 524.0}, 'B': {'min': 136.0, 'max': 1300.0}} MEMORY_LIMITS = {'min': 0, 'max': 999} @@ -238,6 +271,8 @@ def same_frequency_band(freq1: int, freq2: int) -> bool: LOCK_OUT_DICT = STATE_DICT PTT_DICT = SIDE_DICT CTRL_DICT = SIDE_DICT +DEFAULT_STEP_VHF = 5 +DEFAULT_STEP_UHF = 25 MENU_DICT = {'beep': {'index': 1, 'values': STATE_DICT['inv']}, 'vhf_aip': {'index': 11, diff --git a/controller710.py b/controller710.py index ac13e19..1859f91 100644 --- a/controller710.py +++ b/controller710.py @@ -15,7 +15,7 @@ __copyright__ = "Copyright 2022, Steve Magnuson" __credits__ = ["Steve Magnuson"] __license__ = "GPL v3.0" -__version__ = "1.0.0" +__version__ = "1.0.2" __maintainer__ = "Steve Magnuson" __email__ = "ag7gn@arrl.net" __status__ = "Production" @@ -37,7 +37,7 @@ def __init__(self, o_serial: serial, args: argparse, **kwargs): self.o_serial = o_serial self.serial_port = args.port self.baudrate = args.baudrate - self.ptt_side = args.rig + self.ptt_pin = args.rig self.xmlrpc_port = args.xmlport self.loc = args.location if args.small: @@ -49,7 +49,7 @@ def __init__(self, o_serial: serial, args: argparse, **kwargs): self.gui = None self.xmlrpc_server = None self.title = None - self.cat = Cat(self.o_serial, self.ptt_side) + self.cat = Cat(self.o_serial, self.ptt_pin) self.cmd_queue = Queue() self.controller_thread = None self.controller_running = False @@ -58,7 +58,7 @@ def __init__(self, o_serial: serial, args: argparse, **kwargs): self.cmd_queue) except OSError as _: self.print_error(f"XML-RPC port {self.xmlrpc_port} is already in use.\n\n" - f"Is this program or already Flrig running?\n" + f"Is this program or Flrig already running?\n" f"Close it before running this program.") self.stop() else: @@ -80,7 +80,8 @@ def _start_xmlrpc_server(self): self.stop() print(f"{stamp()}: Starting XML-RPC server...", file=sys.stderr) self.xmlrpc_thread.start() - print(f"{stamp()}: XML-RPC server running.", file=sys.stderr) + print(f"{stamp()}: XML-RPC server listening on port " + f"{self.xmlrpc_port}.", file=sys.stderr) def start_gui(self, root: object): self.root = root @@ -125,6 +126,8 @@ def start_gui(self, root: object): self.controller_thread.start() print(f"{stamp()}: Controller running.", file=sys.stderr) self._start_xmlrpc_server() + self.msg_queue.put(['INFO', f"{stamp()}: XML-RPC server " + f"listening on port {self.xmlrpc_port}"]) def controller(self): while self.controller_running: @@ -190,6 +193,7 @@ def print_error(self, err: str): parent=self.root) def send_command(self, cmd: str) -> list: + # This is not managed by the job queue! return self.cat.handle_query(cmd) def set_id(self, model: str): diff --git a/gui710.py b/gui710.py index 61d5399..471a443 100644 --- a/gui710.py +++ b/gui710.py @@ -9,6 +9,7 @@ from common710 import TONE_TYPE_DICT from common710 import TONE_FREQUENCY_DICT from common710 import DCS_FREQUENCY_DICT +from common710 import SHIFT_DICT from common710 import MODE_DICT from common710 import POWER_DICT from common710 import MODULATION_DICT @@ -143,7 +144,7 @@ def __init__(self, **kwargs): 'shift': (Display._scr['row'], Display._scr['col'] + 12, 1, 1, 'w', self._default_font, - "TX shift direction.\n'S' is simplex"), + "TX shift direction.\n'S' is simplex\nClick to change"), 'reverse': (Display._scr['row'], Display._scr['col'] + 13, 1, 1, 'e', self._default_font, @@ -264,7 +265,7 @@ def __init__(self, **kwargs): text=key[0:2], fg="black", bg=Display._screen_bg_color, font=value[5]) if key in ('frequency', 'tone', 'tone_frequency', - 'ch_name', 'mode', 'ch_number', + 'ch_name', 'shift', 'mode', 'ch_number', 'power', 'data', 'modulation', 'step'): self.screen_label[side][key]. \ bind("", @@ -457,6 +458,16 @@ def widget_clicked(self, **kwargs): font=self._default_font, content=list(TONE_FREQUENCY_DICT[tone_type]['map'].values()), job_q=self.cmd_q) + elif k == 'shift': + RadioPopup(widget=self.screen_label[s][k], + # title=f" Side {s} Shift ", + pop_label=f"Side {s} Shift", + label=k, + side=s, + font=self._default_font, + initial_value=SHIFT_DICT['inv'][self.screen_label[s][k].cget('text')], + content=SHIFT_DICT['inv'], + job_q=self.cmd_q) elif k == 'mode': RadioPopup(widget=self.screen_label[s][k], # title=f" Side {s} Mode ",