Browse code

ft991 utility replaces writeMemory, and passThrough

Gandolf authored on 11/24/2019 23:01:28
Showing 2 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,442 @@
1
+#!/usr/bin/python -u
2
+# The -u option turns off block buffering of python output. This assures
3
+# that output streams to stdout when output happens.
4
+#
5
+# Module: ft991.py
6
+#
7
+# Description:  This module contains tables for translating common transceiver
8
+#               settings to FT991 CAT parameters.  Low level serial
9
+#               communication functions are also handled by this module.  In
10
+#               particular this module handles:
11
+#                   1. Instantiating a serial connection object
12
+#                   2. Sending character strings to the serial port
13
+#                   3. Reading characters from the serial port
14
+#                   4. Parsing and formatting of FT991 commands
15
+#                   5. Translating radio operating parameters to CAT
16
+#                      commands, i.e., CTCSS tones.  
17
+#
18
+# Copyright 2019 by Jeff Owrey, Intravisions.com
19
+#    This program is free software: you can redistribute it and/or modify
20
+#    it under the terms of the GNU General Public License as published by
21
+#    the Free Software Foundation, either version 3 of the License, or
22
+#    (at your option) any later version.
23
+#
24
+#    This program is distributed in the hope that it will be useful,
25
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
26
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
27
+#    GNU General Public License for more details.
28
+#
29
+#    You should have received a copy of the GNU General Public Licensef
30
+#    along with this program.  If not, see http://www.gnu.org/license.
31
+#
32
+# Revision History
33
+#   * v10 24 Nov 2019 by J L Owrey; first release
34
+#
35
+# This script has been tested with the following
36
+#
37
+#     Python 2.7.15rc1 (default, Nov 12 2018, 14:31:15) 
38
+#     [GCC 7.3.0] on linux2
39
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
40
+
41
+import sys, serial, time
42
+
43
+# General constant defines
44
+_INTERFACE_TIMEOUT = 0.1 # seconds
45
+_SERIAL_READ_TIMEOUT = 0.1 # seconds
46
+_SERIAL_READ_BUFFER_LENGTH = 1024 # characters
47
+
48
+# Define globals
49
+verbose = False
50
+debug = False
51
+ptrDevice = None
52
+
53
+# Define lookup tables for common transceiver settings.  Common settings
54
+# such as modulation mode, repeater offset direction, DCS/CTCSS mode,
55
+# CTCSS tone, and DCS code are translated to the repective FT991 parameter
56
+# value.
57
+
58
+# Modulation modes
59
+dMode = { 'LSB':'1', 'USB':'2', 'CW':'3', 'FM':'4', 'AM':'5',
60
+          'RTTY-LSB':'6', 'CW-R':'7', 'DATA-LSB':'8', 'RTTY-USB':'9',
61
+          'DATA-FM':'A', 'FM-N':'B', 'DATA-USB':'C', 'AM-N':'D',
62
+          'C4FM':'E' }
63
+
64
+# Repeater shift direction
65
+dShift = { 'OFF':'0', '+RPT':'1', '-RPT':'2' }
66
+
67
+# Power settings
68
+dPower = { 'LOW':5, 'MID':020, 'HIGH':50, 'MAX':100 }
69
+
70
+# Repeater signaling modes
71
+dEncode = { 'OFF':'0', 'ENC/DEC':'1', 'TONE ENC':'2',
72
+            'DCS ENC/DEC':'4', 'DCS':'3' }
73
+
74
+# CTCSS Tones
75
+dTones = { '67.0 Hz':'000', '69.3 Hz':'001', '71.9 Hz':'002',
76
+           '74.4 Hz':'003', '77.0 Hz':'004', '79.7 Hz':'005',
77
+           '82.5 Hz':'006', '85.4 Hz':'007', '88.5 Hz':'008',
78
+           '91.5 Hz':'009', '94.8 Hz':'010', '97.4 Hz':'011',
79
+           '100.0 Hz':'012', '103.5 Hz':'013', '107.2 Hz':'014',
80
+           '110.9 Hz':'015', '114.8 Hz':'016', '118.8 Hz':'017',
81
+           '123.0 Hz':'018', '127.3 Hz':'019', '131.8 Hz':'020',
82
+           '136.5 Hz':'021', '141.3 Hz':'022', '146.2 Hz':'023',
83
+           '151.4 Hz':'024', '156.7 Hz':'025', '159.8 Hz':'026',
84
+           '162.2 Hz':'027', '165.5 Hz':'028', '167.9 Hz':'029',
85
+           '171.3 Hz':'030', '173.8 Hz':'031', '177.3 Hz':'032',
86
+           '179.9 Hz':'033', '183.5 Hz':'034', '186.2 Hz':'035',
87
+           '189.9 Hz':'036', '192.8 Hz':'037', '196.6 Hz':'038',
88
+           '199.5 Hz':'039', '203.5 Hz':'040', '206.5 Hz':'041',
89
+           '210.7 Hz':'042', '218.1 Hz':'043', '225.7 Hz':'044',
90
+           '229.1 Hz':'045', '233.6 Hz':'046', '241.8 Hz':'047',
91
+           '250.3 Hz':'048', '254.1 Hz':'049' } 
92
+
93
+# DCS Tones
94
+dDcs = { '23':'000', '25':'001', '26':'002', '31':'003', '32':'004',
95
+         '36':'005', '43':'006', '47':'007', '51':'008', '53':'009',
96
+         '54':'010', '65':'011', '71':'012', '72':'013', '73':'014',
97
+         '74':'015', '114':'016', '115':'017', '116':'018', '122':'019',
98
+         '125':'020', '131':'021', '132':'022', '134':'023', '143':'024',
99
+         '145':'025', '152':'026', '155':'027', '156':'028', '162':'029',
100
+         '165':'030', '172':'031', '174':'032', '205':'033', '212':'034',
101
+         '223':'035', '225':'036', '226':'037', '243':'038', '244':'039',
102
+         '245':'040', '246':'041', '251':'042', '252':'043', '255':'044',
103
+         '261':'045', '263':'046', '265':'047', '266':'048', '271':'049',
104
+         '274':'050', '306':'051', '311':'052', '315':'053', '325':'054',
105
+         '331':'055', '332':'056', '343':'057', '346':'058', '351':'059',
106
+         '356':'060', '364':'061', '365':'062', '371':'063', '411':'064',
107
+         '412':'065', '413':'066', '423':'067', '431':'068', '432':'069',
108
+         '445':'070', '446':'071', '452':'072', '454':'073', '455':'074',
109
+         '462':'075', '464':'076', '465':'077', '466':'078', '503':'079',
110
+         '506':'080', '516':'081', '523':'082', '526':'083', '532':'084',
111
+         '546':'085', '565':'086', '606':'087', '612':'088', '624':'089',
112
+         '627':'090', '631':'091', '632':'092', '654':'093', '662':'094',
113
+         '664':'095', '703':'096', '712':'097', '723':'098', '731':'099',
114
+         '732':'100', '734':'101', '743':'102', '754':'103' }
115
+
116
+# Clarifier state
117
+dRxClar = { 'OFF':'0', 'ON':'1' }
118
+dTxClar = { 'OFF':'0', 'ON':'1' }
119
+
120
+# Define 'set' functions to encapsulate the various FT991 CAT commands.
121
+
122
+def setMemory(dMem):
123
+    """
124
+    Description: Returns a formatted MT command to the calling function.
125
+    Parameters: dMem - a dictionary objected with the following keys
126
+                       defined:
127
+
128
+                       memloc - the memory location to be written
129
+                       rxfreq - the receive frequency of VFO-A in MHz
130
+                       mode - the modulation mode
131
+                       encode - the tone or DCS encoding mode
132
+                       shift - the direction of the repeater shift
133
+                       tag - a label for the memory location
134
+
135
+    Returns: a string containing the formatted command
136
+    """
137
+    sCmd = 'MC%0.3d;' % int(dMem['memloc'])
138
+    sResult = sendCommand(sCmd)
139
+
140
+    # While the 'MW' and 'MT' commands can be used to turn the Rx
141
+    # and Tx clarifiers on, the clarifier states can only be turned
142
+    # off by sending the 'RT0' and 'XT0' commands.  This situation is
143
+    # probably due to a potential bug in the CAT interface.
144
+    sResult = sendCommand('RC;RT0;XT0;')
145
+
146
+    sCmd = 'MT%0.3d' % int(dMem['memloc'])
147
+    sCmd += '%d' % int(float(dMem['rxfreq']) * 1E6)
148
+    sCmd += '%+0.4d' % int(dMem['clarfreq'])
149
+    sCmd += dRxClar[dMem['rxclar']]
150
+    sCmd += dTxClar[dMem['txclar']]
151
+    sCmd += dMode[dMem['mode']]
152
+    sCmd += '0'
153
+    sCmd += dEncode[dMem['encode']]
154
+    sCmd += '00'
155
+    sCmd += dShift[dMem['shift']]
156
+    sCmd += '0'
157
+    sCmd += '%-12s' % dMem['tag']
158
+    sCmd += ';'
159
+    sResult = sendCommand(sCmd)
160
+    return sResult
161
+## end def
162
+
163
+def getMemory(memLoc):
164
+    """
165
+    Description: 
166
+    Parameters: 
167
+    Returns: 
168
+    """
169
+    dMem = {}
170
+
171
+    # Set memory location pointer in FT991.  This is done
172
+    # by sending the memory location select (MC) command.
173
+    sCmd = 'MC%0.3d;' % (memLoc)
174
+    sResult = sendCommand(sCmd)
175
+    # Skip blank memory locations, which return '?;'.
176
+    if sResult == '?;':
177
+        return None
178
+
179
+    # Send the get memory settings string to the FT991.
180
+    sCmd = 'MT%0.3d;' % (memLoc)
181
+    sResult = sendCommand(sCmd)
182
+
183
+    # Parse memory settings string returned by the FT991
184
+    memloc = sResult[2:5]
185
+    rxfreq = sResult[5:14]
186
+    clarfreq = sResult[14:19]
187
+    rxclar = sResult[19]
188
+    txclar = sResult[20]
189
+    mode = sResult[21]
190
+    encode = sResult[23]
191
+    shift = sResult[26]
192
+    tag = sResult[28:40]
193
+
194
+    # Store the memory settings in a dictionary object.
195
+    dMem['memloc'] = str(int(memloc))
196
+    dMem['rxfreq'] = str(float(rxfreq) / 10**6)
197
+    dMem['clarfreq'] = str(int(clarfreq))
198
+    dMem['rxclar'] = dRxClar.keys()[dRxClar.values().index(rxclar)]
199
+    dMem['txclar'] = dTxClar.keys()[dTxClar.values().index(txclar)]
200
+    dMem['mode'] = dMode.keys()[dMode.values().index(mode)]
201
+    dMem['encode'] = dEncode.keys()[dEncode.values().index(encode)]
202
+    dMem['shift'] = dShift.keys()[dShift.values().index(shift)]
203
+    dMem['tag'] = tag.strip()
204
+
205
+    return dMem
206
+## end def
207
+
208
+def getCTCSS():
209
+    """
210
+    Description: 
211
+    Parameters: 
212
+    Returns: 
213
+    """
214
+    # Get result CTCSS tone
215
+    sResult = sendCommand('CN00;')
216
+    tone = sResult[4:7]
217
+    return dTones.keys()[dTones.values().index(tone)]
218
+## end def
219
+
220
+def getDCS():
221
+    """
222
+    Description: 
223
+    Parameters: 
224
+    Returns: 
225
+    """
226
+    # Get result of CN01 command
227
+    sResult = sendCommand('CN01;')
228
+    dcs = sResult[4:7]
229
+    return dDcs.keys()[dDcs.values().index(dcs)]
230
+## end def
231
+
232
+def setCTCSS(tone):
233
+    """
234
+    Description:  returns a formatted CN command that sets the desired
235
+                  CTCSS tone.
236
+    Parameters:   tone - a string containing the CTCSS tone in Hz, e.g.,
237
+                         '100 Hz'
238
+    Returns: a string containing the formatted command
239
+    """
240
+    sCmd = 'CN00%s;' % dTones[tone]
241
+    return sendCommand(sCmd)
242
+## end def
243
+
244
+def setDCS(code):
245
+    """
246
+    Description:  returns a formatted CN command that sets the desired
247
+                  DCS code.
248
+    Parameters:   code - a string containing the DCS code, e.g., '23'
249
+    Returns: a string containing the formatted command
250
+    """
251
+    sCmd = 'CN01%s;' % dDcs[code]
252
+    return sendCommand(sCmd)
253
+## end def
254
+
255
+def setPower(power):
256
+    """
257
+    Description:  returns a formatted PC command that sets the desired
258
+                  RF transmit power level.
259
+    Parameters:   power - Watts, an integer between 5 and 100
260
+    Returns: a string containing the formatted command
261
+    """
262
+    sCmd = 'PC'
263
+    sCmd += '%03.d;' % power 
264
+    return sendCommand(sCmd)
265
+## end def
266
+
267
+def parseCsvData(sline):
268
+    """
269
+    Description:  stores each item in the comma delimited line in a single
270
+                  dictionary object using a key appropriate for that item.
271
+    Parameters: a string containing the comma delimited items to be parsed.
272
+    Returns: a dictionary object containing the parsed line.
273
+    """
274
+    dChan = {} # define an empty dictionary object
275
+    lchan = sline.split(',') # split the line at the commas
276
+    # If the first line is a header line, ignore it.
277
+    if not lchan[0].isdigit():
278
+        return None
279
+    # Store the parsed items with the appropriate key in the dictionary object.
280
+    dChan['memloc'] = lchan[0]
281
+    dChan['rxfreq'] = lchan[1]
282
+    dChan['txfreq'] = lchan[2]
283
+    dChan['offset'] = lchan[3]
284
+    dChan['shift'] = lchan[4]
285
+    dChan['mode'] = lchan[5]
286
+    dChan['tag'] = lchan[6]
287
+    dChan['encode'] = lchan[7]
288
+    dChan['tone'] = lchan[8]
289
+    dChan['dcs'] = str(int(lchan[9]))
290
+    dChan['clarfreq'] = lchan[10]
291
+    dChan['rxclar'] = lchan[11]
292
+    dChan['txclar'] = lchan[12]
293
+    return dChan # return the dictionary object
294
+## end def
295
+
296
+# Define serial communications functions.
297
+
298
+def begin(baud=9600):
299
+    """
300
+    Description: Initiates a serial connection the the FT991. Should
301
+                 always be called before sending commands to or
302
+                 receiving data from the FT991.  Only needs to be called
303
+                 once.
304
+    Parameters: none
305
+    Returns: a pointer to the FT991 serial connection
306
+    """
307
+    global ptrDevice
308
+
309
+    # Determine OS type and set device port accordingly.
310
+    OS_type = sys.platform
311
+    if 'WIN' in OS_type.upper():
312
+        port = 'COM5'
313
+    else:
314
+        port = '/dev/ttyUSB0'
315
+
316
+    # In debug mode do not actually send commands to the FT991.
317
+    if debug:
318
+        return
319
+    # Create a FT991 object for serial communication
320
+    try:
321
+        ptrDevice = serial.Serial(port, baud,      
322
+                                  timeout=_INTERFACE_TIMEOUT)
323
+    except Exception, error:
324
+        if str(error).find('could not open port') > -1:
325
+            print 'Please be sure the usb cable is properly connected to\n' \
326
+                  'your FT991 and to your computer, and that the FT991 is\n' \
327
+                  'turned ON.  Then restart this program.'
328
+        else:
329
+            print 'Serial port error: %s\n' % error
330
+        exit(1)         
331
+    time.sleep(.1) # give the connection a moment to settle
332
+    return ptrDevice
333
+## end def
334
+
335
+def sendCommand(sCmd):
336
+    """
337
+    Description: Sends a formatted FT911 command to the communication
338
+                 port connected to the FT991.  Prints to stdout the
339
+                 answer from the FT991 (if any).
340
+    Parameters: device - a pointer to the FT991 comm port
341
+                sCmd - a string containing the formatted command
342
+    Returns: nothing
343
+    """
344
+    # Debug mode in conjunction with verbose mode is for verifying
345
+    # correct formatting of commands before they are actually sent
346
+    # to the FT991.
347
+    if verbose:
348
+        print sCmd,
349
+    # In debug mode do not actually send commands to the FT991.
350
+    if debug:
351
+        return ''
352
+
353
+    # Send the formatted command to the FT991 and get an answer, if any.
354
+    # If the command does not generate an answer, no characters will be
355
+    # returned by the FT991, resulting in an empty string returned by
356
+    # the receiveSerial function.
357
+    sendSerial(sCmd)
358
+    sResult  = receiveSerial();
359
+    if verbose:
360
+        print sResult
361
+    return sResult
362
+## end def
363
+
364
+def receiveSerial(termchar=';'):
365
+    """
366
+    Description: Reads output one character at a time from the device
367
+                 until a terminating character is received.  Returns a     
368
+                 string containing the characters read from the serial
369
+                 port.
370
+    Parameters:  termchar - character terminating the answer string
371
+    Returns: a string containing the received data
372
+    """
373
+    answer = '' # initialize answer string to empty string
374
+    charCount = 0  # reset read character count to zero
375
+
376
+    while True:
377
+        startTime = time.time() # Start read character timer
378
+        c =''
379
+        while True:
380
+            # Check for a character available in the serial read buffer.
381
+            if ptrDevice.in_waiting:
382
+                c = ptrDevice.read()
383
+                break
384
+            # Timeout if a character does not become available.
385
+            if time.time() - startTime > _SERIAL_READ_TIMEOUT:
386
+                break # Character waiting timer has timed out.
387
+        # Return empty string if a character has not become available.
388
+        if c == '':
389
+            break;
390
+        answer += c # Form a string from the received characters.
391
+        charCount += 1 # Increment character count.
392
+        # If a semicolon has arrived then the FT991 has completed
393
+        # sending output to the serial port so stop reading characters.
394
+        # Also stop if max characters received. 
395
+        if c == termchar:
396
+            break
397
+        if charCount > _SERIAL_READ_BUFFER_LENGTH:
398
+            raise Exception('serial read buffer overflow')
399
+    ptrDevice.flushInput() # Flush serial buffer to prevent overflows.
400
+    return answer           
401
+## end def
402
+
403
+def sendSerial(command):
404
+    """
405
+    Description: Writes a string to the device.
406
+    Parameters: command - string containing the FT991 command
407
+    Returns: nothing
408
+    """
409
+    # In debug we only want to see the output of the command formatter,
410
+    # not actually send commands to the FT991.  Debug mode should be
411
+    # used in conjunction with verbose mode.
412
+    ptrDevice.write(command) # Send command string to FT991
413
+    ptrDevice.flushOutput() # Flush serial buffer to prevent overflows
414
+## end def
415
+
416
+def main():
417
+    """
418
+    Description: Place code for testing this module here.
419
+    Parameters: none
420
+    Returns: nothing
421
+    """
422
+    # Test this module.
423
+    global verbose, debug
424
+
425
+    verbose = True
426
+    debug = False
427
+
428
+    begin()
429
+    sendCommand('IF;')
430
+    sendCommand('MC001;')
431
+    sendCommand('ZZZ;')
432
+
433
+    dMem = {'rxfreq': '146.52', 'shift': 'OFF', 'encode': 'OFF', \
434
+        'txclar': 'OFF', 'tag': 'KA7JLO', 'mode': 'FM', 'rxclar': 'OFF', \
435
+        'memloc': '99', 'clarfreq': '0'}
436
+    setMemory(dMem)
437
+    print getMemory(99)
438
+   
439
+## end def
440
+
441
+if __name__ == '__main__':
442
+    main()
0 443
new file mode 100755
... ...
@@ -0,0 +1,517 @@
1
+#!/usr/bin/python -u
2
+# The -u option turns off block buffering of python output. This assures
3
+# that output streams to stdout when output happens.
4
+#
5
+# Module: ft991utility.py
6
+#
7
+# Description:  A utility for backing up Yaesu FT991 memory and menu settings,
8
+#               and also for restoring memory and menu settings from
9
+#               a file.  Can be used both interactively or with command line
10
+#               arguments. Before running this utility, be sure to copy
11
+#               the file 'ft911.py' to the same folder as this utility.
12
+#
13
+# Copyright 2019 by Jeff Owrey, Intravisions.com
14
+#    This program is free software: you can redistribute it and/or modify
15
+#    it under the terms of the GNU General Public License as published by
16
+#    the Free Software Foundation, either version 3 of the License, or
17
+#    (at your option) any later version.
18
+#
19
+#    This program is distributed in the hope that it will be useful,
20
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
21
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
+#    GNU General Public License for more details.
23
+#
24
+#    You should have received a copy of the GNU General Public Licensef
25
+#    along with this program.  If not, see http://www.gnu.org/license.
26
+#
27
+# Revision History
28
+#   * v10 23 Nov 2019 by J L Owrey; first release
29
+#
30
+# This script has been tested with the following
31
+#
32
+#     Python 2.7.15rc1 (default, Nov 12 2018, 14:31:15) 
33
+#     [GCC 7.3.0] on linux2
34
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
35
+
36
+import os, sys, serial, time
37
+import ft991 # module should be in same directory as this utility
38
+
39
+# Constant definitions
40
+
41
+_DEFAULT_MENU_SETTINGS_FILE = 'ft991menu.cfg'
42
+_DEFAULT_MEMORY_SETTINGS_FILE = 'ft991mem.csv'
43
+_MAX_NUMBER_OF_MENU_ITEMS = 154
44
+_MAX_NUMBER_OF_MEMORY_ITEMS = 118
45
+
46
+# Global definitions
47
+
48
+menuBackupFile = _DEFAULT_MENU_SETTINGS_FILE
49
+memoryBackupFile = _DEFAULT_MEMORY_SETTINGS_FILE
50
+commandLineOption = ''
51
+
52
+# Command processing functions
53
+
54
+def doUserCommand():
55
+    """
56
+    Description: Provides an interactive user interface where the user can
57
+                 enter simple text commands at a command line prompt.  The
58
+                 commands that a user can enter are listed in a menu splash
59
+                 that the user can display anytime with the 'm' command.
60
+                 Also this function processes commands provided as command
61
+                 line options.  In the case of a command line option this
62
+                 function operates non-interactively.
63
+    Parameters: none
64
+    Returns: nothing
65
+    """
66
+    # When command line arguments have not been provided,
67
+    # use interactive mode and give the user a prompt.
68
+    if commandLineOption == '':
69
+        cmd = raw_input('>').strip()
70
+    # If command line arguments have been provided, then
71
+    # execute the command non-interactively.
72
+    else:
73
+        cmd = commandLineOption
74
+
75
+    # Process the user command.   
76
+    if cmd == '':
77
+        return
78
+    elif cmd == 'm':
79
+        printMenuSplash()
80
+    elif cmd == 'bu':
81
+        backupMenuSettings()
82
+    elif cmd == 'ru':
83
+        restoreMenuSettings()
84
+    elif cmd == 'bm':
85
+        backupMemorySettings()
86
+    elif cmd == 'rm':
87
+        restoreMemorySettings()
88
+    elif cmd == 'p':
89
+        passThroughMode()
90
+    elif cmd == 'v':
91
+        toggleVerboseMode()
92
+    elif cmd == 'x':
93
+        exit(0)
94
+    else:
95
+        print "invalid command"
96
+## end def
97
+
98
+def backupMemorySettings():
99
+    """
100
+    Description: Backs up all memory settings to a file.  The user has the
101
+                 option of providing a file name or accepting a default
102
+                 file name.
103
+    Parameters: none
104
+    Returns: nothing
105
+    """
106
+    # Prompt the user for a file name in which to store
107
+    # backed up memory settings.
108
+    fileName = getFileName(memoryBackupFile)
109
+    # Read the memory settings from the FT991...
110
+    print 'Backing up memory settings...'
111
+    settings = readMemorySettings()
112
+    # and write them to the file.
113
+    writeToFile(settings, fileName)
114
+    print 'Memory settings backed up to \'%s\'' % fileName
115
+## end def
116
+
117
+def restoreMemorySettings():
118
+    """
119
+    Description: Restores all memory settings from a file.  The user has the
120
+                 option of providing a file name or accepting a default
121
+                 file name.
122
+    Parameters: none
123
+    Returns: nothing
124
+    """
125
+    # Prompt the user for a file name from which to retrieve backed up
126
+    # memory settings.  Also make sure the file exists.
127
+    fileName = getFileName(memoryBackupFile)
128
+    if not os.path.isfile(fileName):
129
+        print 'File not found.\n' \
130
+              'Please enter a valid file name.  Be sure to correctly ' \
131
+              'enter\nthe full path name or relative path name of the file.'
132
+        return
133
+    # Read the memory settings from the file...
134
+    print 'Restoring memory settings...'
135
+    settings = readFromFile(fileName)
136
+    # and write them to the FT991.
137
+    writeMemorySettings(settings)
138
+    print 'Memory settings restored from \'%s\'' % fileName
139
+## end def
140
+
141
+def backupMenuSettings():
142
+    """
143
+    Description: Backs up all menu settings to a file.  The user has the
144
+                 option of providing a file name or accepting a default
145
+                 file name.
146
+    Parameters: none
147
+    Returns: nothing
148
+    """
149
+    # Prompt the user for a file name in which to store
150
+    # backed up menu settings.
151
+    fileName = getFileName(menuBackupFile)
152
+    # Read the menu settings from the FT991...
153
+    print 'Backing up menu settings...'
154
+    settings = readMenuSettings()
155
+    # and write them to the file.
156
+    writeToFile(settings, fileName)
157
+    print 'Menu settings backed up to \'%s\'' % fileName
158
+## end def
159
+
160
+def restoreMenuSettings():
161
+    """
162
+    Description: Restores all menu settings from a file.  The user has the
163
+                 option of providing a file name or accepting a default
164
+                 file name.
165
+    Parameters: none
166
+    Returns: nothing
167
+    """
168
+    # Prompt the user for a file name from which to retrieve backed up
169
+    # menu settings.  Also make sure the file exists.
170
+    fileName = getFileName(menuBackupFile)
171
+    if not os.path.isfile(fileName):
172
+        print 'File not found.\n' \
173
+              'Please enter a valid file name.  Be sure to correctly ' \
174
+              'enter\nthe full path name or relative path name of the file.'
175
+        return
176
+    # Read the menu settings from the file...
177
+    print 'Restoring menu settings...'
178
+    settings = readFromFile(fileName)
179
+    # and write them to the FT991.
180
+    writeMenuSettings(settings)
181
+    print 'Menu settings restored from \'%s\'' % fileName
182
+## end def
183
+
184
+def passThroughMode():
185
+    """
186
+    Description: An interactive mode whereby the user an enter FT991 CAT
187
+                 commands directly on the command line.  This mode greatly
188
+                 facilitates development and debugging.
189
+    Parameters: none
190
+    Returns: nothing
191
+    """
192
+    print 'Entering passthrough mode. Type \'exit\' to exit mode.'
193
+    while(True):
194
+        # Prompt the user to enter an FT991 CAT command, and
195
+        # process the  command string.
196
+        sCommand = raw_input('CAT# ').upper()
197
+        if sCommand == 'EXIT': # exit this utility
198
+            break
199
+        # If the user fails to end a CAT command with a semi-colon,
200
+        # then provide one.
201
+        elif sCommand[-1:] != ';':
202
+            sCommand += ';'
203
+
204
+        if sCommand == '': # no command - do nothing
205
+            continue
206
+        else: # run a user command
207
+            ft991.sendSerial(sCommand)
208
+            sResult = ft991.receiveSerial();
209
+            if sResult != '':
210
+                print sResult
211
+## end def
212
+
213
+def toggleVerboseMode():
214
+    """
215
+    Description: Toggles the verbose mode on or off, depending on the
216
+                 previous state.  Verbose mode causes CAT commands to be
217
+                 echoed to STDOUT as they are sent to the FT991.  If a
218
+                 CAT command returns a string, that is also echoed. 
219
+    Parameters: none
220
+    Returns: nothing
221
+    """
222
+    if ft991.verbose:
223
+        ft991.verbose = False
224
+        print 'Verbose is OFF'
225
+    else:
226
+        ft991.verbose = True
227
+        print 'Verbose is ON'  
228
+## end def
229
+
230
+def getFileName(defaultFile):
231
+    """
232
+    Description: Prompts the user for a file name.
233
+    Parameters: defaultFile - file name to use if the user does not
234
+                              provide a file name
235
+    Returns: the user provided file name if provided, or the default
236
+             file name, otherwise.
237
+    """
238
+    # If a command backup or restore argument provided, then do not
239
+    # query the user for a file name.  A file name may be provided
240
+    # as an option on the command line.
241
+    if commandLineOption != '':
242
+        return defaultFile
243
+    # Otherwise query the user for a file name
244
+    fileName = raw_input("Enter file name or <CR> for default: ")
245
+    if fileName == '':
246
+        return defaultFile
247
+    else:
248
+        return fileName
249
+## end def
250
+
251
+def readMenuSettings():
252
+    """
253
+    Description: Reads all menu settings from the FT991.
254
+    Parameters: none
255
+    Returns: a list object containing all the menu settings
256
+    """
257
+    lMenuSettings = []
258
+    # Iterate through all menu items, getting each setting and storing
259
+    # the setting in a file.
260
+    for inx in range(1, _MAX_NUMBER_OF_MENU_ITEMS):
261
+        # Format the read menu item CAT command.
262
+        sCommand = 'EX%0.3d;' % inx
263
+        # Send the command to the FT991.
264
+        sResult = ft991.sendCommand(sCommand)
265
+        # Add the menu setting to a list object.
266
+        lMenuSettings.append(sResult)
267
+    return lMenuSettings
268
+## end def
269
+
270
+def writeMenuSettings(lMenuSettings):
271
+    """
272
+    Description: Writes supplied menu settings to the FT991.
273
+    Parameters: lMenuSettings - a list object containing menu settings
274
+    Returns: nothing
275
+    """
276
+    for item in lMenuSettings:
277
+
278
+        # Do not write read-only menu settings as this results
279
+        # in the FT-991 returning an error.  The only read-only
280
+        # setting is the "Radio ID" setting.
281
+        if item.find('EX087') > -1:
282
+            continue;
283
+        # Send the pre-formatted menu setting to the FT991.
284
+        sResult = ft991.sendCommand(item)
285
+        if sResult.find('?;') > -1:
286
+            print 'error restoring menu setting: %s' % item
287
+            exit(1)
288
+## end def
289
+
290
+def readMemorySettings():
291
+    """
292
+    Description: Reads all defined memory settings from the FT991.  The
293
+                 settings are reformatted into a readable, comma-delimited
294
+                 (csv) file, which can be viewed and modified with a
295
+                 spreadsheet application such as LibreOffice Calc.
296
+    Parameters: none
297
+    Returns: a list object containing memory location settings. Each item
298
+             in the list represents a row, in comma-delimited form, that
299
+             specifies the settings for a single memory location. The first
300
+             item in the list are the column headers for remaining rows
301
+             contained in the list.
302
+    """
303
+    # Define the column headers as the first item in the list.
304
+    lMemorySettings = [ 'Memory Ch,Rx Frequency,Tx Frequency,Offset,' \
305
+                        'Repeater Shift,Mode,Tag,Encoding,Tone,DCS,' \
306
+                        'Clarifier, RxClar, TxClar' ]
307
+
308
+    for memoryLocation in range(1, _MAX_NUMBER_OF_MEMORY_ITEMS):
309
+        # For each memory location get the memory contents.  Note that
310
+        # several CAT commands are required to get the entire contents
311
+        # of a memory location.  Specifically, additional commands are
312
+        # required to get DCS code and CTCSS tone.
313
+        dMem = ft991.getMemory(memoryLocation)
314
+        # If a memory location is empty (has not been programmed or has
315
+        # been erased), do not created a list entry for that location.
316
+        if dMem == None:
317
+            continue
318
+        # Get DCS and CTCSS.
319
+        tone = ft991.getCTCSS()
320
+        dcs = ft991.getDCS()
321
+        # getMemory, above, stores data in a dictionary object.  Format
322
+        # the data in this object, as well as, the DCS code and CTCSS
323
+        # tone into a comma-delimited string.
324
+        sCsvFormat = '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,' % \
325
+               ( dMem['memloc'], dMem['rxfreq'], '', '', \
326
+                 dMem['shift'], dMem['mode'], dMem['tag'], dMem['encode'], \
327
+                 tone, dcs, dMem['clarfreq'], dMem['rxclar'], \
328
+                 dMem['txclar'] )
329
+        # Add the comma-delimited string to the list object.
330
+        lMemorySettings.append(sCsvFormat)
331
+        if ft991.verbose:
332
+            print
333
+    return lMemorySettings
334
+# end def
335
+
336
+def writeMemorySettings(lMemorySettings):
337
+    """
338
+    Description: Writes the supplied memory settings to the FT991.
339
+    Parameters: lMemorySettings - a list object containing the memory
340
+                                  settings in comma delimited format
341
+    Returns: nothing
342
+    """
343
+    for item in lMemorySettings:
344
+        # Parse the comma-delimited line and store in a dictionary object.
345
+        dItem = ft991.parseCsvData(item)
346
+        # The first item in the memory settings list are the column headers;
347
+        # so ignore this item.  (parseData returns None for this item.)
348
+        if dItem == None:
349
+            continue
350
+        # Set memory channel vfo, mode, and other data.
351
+        sResult = ''
352
+        sResult += ft991.setMemory(dItem)
353
+        # Set CTCSS tone for memory channel.
354
+        sResult += ft991.setCTCSS(dItem['tone'])
355
+        # Set DCS code for memory channel. 
356
+        sResult += ft991.setDCS(dItem['dcs'])
357
+        # Process any errors returned by the CAT interface.
358
+        if sResult.find('?;') > -1:
359
+            print 'error restoring memory setting: %s' % sResult
360
+        if ft991.verbose:
361
+            print
362
+## end def
363
+
364
+def writeToFile(lSettings, fileName):
365
+    """
366
+    Description: Writes supplied settings to the specified file.
367
+    Parameters: lSettings - a list object containing the settings
368
+                 fileName - the name of the output file
369
+    Returns: nothing
370
+    """
371
+    fout = open(fileName, 'w')
372
+    for item in lSettings:
373
+        fout.write('%s\n' % item)
374
+    fout.close()
375
+## end def
376
+
377
+def readFromFile(fileName):
378
+    """
379
+    Description: Reads settings from the specified file.
380
+    Parameters:  fileName - the name of the input file
381
+    Returns: a list object containing the settings
382
+    """
383
+    lSettings = []
384
+
385
+    fin = open(fileName, 'r')
386
+    for line in fin:
387
+        item = line.strip() # remove new line characters
388
+        lSettings.append(item)
389
+    fin.close()
390
+    return lSettings
391
+## end def
392
+
393
+def setIOFile(option, commandLineFile):
394
+    """
395
+    Description: Provides a connector between the command line and the
396
+                 interactive interface.  Commands included as arguments
397
+                 on the command line determine default file names, as
398
+                 well as the command executed by doUserCommand above. 
399
+    Parameters: option - the command supplied by the command line
400
+                         argument interpreter getCLarguments.
401
+                commandLineFile - the name of the file supplied by the
402
+                          -f command line argument (if supplied). 
403
+    Returns: nothing
404
+    """
405
+    global menuBackupFile, memoryBackupFile, commandLineOption
406
+
407
+    commandLineOption = option
408
+    if commandLineFile == '':
409
+        return
410
+    if (option == 'bu' or option == 'ru'):
411
+        menuBackupFile = commandLineFile # set menu backup file name
412
+    elif (option == 'bm' or option == 'rm'):
413
+        memoryBackupFile = commandLineFile # set memory backup file name
414
+## end def
415
+
416
+def printMenuSplash():
417
+    """
418
+    Description: Prints an menu of available commands for use in
419
+                 interactive mode.
420
+    Parameters:  none
421
+    Returns: nothing
422
+    """
423
+    splash = \
424
+"""
425
+Enter menu item number.
426
+m - show this menu
427
+bm - backup memory to file
428
+rm - restore memory from file
429
+bu - backup menu to file
430
+ru - restore menu from file
431
+p - enter passthrough mode
432
+v - toggle verbose mode
433
+x - exit this program
434
+"""
435
+    print splash
436
+## end def
437
+
438
+def getCLarguments():
439
+    """ Description: gets command line arguments and configures this program
440
+                     to run accordingly.  See the variable 'usage', below,
441
+                     for possible arguments that may be used on the command
442
+                     line.
443
+        Parameters: none
444
+        Returns: nothing
445
+    """
446
+    index = 1
447
+    fileName = ''
448
+    backupOption = ''
449
+
450
+    # Define a splash to help the user enter command line arguments.
451
+    usage =  "Usage: %s [-v] [OPTION] [-f file]\n"  \
452
+             "  -b: backup memory\n"                \
453
+             "  -r: restore memory\n"               \
454
+             "  -m: backup menu\n"                  \
455
+             "  -s: restore menu\n"                 \
456
+             "  -f: backup/restore file name\n"     \
457
+             "  -v: verbose mode\n"                 \
458
+             % sys.argv[0].split('/')[-1]
459
+
460
+    # Process all command line arguments until done.  Note that the last
461
+    # dash b, m, r, or s argument encounterd on the command line will be
462
+    # the one actually execute; any previous instances will be ignored.
463
+    while index < len(sys.argv):
464
+        if sys.argv[index] == '-f': # Backup file provided.
465
+            # Get the backup file name.
466
+            if len(sys.argv) < index + 2:
467
+                print "-f option requires file name"
468
+                exit(1);
469
+            fileName = sys.argv[index + 1]
470
+            index += 1
471
+        elif sys.argv[index] == '-b': # backup memory
472
+            backupOption = 'bm' 
473
+        elif sys.argv[index] == '-r': # restore memory
474
+            backupOption = 'rm'
475
+        elif sys.argv[index] == '-m': # backup menu
476
+            backupOption = 'bu'
477
+        elif sys.argv[index] == '-s': # restore menu
478
+            backupOption = 'ru'
479
+        elif sys.argv[index] == '-v': # set verbose mode 'ON'
480
+            ft991.verbose = True
481
+        elif sys.argv[index] == '-d': # set debug mode 'ON'
482
+            ft991.debug = True
483
+        else:
484
+            print usage
485
+            exit(-1)
486
+        index += 1
487
+    ## end while
488
+
489
+    # Set backup file name and backup command to execute.
490
+    setIOFile(backupOption, fileName)
491
+##end def
492
+
493
+def main():
494
+    """
495
+    Description: Opens a com port connection to the FT991. Processes any
496
+                 supplied command line options.  If no command line options
497
+                 provides, then enters interactive mode.
498
+    Parameters: none
499
+    Returns: nothing
500
+    """
501
+    getCLarguments() # get command line options
502
+    
503
+    ft991.begin() # open com port session to FT991
504
+
505
+    # Process command line options (if any).
506
+    if commandLineOption != '':
507
+       doUserCommand()
508
+       return
509
+
510
+    # Else enter user interactive mode.
511
+    printMenuSplash()
512
+    while(1):
513
+        doUserCommand()
514
+## end def
515
+
516
+if __name__ == '__main__':
517
+    main()