When my TV remote went out recently I replaced it with a universal remote that would also operate the sound bar. This worked well but there was no option for the sound bar power.
I decided to build a simple infra red transceiver that would operate from the TV power signal and relay the power signal to the sound bar. I know there are a few libraries out there for IR RX and TX but I wanted to code my own methods.
The first thing I did was measure the infra red signals with a logic analyzer and found that the TV was NEC protocol and the sound bar was Philips RC6, I didn't know this by just looking at them I had to do a little research.
Starting with the receiver here is a link to the NEC protocol https://www.sbprojects.net/knowledge/ir/nec.php
You have to bear in mind that the IR receiver pin is held high and produces a signal that is inverse to the transmitter. Basically there are two start pulses, the first (at the receiver) is 9 milliseconds low followed by 4.5 milliseconds high. These two bits are then followed by the 32 bit IR code (plus a stop bit) which are made up of two bytes for the address and two bytes for the data. Each bit of the IR code is made up with a low pulse of ~560 uS and a high pulse of ~560 uS for a 0 bit and for a 1 bit it starts with a low pulse of ~560 uS and a high pulse of ~1680 uS. The stop bit is ~560 uS low. So although we have 32 bits we actually have 64 timed values, 32 low and 32 high not forgetting the stop bit which makes 65 timed values.
I put the controller in a box and sat it on a credenza we have below the TV. It looks ok from a distance.
The logic analyzer picture represents the four bytes 0x2 0x7D 0x46 0xB9 (which is 0x46 xor'd)
This is the micropython NEC receiver code I made, it's not strictly protocol but it will display quite a bit of information about most NEC remote codes.
import esp32
import machine
from machine import Pin,Timer
import time
ir_led=Pin(15,Pin.IN,Pin.PULL_UP)
start_bits=[0,0]
byte_list=[]
bit_list=[]
time_list=[]
@micropython.viper
def start_burst(x:int)->bool:
try:
if not ir_led.value():
x=int(time.ticks_us())
while not ir_led.value():pass
delta=time.ticks_diff(int(time.ticks_us()) , x)
if 8000 <= int(delta) <=10000: # 9 mS lead bit low
start_bits[0]=delta
x=int(time.ticks_us())
while ir_led.value():pass
delta=time.ticks_diff(int(time.ticks_us()) , x)
if 4000 <= int(delta) <=5000: # 4.5 mS lead bit high
start_bits[1]=delta
return True
except:
return False
@micropython.viper
def code_bits(x:int):
for _ in range(65):
if not ir_led.value():
x=int(time.ticks_us())
while not ir_led.value():pass
delta=time.ticks_diff(int(time.ticks_us()) , x)
if int(delta) <400:
time_list.clear()
break
if 400 <= int(delta) <=650:
time_list.append(int(delta))
elif ir_led.value():
x=int(time.ticks_us())
while ir_led.value():pass
delta=time.ticks_diff(int(time.ticks_us()) , x)
if 400 <= int(delta) <=650:
time_list.append(int(delta))
bit_list.append(0)
elif 1400 <= int(delta) <=1650:
time_list.append(int(delta))
bit_list.append(1)
def parse_bytes():
if not (len(bit_list) ==32):
return False
for i in range(0,32,8):
ir_bits=bit_list[i:i+8]
ir_byte=0
for j in range(8):
mask= (1 & ir_bits[j] ) << j
ir_byte = ir_byte | mask
byte_list.append(hex(ir_byte))
return True
while True:
if start_burst(0):
code_bits(0)
if parse_bytes():
print("\nStart Bits uS:"," Low bit :",start_bits[0]," High bit :",start_bits[1],'\n')
print("Bit List:",bit_list,'\n')
print("Time List uS:",time_list,'\n')
print("Number of time values :",len(time_list),'\n')
print("Byte List:"," Address : ",byte_list[0],byte_list[1]," Data :",byte_list[2],byte_list[3])
start_bits=[0,0]
time_list.clear()
bit_list.clear()
byte_list.clear()
This is the basis of the RC6 transmitter code with 36 KHz carrier, just a simple power command.
import esp32
import machine
from machine import Pin,Timer
import time
rmt_tx = esp32.RMT(4, pin=Pin(23), clock_div=8, idle_level=0, tx_carrier=(36000, 50, 1))
def soundbar_power(): #RC6_M0
t1=4440
t2=8950
t3=26850
data=(1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1)
duration=(t3,t2,t1,t2,t1,t1,t1,t1,t1,t2,t2,t1,t1,t1,t1,t1,t2,t2,t2,t2,t2,t2,t1,t1,t1,t1,t1,t1,t2,t1,t1,t2,t1,t1,t1)
rmt_tx.write_pulses(duration, data)
time.sleep(0.85)
Nice.
It's sad that they can't both use the same protocol.
TY @granz, it seems that most remotes use the NEC protocol, I have all kind of remotes for led lighting, sound devices video devices, TV's etc. and the sound bar is the only one I could find that was not NEC.
The second code snippet is interesting it uses the Remote Control Transceiver (RMT) library but I don't think the receive part of it works in micropython.
This function can be used for other ir protocols. The first line initializes a RMT object with things like output pin and carrier frequency, then inside the function we set up the time base (t1) which for the RC6 protocol is 444 uS then t2 which is approximately t1 * 2 and t3 which is approximately t1 * 6.
The data is a series of 1's and 0's that represent high or low pulses and the duration matches each high or low with a time in the range t1 - t3. To get these values I copied them straight from a logic analyzer trace. We then send these values to the IR transmitter pin (pin 23).
Quote from: Jeff_T on Jun 19, 2025, 04:50 PMTY @granz, it seems that most remotes use the NEC protocol, I have all kind of remotes for led lighting, sound devices video devices, TV's etc. and the sound bar is the only one I could find that was not NEC.
The PicoMite has the Sony protocol built-in to the language. Other protocols can be used, but they would require bit-banging the protocol.
As you mentioned, the Python Remote Control Transceiver (RMT) library is more versatile in that it is supposed to be able to use any protocol. However that means more complexity. I took a look at the RMT library, but nothing jumped out at me. Good luck with this, and let us know what fixes it.