ft991/ft991utility/ft991utility.py
616f4281
 #!/usr/bin/python3 -u
0ad18757
 # 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
616f4281
 #   * v10 24 Nov 2019 by J L Owrey; first release
 #   * v11 03 Oct 2020 by J L owrey; upgraded to Python 3
0ad18757
 #
 # This script has been tested with the following
 #
616f4281
 #     Python 3.8.10 (default, Mar 15 2022, 12:22:08) 
 #     [GCC 9.4.0] on linux
0ad18757
 #2345678901234567890123456789012345678901234567890123456789012345678901234567890
 
616f4281
 # Environment Setup
 
 _WINDOWS_COM_PORT = 'COM5'
 _LINUX_COM_PORT = '/dev/ttyUSB0'
 _FT991_BAUD_RATE = 9600
 
0ad18757
 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
cdcdd076
 _DEBUG = False
0ad18757
 
 # Global definitions
 
 menuBackupFile = _DEFAULT_MENU_SETTINGS_FILE
 memoryBackupFile = _DEFAULT_MEMORY_SETTINGS_FILE
 commandLineOption = ''
 
 # 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 == '':
616f4281
         cmd = input('>').strip()
0ad18757
     # 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:
616f4281
         print("invalid command")
0ad18757
 ## 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...
616f4281
     print('Backing up memory settings...')
0ad18757
     settings = readMemorySettings()
     # and write them to the file.
     writeToFile(settings, fileName)
616f4281
     print('Memory settings backed up to \'%s\'' % fileName)
0ad18757
 ## 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):
616f4281
         print('File not found.\n' \
0ad18757
               'Please enter a valid file name.  Be sure to correctly ' \
616f4281
               'enter\nthe full path name or relative path name of the file.')
0ad18757
         return
     # Read the memory settings from the file...
616f4281
     print('Restoring memory settings...')
0ad18757
     settings = readFromFile(fileName)
     # and write them to the FT991.
     writeMemorySettings(settings)
616f4281
     print('Memory settings restored from \'%s\'' % fileName)
0ad18757
 ## 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...
616f4281
     print('Backing up menu settings...')
0ad18757
     settings = readMenuSettings()
     # and write them to the file.
     writeToFile(settings, fileName)
616f4281
     print('Menu settings backed up to \'%s\'' % fileName)
0ad18757
 ## 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):
616f4281
         print('File not found.\n' \
0ad18757
               'Please enter a valid file name.  Be sure to correctly ' \
616f4281
               'enter\nthe full path name or relative path name of the file.')
0ad18757
         return
     # Read the menu settings from the file...
616f4281
     print('Restoring menu settings...')
0ad18757
     settings = readFromFile(fileName)
     # and write them to the FT991.
     writeMenuSettings(settings)
616f4281
     print('Menu settings restored from \'%s\'' % fileName)
0ad18757
 ## 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
     """
616f4281
     print('Entering passthrough mode. Type \'exit\' to exit mode.')
0ad18757
     while(True):
         # Prompt the user to enter an FT991 CAT command, and
         # process the  command string.
616f4281
         sCommand = input('CAT# ').upper()
0ad18757
         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 != '':
616f4281
                 print(sResult)
0ad18757
 ## 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
616f4281
         print('Verbose is OFF')
0ad18757
     else:
         ft991.verbose = True
616f4281
         print('Verbose is ON')  
0ad18757
 ## 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
616f4281
     fileName = input('Enter file name or <CR> for default: ')
0ad18757
     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.
cdcdd076
     lSettings = [ 'Memory Ch,VFO_A,VFO_B,' \
                   'RepeaterShift,Mode,Tag,Encoding,Tone,DCS,' \
                   'Clarifier,RxClar,TxClar,PreAmp,RfAttn,NB,IFshift,' \
                   'IFwidthIndex,ContourState,ContourFreq,' \
                   'APFstate,APFfreq,DNRstate,DNRalgorithm,DNFstate,' \
                   'NBFstate,NotchState,NotchFreq' ]
0ad18757
 
91164a30
     for iLocation in range(1, _MAX_NUMBER_OF_MEMORY_ITEMS):
 
b844f45d
         # 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
0ad18757
         # 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.
91164a30
         dMem = ft991.getMemory(iLocation)
0ad18757
         tone = ft991.getCTCSS()
         dcs = ft991.getDCS()
cdcdd076
         preamp = ft991.getPreamp()
         rfattn = ft991.getRfAttn()
         nblkr = ft991.getNoiseBlanker()
         shift = ft991.getIFshift()
         width = ft991.getIFwidth()
         lContour = ft991.getContour()
         dnrstate = ft991.getDNRstate()
         dnralgorithm = ft991.getDNRalgorithm()
         dnfstate = ft991.getDNFstate()
         narstate = ft991.getNARstate()
         notch = ft991.getNotchState()
0ad18757
         # 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.
cdcdd076
         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,%s' % \
                (
                  dMem['memloc'], dMem['vfoa'], '', \
                  dMem['rpoffset'], dMem['mode'], dMem['tag'], dMem['encode'], \
0ad18757
                  tone, dcs, dMem['clarfreq'], dMem['rxclar'], \
cdcdd076
                  dMem['txclar'], preamp, rfattn, nblkr, shift, width, \
                  lContour[0], lContour[1], lContour[2], lContour[3], \
                  dnrstate, dnralgorithm, dnfstate, narstate, notch[0],
                  notch[1]
                )
0ad18757
         # Add the comma-delimited string to the list object.
75d24721
         lSettings.append(sCsvFormat)
     return lSettings
0ad18757
 # end def
 
75d24721
 def writeMemorySettings(lSettings):
0ad18757
     """
     Description: Writes the supplied memory settings to the FT991.
