# Notes: # ------ # # This class creates an instance of a Ten-Tec RX-320 radio with methods # for tunning. The specific goal is to be able to use a database which # is compatible with that of the 'xclass' RX-320 graphical tunning # software for Linux which is in turn, I guess, compatible with that # developed by 'Clifton Turner'. This is the basis for the input formats # accepted as legal by this class. # # Reference documentation I used: # - Ten-Tec's 'rx320prg.pdf' Programmer's Reference Guide. # - Hector Peraza's RX320 control program ( http://xclass.sourceforge.net ) # - A. Maitland Bottoms rx320 control program. # # Needed extra libraries: # - 'pyserial' ( http://pyserial.sourceforge.net/ ) # # License: # My intent is for this code to be under the BSD style license. # Google for details, but in essence; do whatever you want with it. # Of course I accept no responsibility for broken radio's ;) # !!!Use at own risk!!! # # Development Platform: # - FreeBSD 5.3-STABLE #0: Sat Jan 8 12:53:01 PST 2005 # - Python 2.4 # # Version: # I'll call this # - 'rx320_tune 0.9.0' (Not fully tested.) # - Sat Jan 15 01:24:45 PST 2005 # # Example of use: # -------------- # >>> import rx320_tune # || # >>> rad_obj = rx320_tune.Rx320() # vv set two params # >>> rad_obj.update_state({'bwf':'5100','svol':'35'}) # [0, []] # return good. # >>> rad_obj.burn() # send sig to radio # >>> rad_obj.update_state({'bwf':'5200'}) # set one parameter # [1, [["'bwf' out of range."]]] # return bad! # # Look at the "Various Checks" functions to see what parameters # are available to set, and what the legal ranges are. # # # Oh ya... # I'm Tom Huppi # Please let me know if you use|find_useful|fix|extend|etc this code. # class Rx320: "Object for state and manipulation of the RX-320 radio." # CHECK ME / ADJUST ME! # ||||||||||||||||||||| # vvvvvvvvvvvvvvvvvvvvv PORT = '/dev/cuaa0' # FreeBSD style. Linux similar; ttyS0 I think. # PORT = 'COM1' # Would be legal for Windows (I think.) # ^^^^^^^^^^^^^^^^^^^^^ # ||||||||||||||||||||| # imports: # -------- import serial import string from sys import exit from binascii import hexlify, unhexlify from time import sleep # Constructor and Destructors: # ---------------------------- def __init__(self): """Constructor deals with locks and sets sane state.""" # Set sane initial values self.last_set_status = [-1,[]] self.cur_state = {\ 'freq':'.810000', \ 'mode':'AM', \ 'bwf':'5100', \ 'pbt':'-1800', \ 'svol':'20', \ 'lvol':'40', \ 'agc':'3' \ } self.set_state = {'freq':'NULL','mode':'NULL','bwf':'NULL','pbt':'NULL',\ 'svol':'NULL','lvol':'NULL','agc': 'NULL'} # mapping structures used by code... self.bwf_dict = {'6000':'0','5700':'1','5400':'2','5100':'3','4800':'4',\ '4500':'5','4200':'6','3900':'7','3600':'8','3300':'9',\ '3000':'10','2850':'11','2700':'12','2550':'13',\ '2400':'14','2250':'15','2100':'16','1950':'17',\ '1800':'18','1650':'19','1500':'20','1350':'21',\ '1200':'22','1050':'23','900':'24','750':'25','675':'26',\ '600':'27','525':'28','450':'29','375':'30','330':'31',\ '300':'32','8000':'33'} self.agc_dict = {'1':'slow', '2':'med', '3':'fast'} self.mode_dict ={'AM':'0', 'USB':'1', 'LSB':'2', 'CW':'3'} self.m_cor_dict = {'AM':0, 'USB':1, 'LSB':-1, 'CW':-1} # TODO: Implement some sort of locking? # Instantiate a serial port opject now... # TODO: If at some point it becomes desirable to make this # utility more portable, accept PORT as a parameter # upon instanciation. self.ser = self.serial.Serial(self.PORT, 1200, timeout=1) def __del__(self): """Destructor deals with locks and closes serial port.""" self.ser.close() # non-interesting methods... # ----------------------- def show_cur_state(self): """Returns current state dictionary.""" return self.cur_state def show_set_state(self): """Returns set state dictionary.""" return self.set_state # Various Checks implemented as functions... # ------------------------------------------ # # Return Paradigm: [int, [["err msg 1"], ["err msg 2"],...]] # # The idea is to try to capture as many errors as possible # and hand them up-hill in one fell swoop. # def check_freq(self, freq_val): # for 'freq' """Check a value as valid frequency (freq).""" ex = [0, []] if not type(freq_val) is str: ex[0] = 1 ex[1].append(["'freq' is not a string."]) try: fr = float(freq_val) except: ex[0] = 1 ex[1].append(["'freq' not reasonable string."]) else: if fr < 0 or fr > 30.000000: ex[0] = 1 ex[1].append(["'freq' out of range."]) return ex def check_bwf(self, bwf_val): # for 'bwf' """Check a value as valid bandwidth filter (bwf).""" ex = [0, []] if not type(bwf_val) is str: ex[0] = 1 ex[1].append(["'bwf' is not a string."]) try: bw = int(bwf_val) except: ex[0] = 1 ex[1].append(["'bwf' not reasonable string."]) else: if not bwf_val in self.bwf_dict.keys(): ex[0] = 1 ex[1].append(["'bwf' out of range."]) return ex def check_pbt(self, pbt_val): # for 'pbt' """Check a value as valid pass band tunner (pbt).""" ex = [0, []] if not type(pbt_val) is str: ex[0] = 1 ex[1].append(["'pbt' is not a string."]) try: pb = int(pbt_val) except: ex[0] = 1 ex[1].append(["'pbt' not reasonable string."]) else: if (pb < -2000) or (pb > 2000) or ((pb % 100) != 0): ex[0] = 1 ex[1].append(["'pbt' out of range."]) return ex def check_mode(self, mode_val): # for 'mode' """Check a value as valid mode (AM,USB,etc) (mode).""" ex = [0, []] if not type(mode_val) is str: ex[0] = 1 ex[1].append(["'mode' is not a string."]) if not mode_val in self.mode_dict.keys(): ex[0] = 1 ex[1].append(["'mode' invalid (case?)."]) return ex def check_agc(self, agc_val): # for 'agc' """Check a value as valid agc (AM,USB,etc) (mode).""" ex = [0, []] if not type(agc_val) is str: ex[0] = 1 ex[1].append(["'agc' is not a string."]) if not agc_val in self.agc_dict.keys(): ex[0] = 1 ex[1].append(["'agc' invalid."]) return ex def check_svol(self, svol_val): # for 'svol' """Check a value as valid speaker volume (svol).""" ex = [0, []] if not type(svol_val) is str: ex[0] = 1 ex[1].append(["'svol' is not a string."]) try: fr = int(svol_val) except: ex[0] = 1 ex[1].append(["'svol' not reasonable string."]) else: if fr < 0 or fr > 63: ex[0] = 1 ex[1].append(["'svol' out of range."]) return ex def check_lvol(self, lvol_val): # for 'lvol' """Check a value as valid line volume (lvol).""" ex = [0, []] if not type(lvol_val) is str: ex[0] = 1 ex[1].append(["'lvol' is not a string."]) try: fr = int(lvol_val) except: ex[0] = 1 ex[1].append(["'lvol' not reasonable string."]) else: if fr < 0 or fr > 63: ex[0] = 1 ex[1].append(["'lvol' out of range."]) return ex # Update State: Takes a dict of updates and returns an error code ds. # ------------- def update_state(self, state_up): """Accept, check, and update current state.""" ex = [0,[]] # Check all input values and collect all errors... if not type(state_up) is dict: return [1, [["input not valid (not a dictionary)"]]] for item in state_up.keys(): if item == 'freq': ex = self.blend_ex(ex,self.check_freq(state_up[item])) elif item == 'mode': ex = self.blend_ex(ex,self.check_mode(state_up[item])) elif item == 'bwf': ex = self.blend_ex(ex,self.check_bwf(state_up[item])) elif item == 'pbt': ex = self.blend_ex(ex,self.check_pbt(state_up[item])) elif item == 'svol': ex = self.blend_ex(ex,self.check_svol(state_up[item])) elif item == 'lvol': ex = self.blend_ex(ex,self.check_lvol(state_up[item])) elif item == 'agc': ex = self.blend_ex(ex,self.check_agc(state_up[item])) else: ex = self.blend_ex(ex,[1,[["Unrecognized param: " + item]]]) # No Errors? Set cur_state values... if ex[0] == 0: for item in state_up.keys(): self.cur_state[item] = state_up[item] # Return exit status ds always... return ex # 'form_*' functions use self.cur_state so they don't require input. # ------------------ def form_svol(self): # int -> hex """ Form serial command string for speaker volume (svol)""" hex_svol = self.frontpad_hex(hex(int(self.cur_state['svol'])).split('0x')[1],2) return (self.unhexlify('5600' + hex_svol + '0d')) def form_lvol(self): # int -> hex """ Form serial command string for line volume (lvol)""" hex_lvol = self.frontpad_hex(hex(int(self.cur_state['lvol'])).split('0x')[1],2) return (self.unhexlify('4100' + hex_lvol + '0d')) def form_agc(self): # char of 1,2,3 -> hex """ Form serial command string for agc (agc)""" hex_agc = hex(ord(self.cur_state['agc'])).split('0x')[1] return (self.unhexlify('47' + hex_agc + '0d')) def form_mode(self): # char of 1,2,3,4 -> hex """ Form serial command string for mode (mode)""" hex_mode = hex(ord(self.mode_dict[self.cur_state['mode']])).split('0x')[1] return (self.unhexlify('4d' + hex_mode + '0d')) def form_bwf(self): # int -> hex """ Form serial command string for filter (bwf)""" hex_bwf = self.frontpad_hex(hex(int(self.bwf_dict[self.cur_state['bwf']])).split('0x')[1],2) return (self.unhexlify('57' + hex_bwf + '0d')) def form_freq(self): # math -> lots of hex """ Form serial command string for frequency (freq)""" # # Note: I really don't understand the tunning that well. # Mainly I tried to copy the code from the 'xclass RX320' # program which uses 'passband tunning offest' (pbt). # The code is here for reference: # # Fcor = ((float) _filter->bandwidth / 2.0) + 200.0; # AdjTfreq = freq - 1250 + (int) (Mcor * (Fcor + _pbt)); # Ctf = (int) AdjTfreq / 2500 + 18000; # Ftf = (int) ((float) (AdjTfreq % 2500) * 5.46); # Btf = (int) ((Fcor + (float) _pbt + Cbfo + 8000.0) * 2.73); # if self.cur_state['mode'] == '4': # i.e., 'CW' f_cor = 0.0 # The following was in RX320, (float) _cwo; # The database doesn't seem store this val, so for now I'm # zaping it. It shouldn't be hard to add it if CW becomes # interesting at some point in the future. c_bfo = 0.0 else: f_cor = self.string.atof(self.cur_state['bwf']) / 2.0 + 200.0 c_bfo = 0.0; m_cor = self.m_cor_dict[self.cur_state['mode']] c_freq = float(self.cur_state['freq']) * 1000000 # Hz now. c_pbt = int(self.cur_state['pbt']) adj_t_freq = c_freq - 1250 + int(m_cor * (f_cor + c_pbt)) c_tf = int(adj_t_freq / 2500 + 18000) # Where's the '* 2500'? It isn't in xclass RX320, and adding # it seems to produce huge values???... f_tf = int((adj_t_freq % 2500) * 5.46) b_tf = int((f_cor + float(c_pbt) + c_bfo + 8000.0) * 2.73) # print 'f_cor: ' + str(type(f_cor)) + '\t: ' + str(f_cor) # print 'c_bfo: ' + str(type(c_bfo)) + '\t: ' + str(c_bfo) # print 'm_cor: ' + str(type(m_cor)) + '\t: ' + str(m_cor) # print 'c_freq: ' + str(type(c_freq)) + '\t: ' + str(c_freq) # print 'c_pbt: ' + str(type(c_pbt)) + '\t: ' + str(c_pbt) # print 'adj_t_freq: ' + str(type(adj_t_freq)) + '\t: ' + str(adj_t_freq) # print 'c_tf: ' + str(type(c_tf)) + '\t: ' + str(c_tf) # print 'f_tf: ' + str(type(f_tf)) + '\t: ' + str(f_tf) # print 'b_tf: ' + str(type(b_tf)) + '\t: ' + str(b_tf) c_tf_hex = self.frontpad_hex(hex(c_tf).split('0x')[1],4) f_tf_hex = self.frontpad_hex(hex(f_tf).split('0x')[1],4) b_tf_hex = self.frontpad_hex(hex(b_tf).split('0x')[1],4) return (self.unhexlify('4e' + c_tf_hex + f_tf_hex + b_tf_hex + '0d')) # Burn function actually tunes rado. # ------------- # It uses self.cur_state, and the radio gives back no errors, so # the input and return values for this method are both void. # The basic flow is to build a stack of commands, then execute them. # def burn(self): "Write current state to radio." if self.cur_state == self.set_state: # Nothing to do! return outdated = [] burn_cmnds = [] for ikey in self.cur_state.keys(): if self.cur_state[ikey] != self.set_state[ikey]: outdated.append(ikey) if 'svol' in outdated: burn_cmnds.append(self.form_svol()) if 'lvol' in outdated: burn_cmnds.append(self.form_lvol()) if 'agc' in outdated: burn_cmnds.append(self.form_agc()) if 'mode' in outdated: burn_cmnds.append(self.form_mode()) if 'bwf' in outdated: burn_cmnds.append(self.form_bwf()) if ('mode' in outdated) or ('freq' in outdated)\ or ('pbt' in outdated) or ('bwf' in outdated): burn_cmnds.append(self.form_freq()) for cmnd in burn_cmnds: self.ser.write(cmnd) self.set_state = self.cur_state.copy() return # General Utilities: # ------------------ def blend_ex(self, cur_ex, new_ex): """Merge an existing exit ds and a new one.""" ret = cur_ex if new_ex[0] == 0: return cur_ex else: ret[0] = 1 for msg in new_ex[1][:]: ret[1].append(msg) return ret def frontpad_hex(self, hex_val, num_digits): """Make sure hex number is either 2 or 4 digits in length.""" if num_digits == 2: if len(hex_val) == 2: return (hex_val) elif len(hex_val) == 1: return ('0' + hex_val) else: print "WARNING: Unexpected hex value (probably _not_ in freq calcs)" print " rx320_burn probably poorly coded!" elif num_digits == 4: if len(hex_val) == 4: return (hex_val) elif len(hex_val) == 3: return ('0' + hex_val) elif len(hex_val) == 2: return ('00' + hex_val) elif len(hex_val) == 1: return ('000' + hex_val) else: print "WARNING: Unexpected hex value (probably in freq calcs)!" print " rx320_burn probably poorly coded!" else: self.sys.exit(1)