Volumio UI + IR blaster for AV Amp Control

I have Volumio running on Raspberry Pi 2 w/ the Hifiberry DAC+ Standard RCA. Love Volumio, but wanted a way to control my AV Receiver (Yamaha RX-V663) via IR blasting/emitting from the Pi to the receiver driven by the Volumio web UI.

I thought about a LIRC-based approach but the Hifiberry blocks all of the GPIO pins (unless you solder on a stacking header, which voids your Hifiberry warranty), so that was out. There are USB-based emitters, but they are expensive and online reports say that some have spotty history working w/ LIRC.

I had an Arduino Uno sitting around, so I used that connected to the Pi via USB to accomplish this. Below is how I did it.

I was helped greatly in this effort by this post. Big shout out to Emixam for paving the way for me here!

Step 1: Get my receiver’s IR remote’s codes
Step 1a) Arduino Hardware/Circuit get my receiver’s IR remote’s codes
Connect Arduino Pin 2 to the “output” pin on IR sensor (I used a TSOP2438 I had lying around, output is pin 1)
Connect Arduino 5V to 5V on sensor (pin 2 on mine)
Connect Arduino ground to ground on sensor (pin 3 on mine)

Step 1b) Arduino sketch to get my receiver’s IR remote’s codes
I based this setup on a recipe in O’Reilly’s Arduino Cookbook.

#include <IRremote.h>           // IR remote control library

const int irReceivePin = 2;     // pin connected to IR detector output

const int numberOfKeys = 100;     
long irKeyCodes[numberOfKeys];  

IRrecv irrecv(irReceivePin);    // create the IR library
decode_results results;         // IR data goes here

void setup()
{
  Serial.begin(9600);
  pinMode(irReceivePin, INPUT);

  irrecv.enableIRIn();              // Start the IR receiver
  learnKeycodes();                  // learn remote control key  codes
  Serial.println("Press a remote key");
}

void loop()
{
  long key;
  //int  brightness;

  if (irrecv.decode(&results))
  {
    // here if data is received
    irrecv.resume();
    key = convertCodeToKey(results.value);
    if(key >= 0)
    {
      Serial.print("Got key ");
      Serial.println(key);
    }
  }
}

/*
 * get remote control codes
 */
void learnKeycodes()
{
  while(irrecv.decode(&results))   // empty the buffer
    irrecv.resume();
 
  Serial.println("Ready to learn remote codes");
  long prevValue = -1;
  int i=0;
  while( i < numberOfKeys)
  {
    Serial.print("press remote key ");
    Serial.print(i);
    while(true)
    {
      if( irrecv.decode(&results) )
      {
          if(results.value != -1 && results.value != prevValue)
          {
            showReceivedData();
            irKeyCodes[i] = results.value;
            i = i + 1;
            prevValue = results.value;
            irrecv.resume(); // Receive the next value
            break;
          }
        irrecv.resume(); // Receive the next value
      }
    }
  }
  Serial.println("Learning complete");
}

/*
 * converts a remote protocol code to a logical key code 
 * (or -1 if no digit received)
 */
int convertCodeToKey(long code)
{
  for( int i=0; i < numberOfKeys; i++)
  {
    if( code == irKeyCodes[i])
    {
      return i; // found the key so return it
    }
  }
  return -1;
}

/*
 * display the protocol type and value
 */
void showReceivedData()
{
  if (results.decode_type == UNKNOWN)
  {
    Serial.println("-Could not decode message");
  }
  else
  {
    if (results.decode_type == NEC) {
      Serial.print("- decoded NEC: ");
    }
    else if (results.decode_type == SONY) {
      Serial.print("- decoded SONY: ");
    }
    else if (results.decode_type == RC5) {
      Serial.print("- decoded RC5: ");
    }
    else if (results.decode_type == RC6) {
      Serial.print("- decoded RC6: ");
    }
    Serial.print("hex value = ");
    Serial.println( results.value, HEX);
  }
}

I opened the Serial Monitor in the Arduino IDE and I pressed the following on the Yamaha receiver’s remote aimed at the sensor: Power (0), Standby (1), Mute (2) , CD (the receiver input that I have the Pi attached to) (3), Vol Up (4), Vol Down (5). The sketch returned this on serial monitor
press remote key 0- decoded NEC: hex value = 7E817E81
press remote key 1- decoded NEC: hex value = 7E81FE01
press remote key 2- decoded NEC: hex value = 5EA138C7
press remote key 3- decoded NEC: hex value = 5EA1A857
press remote key 4- decoded NEC: hex value = 5EA158A7
press remote key 5- decoded NEC: hex value = 5EA1D827

