 Remote controlling Windows the Sony way |
Posted - 5/27/2009 9:05:41 AM | It's been a while since I last posted, and unfortunately this post is to do with Sony remote controls again. 
This time I'm attempting to use Sony (or compatible) remote controls to control software running on a Windows PC. I've recently been watching more films in PowerDVD, and some of the keyboard shortcuts (eg Ctrl+P for the menu) are a little difficult to hit in the dark and from a distance. I have a ready supply of universal remote controls as well as the PlayStation 2 DVD remote control, all of which work with the SIRCS protocol.

Serial port infra-red receiver built into an old TI GraphLink cable
First up is the required hardware. This involves an infrared demodulator connected to a free serial port. I chose the serial port as .NET provides a way to handle pin change events and you do not need administrator rights to access it (as per the parallel port). I also had a broken Texas Instruments GraphLink cable that could be ripped apart to act as a case.

Infra-red receiver module schematic
The circuit is pretty simple. Pin 4 (DTR) and 5 (GND) from the serial port form the power supply. DTR can be set to either +12V or -12V, so a rectifier diode is used to keep the input voltage above 0V. Following that is a reverse-biased zener diode and resistor to regulate the voltage below 5.1V. Finally, the output pin of the infra-red demodulator is connected to the input pin 8 (CTS) of the serial port.

Infra-red receiver module assembled on stripboard
The software handles the SerialPort.PinChanged event to time the length of input pulses. Once it detects a start bit (2.4mS) it starts decoding the rest of the command. When it's finished receiving a command it fires an event of its own, which the main software can react to.
using System;
using System.Diagnostics;
using System.IO.Ports;
namespace BeeDevelopment.Sircs {
public struct SircsCommand : IEquatable<SircsCommand> {
#region Properties
private byte command;
public byte Command {
get { return this.command; }
set { this.command = value; }
}
private short device;
public short Device {
get { return this.device; }
set { this.device = value; }
}
private int length;
public int Length {
get { return this.length; }
set { this.length = value; }
}
#endregion
#region Construction
public SircsCommand(byte command, short device, int length) {
this.command = command;
this.device = device;
this.length = length;
}
#endregion
#region Methods
public override string ToString() {
return string.Format("Command={0:X2}, Device={1:X4}, Length={2}", this.command, this.device, this.length);
}
public override int GetHashCode() {
return this.command ^ this.device ^ this.length;
}
public bool Equals(SircsCommand other) {
return this.command == other.command && this.device == other.device && this.length == other.length;
}
public override bool Equals(object other) {
return other != null && other is SircsCommand && ((SircsCommand)other).Equals(this);
}
#endregion
#region Operators
public static bool operator ==(SircsCommand a, SircsCommand b) { return a.Equals(b); }
public static bool operator !=(SircsCommand a, SircsCommand b) { return !a.Equals(b); }
#endregion
}
#region Events
public delegate void SircsCommandReceivedEventHandler(object sender, SircsCommandReceivedEventArgs e);
public class SircsCommandReceivedEventArgs : EventArgs {
#region Properties
public SircsCommand Command { get; private set; }
public int Repeat { get; private set; }
#endregion
#region Construction
public SircsCommandReceivedEventArgs(SircsCommand command, int repeat) {
this.Command = command;
this.Repeat = repeat;
}
#endregion
#region Methods
public override string ToString() {
return string.Format("{0}, Repeat={1}", this.Command, this.Repeat);
}
#endregion
}
#endregion
public class SircsReceiver : IDisposable {
#region Constants
private const double StartBitMinLength = 2.0E-3;
private const double DataBitLengthThreshold = 0.9E-3;
private const double IntraBitMaxLength = 0.8E-3;
private const double RepeatCommandMaxLength = 120.0E-3;
#endregion
#region Private Fields
private SerialPort Port = null;
private long LastPinChangedTime = 0;
private Stopwatch BitTimer = null;
private bool ReceivingCommand = false;
private int BitsReceived = 0;
private uint Command = 0;
private SircsCommand LastCommand = default(SircsCommand);
private int LastCommandRepeatCount = 0;
private Stopwatch RepeatTimer = null;
#endregion
#region Construction/Destruction
public SircsReceiver(string portName) {
this.Port = new SerialPort(portName);
this.Port.PinChanged += new SerialPinChangedEventHandler(PinChanged);
this.Port.Open();
this.Port.DtrEnable = true;
this.Port.RtsEnable = true;
this.BitTimer = new Stopwatch();
this.BitTimer.Start();
this.RepeatTimer = new Stopwatch();
this.RepeatTimer.Start();
}
public void Dispose() {
if (this.Port != null) {
this.Port.PinChanged -= new SerialPinChangedEventHandler(PinChanged);
this.Port.Dispose();
this.Port = null;
}
}
~SircsReceiver() {
this.Dispose();
}
#endregion
#region Events
public event SircsCommandReceivedEventHandler SircsCommandReceived;
protected virtual void OnSircsCommandReceived(SircsCommandReceivedEventArgs e) {
if (this.SircsCommandReceived != null) this.SircsCommandReceived(this, e);
}
#endregion
#region SIRCS protocol handling
void PinChanged(object sender, SerialPinChangedEventArgs e) {
if (e.EventType == SerialPinChange.CtsChanged) {
long CurrentPinChangedTime = this.BitTimer.ElapsedTicks;
bool CurrentLevel = this.Port.CtsHolding;
long DeltaTime = CurrentPinChangedTime - this.LastPinChangedTime;
double SecondsElapsed = (double)DeltaTime / (double)Stopwatch.Frequency;
this.LastPinChangedTime = CurrentPinChangedTime;
if (CurrentLevel) {
if (SecondsElapsed > SircsReceiver.StartBitMinLength) {
this.ReceivingCommand = true;
this.BitsReceived = 0;
this.Command = 0;
} else if (this.ReceivingCommand) {
this.Command >>= 1;
if (SecondsElapsed > SircsReceiver.DataBitLengthThreshold) {
this.Command |= unchecked((uint)(1 << 31));
}
switch (++this.BitsReceived) {
case 12:
case 15:
case 20:
long EndTime = CurrentPinChangedTime + (long)(Stopwatch.Frequency * SircsReceiver.IntraBitMaxLength);
while (BitTimer.ElapsedTicks < EndTime) {
if (!(CurrentLevel = this.Port.CtsHolding)) break;
}
if (CurrentLevel) {
SircsCommand ReceivedCommand = new SircsCommand(
(byte)((this.Command >> (32 - this.BitsReceived)) & 0x7F),
(short)(this.Command >> ((32 + 7) - this.BitsReceived)),
this.BitsReceived
);
this.ReceivingCommand = false;
this.BitTimer.Reset();
this.BitTimer.Start();
this.LastPinChangedTime = 0;
long RepeatTimeTicks = this.RepeatTimer.ElapsedTicks;
this.RepeatTimer.Reset();
this.RepeatTimer.Start();
double RepeatTimeSeconds = (double)RepeatTimeTicks / (double)Stopwatch.Frequency;
if (ReceivedCommand == this.LastCommand && RepeatTimeSeconds < SircsReceiver.RepeatCommandMaxLength) {
++this.LastCommandRepeatCount;
} else {
this.LastCommandRepeatCount = 1;
this.LastCommand = ReceivedCommand;
}
this.OnSircsCommandReceived(new SircsCommandReceivedEventArgs(ReceivedCommand, this.LastCommandRepeatCount));
}
break;
}
}
} else {
if (SecondsElapsed > SircsReceiver.IntraBitMaxLength) {
this.ReceivingCommand = false;
this.BitTimer.Reset();
this.BitTimer.Start();
this.LastPinChangedTime = 0;
}
}
}
}
#endregion
}
}
Currently, the software reacts to input events by running through a list of scripts, passing the command ID, device ID and command length (in bits) to each until one of them returns zero (ie, success) to indicate that it has processed the button.

