Programming is a wonderful mix of art and science; source code is both a poem and a math problem. It should be as simple and elegant as it is functional and fast. This blog is about that (along with whatever else I feel like writing about).

Tuesday, December 06, 2005

Web Based Remote Control

I spend a lot of time at my computer, but I spend even more time near my computer but not quite at the keyboard. It's a Mac Mini, and most of the time it's playing music in iTunes or a movie in VLC. I wanted a way to control those applications remotely, so I could pause without going all the way over to the computer. Since I have a Nokia 770, I thought the easiest way to do this would be to write a web page that can control these applications, and connect to it from the 770.

The first thing I needed was a way to control the applications. This was quite simple, of course, using Applescript. I made a small script for each action I wanted to perform. For example:

tell application "iTunes"
   playpause
end tell

All the scripts I wrote were similar in complexity, but more complicated scripts can of course be written.

The next step was to run these scripts from a web page. PHP handles this quite nicely, using the system() command. However, it won't work right out of the box, because OS X runs the web browser as a different user than the person running iTunes (for obvious security reasons). So I had to add a line to the /etc/sudoers file:

www ALL = NOPASSWD; /usr/bin/osascript

This gives the user running Apache the right to run osascript (which executes Applescript files) without typing in a password. This is probably not the best thing to do, security-wise, so it's pretty important to make sure that your machine is only accessible to computers on your local network.

Next, the PHP. It's quite simple. I only need one page, which takes a script and executes it as if it were on the command line. Here it is (execute.php):

<?php

$script = $_REQUEST['script'];

system($script);

?>

I'm planning on sending requests to this PHP file via XMLHttpRequest, of course. I'm using my standard xmlhttp() function which you can get from a previous blog. The only other Javascript function I actually need is the execute() function, which sends a request to execute.php. Here it is:

function execute(script) {
   var req = xmlhttp();
   var param = 'script='+script;
 
   //this should be made async
   req.open('POST', 'execute.php', true);
   req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
   req.send(param);
}


This is a pretty simple function, sending a single parameter asyncronously. Since it's only sending the request, and doesn't really need to return anything, I don't even bother checking the return. We just shoot off the request and wait for more input. You'll know if it worked it the music starts playing.

It was important to me that I didn't limit this program to any set of functionality that would be difficult to augment later. So I created an XML file that holds records for each program that can be controlled and the scripts to run in order to control it. Originally, I wanted the interface to be built dynamically via Javascript, so I crafted the XML file such that it had no attributes, only elements, because my JS XML parser doesn't read attributes (I should work on that). Unfortunately, it seems that the browser on the 770 doesn't support dynamically created DOM elements, so this approach didn't work. Plenty of wasted work there (and a lot more Javascript than I've included here).

This was when it dawned on me that I don't actually need to build the interface every time the page loads. It only needs to be done when the XML file is changed. So I wrote a Python program to take the XML file, parse it, and output the same HTML as would have been produced by the Javascript. I recreated the XML file to use attributes instead of just elements, because I find that to be simpler to read, and it's much easier to handle that in Python's xml.sax library. Here's the Python (buildinterface.py):

import os, sys
from xml.sax import saxutils, handler, make_parser

#set up the environment variables
config_file = "server2.xml"
html_file = "index.html"
output_file = file(html_file, 'w')

#globals programs dictionary
programs = []

class Program:
   def __init__(self, name):
       self.name = name
       self.commands = []
     
       programs.append(self)
     
   def addCommand(self, cmd):
       self.commands.append(cmd)
     
class Command:
   def __init__(self, name, script):
       self.name = name
       self.script = script
     
class ConfParser(handler.ContentHandler):
   def __init__(self):
       handler.ContentHandler.__init__(self)
     
   def startElement(self, name, attrs):
       global temp_prog
       if name == "program":
           temp_prog = Program(attrs['name'])
       elif name == "command":
           temp_prog.addCommand(Command(attrs['name'], attrs['script']))
         
def write_html(out = sys.stdout):
   out.write("<html><head><title>Remote Control</title>")
   out.write("<link rel='stylesheet' type='text/css' href='remote.css' />")
   out.write("<script type='text/javascript' src='remote.js'></script>")
   out.write("</head><body>")
   for prog in programs:
       out.write("<fieldset><legend>" + prog.name + "</legend>")
       out.write("<div id='" + prog.name + "_div'>")
     
       for cmd in prog.commands:
           out.write("<span class='link' onclick='execute(\"" + cmd.script + "\");'>" + cmd.name + "</span>")
           out.write("<br />")
         
       out.write("</div></fieldset>")
   out.write("</body></html>")

parser = make_parser()
parser.setContentHandler(ConfParser())
parser.parse(config_file)

write_html(output_file)

output_file.close()


As you can see, it outputs directly to index.html, which is convenient. Every time you run buildinterface.py, you have a fully updated interface waiting for you to load up in your browser. And in case you were wondering what those span tags are all about, they are to mimic links without the bother of actually putting a link on there. Here's the CSS for that:


.link {
   text-decoration: underline;
   font-size: 10pt;
   color: black;
   cursor: pointer;
}


Each link has its onclick event set to call execute() with the script to run. It's a fairly simple scheme, and when I loaded it in my browser to test it out, it worked swimmingly. I could control iTunes, VLC, Quicktime, and the system volume. I pulled out my laptop and connected, and found that it worked from remote systems. Now it was time to try it out on the 770. I fired up the browser and opened my page. This time the interface loaded, which was of course promising. However, none of the links worked. So the remote control program works, and is extensible, but I haven't been able to get it working on my Nokia. It seems that the browser doesn't want to send the XMLHttpRequests, which is weird, because it's Opera, and I tested this in Opera on both OS X and Linux, and it works there. I'll be trying to get it to work on the 770, but until then, here's the working code.

Things that could be added:
1. Get it to work on the Nokia 770.
2. Write plugins for other programs, to build out the capabilities of the remote.
3. Have the PHP return something, so it's possible to tell if the command has been executed or not. This will allow for more interesting possibilities on the front end.
4. Anything else ... ?

You can download the code here. Let me know how it goes, or if there are any problems you find with it. And let's here about those plugins!

2 comments:

hidden_premise said...

Sean, how were you able to modify your sudoers file with that line. Every time I try I get a syntax error. I am using an OS X unix machine.

Anonymous said...

This worked for me on OSX 10.4:
www ALL= NOPASSWD: /usr/bin/osascript

(no space after "ALL")
(colon after "NOPASSWD")