Step 2: Build Arduino blaster
Step 2a) Arduino Hardware/Circuit for blaster
The Pi communicates with the Arduino over the Arduino’s built-in USB serial connection. I’m using red and green LEDs for diagnostic “messages” (red=failed to decode serial message, green=succeeded). I’m using a 3.5mm headset jack to output the IR signals. I did this b/c my AV receiver has a 3.5mm female jack to receive IR signal impulses directly (rather than needing to use an IR LED in front of the receiver).

Dismantle the circuit from step 1. Connect Arduino Pin 9 to anode of red LED, connect cathode to 220 ohm resistor, resistor to Arduino ground
Connect Arduino Pin 10 to anode of green LED, connect cathode to 220 ohm resistor, resistor to ground
Connect Arduino Pin 3 to tip terminal of 3.5mm headset jack, connect sleeve terminal of jack to 220 ohm resistor, resistor to ground. If your AV receiver doesn’t have a direct IR jack,you could replace this with an IR LED (Pin 3 to anode, cathode to 220 ohm resistor, resistor to ground).

I put the Arduino into a small plastic enclosure that I got at Radio Shack w/ holes drilled for the jack, LEDs (in holders) and the USB cable.

Attached is a picture of the Arduino wiring.

Step 2a) Arduino Sketch for blaster
Take a 14 character msg over serial that is comprised of

  1. Brand name of IR signal (NEC, Sony, etc) - three characters

  2. Signal as decoded in step 1 above in hex (0x________) - 10 characters

  3. A flag for whether or not to flash the diagnostic LEDs (0=don’t flash, any other character = yes flash) - 1 character

emit this over the IR LED/3.5mm remote out, flash diagnostic LEDs if requested, log to serial. Here is the sketch

#include <IRremote.h>       // IR remote control library

IRsend irsend;

#define SIB_DEBUG

#ifdef SIB_DEBUG
# define SIB_DEBUG_TRACE(x, y) Serial.print(x);Serial.println(y)
#else
# define SIB_DEBUG_TRACE(x,y) (void)(x);(void)(y)
#endif

const int ledRedPin       = 9;     // LED is connected to a PWM pin
const int ledGreenPin = 10;

const int BRAND_LEN = 3;
const int HEX_CODE_LEN = 10;
const int FLASH_LED_LEN = 1;

struct IRInstruction
{
  char brand[BRAND_LEN+1];
  char hexCode[HEX_CODE_LEN+1];
  char flashLED;
};

const int IR_INSTRUCTION_LEN = BRAND_LEN + HEX_CODE_LEN + FLASH_LED_LEN;

typedef void  (IRsend::*LPIRSENDFUNC)(unsigned long, int);


struct Name2IRSendFunc
{
  String szBrand;
  LPIRSENDFUNC lpIRSendFunc;
};

LPIRSENDFUNC getIRSendFunc(const char* lpszBrand)
{
  const static Name2IRSendFunc arrName2IRSendFuncs[] =  {
                                                          {"RC5", &IRsend::sendRC5},
                                                          {"RC6", &IRsend::sendRC6},
                                                          {"NEC", &IRsend::sendNEC},
                                                          {"SAM", &IRsend::sendSAMSUNG},
                                                          {"WHY", &IRsend::sendWhynter},
                                                          {"LG_", &IRsend::sendLG},
                                                          {"DIS", &IRsend::sendDISH},
                                                          {"SHA", &IRsend::sendSharpRaw},
                                                          {"DEN", &IRsend::sendDenon}
                                                         };
  const static int NUM_IR_BRANDS = sizeof(arrName2IRSendFuncs) / sizeof(Name2IRSendFunc);                                                      
                                     
  for(int i = 0; i < NUM_IR_BRANDS; i++){
    if(arrName2IRSendFuncs[i].szBrand == lpszBrand){
      return arrName2IRSendFuncs[i].lpIRSendFunc;
    }
  }
  return NULL;
}

void setup() {
  Serial.begin(9600);
  pinMode(ledRedPin, OUTPUT);
  pinMode(ledGreenPin, OUTPUT);
}

