Sunday, November 18, 2018

Controlling a Yamaha V-RX657 reciever from KODI

After fifteen years of use some of the buttons on the remote for my Yamaha V-RX657 began to work sporadicly. In fact, that happened years ago, even the learning remote have become reluctant in changing the volume. So I wanted a way to control it via KODI.

So I got myself some IR transistors and IR LEDs and breadboarded this circuit:

Image from learn.parallax.com


And while tracking the voltage at VA3 with a Salea logic analyser I got plots like these:



Zooming in on each peak revealed this pattern:



Combining this information and doing a duckduckgo revealed that this were the NEC1 protocol. Carrier is 38 kHz, there are 32 bits of data and a stop bit in each burst. Every burst starts with a 9 ms carrier on followed by 4.5 ms off. A pulse of 564 us followed by 564 us off is a "0". A pulse and 1692 us off is 1 one. And there is a last pulse indicating the end of the burst. Repeating commands, e.g. "Volume Up" can be done by a 40 ms delay followed by a 9 ms carrier on, 2.25 ms off and a stop bit. More repeats are done with a 96 ms delay instead of the original 40 ms delay.

MDCDR    01011110101000011001001101101100
DTVCBL   01011110101000010010101011010101
VOLUP    01011110101000010101100010100111
VOLDOWN  01011110101000011101100000100111
PWRON    01011110101000011011100001000111
PWROFF   01011110101000010111100010000111
TUNER    01011110101000010110100010010111
CD       01011110101000011010100001010111
MUTE     01011110101000010011100011000111
DVD      01011110101000011000001101111100
VAUX     01011110101000011010101001010101
VCR1     01011110101000011111000000001111
DVRVCR2  01011110101000011100100000110111

The PWRON, PWROFF and MUTE buttons did not work on my remote, but I found them on irdb.tk. They had all of the codes.

I then used some eevblog code to transmit this through  a IR LED. The repeats for the volume controls are enough to change the volume 3 dB.


http://www.learningaboutelectronics.com/Articles/LED-driver-circuit.php

#define IRLEDpin  2              //the arduino pin connected to IR LED to ground. HIGH=LED ON
#define BITtime   560            //length of the carrier bit in microseconds 562
//put your own code here - 4 bytes (ADDR1 | ADDR2 | COMMAND1 | COMMAND2)

unsigned long TVON    = 0b00100000110111110001000011101111;

unsigned long MDCDR   = 0b01011110101000011001001101101100;
unsigned long DTVCBL  = 0b01011110101000010010101011010101;
unsigned long VOLUP   = 0b01011110101000010101100010100111;
unsigned long VOLDOWN = 0b01011110101000011101100000100111;
unsigned long PWRON   = 0b01011110101000011011100001000111;
unsigned long PWROFF  = 0b01011110101000010111100010000111;
unsigned long TUNER   = 0b01011110101000010110100010010111;
unsigned long CD      = 0b01011110101000011010100001010111;
unsigned long MUTE    = 0b01011110101000010011100011000111;
unsigned long DVD     = 0b01011110101000011000001101111100;
unsigned long VAUX    = 0b01011110101000011010101001010101;
unsigned long VCR1    = 0b01011110101000011111000000001111;
unsigned long DVRVCR2 = 0b01011110101000011100100000110111;

String serial_command="";

void setup()
{
  Serial.begin(9600);
  Serial.setTimeout(1000);
  IRsetup();//Only need to call this once to setup
}

void IRsetup(void)
{
  pinMode(IRLEDpin, OUTPUT);
  digitalWrite(IRLEDpin, LOW);    //turn off IR LED to start
}

// Ouput the 38KHz carrier frequency for the required time in microseconds
// This is timing critial and just do-able on an Arduino using the standard I/O functions.
// If you are using interrupts, ensure they disabled for the duration.
void IRcarrier(unsigned int IRtimemicroseconds)
{
  for(int i=0; i < (IRtimemicroseconds / 26); i++)
    {
    digitalWrite(IRLEDpin, HIGH);   //turn on the IR LED
    //NOTE: digitalWrite takes about 3.5us to execute, so we need to factor that into the timing.
    delayMicroseconds(9);          //delay for 13us (9us + digitalWrite), half the carrier frequnecy
    digitalWrite(IRLEDpin, LOW);    //turn off the IR LED
    delayMicroseconds(9);          //delay for 13us (9us + digitalWrite), half the carrier frequnecy
    }
}

//Sends the IR code in 4 byte NEC format
void IRsendCode(unsigned long code)
{
  //send the leading pulse
  IRcarrier(9000);            //9ms of carrier
  delayMicroseconds(4500);    //4.5ms of silence

  //send the user defined 4 byte/32bit code
  for (int i=0; i<32; i++)
    {
    IRcarrier(BITtime);   //turn on the carrier for one bit time
    if (code & 0x80000000)  //get the current bit by masking all but the MSB
      delayMicroseconds(3 * BITtime); //a HIGH is 3 bit time periods
    else
      delayMicroseconds(BITtime);
     code<<=1;
    }
  IRcarrier(BITtime);  //STOP bit.
}