75d24721
     Parameters: lSettings - a list object containing the memory
0ad18757
                                   settings in comma delimited format
     Returns: nothing
     """
75d24721
     for item in lSettings:
         try:
cdcdd076
             # 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
b844f45d
             # Set the parameters for the memory location.
             ft991.setMemory(dItem)
796cb306
             # Set current channel to memory location being set.
             ft991.setMemoryLocation(int(dItem['memloc']))
75d24721
             # Set CTCSS tone for memory channel.
91164a30
             ft991.setCTCSS(dItem['tone'])
75d24721
             # Set DCS code for memory channel. 
91164a30
             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'])
cdcdd076
             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'])
             # Set IF shift amount
             ft991.setIFshift(dItem['shift'])
             # Set IF width index
             ft991.setIFwidth(dItem['width'])
             # Set Contour parameters
             ft991.setContour(dItem['contour'])
             # 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']))
616f4281
         except Exception as e:
             print('Memory settings restore operation failed. Most likely\n' \
91164a30
                   'this is due to the backup settings file corrupted or\n' \
616f4281
                   'incorrectly formatted. Look for the following error: \n')
             print(e)
cdcdd076
             raise
             #exit(1)
     # end for
0ad18757
 ## end def
 
75d24721
 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;
         # Send the pre-formatted menu setting to the FT991.
         sResult = ft991.sendCommand(item)
         if sResult.find('?;') > -1:
616f4281
             print('error restoring menu setting: %s') % item
75d24721
             exit(1)
 ## end def
 
0ad18757
 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.
75d24721
                 commandLineFile - the name of the file supplied by the -f
                          command line argument (if supplied). 
0ad18757
     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 = \
 """
ff1ca398
 Enter a command from the list below:
0ad18757
 m - show this menu
 bm - backup memory to file
 rm - restore memory from file
 bu - backup menu to file
 ru - restore menu from file
f7f21820
 p - enter pass through mode
0ad18757
 v - toggle verbose mode
 x - exit this program
 """
616f4281
     print(splash)
0ad18757
 ## end def
 
 def getCLarguments():
75d24721
     """ Description: Gets command line arguments and configures this program
0ad18757
                      to run accordingly.  See the variable 'usage', below,
                      for possible arguments that may be used on the command
                      line.
         Parameters: none
         Returns: nothing
     """
     index = 1
     fileName = ''
     backupOption = ''
 
     # Define a splash to help the user enter command line arguments.
     usage =  "Usage: %s [-v] [OPTION] [-f file]\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"                 \
              % 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:
616f4281
                 print("-f option requires file name")
0ad18757
                 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
         else:
616f4281
             print(usage)
0ad18757
             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
616f4281
 
     # 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, _FT991_BAUD_RATE) # open com port session to FT991
0ad18757
 
     # Process command line options (if any).
     if commandLineOption != '':
        doUserCommand()
        return
 
     # Else enter user interactive mode.
     printMenuSplash()
b844f45d
     while(True):
0ad18757
         doUserCommand()
 ## end def
 
 if __name__ == '__main__':
     main()