Jump to content
  • Advertisement
  • entries
    222
  • comments
    606
  • views
    592238

Remote controlling Windows the Sony way

Sign in to follow this  
benryves

1453 views

It's been a while since I last posted, and unfortunately this post is to do with Sony remote controls again. [rolleyes]

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
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
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
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 {

///
/// Represents a command sent by a SIRCS remote control.
///

public struct SircsCommand : IEquatable {

#region Properties

private byte command;
///
/// Gets or sets the command value.
///

public byte Command {
get { return this.command; }
set { this.command = value; }
}

private short device;
///
/// Gets or sets the device identifier.
///

public short Device {
get { return this.device; }
set { this.device = value; }
}

private int length;
///
/// Gets or sets the length of the command in bits.
///

public int Length {
get { return this.length; }
set { this.length = value; }
}

#endregion

#region Construction

///
/// Creates an instance of a structure.
///

/// The command value.
/// The device identifier.
/// The length of the command in bits.
public SircsCommand(byte command, short device, int length) {
this.command = command;
this.device = device;
this.length = length;
}

#endregion

#region Methods

///
/// Converts the into a string.
///

/// A string representation of the .
public override string ToString() {
return string.Format("Command={0:X2}, Device={1:X4}, Length={2}", this.command, this.device, this.length);
}

///
/// Returns the hash code for this instance.
///

/// The hash code for this instance.
public override int GetHashCode() {
return this.command ^ this.device ^ this.length;
}

///
/// Returns a value indicating whether this instance is equal to another instance.
///

/// The instance to compare to this one for equality.
/// True if the instances are equal, false otherwise.
public bool Equals(SircsCommand other) {
return this.command == other.command && this.device == other.device && this.length == other.length;
}

///
/// Returns a value indicating whether this instance is equal to another instance.
///

/// The instance to compare to this one for equality.
/// True if the instances are equal, false otherwise.
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

///
/// Represents the method that will handle the SircsCommandReceived event.
///

/// The object that fired the event.
/// Information about the event.
public delegate void SircsCommandReceivedEventHandler(object sender, SircsCommandReceivedEventArgs e);

///
/// Provides data for the SircsReceived.SircsCommandReceived event.
///

public class SircsCommandReceivedEventArgs : EventArgs {

#region Properties

///
/// Gets the that was received.
///

public SircsCommand Command { get; private set; }

///
/// Gets the number of times that the incoming command has been repeated when held.
///

public int Repeat { get; private set; }

#endregion

#region Construction

///
/// Creates a instance.
///

/// The that was recieved.
/// The number of times that the incoming command has been repeated when held.
public SircsCommandReceivedEventArgs(SircsCommand command, int repeat) {
this.Command = command;
this.Repeat = repeat;
}

#endregion

#region Methods

///
/// Converts the into a string.
///

/// A string representation of the .
public override string ToString() {
return string.Format("{0}, Repeat={1}", this.Command, this.Repeat);
}

#endregion
}

#endregion

///
/// Provides a way to receive SIRCS commands from a simple receiver attached to a serial port.
///

public class SircsReceiver : IDisposable {

#region Constants

///
/// The minimum time length for a start bit (nominally 2.4ms).
///

private const double StartBitMinLength = 2.0E-3;

///
/// Threshold time length between a "low" (0.6ms) and a "high" (1.2ms) bit.
///

private const double DataBitLengthThreshold = 0.9E-3;

///
/// The maximum time length between data bits. If this is exceeded, any data command transfer is cancelled.
///

private const double IntraBitMaxLength = 0.8E-3;

///
/// The maximum time length between repeating commands. Commands are supposed to repeat every 45ms.
///

private const double RepeatCommandMaxLength = 120.0E-3;

#endregion

#region Private Fields

///
/// The that the receiver is connected to.
///

private SerialPort Port = null;

///
/// The last time that the pin state changed in ticks.
///

private long LastPinChangedTime = 0;

///
/// A instance used to time incoming bits.
///

private Stopwatch BitTimer = null;

///
/// Set to true when receiving a command, false otherwise.
///

private bool ReceivingCommand = false;

///
/// Counts the number of bits currently received.
///

private int BitsReceived = 0;

///
/// Stores the command as it gets built up.
///

private uint Command = 0;

///
/// Stores the last received command.
///

private SircsCommand LastCommand = default(SircsCommand);

///
/// Stores the number of times the received command has been repeated.
///

private int LastCommandRepeatCount = 0;

///
/// A instance used to time repeating commands.
///

private Stopwatch RepeatTimer = null;

#endregion

#region Construction/Destruction

///
/// Creates an instance of a from a serial port name.
///

/// The name of the serial port the receiver is connected to.
public SircsReceiver(string portName) {

// Set up the serial port.
this.Port = new SerialPort(portName);
this.Port.PinChanged += new SerialPinChangedEventHandler(PinChanged);

// Open the port for access.
this.Port.Open();
this.Port.DtrEnable = true;
this.Port.RtsEnable = true;

// Get the timers running.
this.BitTimer = new Stopwatch();
this.BitTimer.Start();
this.RepeatTimer = new Stopwatch();
this.RepeatTimer.Start();
}

///
/// Releases the resources used by this instance.
///

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

///
/// An event that is fired when a is received.
///

public event SircsCommandReceivedEventHandler SircsCommandReceived;

///
/// A method that is invoked when a is received.
///

///
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) {

// Respond to changes on the CTS pin.
if (e.EventType == SerialPinChange.CtsChanged) {

// Quickly grab the current time and current CTS level.
long CurrentPinChangedTime = this.BitTimer.ElapsedTicks;
bool CurrentLevel = this.Port.CtsHolding;

// Calculate the time elapsed.
long DeltaTime = CurrentPinChangedTime - this.LastPinChangedTime;
double SecondsElapsed = (double)DeltaTime / (double)Stopwatch.Frequency;
this.LastPinChangedTime = CurrentPinChangedTime;

if (CurrentLevel) { // If the current signal level is high, we may assume that we've just timed a low pulse.

// Have we received a start bit?
if (SecondsElapsed > SircsReceiver.StartBitMinLength) {
this.ReceivingCommand = true;
this.BitsReceived = 0;
this.Command = 0;
} else if (this.ReceivingCommand) {
// Process incoming bit.
this.Command >>= 1;
if (SecondsElapsed > SircsReceiver.DataBitLengthThreshold) {
this.Command |= unchecked((uint)(1 << 31));
}

// Have we received enough bits?
switch (++this.BitsReceived) {
case 12:
case 15:
case 20:
// We've received enough bits to handle the input as a received command.
// Check to see if there's any more data forthcoming.
long EndTime = CurrentPinChangedTime + (long)(Stopwatch.Frequency * SircsReceiver.IntraBitMaxLength);
while (BitTimer.ElapsedTicks < EndTime) {
if (!(CurrentLevel = this.Port.CtsHolding)) break;
}
// The input is still high - there's no more data coming in; we've received a command.
if (CurrentLevel) {
// Construct a struct to hold information about the recieved data.
SircsCommand ReceivedCommand = new SircsCommand(
(byte)((this.Command >> (32 - this.BitsReceived)) & 0x7F),
(short)(this.Command >> ((32 + 7) - this.BitsReceived)),
this.BitsReceived
);

// Reset the timer.
this.ReceivingCommand = false;
this.BitTimer.Reset();
this.BitTimer.Start();
this.LastPinChangedTime = 0;

// Calculate the repeat count.

// Quickly grab the current time and current CTS level.
long RepeatTimeTicks = this.RepeatTimer.ElapsedTicks;
this.RepeatTimer.Reset();
this.RepeatTimer.Start();

// Calculate the repeat time elapsed.
double RepeatTimeSeconds = (double)RepeatTimeTicks / (double)Stopwatch.Frequency;

// Is the command repeating?
if (ReceivedCommand == this.LastCommand && RepeatTimeSeconds < SircsReceiver.RepeatCommandMaxLength) {
++this.LastCommandRepeatCount;
} else {
this.LastCommandRepeatCount = 1;
this.LastCommand = ReceivedCommand;
}

// Fire the event.
this.OnSircsCommandReceived(new SircsCommandReceivedEventArgs(ReceivedCommand, this.LastCommandRepeatCount));
}
break;
}
}
} else { // If the current signal level is low, we may assume that we've just timed a high pulse.
// If a high pulse is too long, cancel any incoming commands.
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
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:

// Table of commands.
var Commands = [
{ Command : 0x00, Device : 0x093A, Length : 20, Shortcut : '1' }, // 1
{ Command : 0x01, Device : 0x093A, Length : 20, Shortcut : '2' }, // 2
{ Command : 0x02, Device : 0x093A, Length : 20, Shortcut : '3' }, // 3
{ Command : 0x03, Device : 0x093A, Length : 20, Shortcut : '4' }, // 4
{ Command : 0x04, Device : 0x093A, Length : 20, Shortcut : '5' }, // 5
{ Command : 0x05, Device : 0x093A, Length : 20, Shortcut : '6' }, // 6
{ Command : 0x06, Device : 0x093A, Length : 20, Shortcut : '7' }, // 7
{ Command : 0x07, Device : 0x093A, Length : 20, Shortcut : '8' }, // 8
{ Command : 0x08, Device : 0x093A, Length : 20, Shortcut : '9' }, // 9
{ Command : 0x09, Device : 0x093A, Length : 20, Shortcut : '0' }, // 0
{ Command : 0x0B, Device : 0x093A, Length : 20, Shortcut : '{ENTER}' }, // Enter
{ Command : 0x0E, Device : 0x093A, Length : 20, Shortcut : '{ESC}' }, // Return
{ Command : 0x1A, Device : 0x093A, Length : 20, Shortcut : 'lt' }, // Title
{ Command : 0x2A, Device : 0x093A, Length : 20, Shortcut : 'x' }, // A<->B
{ Command : 0x28, Device : 0x093A, Length : 20, Shortcut : 'd' }, // Time
{ Command : 0x2C, Device : 0x093A, Length : 20, Shortcut : '^r' }, // Repeat
{ Command : 0x30, Device : 0x093A, Length : 20, Shortcut : 'p' }, // Previous
{ Command : 0x31, Device : 0x093A, Length : 20, Shortcut : 'n' }, // Next
{ Command : 0x32, Device : 0x093A, Length : 20, Shortcut : '{ENTER}' }, // Play
{ Command : 0x33, Device : 0x093A, Length : 20, Shortcut : 'b' }, // Scan <<
{ Command : 0x34, Device : 0x093A, Length : 20, Shortcut : 'f' }, // Scan >>
{ Command : 0x38, Device : 0x093A, Length : 20, Shortcut : 's' }, // Stop
{ Command : 0x39, Device : 0x093A, Length : 20, Shortcut : ' ' }, // Pause
{ Command : 0x54, Device : 0x093A, Length : 20, Shortcut : 'z' }, // Display
{ Command : 0x60, Device : 0x093A, Length : 20, Shortcut : '^b' }, // Slow <<
{ Command : 0x61, Device : 0x093A, Length : 20, Shortcut : 't' }, // Slow >>
{ Command : 0x63, Device : 0x093A, Length : 20, Shortcut : 'u' }, // Subtitle
{ Command : 0x64, Device : 0x093A, Length : 20, Shortcut : 'h' }, // Audio
{ Command : 0x65, Device : 0x093A, Length : 20, Shortcut : 'a' }, // Angle
{ Command : 0x79, Device : 0x093A, Length : 20, Shortcut : '{UP}' }, // Up
{ Command : 0x7A, Device : 0x093A, Length : 20, Shortcut : '{DOWN}' }, // Down
{ Command : 0x7B, Device : 0x093A, Length : 20, Shortcut : '{LEFT}' }, // Left
{ Command : 0x7C, Device : 0x093A, Length : 20, Shortcut : '{RIGHT}' }, // Right
];

// Search for the matching command.
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;
}
}

// No command.
if (!Command) WScript.Quit(1);

// Find the PowerDVD process ID.
var PowerDvdId = null;
var WmiService = GetObject('winmgmts://./root/cimv2');
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 we haven't found the process ID, quit with an error.
if (!PowerDvdId) WScript.Quit(1);

// Activate the PowerDVD instance.
var WshShell = new ActiveXObject('WScript.Shell');
WshShell.AppActivate(PowerDvdId);

// Send the shortcut keys.
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.
Sign in to follow this  


3 Comments


Recommended Comments

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.

Share this comment


Link to comment
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:
Original post by ukdm
Thought you'd appreciate this Ben:
http://www.wired.com/gadgetlab/2009/05/homebrewed-cpu/
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. [smile]

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!