#!/usr/bin/python3 -u # The -u option turns off block buffering of python output. This assures # that output streams to stdout when output happens. # # Module: ft991utility.py # # Description: A utility for backing up Yaesu FT991 memory and menu settings, # and also for restoring memory and menu settings from # a file. Can be used both interactively or with command line # arguments. Before running this utility, be sure to copy # the file 'ft911.py' to the same folder as this utility. # # Copyright 2019 by Jeff Owrey, Intravisions.com # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public Licensef # along with this program. If not, see http://www.gnu.org/license. # # Revision History # * v10 24 Nov 2019 by J L Owrey; first release # * v11 03 Oct 2020 by J L Owrey; upgraded to Python 3 # * v12 24 Aug 2022 by J L Owrey; fixed error occurring when setting # the FT991 IF filter width for unsupported modulation modes; added # command line parameter to set the CAT baud rate; fixed error that # occurs when the restore menu settings tries to restore the CAT baud # rate setting # #2345678901234567890123456789012345678901234567890123456789012345678901234567890 # Environment Setup _WINDOWS_COM_PORT = 'COM5' _LINUX_COM_PORT = '/dev/ttyUSB0' _DEFAULT_FT991_BAUD_RATE = 4800 import os, sys, serial, time import ft991 # module should be in same directory as this utility # Constant definitions _DEFAULT_MENU_SETTINGS_FILE = 'ft991menu.cfg' _DEFAULT_MEMORY_SETTINGS_FILE = 'ft991mem.csv' _MAX_NUMBER_OF_MENU_ITEMS = 154 _MAX_NUMBER_OF_MEMORY_ITEMS = 118 _DEBUG = False # Global definitions menuBackupFile = _DEFAULT_MENU_SETTINGS_FILE memoryBackupFile = _DEFAULT_MEMORY_SETTINGS_FILE commandLineOption = '' baudRate = _DEFAULT_FT991_BAUD_RATE # Command processing functions def doUserCommand(): """ Description: Provides an interactive user interface where the user can enter simple text commands at a command line prompt. The commands that a user can enter are listed in a menu splash that the user can display anytime with the 'm' command. Also this function processes commands provided as command line options. In the case of a command line option this function operates non-interactively. Parameters: none Returns: nothing """ # When command line arguments have not been provided, # use interactive mode and give the user a prompt. if commandLineOption == '': cmd = input('>').strip() # If command line arguments have been provided, then # execute the command non-interactively. else: cmd = commandLineOption # Process the user command. if cmd == '': return elif cmd == 'm': printMenuSplash() elif cmd == 'bu': backupMenuSettings() elif cmd == 'ru': restoreMenuSettings() elif cmd == 'bm': backupMemorySettings() elif cmd == 'rm': restoreMemorySettings() elif cmd == 'p': passThroughMode() elif cmd == 'v': toggleVerboseMode() elif cmd == 'x': exit(0) else: print("invalid command") ## end def def backupMemorySettings(): """ Description: Backs up all memory settings to a file. The user has the option of providing a file name or accepting a default file name. Parameters: none Returns: nothing """ # Prompt the user for a file name in which to store # backed up memory settings. fileName = getFileName(memoryBackupFile) # Read the memory settings from the FT991... print('Backing up memory settings...') settings = readMemorySettings() # and write them to the file. writeToFile(settings, fileName) print('Memory settings backed up to \'%s\'' % fileName) ## end def def restoreMemorySettings(): """ Description: Restores all memory settings from a file. The user has the option of providing a file name or accepting a default file name. Parameters: none Returns: nothing """ # Prompt the user for a file name from which to retrieve backed up # memory settings. Also make sure the file exists. fileName = getFileName(memoryBackupFile) if not os.path.isfile(fileName): print('File not found.\n' \ 'Please enter a valid file name. Be sure to correctly ' \ 'enter\nthe full path name or relative path name of the file.') return # Read the memory settings from the file... print('Restoring memory settings...') settings = readFromFile(fileName) # and write them to the FT991. writeMemorySettings(settings) print('Memory settings restored from \'%s\'' % fileName) ## end def def backupMenuSettings(): """ Description: Backs up all menu settings to a file. The user has the option of providing a file name or accepting a default file name. Parameters: none Returns: nothing """ # Prompt the user for a file name in which to store # backed up menu settings. fileName = getFileName(menuBackupFile) # Read the menu settings from the FT991... print('Backing up menu settings...') settings = readMenuSettings() # and write them to the file. writeToFile(settings, fileName) print('Menu settings backed up to \'%s\'' % fileName) ## end def def restoreMenuSettings(): """ Description: Restores all menu settings from a file. The user has the option of providing a file name or accepting a default file name. Parameters: none Returns: nothing """ # Prompt the user for a file name from which to retrieve backed up # menu settings. Also make sure the file exists. fileName = getFileName(menuBackupFile) if not os.path.isfile(fileName): print('File not found.\n' \ 'Please enter a valid file name. Be sure to correctly ' \ 'enter\nthe full path name or relative path name of the file.') return # Read the menu settings from the file... print('Restoring menu settings...') settings = readFromFile(fileName) # and write them to the FT991. writeMenuSettings(settings) print('Menu settings restored from \'%s\'' % fileName) ## end def def passThroughMode(): """ Description: An interactive mode whereby the user an enter FT991 CAT commands directly on the command line. This mode greatly facilitates development and debugging. Parameters: none Returns: nothing """ print('Entering passthrough mode. Type \'exit\' to exit mode.') while(True): # Prompt the user to enter an FT991 CAT command, and # process the command string. sCommand = input('CAT# ').upper() if sCommand == 'EXIT': # exit this utility break # If the user fails to end a CAT command with a semi-colon, # then provide one. elif sCommand[-1:] != ';': sCommand += ';' if sCommand == '': # no command - do nothing continue else: # run a user command ft991.sendSerial(sCommand) sResult = ft991.receiveSerial(); if sResult != '': print(sResult) ## end def def toggleVerboseMode(): """ Description: Toggles the verbose mode on or off, depending on the previous state. Verbose mode causes CAT commands to be echoed to STDOUT as they are sent to the FT991. If a CAT command returns a string, that is also echoed. Parameters: none Returns: nothing """ if ft991.verbose: ft991.verbose = False print('Verbose is OFF') else: ft991.verbose = True print('Verbose is ON') ## end def def getFileName(defaultFile): """ Description: Prompts the user for a file name. Parameters: defaultFile - file name to use if the user does not provide a file name Returns: the user provided file name if provided, or the default file name, otherwise. """ # If a command backup or restore argument provided, then do not # query the user for a file name. A file name may be provided # as an option on the command line. if commandLineOption != '': return defaultFile # Otherwise query the user for a file name fileName = input('Enter file name or for default: ') if fileName == '': return defaultFile else: return fileName ## end def def readMemorySettings(): """ Description: Reads all defined memory settings from the FT991. The settings are reformatted into a readable, comma-delimited (csv) file, which can be viewed and modified with a spreadsheet application such as LibreOffice Calc. Parameters: none Returns: a list object containing memory location settings. Each item in the list represents a row, in comma-delimited form, that specifies the settings for a single memory location. The first item in the list are the column headers for remaining rows contained in the list. """ # Define the column headers as the first item in the list. lSettings = [ 'Memory Ch,VFO_A,' \ 'RepeaterShift,Mode,Tag,Encoding,Tone,DCS,' \ 'Clarifier,RxClar,TxClar,PreAmp,RfAttn,NB,IFshift,' \ 'IFwidthIndex,ContourState,ContourFreq,' \ 'APFstate,APFfreq,DNRstate,DNRalgorithm,DNFstate,' \ 'NBFstate,NotchState,NotchFreq' ] for iLocation in range(1, _MAX_NUMBER_OF_MEMORY_ITEMS): # If a memory location is empty (has not been programmed or has # been erased), skip that location and do not create a list entry # for that location. if ft991.setMemoryLocation(iLocation) == None: continue # For each memory location get the memory contents. Note that # several CAT commands are required to get the entire contents # of a memory location. Specifically, additional commands are # required to get DCS code and CTCSS tone. dMem = ft991.getMemory(iLocation) tone = ft991.getCTCSS() dcs = ft991.getDCS() preamp = ft991.getPreamp() rfattn = ft991.getRfAttn() nblkr = ft991.getNoiseBlanker() shift = ft991.getIFshift() width = ft991.getIFwidth() lContour = ft991.getContour() lAFP = ft991.getAPF() dnrstate = ft991.getDNRstate() dnralgorithm = ft991.getDNRalgorithm() dnfstate = ft991.getDNFstate() narstate = ft991.getNARstate() notch = ft991.getNotchState() # getMemory, above, stores data in a dictionary object. Format # the data in this object, as well as, the DCS code and CTCSS # tone into a comma-delimited string. sCsvFormat = '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,' \ '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s' % \ ( dMem['memloc'], dMem['vfoa'], \ dMem['rpoffset'], dMem['mode'], dMem['tag'], dMem['encode'], \ tone, dcs, dMem['clarfreq'], dMem['rxclar'], \ dMem['txclar'], preamp, rfattn, nblkr, shift, width, \ lContour[0], lContour[1], lAFP[0], lAFP[1], \ dnrstate, dnralgorithm, dnfstate, narstate, notch[0], notch[1] ) # Add the comma-delimited string to the list object. lSettings.append(sCsvFormat) return lSettings # end def def writeMemorySettings(lSettings): """ Description: Writes the supplied memory settings to the FT991. Parameters: lSettings - a list object containing the memory settings in comma delimited format Returns: nothing """ for item in lSettings: try: # Parse the comma-delimited line and store in a dictionary object. dItem = ft991.parseCsvData(item) # The first item in the memory settings list are the # column headers, so ignore this item. (parseData returns # None for this item.) if dItem == None: continue # Set the parameters for the memory location. ft991.setMemory(dItem) # Set current channel to memory location being set. ft991.setMemoryLocation(int(dItem['memloc'])) # Set CTCSS tone for memory channel. ft991.setCTCSS(dItem['tone']) # Set DCS code for memory channel. ft991.setDCS(dItem['dcs']) # Set clarifier mode. Note that # while the 'MW' and 'MT' commands can be used to turn the Rx # and Tx clarifiers on, the clarifier states can only be turned # off by sending the 'RT0' and 'XT0' commands. This situation # is probably due to a bug in the CAT interface. ft991.setRxClarifier(dItem['rxclar']) ft991.setTxClarifier(dItem['txclar']) # Set Rx preamplifier state ft991.setPreamp(dItem['preamp']) # Set RF attenuator state ft991.setRfAttn(dItem['rfattn']) # Set Noise Blanker state ft991.setNoiseBlanker(dItem['nblkr']) # v12 24 Aug 2022 by J L Owrey; fixed error occurring when setting # the FT991 IF filter width for unsupported modulation modes if dItem['mode'] in ['LSB', 'USB', 'CW', 'RTTY-LSB', 'RTTY-USB', 'DATA-LSB', 'DATA-USB']: # Set IF width index ft991.setIFwidth(dItem['width']) # Set IF shift amount ft991.setIFshift(dItem['shift']) # Set Contour parameters; also sets audio peak filter (APF) ft991.setContour(dItem['contour']) if dItem['mode'] in ['CW']: # Set audio peak filter (APF) ft991.setAPF(dItem['apf']) # Set DNR state and algorithm ft991.setDNRstate(dItem['dnrstate']) ft991.setDNRalgorithm(dItem['dnralgorithm']) # Set DNF state ft991.setDNFstate(dItem['dnfstate']) # Set NAR state ft991.setNARstate(dItem['narstate']) # Set Notch state ft991.setNotchState((dItem['notchstate'], dItem['notchfreq'])) except Exception as e: print('Memory settings restore operation failed. Most likely\n' \ 'this is due to the backup settings file corrupted or\n' \ 'incorrectly formatted. Look for the following error: \n') print(e) raise #exit(1) # end for ## end def def readMenuSettings(): """ Description: Reads all menu settings from the FT991. Parameters: none Returns: a list object containing all the menu settings """ lSettings = [] # Iterate through all menu items, getting each setting and storing # the setting in a file. for inx in range(1, _MAX_NUMBER_OF_MENU_ITEMS): # Format the read menu item CAT command. sCommand = 'EX%0.3d;' % inx # Send the command to the FT991. sResult = ft991.sendCommand(sCommand) # Add the menu setting to a list object. lSettings.append(sResult) return lSettings ## end def def writeMenuSettings(lSettings): """ Description: Writes supplied menu settings to the FT991. Parameters: lSettings - a list object containing menu settings Returns: nothing """ for item in lSettings: # Do not write read-only menu settings as this results # in the FT-991 returning an error. The only read-only # setting is the "Radio ID" setting. if item.find('EX087') > -1: continue; # Do not change the CAT baud rate while restoring menu # items as this could result in failure to restore menu # items after item #31. This situation might happen if # the FT991 CAT baud rate gets changed some time after # the menu settings have been backed up. if item.find('EX031') > -1: continue; # Send the pre-formatted menu setting to the FT991. sResult = ft991.sendCommand(item) if sResult.find('?;') > -1: print('error restoring menu setting: %s') % item exit(1) ## end def def writeToFile(lSettings, fileName): """ Description: Writes supplied settings to the specified file. Parameters: lSettings - a list object containing the settings fileName - the name of the output file Returns: nothing """ fout = open(fileName, 'w') for item in lSettings: fout.write('%s\n' % item) fout.close() ## end def def readFromFile(fileName): """ Description: Reads settings from the specified file. Parameters: fileName - the name of the input file Returns: a list object containing the settings """ lSettings = [] fin = open(fileName, 'r') for line in fin: item = line.strip() # remove new line characters lSettings.append(item) fin.close() return lSettings ## end def def setIOFile(option, commandLineFile): """ Description: Provides a connector between the command line and the interactive interface. Commands included as arguments on the command line determine default file names, as well as the command executed by doUserCommand above. Parameters: option - the command supplied by the command line argument interpreter getCLarguments. commandLineFile - the name of the file supplied by the -f command line argument (if supplied). Returns: nothing """ global menuBackupFile, memoryBackupFile, commandLineOption commandLineOption = option if commandLineFile == '': return if (option == 'bu' or option == 'ru'): menuBackupFile = commandLineFile # set menu backup file name elif (option == 'bm' or option == 'rm'): memoryBackupFile = commandLineFile # set memory backup file name ## end def def printMenuSplash(): """ Description: Prints an menu of available commands for use in interactive mode. Parameters: none Returns: nothing """ splash = \ """ Enter a command from the list below: m - show this menu bm - backup memory to file rm - restore memory from file bu - backup menu to file ru - restore menu from file p - enter pass through mode v - toggle verbose mode x - exit this program """ print(splash) ## end def def getCLarguments(): """ Description: Gets command line arguments and configures this program to run accordingly. See the variable 'usage', below, for possible arguments that may be used on the command line. Parameters: none Returns: nothing """ global baudRate index = 1 fileName = '' backupOption = '' # Define a splash to help the user enter command line arguments. usage = "Usage: %s [-v] [OPTION] [-f file] [-u baud rate]\n" \ " -b: backup memory\n" \ " -r: restore memory\n" \ " -m: backup menu\n" \ " -s: restore menu\n" \ " -f: backup/restore file name\n" \ " -v: verbose mode\n" \ " -u: baud rate\n" \ % sys.argv[0].split('/')[-1] # Process all command line arguments until done. Note that the last # dash b, m, r, or s argument encounterd on the command line will be # the one actually execute; any previous instances will be ignored. while index < len(sys.argv): if sys.argv[index] == '-f': # Backup file provided. # Get the backup file name. if len(sys.argv) < index + 2: print("-f option requires file name") exit(1); fileName = sys.argv[index + 1] index += 1 elif sys.argv[index] == '-b': # backup memory backupOption = 'bm' elif sys.argv[index] == '-r': # restore memory backupOption = 'rm' elif sys.argv[index] == '-m': # backup menu backupOption = 'bu' elif sys.argv[index] == '-s': # restore menu backupOption = 'ru' elif sys.argv[index] == '-v': # set verbose mode 'ON' ft991.verbose = True elif sys.argv[index] == '-d': # set debug mode 'ON' ft991.debug = True elif sys.argv[index] == '-u': # set the baud rate # v12 24 Aug 2022 by J L Owrey; added command line parameter # for setting the CAT baud rate # Get the backup file name. baudRate = sys.argv[index + 1] if not baudRate in ['4800', '9600', '19200', '38400']: print("invalid baud rate") exit(1) baudRate = int(baudRate) index += 1 else: print(usage) exit(-1) index += 1 ## end while # Set backup file name and backup command to execute. setIOFile(backupOption, fileName) ##end def def main(): """ Description: Opens a com port connection to the FT991. Processes any supplied command line options. If no command line options provides, then enters interactive mode. Parameters: none Returns: nothing """ getCLarguments() # get command line options # Determine OS type and set device port accordingly. #OS_type = sys.platform #if 'WIN' in OS_type.upper(): if 'WIN' in sys.platform.upper(): port = _WINDOWS_COM_PORT else: port = _LINUX_COM_PORT ft991.begin(port, baudRate) # open com port session to FT991 # Process command line options (if any). if commandLineOption != '': doUserCommand() return # Else enter user interactive mode. printMenuSplash() while(True): doUserCommand() ## end def if __name__ == '__main__': main()