#!/usr/bin/python # vim:fileencoding=utf8 # Both python and vim understand the above encoding declaration # """ Control program for the Home Electronics Tira-2 IR transceiver Copyright © 2006,2007 Lincor Solutions. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. """ # 1.0, 28 Apr 2006, Initial release # 1.1, 02 May 2006, Support delays in transmit list # 1.2, 03 May 2006, Handle Tira-2 responses (to transmit command) # with interspersed 6 bytes data # 1.3, 03 May 2006, Retry button capture if remote too close to Tira-2 # 1.4, 29 Mar 2007, Retry button capture if get "remote code too complex" # as this was reported as being a transient error. # Don't prompt for button name again on transient errors. # Don't write a new config file if nothing was changed. # Check config file before checking Tira-2 communications. # 1.5, 16 Jul 2007, Increase time to wait for data to finish arriving # from 200ms to 340ms, as this was seen to be needed with the # Tira-2.4 (compared to the Tira-2.1 as originally tested). # Display a hexdump of received data when --verbose specified, # to aid debugging, and to help show erroneous repeats # (holding the remote button too long), to the user. # Warn user about the possibility of holding the remote button # too long, when invalid data is received. # 1.6, 12 Aug 2008, Support from Jake Luck for older Tira 1.x # 1.7, 16 Sep 2009, Add a --find option to report where devices are connected. # Notes: # Has only been tested on linux at present (with python 2.3 - 2.6). # # This script takes around 0.1s to be parsed (and transmit a single code), # with python 2.4.1 on a 1.7GHz pentium-m. # One could py_compile.compile() it to save another 0.065s. # Update: Python 2.5 startup is much faster and only takes 0.055s # to parse this script and transmit a single code # (without waiting for confirmation from the Tira-2). # # Details of the Tira-2 protocol implemented here, are at: # http://www.home-electro.com/download/Protocol2.pdf # TODO: # Implement locking for the serial port # Support serial versions of device as well as USB # Maybe support repeats (*num) in transmit list? ################################################################## import sys,os,time,tty def get_options(): global verbose,configs,remote,port,transmit,capture,find verbose=False configs="." remote=None port="/dev/ttyUSB0" transmit=None capture=False find=False def Usage(): print "Usage: %s OPTIONS" % os.path.split(sys.argv[0])[1] print print " [--help]" print " [--verbose]" print print " --remote=remote_name" print print " --capture" print " or" print " --transmit=button_name[:delay_in_ms][,button_name,...]" print " or" print " --find" print print " [--port=%s]" % port print " [--configs=%s]" % configs import getopt try: lOpts, lArgs = getopt.getopt(sys.argv[1:], "", ["help","verbose","port=","capture","transmit=", "find", "remote=","configs="]) if ("--help","") in lOpts: Usage() sys.exit(None) for opt in lOpts: if opt[0] == "--verbose": verbose=True elif opt[0] == "--port": port=opt[1] elif opt[0] == "--capture": capture=True elif opt[0] == "--transmit": transmit=opt[1] elif opt[0] == "--find": find=True elif opt[0] == "--remote": remote=opt[1] elif opt[0] == "--configs": configs=opt[1] if configs[-1]=='/': configs=configs[:-1] if (not capture and not transmit and not find) or \ (capture+(transmit!=None)+find > 1) or \ (not remote and (transmit!=None or capture)): Usage() sys.exit(1) except getopt.error, msg: print msg print Usage() sys.exit(2) def handle_ctrl_c(): def cli_exception(type, value, tb): if not issubclass(type, KeyboardInterrupt): sys.__excepthook__(type, value, tb) if sys.stdin.isatty(): sys.excepthook=cli_exception #http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/142812 FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)]) def hexdump(src, length=16): result=[] for i in xrange(0, len(src), length): s = src[i:i+length] hexa = ' '.join(["%02X"%ord(x) for x in s]) printable = s.translate(FILTER) result.append("%04X %-*s %s\n" % (i, length*3, hexa, printable)) return ''.join(result) def read_chunk(fd,num=0,timeout=0.5): """ num = number of bytes to wait for. 0 = don't care timeout = time in seconds to wait for data. 0 = don't care """ check_delay=20 #ms. Wait this long between data polls. TODO: Use VTIME wait_delay=340 #ms. Wait this long for data to finish (if num not specified) ret = "" num_left=num wait=0 start = time.time() while (not timeout) or (time.time()-start < timeout): try: ret += os.read(fd, num_left or 256) if num: if len(ret) == num: break num_left=num-len(ret) except OSError, e: if e.errno == 11: if len(ret): wait+=1 if wait > (wait_delay/check_delay): break time.sleep(check_delay/1000.0) if verbose and len(ret): print "------- rxdata -------" print hexdump(ret), print "----------------------" return ret def dump_txdata(txdata,c=False,name="txdata"): if c: print "unsigned char %s[]={" % name else: name_width = len(name)+2 dash_width = (23 - name_width) / 2 print "-"*dash_width + " " + name + " " + "-"*dash_width if c: format="0x%02X," else: format="%02X" print " ".join([ format % ord(x) for x in txdata[:4]]) for i in (0,1,2): for j in range(4+(i*8),4+((i+1)*8),2): print (format*2) % (ord(txdata[j]),ord(txdata[j+1])), print i=0 for x in txdata[28:]: print format % ord(x), i+=1 if i%8==0: print print if c: print "};" else: print "-"*23 def read_response(search_str): """When the Tira-2 is in "6 bytes" mode (it's default state) it will send a 6 byte representation of any received IR code it receives. Since this can happen around our response we need to search for our response in the noise""" TX_OK=False response="" retry=0 while retry < 2: if retry: read_size=0 #subsequent reads need to read everything else: read_size=len(search_str) #shortcut the common case response += read_chunk(tira,num=read_size) if response.find(search_str) != -1: TX_OK=True break retry+=1 #if retry and verbose: # print hexdump(response), return TX_OK def transmit_tira2(txdata): if not transmit: #For speed, assume not in timing mode if just transmitting os.write(tira,"IR") #Can't transmit in timing mode if read_chunk(tira,2) != "OK": print "Error changing to sixbytes mode" sys.exit(1) tty.tcflush(tira,tty.TCIFLUSH) #bin any pending data os.write(tira,txdata) #if transmit: return True #A dangerous optimization return read_response("OIX") # ripped from pyserial. This is linux specific, # see pyserial for OS X support. # Hmm can do this externally to match what kernel sets for USB like: # sudo setserial /dev/ttyS0 spd_cust baud_base "24000000"\ # divisor "240" closing_wait "0"\ # close_delay "0" low_latency def set_custom_baudrate(fd, baudrate): import fcntl, termios import array buf = array.array('i', [0] * 32) # get serial_struct fcntl.ioctl(fd, termios.TIOCGSERIAL, buf) print buf # set custom divisor and baud_base buf[6] = 240 buf[7] = 240 * 100000 #need root access to change this # update flags ASYNC_SPD_MASK = 0x1030 ASYNC_SPD_CUST = 0x0030 buf[4] &= ~ASYNC_SPD_MASK buf[4] |= ASYNC_SPD_CUST # set serial_struct try: res = fcntl.ioctl(fd, termios.TIOCSSERIAL, buf) except IOError: raise ValueError('Failed to set custom baud rate: %r' % baudrate) def open_tira2(): tira = os.open(port,os.O_RDWR|os.O_NONBLOCK|os.O_NOCTTY) tty.setraw(tira,tty.TCSAFLUSH) mode = tty.tcgetattr(tira) mode[tty.CFLAG] &= ~tty.CSTOPB #NB: 1 stop bit mode[tty.CFLAG] |= tty.CLOCAL #Don't worry about DCD mode[tty.CFLAG] |= tty.CREAD #Enable the receiver #Note baud & flow control are automatically setup (and fixed) by the linux #ftdi_sio module, i.e. this is redundant on the USB versions of the device mode[tty.CFLAG] |= tty.CRTSCTS mode[tty.ISPEED] = mode[tty.OSPEED] = tty.B38400 #select custom tty.tcsetattr(tira,tty.TCSAFLUSH,mode) #Note the following doesn't get the serial versions of #the device working, so just ignore for now # set_custom_baudrate(tira, 100000) #Note Receiver enabled so can now receive erroneous (pending) data #(for example if user is holding a remote button we will get "6 bytes" info) #The flushes above are done too early to ignore this. if not transmit: #Be more robust as speed not an issue os.write(tira,"IV") version = read_chunk(tira) if "Tira-2." not in version and "1.1" not in version: return 0 return tira def capture_tira2(button): while 1: os.write(tira,"IC\x00\x00") if not read_response("OIC"): print "Error entering timing mode" sys.exit(1) print "Hit '%s' remote button now" % button timing_data = read_chunk(tira,timeout=0) if timing_data[-1]=="\xB2" and timing_data[-4:-2]=="\x00\x00": print "Valid IR code captured" clock = ord(timing_data[-2]) timing_data = timing_data[:-4] timings=[] for i in range(0,len(timing_data),2): timings.append((ord(timing_data[i]),ord(timing_data[i+1]))) break else: print "Invalid IR code received." print "Try moving the remote to around 5cm from the Tira-2, and" print "ensure you are not holding the button too long." class counter: def __init__(self): self.dict = {} def add(self,item): count = self.dict.get(item,0) self.dict[item] = count + 1 def counts(self,descending=False): """Returns list of keys, sorted by values.""" result = zip(self.dict.values(),self.dict.keys()) result.sort() if descending: result.reverse() return result def replace_timings(old,new): for i in range(len(timings)): hi,lo = timings[i] if hi*256+lo == old: timings[i]=(new/256, new%256) merge_width=clock/16 #within a pulse period widths=counter() for hi,lo in timings: width = hi*256+lo widths.add(width) counts = widths.counts(descending=True) def merge(counts): for freq, val in counts: for freq2, val2 in counts: if (freq,val) == (freq2,val2): continue if val2 in range(val-merge_width,val+merge_width+1): replace_timings(val2,val) counts.remove((freq2,val2)) counts[counts.index((freq,val))]=(freq+freq2,val) return True return False while merge(counts): pass if len(counts) > 12: print "Error: The remote code was too complex!" return None counts.sort() counts.reverse() counts+=[(0,0)]*(12-len(counts)) counts = [ val for freq,val in counts ] txdata = "IX"+chr(clock)+chr(0x00) for val in counts: txdata += chr(val/256) txdata += chr(val%256) iter=0 byte=0 for hi,lo in timings: index = counts.index(hi*256+lo) if iter%2 == 0: byte = index<<4 else: txdata += chr(byte+index) iter+=1 else: if iter%2 == 0: txdata += chr(0xFF) else: txdata += chr(byte+0xF) if verbose: dump_txdata(txdata) #Retransmit so that the Tira-2 can verify things if transmit_tira2(txdata): print "The Tira-2 successfully retransmitted the captured code" return txdata else: print "Warning the Tira-2 did not retransmit the captured code" return None ################################################################## handle_ctrl_c() get_options() if find: import glob ports = glob.glob("/dev/ttyUSB*")[::-1] + glob.glob("/dev/ttyS*")[::-1] for port in ports: try: if verbose: print "trying", port tira = open_tira2() if tira: print port sys.exit(0) except (tty.error, OSError): pass sys.exit(1) config_filename = "%s/%s.tira2" % (configs,remote) if os.path.isfile(config_filename): config = eval(open(config_filename).read()) else: config={} if transmit: print "Error: %s does not exist" % config_filename sys.exit(1) tira = open_tira2() if tira: if verbose: print "Found Tira-2" else: print "Error communicating with Tira-2" sys.exit(1) if transmit: delay=0 for button in transmit.split(','): if delay: #from previous button time.sleep(delay/1000.0) if ':' in button: button,delay=button.split(':') delay=int(delay) else: delay=0 if not config.has_key(button): print "Error: missing definition for %s" % repr(button) sys.exit(1) else: if verbose: dump_txdata(config[button],button) if transmit_tira2(config[button]): if verbose: print "Successfully transmitted %s" % repr(button) else: print "Error transmitting %s" % repr(button) sys.exit(1) else: def get_button_name(): button = raw_input("Enter button name (blank to end): ") for char in button: if char in "\"' ,:*": #some restrictions to future proof print "Sorry the %s character is not allowed" % repr(char) button=None break return button newdata=False while 1: button = get_button_name() if button==None: continue #invalid char if button=="": break #user finished txdata=None while not txdata: #loop over transient errors txdata = capture_tira2(button) olddata=config.get(button) if olddata != txdata: config[button]=txdata newdata=True if not newdata: sys.exit(0) config_text="# vim:nowrap syntax=python\n{\n" buttons=config.keys() buttons.sort() for button in buttons: spacing=" " * (16-(len(repr(button))+1)) config_text += "%s:%s%s,\n" % (repr(button),spacing, repr(config[button])) config_text+="}\n" open(config_filename, 'w').write(config_text) print "Sucessfully updated %s" % config_filename