void loop()
{
 serial_command="";
 if (Serial.available()>0)
 {
  serial_command = Serial.readString();
  if (serial_command.indexOf("PWRON") == 0)
  {
    IRsendCode(PWRON);
    delay(500);
  }
  else if (serial_command.indexOf("PWROFF") == 0)
  {
    IRsendCode(PWROFF);
    delay(500);
  }
  else if (serial_command.indexOf("TUNER") == 0)
  {
    IRsendCode(TUNER);
    delay(500);
  }
  else if (serial_command.indexOf("CD") == 0)
  {
    IRsendCode(CD);
    delay(500);
  }
  else if (serial_command.indexOf("MUTE") == 0)
  {
    IRsendCode(MUTE);
    delay(500);
  }
  else if (serial_command.indexOf("DVD") == 0)
  {
    IRsendCode(DVD);
    delay(500);
  }
  else if (serial_command.indexOf("VAUX") == 0)
  {
    IRsendCode(VAUX);
    delay(500);
  }
    else if (serial_command.indexOf("VCR1") == 0)
  {
    IRsendCode(VCR1);
    delay(500);
  }
  else if (serial_command.indexOf("DVRVCR2") == 0)
  {
    IRsendCode(DVRVCR2);
    delay(500);
  }
  else if (serial_command.indexOf("MDCDR") == 0)
  {
    IRsendCode(MDCDR);
    delay(500);
  }
  else if (serial_command.indexOf("DTVCBL") == 0)
  {
    IRsendCode(DTVCBL);
    delay(500);
  }
  else if (serial_command.indexOf("VOLUP") == 0)
  {
    IRsendCode(VOLUP);
    delay(40);
    IRcarrier(9000);
    delay(2.25);//2.23
    IRcarrier(BITtime);
    delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.25);//2.23
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
    }
  else if (serial_command.indexOf("VOLDOWN") == 0)
  {
    IRsendCode(VOLDOWN);
    delay(40);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
    delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
        delay(96.28);
    IRcarrier(9000);
    delay(2.23);
    IRcarrier(BITtime);
    }
   else if (serial_command.indexOf("TVON") == 0)
  {
    IRsendCode(TVON);
    delay(40);
  }
 }
}

I used an old Arduino Nano and the 5V USB source on that board to power the LED.

I then put the IR LED in front of the IR receiver on the amplifier.



Connecting to the '/dev/ttyUSB0' port gave me a lot of trouble, it turned out that I had to wait 4 seconds from opening the port to actually using it. This made it clear that I had to create a system with a server controlling the serial port.


#! /usr/bin/env python2
# -*- coding: utf-8 -*-


import socket, sys, signal, serial, time
bytesize=8
parity='N'
stopbits=1
timeout=3
port_name = '/dev/ttyUSB0'
ser = serial.Serial(port_name, baudrate=9600, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=timeout)
time.sleep(4)
#print(ser.name) #which port



SOCKET_TIMEOUT = 1

sigterm_exit = False
def exit_check(_signo, _stack_frame):
    global sigterm_exit
    sigterm_exit = True

signal.signal(signal.SIGINT, exit_check)
signal.signal(signal.SIGTERM, exit_check)

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.settimeout(SOCKET_TIMEOUT)
sock.bind(('',31442))

while not sigterm_exit:
    try:
        msg, addr = sock.recvfrom(1024)
        print time.ctime(), msg
        ser.write(msg)
        ser.flush()
    except socket.timeout:
        pass
    except socket.error: # prob the SIGINT, sigterm_exit is True now
        pass
ser.close()
print 'done'

This is a UDP server, listening at port 31442. It just relays commands to the serial port.

The final steps is taken in KODI.

At startup KODI now selects the sound channel KODI is playing through. This is the file 'autoexec.py' in '.kodi/userdata'.


import socket
import xbmc
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
addr = ("127.0.0.1", 31442)
sock.sendto('DTVCBL', addr) #this is the channel for kodi/youtube
sock.close()

xbmc.executebuiltin('ActivateWindow(Videos, Files)')


The last change were create a keymap file in '.kodi/userdata/keymaps'

<keymap>
  <global>
    <keyboard>
      <numpadminus>RunScript(/home/pwr/.kodi/userdata/custom_IR.py, VOLDOWN)</numpadminus>
      <numpadplus>RunScript(/home/pwr/.kodi/userdata/custom_IR.py, VOLUP)</numpadplus>
      <numpadtimes>RunScript(/home/pwr/.kodi/userdata/custom_IR.py, DTVCBL)</numpadtimes>
    </keyboard>
  </global>
  <FullscreenVideo>
    <keyboard>
      <numpadminus>RunScript(/home/pwr/.kodi/userdata/custom_IR.py, VOLDOWN)</numpadminus>
      <numpadplus>RunScript(/home/pwr/.kodi/userdata/custom_IR.py, VOLUP)</numpadplus>
      <numpadtimes>RunScript(/home/pwr/.kodi/userdata/custom_IR.py, DTVCBL)</numpadtimes>
    </keyboard>
  </FullscreenVideo>
</keymap>