1 | 1 |
deleted file mode 100755 |
... | ... |
@@ -1,442 +0,0 @@ |
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() |
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() |