#!/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.
"""
# 28 Apr 2006
1.0 Initial release
# 02 May 2006
1.1 Support delays in transmit list
# 03 May 2006
1.2 Handle Tira-2 responses (to transmit command)
# with interspersed 6 bytes data
# 03 May 2006
1.3 Retry button capture if remote too close to Tira-2
# 29 Mar 2007
1.4 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.
# 16 Jul 2007
1.5 Increase time to wait for data to finish comming from the Tira
# 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.
# 12 Aug 2008 1.6 Support from Jake Luck for older Tira 1.x devices.
# Notes:
# Has only been tested on linux at present (with python 2.3, 2.4 & 2.5).
#
# 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
# Maybe support repeats (*num) in transmit list?
##################################################################
import sys,os,time,tty
def get_options():
global verbose,configs,remote,port,transmit,capture
verbose=False
configs="."
remote=None
port="/dev/ttyUSB0"
transmit=None
capture=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
print " [--port=%s]" % port
print " [--configs=%s]" % configs
import getopt
try:
lOpts, lArgs = getopt.getopt(sys.argv[1:], "",
["help","verbose","port=","capture","transmit=","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] == "--remote":
remote=opt[1]
elif opt[0] == "--configs":
configs=opt[1]
if configs[-1]=='/':
configs=configs[:-1]
if (not capture and not transmit) or (capture and transmit) or not remote:
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")
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] = mode[tty.CFLAG] & ~tty.CSTOPB #NB: 1 stop bit
mode[tty.CFLAG] = mode[tty.CFLAG] | tty.CLOCAL #Don't worry about DCD
mode[tty.CFLAG] = mode[tty.CFLAG] | tty.CREAD #Enable the receiver
#Note baud automatically setup (and fixed) by the linux ftdi_sio module
tty.tcsetattr(tira,tty.TCSAFLUSH,mode)
#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")
verson = 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 successfully retransmit the captured code"
return None
##################################################################
handle_ctrl_c()
get_options()
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