Like Mitch Altman, the inventor of the TV-B-Gone, I find it too easy to waste time with a television and so I don't own one. For this reason I think the Telly Terminator was an apt gift and it works well. Also, having recently taken up a bit of a hobby in electronics, the Telly Terminator provided me with an excellent opportunity to "teardown" a very simple electronic toy. I wanted to know what it was doing and find out if its creators had simply ripped the TV-B-Gone data.
Unfortunately Helios do not currently make the datasheet for their H5A02HP chip generally avaliable and did not respond to an email I sent them asking for it. For this reason I was unable to probe the chip in as much detail as I would have liked. All that I could get my hands on was the following block diagram:
I used my USBee SX to probe the chip in standard fashion by connecting its various digital lines to the pins of the H5A02HP chip, triggering off several different events (including the state of the IR LED). According to my USBee SX, only pins 1, 10 and 16 change their state. Pin 10 sends out the on/off signals and carries the data I am most interested in. Pin 1 is grounded when the push switch is pressed and pin 16 sends a signal at ~2Hz which is responsible for causing the small red LED to flash (oddly though this signal is not completely regular). I would really like to investigate using an oscilloscope but unfortunately I do not own one (yet).
It is pretty easy to decode the above data for my laptop remote by hand just because there is so little of it. However I also wanted to decode the signals sent by the Telly Terminator a little so I wrote a quick python script to help analyse the data a little. The script reads the sampled data generated by the USBee, separates out the 46 different on/off signals and in each case calculates the carrier frequency, the total signal length, the carrier duty cycle and the timing data for the pulses of carrier signal in each case. For example, the script generates the following using the data from my laptop remote control.
Signal 0 : 36.17kHz, 36.921ms, 35% On time (periods) Off time (periods) 2.654ms (96.0) 0.885ms (32.0) 0.442ms (16.0) 0.443ms (16.0) 0.442ms (16.0) 0.443ms (16.0) 0.442ms (16.0) 0.889ms (32.1) 0.443ms (16.0) 0.881ms (31.9) 1.327ms (48.0) 0.889ms (32.1) 0.443ms (16.0) 0.450ms (16.3) 0.443ms (16.0) 0.450ms (16.3) 0.442ms (16.0) 0.451ms (16.3) 0.442ms (16.0) 0.451ms (16.3) 0.442ms (16.0) 0.451ms (16.3) 0.442ms (16.0) 0.451ms (16.3) 0.442ms (16.0) 0.450ms (16.3) 0.443ms (16.0) 0.450ms (16.3) 0.443ms (16.0) 0.450ms (16.3) 0.885ms (32.0) 0.893ms (32.3) 0.443ms (16.0) 0.450ms (16.3) 0.443ms (16.0) 0.450ms (16.3) 0.885ms (32.0) 0.893ms (32.3) 0.442ms (16.0) 0.451ms (16.3) 0.442ms (16.0) 0.451ms (16.3) 0.442ms (16.0) 0.451ms (16.3) 0.442ms (16.0) 0.451ms (16.3) 0.885ms (32.0) 0.892ms (32.3) 0.443ms (16.0) 0.450ms (16.3) 0.443ms (16.0) 0.450ms (16.3) 0.443ms (16.0) 0.450ms (16.3) 0.443ms (16.0) 0.450ms (16.3) 0.443ms (16.0) 0.450ms (16.3) 0.885ms (32.0) 0.446ms (16.1) 0.443ms (16.0) 0.889ms (32.1) 0.442ms (16.0) 0.451ms (16.3) 0.443ms (16.0)The script has thus estimated the carrier frequency as 36.17kHz, the total signal length as 36.921ms and the duty cycle of the carrier as 35%. The script has also calculated that the first burst of carrier marking lasts for 2.654ms which is 96.0 periods of carrier frequency, this is followed by 0.885ms of space which is 32.0 periods of carrier frequency and so on.
import sys, csv
SPACE = 0
MARK = 1
class StreamWithPush:
def __init__(self, data):
self.data = data
self.buffer = []
def __iter__(self):
return self
def next(self):
if len(self.buffer) == 0:
return self.data.next()
else:
x = self.buffer[-1]
del self.buffer[-1]
return x
def push(self, x):
self.buffer.append(x)
def push_l(self, l):
self.buffer.extend(l)
class Signal:
def __init__(self, frequency, duty_cycle, data):
self.frequency = frequency
self.duty_cycle = duty_cycle
self.data = data
def __str__(self):
s = 'On time (periods)\tOff time (periods)\n'
tm = 0
assert len(self.data) % 2 == 1, 'Uh oh.'
for i in range(len(self.data) / 2):
s += '%.3fms (%.1f)\t\t%.3fms (%.1f)\n' % (self.data[2*i][1] * 1000, self.data[2*i][1] * self.frequency,
self.data[2*i+1][1] * 1000, self.data[2*i+1][1] * self.frequency)
tm += self.data[2*i][1] + self.data[2*i+1][1]
s += '%.3fms (%.1f)' % (self.data[-1][1] * 1000, self.data[-1][1] * self.frequency)
return ('%.2fkHz, %.3fms, %d%%\n' % (self.frequency * 0.001, tm * 1000, int(self.duty_cycle * 100))) + s
def get_freq_dcycle(mark_data, sample_tm_ns):
# TODO Check mark_data is consistent, i.e. all same frequency and duty cycle (or even same shape).
tot = on = 0
for (h, l) in mark_data:
tot += h + l
on += h
return (len(mark_data) / (tot * sample_tm_ns * 1e-9), float(on) / tot)
def get_signals(pattern, sample_tm_ns):
NEW_SIG_TM = 0.38 # I measure a gap of ~391ms between signals. TODO learn this from the data.
signals = []
i_pattern = iter(pattern)
done = False
while not done:
signal_data = []
mark_data = []
l_mark = 0
while True:
try:
packet = i_pattern.next()
except StopIteration:
signal_data.append((MARK, l_mark * sample_tm_ns * 1e-9))
done = True
break
if packet[0] == SPACE:
signal_data.append((MARK, l_mark * sample_tm_ns * 1e-9))
l_mark = 0
if packet[1] * sample_tm_ns * 1e-9 >= NEW_SIG_TM: # End of this signal.
break
else:
signal_data.append((SPACE, packet[1] * sample_tm_ns * 1e-9))
else: # packet[0] == MARK
l_mark += packet[1] + packet[2]
mark_data.append((packet[1], packet[2]))
(frequency, duty_cycle) = get_freq_dcycle(mark_data, sample_tm_ns)
signals.append(Signal(frequency, duty_cycle, signal_data))
return signals
def markate(csv_data, channel):
pattern = []
marking = False
l_not_marking = l_last_period = 0
slippage = 15 # samples.
sample_tm_ns = 333.333333333 # nanoseconds between samples. TODO learn this from data and verify all samples consistent.
for row in csv_data:
(tm, v) = (int(''.join(row[0].split('.'))), row[channel])
if not marking:
if v == '0':
l_not_marking += 1
else:
marking = True
pattern.append((SPACE, l_not_marking))
l_not_marking = l_last_period = 0
csv_data.push(row)
else: # marking == True
if v == '1':
for (n_ones, row) in enumerate(csv_data):
if row[channel] != '1':
break
n_ones += 1
buffer = []
for (n_zeros, row) in enumerate(csv_data):
if row[channel] != '0':
csv_data.push(row)
break
if l_last_period > 0:
d = n_ones + n_zeros + 2 - l_last_period
if d > 0:
buffer.append(row)
if d > slippage:
csv_data.push_l(buffer)
n_zeros -= slippage
marking = False
break
n_zeros += 1
pattern.append((MARK, n_ones, n_zeros))
l_last_period = n_ones + n_zeros
else: # v == '0'
sys.stderr.write('Uh oh\n') # Shouldn't happen and causes us to lose data.
marking = False
csv_data.push(row)
return (sample_tm_ns, pattern[1:]) # Exclude first element which is just ('SPACE', 0).
def main(argv=None):
if argv is None:
argv = sys.argv
if len(argv) != 3:
sys.stderr.write('Usage: %s \n' % argv[0])
sys.exit(1)
csv_data = StreamWithPush(csv.reader(open(argv[1])))
csv_data.next() # Field headings.
(sample_tm_ns, pattern) = markate(csv_data, int(argv[2]) + 1)
signals = get_signals(pattern, sample_tm_ns)
for (i, sig) in enumerate(signals):
print 'Signal %d : %s\n\n' % (i, sig)
if __name__ == '__main__':
sys.exit(main())
Finally I thought I'd also mention that USBee Suite can be downloaded for free and so I might as well make the session file with my Telly Terminator data and my laptop's remote control data available for download.