void loop() {
  IRInstruction ii = {0};

  while(Serial.available() >=  IR_INSTRUCTION_LEN ){  
    SIB_DEBUG_TRACE("Got serial!", "");
    
    readSerial(BRAND_LEN, ii.brand);
    SIB_DEBUG_TRACE("brand: ", ii.brand);
    
    readSerial(HEX_CODE_LEN, ii.hexCode);  
    SIB_DEBUG_TRACE("hexCode: ", ii.hexCode);

    readSerial(FLASH_LED_LEN, &(ii.flashLED));
    SIB_DEBUG_TRACE("flashLed: ", ii.flashLED);
    
    LPIRSENDFUNC lpIRSendFunc = getIRSendFunc(ii.brand);
    if(lpIRSendFunc){
      char* endptr = NULL;
      unsigned long code = strtoul(ii.hexCode, &endptr, 16);
      int readLen = endptr - ii.hexCode;
      if(readLen == HEX_CODE_LEN){
        SIB_DEBUG_TRACE("code: ", code);
        
        ((irsend).*(lpIRSendFunc))(code, 32);

        if(ii.flashLED != '0'){
          blinkLED(ledGreenPin, 500);
        }
        SIB_DEBUG_TRACE("Success!!", "");
      }else{
        SIB_DEBUG_TRACE("ERROR: HEX CODE PARSE FAILED. INVALID LENGTH: ", readLen);
        blinkLED(ledRedPin, 500);
      }
    }else{
      SIB_DEBUG_TRACE("ERROR: UNKNOWN BRAND: ", ii.brand);
      blinkLED(ledRedPin, 500);
      delay(500);
      blinkLED(ledRedPin, 500);
    }
  }
}

void blinkLED(int pin, int duration) {
  digitalWrite(pin, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(duration);              // wait for a second
  digitalWrite(pin, LOW);    // turn the LED off by making the voltage LOW
}

unsigned int readSerial(unsigned int count, char* buf){
    for(int i = 0; i < count; i++){
      buf[i] = Serial.read();
    }
}

Step 3: Connect Arduino and entitle web framework logon to connect to it
Once connected, the Arduino’s serial USB will reflect as /dev/ttyACM0, belonging to the dialout group. Add the user the volumio web server runs under to this group:

sudo usermod -aG dialout www-data

Step 4: Shell script for communicating with Arduino
Created the following as /var/www/command/av_irblast.sh (don’t forget to chmod +x it). The correct stty parameters took me a looooong set of trial and error to get right!

#! /bin/bash
ard_tty=/dev/ttyACM0
stty -F $ard_tty -parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr -icrnl -ixon -ixoff -iuclc -ixany -imaxbel -iutf8 -opost -olcuc -ocrnl -onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 -isig -icanon -iexten -echo -echoe -echok -echonl noflsh -xcase -tostop -echoprt -echoctl -echoke
av_cmd=$1
declare -a arr_cmds
brand='NEC'
flash_led='1'
case "$av_cmd" in
        pwr_on)
            arr_cmds=('0x7E817E81' '0x5EA1A857')
            ;;

        pwr_off)
            arr_cmds=('0x7E81FE01')
            ;;

        vol_up)
            arr_cmds=('0x5EA158A7')
            ;;

        vol_down)
            arr_cmds=('0x5EA1D827')
            ;;

        mute)
            arr_cmds=('0x5EA138C7')
            ;;

        *)
            echo $"Usage: $0 {pwr_on|pwr_off|vol_up|vol_down|mute}"
            exit 1

esac

for cmd in ${arr_cmds[@]}; do
        echo -n "${brand}${cmd}${flash_led}" > $ard_tty
done

Step 5: Daemon script for keeping Arduino from rebooting on DTR
Most Arduinos (including the uno) are programmed to auto-boot when they receive a serial “DTR” signal. Has something to do w/ how the Atmega microcontroller is triggered to flash the compiled sketch onto the EEPROM over serial from the IDE. This would cause us a problem as the Arduno would reboot every time we echo’ed to its /dev/tty in the shell script above and this causes it to miss processing the message. There are hardware ways around this (too hard) and it should also be possible to suppress the DTR through an stty switch (-hupcl), but there is some problem w/ the Pi’s implementation of USB serial, or something, so that doesn’t work. (Google it).

The way around this is to keep the serial port open at all times. I accomplish this by starting a background read (cat /dev/ttyACM0) at boot up. Create this script as /home/volumio/log_arduino.sh. Don’t forget to chmod +x it after creating it.

#! /bin/bash
stty -F /dev/ttyACM0 -parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr -icrnl -ixon -ixoff -iuclc -ixany -imaxbel -iutf8 -opost -olcuc -ocrnl -onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 -isig -icanon -iexten -echo -echoe -echok -echonl noflsh -xcase -tostop -echoprt -echoctl -echoke

cat /dev/ttyACM0 > /dev/null 2>&1 &

Then add this line to the end of /etc/crontab to start the script each time Pi boots

