I’ve been busy since I last spoke about my lamp. I’ve added code to put the lamp into various modes – so far I’ve implemented modes for ‘white’ (needs some calibration), randomly changing colours, randomly fading in and out with random colours, and of course just off.
Modes are changed by sending characters to the arduino over the serial connection, so that’s fairly easy. In fact I decided that was too easy, and also too inconvenient for me (I don’t want to have to echo characters to the lamp every time I want to turn it on). Therefore, to make changing modes easier I wrote a web interface, which contained some PHP code that echoed characters to the lamp based on which button had been pressed by the user. This was all hosted locally to the lamp, so echoing characters to it was fairly easy. The next step was to let users control the lamp remotely (i.e. across the internet). This turned out to be a lot more challenging (or at least I overcomplicated it to the point that it became challenging), and this is the main focus of the article.
Simply allowing users to change the mode of my lamp over the internet would normally be really simple – open up port 80 on the server that’s hosting my lamp to the internet – however shamefully I’m on a network whose firewall blocks pretty much every inbound port, so hosting the interface locally isn’t an option.
Another simple option would be to have the lamp periodically poll a location on the internet to decide which mode to use, that way the web interface can just set the value of a file, then when the lamp reads that file, it will set its mode appropriately. The downside with this is that the update time for the lamp would be quite slow and it would create a lot of http requests to the server.
So, to summarise
- The connection has to originate from behind the firewall
- Responses to users actions should be fast, and should ideally be fetched through a single, persistent connection
I’ve done work with chat bots before, and I just so happen to have access to a Jabber server so I decided I’d try to allow users to communicate with the lamp through Jabber. This was actually fairly easy, although I admit I’m cheating in so far as the actual Jabber code isn’t running on the arduino, but instead a SheevaPlug (plug computer that runs linux).
The Client
The client side code is pretty simple – just a quick PERL script. Jabber communication was enabled through the Net::Jabber.
#!/usr/bin/perl -w
use strict;
use Net::Jabber;
my $server = "-----";
my $port = 5222;
my $username = "-----";
my $password = "-----";
my $client = new Net::Jabber::Client();
my $device = '/dev/ttyUSB0';
my $allowFork = 1;
my $allowFaffage = 1;
#Try to fork the process, then kill the parent process
#leaving only the child process running. This detaches
#the process from the terminal.
if($allowFork == 1 && fork())
{
exit();
}
#Tells the XMPP librabry which functions should be
#called when certain events happen.
$client->SetCallBacks(message=>\&msgReceived, onauth=>\&authenticated);
#Connect to the server
$client->Execute(hostname=>$server,
tls=>0,
username=>$username,
password=>$password,
resource=>"lamp",
register=>0);
#Sends a message to a user
sub send { my($to,$body) = @_;
$client->MessageSend(to=>"$to",subject=>"Cheese",body=>"$body");
}
sub sendHelp { my($to) = @_;
my $body = <<END;
I'm Harry's Lamp
Talking to me turns me on, and makes me change colours etc. Here are the commands that are enabled atm.
white - Just boring, white light
random - Makes me randomly fade to colours
fade - Makes me fade in and out with random colours
off - Turns me off
END
&send($to, $body);
}
#Once the lamp bot is up and ready,
#send a message to the owner to tell them.
sub authenticated{
&send('admin@-----','Lamp bot online');
}
sub msgReceived { my ($err,$message) = @_;
my $body = $message->GetBody();
my $from = $message->GetFrom();
$from =~ s#/.*$##; #Get rid of anything after bleh@wahtever.com
$body =~ s/[^a-zA-Z0-9]//g; #Get rid of characters that arent alphanumeric
$body = lc($body); #Make $body all lower case.
if($body eq "?" or $body eq "help")
{
sendHelp($from);
}
elsif($allowFaffage == 1)
{
if($body eq "random")
{
`echo 'r' > $device`;
}
elsif($body eq "white")
{
`echo w > $device`;
}
elsif($body eq "off")
{
`echo o > $device`;
}
elsif($body eq "fade")
{
`echo i > $device`;
}
}
#Admin things... Enable and disable the lamp
#so people can't wake you in your sleep.
if($from eq 'admin@-----')
{
if($body eq "quit")
{
exit();
}
elsif($body eq "enable")
{
$allowFaffage = 1;
&send($from,'Faffing enabled');
}
elsif($body eq "disable")
{
$allowFaffage = 0;
&send($from,'Faffing disabled');
}
}
}
If you’re unfamiliar with PERL, the above script might be pretty horrific, but hopefully the comments in the code will help you work out what’s going on. If you’d like to learn more about programming in PERL, there’s a ton of information on the internet, including the PERL documentation.
Most of the logic is in the msgReceived function, which is executed whenever a message is received. The algorithm is very basic – get the body of the message we just received, then sanitise it, then compare it to various strings until we know what the user wants to happen. Simple… The above code runs locally on my plug computer, next I’ll take you through the code that runs on the server.
The Server
The server is actually running two pieces of code – a PERL script, which is similar to the one above and is used to communicate with the client over Jabber; and a PHP script which generates the user interface, and communicates with the PERL script when users choose to perform an action.
This might seem (and probably is) over complicated, but I’ll quickly explain my reasons for choosing this architecture.
- Firstly, PHP is stateless, i.e. variables aren’t stored between page loads (unless using sessions, or cookies), and usually when a script is finished executing any connections that it has opened are closed. If I were to use just a single PHP script for generating the interface then communicating with the lamp, every time the script is loaded, a new connection to the Jabber serer would have to be made. This slows down the load time of the page, and increases the load on both the web server and the jabber server.
- Secondly, there is a good chance that two requests could be made to the server at the same time, if this were to happen, there would be two attempts to connect to the jabber server with the same user (which isn’t allowed).
To fix this I decided that it would be nice to have a single script that is always connected to the jabber server, and can be communicated with using local Unix sockets.
The PERL
This code is fairly similar to the code above, so I won’t explain too much about it. I’ll jump straight to talking about the well named ‘doyourshiz’ subroutine. Avert your gaze if you are of a nervous disposition.
#!/usr/bin/perl
use Net::Jabber;
use Data::Dumper;
use Socket;
my $server = "-----";
my $port = 5222;
my $username = "-----";
my $password = "-----";
my $client = new Net::Jabber::Client();
my $allowFork = 0;
my $insockpath = "/home/freud/lampsocket";
#Detatch from the terminal if we're allowed to.
if($allowFork == 1 && fork())
{
exit();
}
$client->SetCallBacks(message=>\&msgReceived, onauth=>\&authenticated);
$client->Execute(hostname=>$server,
tls=>0,
username=>$username,
password=>$password,
resource=>"lamp",
register=>0);
sub send { my($to,$body) = @_;
$client->MessageSend(to=>"$to",subject=>"Cheese",body=>"$body");
}
sub msgReceived{
print Dumper @_;
}
sub authenticated{
print "Authenticated";
#We're ready to send messages to the lamp,
#Start listening for incomming comands from
#the local socket.
&doyourshiz();
}
sub doyourshiz{
#Create a socket
socket(INSOCKET, PF_UNIX, SOCK_STREAM, 0) or die("$!");
unlink($insockpath); #Delete the file we're binding to if
#it already exists...
bind(INSOCKET,sockaddr_un($insockpath)) or die("$!");
listen(INSOCKET,SOMAXCONN) or die("$!");
#accept blocks (makes the program wait) until someone
# connects to the socket.
for( ;accept(Client,INSOCKET); close Client)
{
open(INPUT, "<&Client");
#Open the client's input stream to 'INPUT' then
# read from INPUT and send it to the lamp until we've
# read everything that was sent to us.
while(defined($line = <INPUT>))
{
chomp $line;
&send('lamp address',$line);
}
close(INPUT);
#close the input. (the Client connection is closed in the for loop
# definition above.).
}
}
OK… The doyourshiz subroutine contains the main application loop. To start with it opens a Unix Socket, which appears as a special file on your file system (in the example above it has the path /home/freud/lampsocket). Any applications wanting to communicate with the above script can connect to that socket by using that special file (this will become clearer when I show you the PHP). The socket is identified inside the script by it’s identifier INSOCKET (the first argument to the socket command).
Whenever anything connects to that socket the script just relays whatever is sent to it to the lamp via Jabber. Note that at the moment, the code above will only actually accept one connection at a time (which I realise conflicts with one of my requirements above), ideally the code within the for loop above would be executed in a new thread to allow new users to connect. I’ll probably implement this soon…
Now you’ve seen the server’s PERL (which is by far the most complicated bit), it’s time to look at the PHP code.
The PHP
I started by writing a wrapper class that handled connecting and sending to the socket for me, keeping the main bulk of PHP free from socket code.
<?php
class HSocket
{
private $socket;
public function __construct($path)
{
$this->socket = socket_create(AF_UNIX,SOCK_STREAM,0);
socket_connect($this->socket,$path);
}
public function __destruct()
{
socket_close($this->socket);
}
public function send($data)
{
socket_write($this->socket,$data,strlen($data));
}
}
?>
As you can see, the code is fairly simple. It’s pretty much the same as the PERL code above. The constructor takes an argument, which is the path to the socket you’re connecting to, and (dis)connecting is all done for you when instantiating and destroying objects from that class.
When sending data to the socket, we use the command socket_write, which takes three parameters – the socket you’re sending to, the data to be sent, and the size of the data in bytes. If you’d like to read more about socket programming in PHP check out the PHP Documentation and for those of you who would like to learn more about PHP, a good place to start is the PHP Manual, and of course Google.
Finally, the code that the user actually executes is below. This contains the buttons the user presses and it also processes the user’s actions to determine what values should be sent to the PERL script (and ultimately the lamp).
<?php
if(isset($_GET['do']))
{
include('HSocket.php');
$s = new HSocket("/home/freud/lampsocket");
//This switch statement makes sure we don't send anything
//malicious to the lamp.
switch(strtolower($_GET['do']))
{
case "off":
case "white":
case "random":
case "fade":
$s->send($_GET['do']);
break;
}
}
?>
<html>
<head>
<title>
Light Controls
</title>
</head>
<body>
<h1>Mood Lamp</h1>
<form action='index.php' method='get'>
<input name='do' type='submit' value='Off' />
<input name='do' type='submit' value='White' />
<input name='do' type='submit' value='Random'/>
<input name='do' type='submit' value='Fade' />
</form>
</body>
</html>
You’ll probably be overjoyed to notice that the code above is really quite simple. It creates a socket with the same path as was used in the PERL script above, and then sends data to it depending on the value of the ‘do’ GET variable. The switch statement helps to make sure that a malicious user can’t send data to the lamp that could be used to compromise either the server, plug, or lamp.
And there you have it. If you’d like to see the code in action, check out the online control panel. Currently you can’t see the state of the light (so you’ll just have to trust me that its working), but my next step is to get a webcam pointed at it, and also to add a feature to allow users to pick a specific colour for the lamp.
If you have any questions, or any suggestions as to what I can do next, please feel free to comment below. I’ll try to respond as soon as I can. If you want to steal any of the code above, then go ahead (it would be nice if you linked back to this post).
0 Responses
Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.