Scripts list
The advantage to this method is that the end-user could customise the behaviour of the software to their own liking very easily. For example, here's the PowerDVD.js file from above, which allows me to control PowerDVD from a PlayStation 2 DVD remote control:
var Commands = [
{ Command : 0x00, Device : 0x093A, Length : 20, Shortcut : '1' },
{ Command : 0x01, Device : 0x093A, Length : 20, Shortcut : '2' },
{ Command : 0x02, Device : 0x093A, Length : 20, Shortcut : '3' },
{ Command : 0x03, Device : 0x093A, Length : 20, Shortcut : '4' },
{ Command : 0x04, Device : 0x093A, Length : 20, Shortcut : '5' },
{ Command : 0x05, Device : 0x093A, Length : 20, Shortcut : '6' },
{ Command : 0x06, Device : 0x093A, Length : 20, Shortcut : '7' },
{ Command : 0x07, Device : 0x093A, Length : 20, Shortcut : '8' },
{ Command : 0x08, Device : 0x093A, Length : 20, Shortcut : '9' },
{ Command : 0x09, Device : 0x093A, Length : 20, Shortcut : '0' },
{ Command : 0x0B, Device : 0x093A, Length : 20, Shortcut : '{ENTER}' },
{ Command : 0x0E, Device : 0x093A, Length : 20, Shortcut : '{ESC}' },
{ Command : 0x1A, Device : 0x093A, Length : 20, Shortcut : 'lt' },
{ Command : 0x2A, Device : 0x093A, Length : 20, Shortcut : 'x' },
{ Command : 0x28, Device : 0x093A, Length : 20, Shortcut : 'd' },
{ Command : 0x2C, Device : 0x093A, Length : 20, Shortcut : '^r' },
{ Command : 0x30, Device : 0x093A, Length : 20, Shortcut : 'p' },
{ Command : 0x31, Device : 0x093A, Length : 20, Shortcut : 'n' },
{ Command : 0x32, Device : 0x093A, Length : 20, Shortcut : '{ENTER}' },
{ Command : 0x33, Device : 0x093A, Length : 20, Shortcut : 'b' },
{ Command : 0x34, Device : 0x093A, Length : 20, Shortcut : 'f' },
{ Command : 0x38, Device : 0x093A, Length : 20, Shortcut : 's' },
{ Command : 0x39, Device : 0x093A, Length : 20, Shortcut : ' ' },
{ Command : 0x54, Device : 0x093A, Length : 20, Shortcut : 'z' },
{ Command : 0x60, Device : 0x093A, Length : 20, Shortcut : '^b' },
{ Command : 0x61, Device : 0x093A, Length : 20, Shortcut : 't' },
{ Command : 0x63, Device : 0x093A, Length : 20, Shortcut : 'u' },
{ Command : 0x64, Device : 0x093A, Length : 20, Shortcut : 'h' },
{ Command : 0x65, Device : 0x093A, Length : 20, Shortcut : 'a' },
{ Command : 0x79, Device : 0x093A, Length : 20, Shortcut : '{UP}' },
{ Command : 0x7A, Device : 0x093A, Length : 20, Shortcut : '{DOWN}' },
{ Command : 0x7B, Device : 0x093A, Length : 20, Shortcut : '{LEFT}' },
{ Command : 0x7C, Device : 0x093A, Length : 20, Shortcut : '{RIGHT}' },
];
var Command = null;
for (var enumerator = new Enumerator(Commands); !enumerator.atEnd(); enumerator.moveNext()) {
var TestCommand = enumerator.item();
if (TestCommand.Command == WScript.Arguments(1) && TestCommand.Device == WScript.Arguments(2) && TestCommand.Length == WScript.Arguments(3)) {
Command = TestCommand;
break;
}
}
if (!Command) WScript.Quit(1);
var PowerDvdId = null;
var WmiService = GetObject('winmgmts:
var Processes = WmiService.ExecQuery('Select ProcessId From Win32_Process Where Name="PowerDVD.exe"');
for (var enumerator = new Enumerator(Processes); !enumerator.atEnd(); enumerator.moveNext()) {
PowerDvdId = enumerator.item().ProcessId;
break;
}
if (!PowerDvdId) WScript.Quit(1);
var WshShell = new ActiveXObject('WScript.Shell');
WshShell.AppActivate(PowerDvdId);
WshShell.SendKeys(Command.Shortcut);
WScript.Quit(0);
Unfortunately, this method has quite a lot of overhead. This becomes a problem when you consider that commands are repeated every 45ms. Currently I avoid the issue by not allowing any keys to repeat, but some keys - such as the volume keys - would need to repeat when held.
I'm unsure as the best path to take. One idea that has crossed my mind would be to set up each remote control you were going to use beforehand (though I suppose I could build up a database of remote controls and bundle them with the software). You could then set whether each key should repeat or not, and attach a meaningful string to each button. This would also allow for more protocols to be supported other than SIRCS, and you could set it up so that the Play button on a Sony remote control generated the string "play" and passed that to the script(s) as well as the Play button on a Panasonic or Toshiba remote control rather than juggling control codes.
| |
Comments
 |  Jotaf
Member since: 1/25/2004 From: Portugal |
Posted - 5/27/2009 11:28:28 PM | Dude this is the bomb. I didn't know .net could work so easily with the serial port! I'll consider it seriously next time a hardware project requires it.
| |
 |  benryves GDNet+
Member since: 9/4/2003 From: Purley, Greater London |
Posted - 5/28/2009 4:24:11 PM | Quote:Original post by Jotaf
Dude this is the bomb. I didn't know .net could work so easily with the serial port! I'll consider it seriously next time a hardware project requires it. |
As silly as the serial port is from a voltage point of view, it certainly is one of the easiest ports to interface with - as long as you don't need many pins!
Quote: Mm, it's an interesting project that one - Hack a Day featured it a few months ago, and I'd since forgotten about it, so it's nice to catch up with it now there's more information on the site. 
| |
|
| S | M | T | W | T | F | S | | | | 1 | 2 | 3 | 4 | | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | | |
OPTIONS
Track this Journal
ARCHIVES
September, 2010
August, 2010
July, 2010
June, 2010
April, 2010
March, 2010
February, 2010
January, 2010
December, 2009
November, 2009
October, 2009
August, 2009
June, 2009
May, 2009
March, 2009
February, 2009
January, 2009
December, 2008
November, 2008
October, 2008
September, 2008
August, 2008
July, 2008
June, 2008
May, 2008
April, 2008
March, 2008
February, 2008
November, 2007
October, 2007
September, 2007
August, 2007
July, 2007
May, 2007
April, 2007
February, 2007
January, 2007
December, 2006
November, 2006
October, 2006
September, 2006
August, 2006
July, 2006
June, 2006
May, 2006
April, 2006
March, 2006
February, 2006
January, 2006
December, 2005
November, 2005
October, 2005
September, 2005
August, 2005
April, 2005
February, 2005
January, 2005
|