@reboot volumio /home/volumio/log_arduino.sh

Finally: Cron is disabled by default on Volumio. Comment out the “killall -9 cron” in /var/www/command/orion_optimize.sh to re-enable it.

Step 6: Include Amplifier Control page in the UI and menu
Step 6a) PHP script
Create file /var/www/av_control.php as

<?php 
// common include
include('inc/connection.php');
include('inc/player_lib.php');
playerSession('open',$db,'',''); 
playerSession('unlock',$db,'','');
?>

<?php 
if (isset($_POST['syscmd'])){
	switch ($_POST['syscmd']) {

	case 'poweron':
		$cmd = '/var/www/command/av_irblast.sh pwr_on';
		sysCmd($cmd);
		
		session_start();
		// set UI notify
		$_SESSION['notify']['msg'] = 'Turned on AV Receiver...';
		// unlock session file
		playerSession('unlock');
		break;
		
	case 'poweroff':
		$cmd = '/var/www/command/av_irblast.sh pwr_off';
		sysCmd($cmd);
		
		session_start();
		// set UI notify
		$_SESSION['notify']['msg'] = 'Turned off AV Receiver...';
		// unlock session file
		playerSession('unlock');
		break;

	case 'voldown':
		$cmd = '/var/www/command/av_irblast.sh vol_down';
		sysCmd($cmd);
		
		session_start();
		// set UI notify
		$_SESSION['notify']['msg'] = 'AV Receiver volume down...';
		// unlock session file
		playerSession('unlock');
		break;

	case 'mute':
		$cmd = '/var/www/command/av_irblast.sh mute';
		sysCmd($cmd);
		
		session_start();
		// set UI notify
		$_SESSION['notify']['msg'] = 'AV Receiver mute toggle...';
		// unlock session file
		playerSession('unlock');
		break;

	case 'volup':
		$cmd = '/var/www/command/av_irblast.sh vol_up';
		sysCmd($cmd);
		
		session_start();
		// set UI notify
		$_SESSION['notify']['msg'] = 'AV Receiver volume up...';
		// unlock session file
		playerSession('unlock');
		break;
	}

}

// set template
$tpl = "av_control.html";
?>

<?php
$sezione = basename(__FILE__, '.php');
include('_header.php'); 
?>

<!-- content --!>
<?php
// wait for worker output if $_SESSION['w_active'] = 1
//waitWorker(1);
eval("echoTemplate(\"".getTemplate("templates/$tpl")."\");");
?>
<!-- content -->

<?php 
debug($_POST);
?>

<?php include('_footer.php'); ?>

Step 6b) HTML template
Create /var/www/template/av_control.html as

<div class="container">
	<h1>Amplifier Control</h1>
	<form class="form-horizontal" method="post">
			<p>
				<button class="btn btn-large btn-primary" type="submit" name="syscmd" value="poweron" id="syscmd-poweron">Power On</button>
				<button class="btn btn-large btn-primary" type="submit" name="syscmd" value="poweroff" id="syscmd-poweroff">Power Off</button>
			</p>
			<p>
				<button class="btn btn-large btn-primary" type="submit" name="syscmd" value="voldown" id="syscmd-voldown">Vol Down</button>
				<button class="btn btn-large btn-primary" type="submit" name="syscmd" value="mute" id="syscmd-mute">Mute</button>
				<button class="btn btn-large btn-primary" type="submit" name="syscmd" value="volup" id="syscmd-volup">Vol Up</button>
			</p>
	</form>
</div>

Step 6c) Add to the main menu in web UI
Backup /var/www/_header.php. Then edit it. After this line:

<li class="<?php ami('index'); ?>"><a href="index.php"><i class="fa fa-play sx"></i> Main</a></li>

add this line:

 <li><a href="av_control.php"><i class="fa fa-dot-cirlce-o sx"></i> Amplifier Control</a></li>

Attached are some pictures of how it looks. It’s been working great for me for the last couple of weeks and let’s me turn the AV receiver on or off and change the volume from anywhere in the house. Hope that this helps someone else or gives someone a better idea of how to do something similar!

20151219_225820.jpg
Screenshot_2015-12-22-00-52-05.png
Screenshot_2015-12-22-00-51-57.png

Excellent build / code / writeup. I’ve been looking to see if anyone has done this for quite some time, I’m not sure why I only just stumbled across this post now. You could avoid voiding the warranty by soldering directly to the pads on the back of the pi (it’s kind of a pain though, the board sucks the heat out of the solder pretty fast).

This is really cool. What is the status with this today?