# # Module to parse and format the configuration files for Yaesu FTH-2008 # and similar handheld transceivers (FTH-2009, FTH-7008, FTH-7009). # # Lookup tables spacings = ( 5000, 6250, 10000, 12500 ) tx_time_outs = ( 30, 60, 120, 180 ) tx_to_delays = ( 0, 6, 20, 60 ) res_mon_types = ( 'toggle', 'momentary' ) pll_types = ( 'MC145156', 'MB87076' ) scan_speeds = ( 50, 150, 200, 250 ) en_dis = ( 'Enabled', 'Disabled' ) dis_en = ( 'Disabled', 'Enabled' ) off_on = ( 'Off', 'On' ) bclo_types = ( 'Off', 'Tone', 'Carrier', '???' ) ctcss_tones = ( '097.4', '091.5', '088.5', '085.4', '082.5', '079.7', '077.0', '074.4', '071.9', '067.0', '250.3', '241.8', '233.6', '225.7', '218.1', '210.7', '203.5', '192.8', '186.2', '179.9', '173.8', '167.9', '162.2', '156.7', '151.4', '146.2', '141.3', '136.5', '131.8', '127.3', '123.0', '118.8', '114.8', '110.9', '107.2', '103.5', '100.0', ' 94.8', ' 88.5', ' 82.5', ' 77.0', ' 71.9', ' 67.0' ) enhex = '0123456789ABCDEF' dehex = { } for x in range(0,len(enhex)): dehex[enhex[x]] = x def splitbits(pattern, byte): "Splits the bits of 'byte' according to the pattern, returning a tuple of integers." pattern = map(lambda x: ord(x) - ord('0'), pattern) result = [ ] for field in pattern: result.append( byte >> (8 - field) ) byte = ( byte << field ) & 0xFF return tuple(result) def packbits(pattern, *fields): "Packs the integers into a byte according to pattern (the inverse of splitbits()." if len(pattern) != len(fields): raise 'pattern and data not the same length (%d, %d)' % (len(pattern), len(fields)) result = 0 for (f, v) in map(None, pattern, fields): f = ord(f) - ord('0') if v < 0 or v >= ( 1 << f ): raise "field has value %d, doesn't fit in width %d" % (v, f) result = ( result << f ) | v return result def unpackbcd(buf): result = '' for b in buf: result = result + enhex[ (b & 0xF0) >> 4 ] + enhex[ b & 0x0F ] return result def packbcd(buf): digits = map(lambda x: ord(x) - ord('0'), buf) result = [ ] for d in range(0, len(digits), 2): v = digits[d] * 16 + digits[d+1] result.append(v) return result class FTH2008: """Holds the configuration data of an FTH-2008 radio except for the channel data. Can parse a savefile into values and can write out a savefile from its instance variables.""" magic = 0x5 pll = 0 prescaler = 64 spacing = 0 IF = '0000' local_offset_high = 0 powersave_interval = 30 scan_speed = 0 lcd_on = 0 reserved_1 = ( 0, 0 ) powersave_duty_cycle = 0 scan_disable = 0 scan_resume_type = 0 pri_key_disable = 0 pri_poweron_enabled = 0 pri_channel = 0 tx_time_out = 0 tx_time_out_delay = 0 tx_timer_enabled = 0 tx_carrier_delay_enabled = 0 res_mon_type = 0 beep_enabled = 0 ptt_scan_pause_only = 0 reserved_2 = 0 magic2 = 0x96 def parse_conf(self, buf): check_sum(buf) self.parse_header(buf) channels = [ ] for channum in range(0, 15): chan = Channel() chan.parse(buf[8 + (12*channum) : 8 + 12 + (12*channum)]) channels.append(chan) self.channels = tuple(channels) self.trailers = ( buf[8+(12*15): 0xC8], buf[0xC9 : ] ) def pack_conf(self): summed_part = self.pack_header() + \ ''.join(map(lambda x: x.pack(), self.channels)) + \ self.trailers[0] sum = compute_sum(summed_part) return summed_part + chr(sum) + self.trailers[1] def parse_header(self, buf): b = map(ord, buf[:8]) ( self.magic, self.pll, self.prescaler, self.spacing ) = splitbits('4112', b[0]) self.magic2 = b[7] if self.magic != 5: raise 'bad magic: byte 0, high nybble=%d, should be 5' % self.magic if self.magic2 != 0x96: print 'uncertain magic: byte 7 should be 0x96, is 0x%02x' % self.magic2 self.prescaler = ( 64, 128 )[self.prescaler] self.IF = '%04X' % ( b[1] * 256 + b[2] ) ( self.local_offset_high, self.powersave_interval, self.scan_speed, resv0, self.lcd_on, resv1 ) = splitbits('112112', b[3]) self.powersave_interval = ( 300, 30 )[self.powersave_interval] self.reserved_1 = ( resv0, resv1 ) ( self.powersave_duty_cycle, self.scan_disable, self.scan_resume_type, self.pri_key_disable, self.pri_poweron_enabled ) = splitbits('41111', b[4]) ( self.pri_channel, self.tx_time_out, self.tx_time_out_delay ) = splitbits('422', b[5]) ( self.tx_timer_enabled, self.tx_carrier_delay_enabled, self.res_mon_type, self.beep_enabled, self.ptt_scan_pause_only, reserved_2 ) = splitbits('111113', b[6]) def pack_header(self): b = [ 0, 0, 0, 0, 0, 0, 0, self.magic2 ] prescaler_packing = { 64: 0, 128: 1 } powersave_packing = { 30: 1, 300: 0 } b[0] = packbits('4112', self.magic, self.pll, prescaler_packing[self.prescaler], self.spacing) b[1] = packbits('44', dehex[self.IF[0]], dehex[self.IF[1]]) b[2] = packbits('44', dehex[self.IF[2]], dehex[self.IF[3]]) b[3] = packbits('112112', self.local_offset_high, powersave_packing[self.powersave_interval], self.scan_speed, self.reserved_1[0], self.lcd_on, self.reserved_1[1]) b[4] = packbits('41111', self.powersave_duty_cycle, self.scan_disable, self.scan_resume_type, self.pri_key_disable, self.pri_poweron_enabled) b[5] = packbits('422', self.pri_channel, self.tx_time_out, self.tx_time_out_delay) b[6] = packbits('111113', self.tx_timer_enabled, self.tx_carrier_delay_enabled, self.res_mon_type, self.beep_enabled, self.ptt_scan_pause_only, self.reserved_2) return ''.join(map(chr, b)) def display(self): self.display_header() for i in range(0, len(self.channels)): self.channels[i].display(i) print "Trailer 1: ", repr(self.trailers[0]) print "Trailer 2: ", repr(self.trailers[1]) def display_header(self): print 'PLL type: %s prescaler: 1:%d IF: %s0 kHz' % \ ( pll_types[self.pll], self.prescaler, self.IF ) print 'Local offset: %s' % (( 'Lower', 'Upper' )[self.local_offset_high] ) print 'Channel spacing: %d Hz' % ( spacings[self.spacing] ) print 'Power save: %dms awake, %dms asleep' % \ ( self.powersave_interval, self.powersave_duty_cycle * self.powersave_interval ) print 'Scan %s, speed: %dms' % ( en_dis[self.scan_disable], scan_speeds[self.scan_speed] ) print 'Continue scan after: ', ( ('5 seconds', 'Carrier loss')[self.scan_resume_type] ) print 'PRI key: %s PRI at poweron: %s Priority channel: %d' % \ ( en_dis[self.pri_key_disable], dis_en[self.pri_poweron_enabled], self.pri_channel ) print 'TX Timer: %s, timeout: %d sec, lockout time: %d sec' % \ ( dis_en[self.tx_timer_enabled], tx_time_outs[self.tx_time_out], tx_to_delays[self.tx_time_out_delay] ) print 'TX carrier delay:', dis_en[self.tx_carrier_delay_enabled] print 'RES/MON button is:', res_mon_types[self.res_mon_type] print 'LCD: %s Beep: %s ' % ( off_on[self.lcd_on], off_on[self.beep_enabled] ) class Channel: "The configuration of one channel of the radio." disabled = 0 tx_disabled = 0 bclo_type = 0 label = ' 0' tx_ctcss = (0, 0) rx_ctcss = (0, 0) skip = 0 powersave_enabled = 0 rx_kHz = 0 tx_kHz = 0 tx_timer_enabled = 0 tx_high_power = 0 reserved = ( 0, 0) def parse(self, buf): if len(buf) != 12: raise 'bad channel info length' b = map(ord, buf) (self.disabled, self.tx_disabled, self.skip, self.powersave_enabled, self.tx_timer_enabled, self.tx_high_power, self.bclo_type) = splitbits('1111112', b[0]) labelnybble = '0123456789 ' ( l0, l1 ) = splitbits('44', b[1]) self.label = labelnybble[l0] + labelnybble[l1] ena, val = splitbits('17', b[2]) self.rx_ctcss = ( ena, val - 20 ) self.rx_kHz = unpackbcd(b[3:6]) self.tx_kHz = unpackbcd(b[9:12]) ena, val = splitbits('17', b[8]) self.tx_ctcss = ( ena, val - 20 ) reserved = ( b[6], b[7] ) # end of parse() def pack(self): b0 = packbits('1111112', self.disabled, self.tx_disabled, self.skip, self.powersave_enabled, self.tx_timer_enabled, self.tx_high_power, self.bclo_type) tobcd = { } for k in range(0,10): tobcd['0123456789'[k]] = k tobcd[' '] = 0xF b1 = packbits('44', tobcd[self.label[0]], tobcd[self.label[1]]) b2 = packbits('116', self.rx_ctcss[0], 0, self.rx_ctcss[1] + 20) b8 = packbits('116', self.tx_ctcss[0], 0, self.tx_ctcss[1] + 20) rxFreq = packbcd(self.rx_kHz) txFreq = packbcd(self.tx_kHz) return ''.join(map(chr, (b0, b1, b2, rxFreq[0], rxFreq[1], rxFreq[2], self.reserved[0], self.reserved[1], b8, txFreq[0], txFreq[1], txFreq[2]))) # end of pack() def display(self, channum): f = ' %2d: Channel "%s" ' % (channum, self.label) if self.disabled: en = 'Disabled ' cantx, txbl, txbr = 0, '[', ']' elif self.tx_disabled: en = 'RX Only ' cantx, txbl, txbr = 0, '[', ']' else: en = 'TX/RX Enabled ' cantx, txbl, txbr = 1, '', '' if self.rx_kHz == self.tx_kHz: f = f + ('%s kHz ' % (self.rx_kHz,)) else: f = f + ('RX: %s kHz %sTX: %s kHz%s' % (self.rx_kHz, txbl, self.tx_kHz, txbr)) print f, en f = [ ] if self.skip: f.append('Skip on scan') if self.powersave_enabled: f.append('Powersave') if self.tx_timer_enabled: f.append(txbl + 'TX timer enabled' + txbr) if self.tx_high_power: f.append(txbl + 'TX high Power' + txbr) f.append('BCLO=' + bclo_types[self.bclo_type]) print ' ' + (', '.join(f)) def fmt_ctcss(cs): ena, num = cs if ena: return ctcss_tones[num] + 'Hz' else: return ' (off) ' if self.rx_ctcss[0] or self.tx_ctcss[0]: print ' CTCSS: RX=%s,%s %s TX=%s,%s %s' % ( off_on[self.rx_ctcss[0]], self.rx_ctcss[1], fmt_ctcss(self.rx_ctcss), off_on[self.tx_ctcss[0]], self.tx_ctcss[1], fmt_ctcss(self.tx_ctcss) ) # end of display() # end of class Channel def compute_sum(buf): return 0xFF & reduce(lambda sum, ch: sum + ord(ch), buf[:0xC8], 0) def check_sum(buf): computed = compute_sum(buf) stored = ord(buf[0xC8]) if computed != stored: raise "checksum mismatch, stored=0x%02x computed=0x%02x" % ( stored, computed )