LEGO Mindstorms. What a great reason to dig out that box of LEGO that you haven't touched since your childhood. What? You're still in your childhood? Even better! When I found out that LEGO was bringing out a programmable brick - the RCX, I went straight to Hamley's on Regent Street and handed over some well-earned cash for a large box containing the RCX—a yellow lump of plastic with buttons, an LCD, and connectors for sensors and motors—an infrared (IR) port plus an IR tower to connect to the serial port of your PC, and a battery compartment. [1] The box also contained a couple of motors, touch and light sensors, and various LEGO Technic parts. The RCX is pictured in Figure 8-1.
There are plenty of ways of interacting with the RCX, also known as the "programmable brick". The Mindstorms Robotics Invention System (RIS) set comes with Windows software with which you can build programs by moving blocks of logic around graphically on the screen and chaining them together ("turn sensor 1 on" ... "while this condition is true" ... "set motor 2 backwards" ... "turn motor 3 off" and so on). In addition, various efforts on the parts of talented individuals have come up with many different ways to program the RCX. O'Reilly's "The Unofficial Guide to LEGO Mindstorms Robots" tells you all you need to know, and gives you the big picture. What's important to know for this recipe is the following:
There are two approaches to programming an RCX. One approach is to write a program on your PC, and download it to the RCX once complete, thence start and stop the program using the buttons on the RCX itself. The download is done over the IR connection.
The other approach is to control the RCX directly from a program that you write and execute on your PC, sending control signals and receiving sensor values over the IR connection.
Both approaches have their merits. How appropriate each one is boils down to one thing: connections. On the one hand, building autonomous machines that find their way around the kitchen to scare the cat and bring you a sandwich calls for the first approach, where, once you've downloaded the program to the RCX, you can dispense with any further connection with your PC because the entire logic is situated in your creation. On the other hand, if you want to build a physical extension to a larger system that, for example, has a connection to the Internet, the second approach is likely to be more fruitful, because you can essentially use the program that runs on your PC and talks to the RCX over the IR link as a conduit, a proxy, to other programs and systems that can be reached over the network.
We're going to use the second approach.
The RIS software that comes as standard centers around an ActiveX control. While there are plenty of ways to talk to the RCX without using this control (the book mentioned earlier describes many of these ways), the features offered by the control—Spirit.ocx— are fine for many a project. And with Perl's Win32::OLE module, we can interact with this ActiveX control without having to resort to Visual Basic.
Everyone knows that one of the virtues of a programmer is laziness. We're going to extend this virtue (perhaps a little too far) and enhance it with a hacker's innate ability to combine two favorite pastimes—programming and playing with LEGO—to build contrived but fun devices.
We've a filter coffee machine that sits in my wife Sabine's study. My office is in the basement. Rather than traipse all the way upstairs to find out there's no coffee left in the pot, I decided to put my RCX to good use and get it to tell me, via Jabber, whether the pot had enough for another cup or not. That way, I didn't have to leave my keyboard to find out.
The idea is that we connect a light sensor to the RCX, and use that to "see" the coffee. The coffee pot is made of glass, and so allows light to pass through it. If there's coffee in the pot, no light passes through. The coffee effectively "gets in the way of" the light. There we have a simple binary switch. No (or a small amount of) light measured: there's coffee in the pot. Some (or a larger amount of) light: there's no coffee in the pot. We want to be able to push the coffee state to all interested parties in a way that their off the shelf Jabber clients can easily understand and display.
Figure 8-2 shows our LEGO Mindstorms device in action. The brick mounted on the gantry which extends to the glass coffee pot is our light sensor; a wire runs from it back to the connector on the RCX. Behind the RCX is the IR tower which is connected to Sabine's PC.
Remembering that <presence/> elements are a simple way of broadcasting information about availability, and contain a <status/> tag to describe the detail or context of that availability in free-form text, [2] we have a perfect mechanism that's ready to be used. What's more, most, if not all, of the off the shelf Jabber client implementations will display the content of the <status/> tag next to the JID to which it applies, in the client user's roster. Figure 8-2 shows how the <status/> tag content is displayed—as a hovering "tooltip"—in WinJab.
Here's what we need to do:
We need to set the RCX up, with the light sensor, near enough to the coffee machine to take reliable and consistent light readings. Luckily the serial cable that comes with the Mindstorms set and connects to the IR tower is long enough to stretch from Sabine's computer over to within the infrared line-of-sight to the RCX.
There are bound to be differences in ambient light, sensitivity of the light sensor, and how strong you make your coffee. So we need a way of calibrating the setup, so that we can find the appropriate "pivot point" light reading value that lies between the two states of coffee and no-coffee.
We need a connection to a Jabber server, and a client account there. We can set one up using the reguser script from the section called User Registration Script in Chapter 6. We also need the script to honor presence subscriptions and unsubscriptions from users who want to be informed of the coffee state.
Once the RCX has been set up, the sensor calibrations taken, and the connection has been made, we need to constantly monitor the light sensor on the RCX at regular intervals. At each interval we determine the coffee state by comparing the value received from the sensor with the pivot point determined in the calibration step, and send any change in that state as a new availability <presence/> element containing an appropriate description in the <status/> tag.
We're going to use Perl, and the Net::Jabber libraries, to build the script. Perl allows us a comfortable way to interact with an ActiveX control, through the Win32::OLE module. It's Let's dive straight in.
We first declare the packages we're going to use. As well as Net::Jabber and Win32::OLE, we're going to use Getopt::Std which affords us a comfortable way of accepting and parsing command line switches. We also want to use the strict pragma, which should keep us from making silly coding mistakes by not allowing undeclared variables and the like.
We specify the "Client" string on our usage declaration for the Net::Jabber package to specify what should be loaded. The package is a large and comprehensive set of modules, and only some of those are relevant for what we wish to do in our script—build and work with a Jabber client connection. Other module sets are pulled in by specifying "Component" or "Server".
use Net::Jabber qw(Client); use Win32::OLE; use Getopt::Std; use strict;
We're going to allow the command line switches -l and -s. This is what the switches do:
When we run the script for the first time, we need to perform the calibration, and read values from the sensor to determine a mid point value, above which signifies the presence of light, and therefore the absence of coffee, and below which signifies the absence of light, and therefore the presence of coffee.
If we specify no switch, the script will start up automatically in calibration mode.
Figure 8-3 shows the script in calibration mode. The values displayed, one each second, represent the values read from the light sensor. When the sensor was picking up lots of light, the values were 60. When I moved the sensor in front of some dark coffee, the values went down to around 45. Based upon this small test, I would set the pivot point value to 50.
Once we've determined a pivot point value, we run the script "for real" and tell it this pivot value with the -l ("light pivot"):
C:\temp> <userinput>perl coffee.pl -l 50</userinput>
The RCX, shown in Figure 8-1, has three connectors to which you can attach sensors. They're the three grey 2x2 pieces, with the legends "1", "2" and "3", near the top end of the brick. Any sensor can be attached to any of the three connectors. The script assumes you've attached the light sensor to the one marked "1", which internally is "0" (don't you just love computer science?). If you attach it to either of the other two, you can specify the connector using the -s ("sensor") with a value of 1 (for the middle connector) or 2 (for the rightmost connector), like this:
C:\temp> <userinput>perl coffee.pl -l 50 -s 2</userinput>
You can specify the -s switch when running in calibration and in normal modes.
Here's where the switches are defined with the Getopt::Std function:
my %opts;
getopt('ls', \%opts);
Next come a raft of constants, describing the script's Jabber relationship (the server it will connect to, and the user, password and resource it will connect with), the representation of the two states of "coffee" and "no coffee" (which will be used to determine the content of the <status/> tag sent along inside any <presence/> element emitted), the identification of the connector to which the light sensor is attached, and the polling granularity of the sensor poll / presence push loop described earlier (measured in seconds).
use constant SERVER => "merlix.dyndns.org";
use constant PORT => 5222;
use constant USERNAME => "coffee";
use constant PASSWORD => "pass";
use constant RESOURCE => "perlscript";
use constant NOCOFFEE => 0;
use constant COFFEE => 1;
use constant SENSOR => defined($opts{'s'}) ? $opts{'s'} : 0;
use constant GRAIN => 1;
The last part of the script's setup deals with the coffee state:
my $current_status = -1; my @status; $status[NOCOFFEE] = 'xa/coffeepot is empty'; $status[COFFEE] = '/coffee is available!';
We use a two-element array @status to represent the two possible coffee states. The value of each array element is a two-part string, each part separated by a slash ("/"). Each of these parts will be transmitted in a <presence/> element, where the first part (which is empty in the element representing the COFFEE state) will represent the presence <show/> value, and the second part will represent the presence <status/> value. Example 8-5 shows what a <presence/> element looks like when built up with values to represent the NOCOFFEE state.
Example 8-5. A <presence/> element representing the NOCOFFEE state
<presence> <show>xa</show> <status>coffeepot is empty</status> </presence>
Most Jabber clients use different icons in the roster to represent different <show/> values. So we use different values here ("xa" for no coffee, and ""—blank, which represents "online" or "available"— for coffee) to trigger the icon change.
Whenever we need to talk to the RCX, some initialization via the ActiveX control is required. That's the same whether we're going to calibrate or poll for values. The setup_RCX() function takes a single argument—the identification of which connector the light sensor is connected to—and performs the initialization, which is described later in the section called setup_RCX(). The function returns a handle on the Win32::OLE object that represents the ActiveX control, which in turn represents the RCX via the IR tower.
my $rcx = &setup_RCX(SENSOR);
If the -l flag is not specified, it means we're going to be running calibration. So we call the calibrate() function to do this for us. We pass the RCX handle (in $rcx) so the calibration can run properly.
# Either calibrate if no parameters given, or
# run with the parameter given as -l, which will
# be taken as the pivot between coffee and no-coffee.
&calibrate($rcx) unless defined($opts{'l'});
As with the setup_RCX() function, calibrate() is described later.
The calibration mode will be terminated by ending the script with Ctrl-C, so the next thing we come across is the call to the function set_status() which represents the first stage in the normal script mode; set_status() is used to determine the initial coffee status.
A value is retrieved by calling the ActiveX control's Poll() function. (Table 8-1 lists and described the ActiveX control's functions and properties used in this script.) We specify that we're after a "Sensor Value" (the 9 as the first argument) from the sensor attached to the connector indicated by the SENSOR constant.
# Determine initial status (will be either 0 or 1) my $s = &set_status($rcx->Poll(9, SENSOR));
The value retrieved—it's going to be something along the lines of one of the values displayed when the script was run in calibration mode—is passed to set_status() which determines whether the value is above or below the pivot value, and whether the "new" status is different to the current one. If it is (and it this case, it will be, because in this first call, the value of $current_status is set to -1, which represents neither the COFFEE nor the NOCOFFEE state) that status will be returned, otherwise undef will be returned.
Table 8-1. RCX "Spirit" ActiveX control properties and functions used
| Property/Function | Description |
|---|---|
| Poll(SOURCE, NUMBER) | Retrieves information from the RCX. In this script, the value for the SOURCE argument is always 9, which represents a "Sensor Value", i.e. a value measured at a sensor, as opposed to an internal RCX variable or a timer, for example. The NUMBER argument represents the connector to which the sensor we want to read is attached. |
| SetSensorMode(NUMBER, MODE [, SLOPE]) | The sort of value returned from a sensor is determined with this function. As with Poll() and SetSensorType(), NUMBER represents the sensor connector. With the MODE argument you can determine the sensor mode: Raw (analogue) data (0), Boolean (1), Transitional (2), Periodic (3), Percentage (4), Celcius (5), Farenheit (6), or Angle (7). The SLOPE argument qualifies the Boolean mode by specifying how True and False are to be determined. |
| SetSensorType(NUMBER, TYPE) | Use this function to specify the type of sensor that you're going to read values from. The NUMBER argument is the same as for the Poll() and represents the sensor connector. The TYPE argument represents the type of sensor that you want to set: None (0), Switch (1), Temperature (2), Light (3), or Angle (4). |
| property:ComPortNo | This is the serial port to which the ir tower is connected (1 = COM1, 2 = COM2, and so on). |
| property:InitComm | This is more like a function than a property. When invoked, the serial communication port is initialized in preparation for the IR connection to the RCX. |
At this stage, we're ready to connect to Jabber. The call to setup_Jabber() does this for us, returning a handle to the Jabber connection object that we store in $jabber. We will use this handle to send out <presence/> elements later in the script. The $jabber variable contains a reference to a Net::Jabber::Client object, and is the equivalent of the con variable used in the earlier Python scripts (the section called CVS notification in Chapter 7 and the section called Presence-sensitive CVS notification in Chapter 7) to hold the Jabber.Connection object, and cb, holding the ConnectionBean object in the earlier Java script (the section called Dialup system watch in Chapter 7).
my $jabber = &setup_Jabber(SERVER, PORT, USERNAME, PASSWORD, RESOURCE, $s);
As well as passing the constants needed for our client connection to the Jabber server, we pass the initial coffee status, held in $s. We'll have a look at what the setup_Jabber() function does with this initial status a bit later when we get to the function's definition.
The main loop starts here:
# Main loop: check Jabber and RCX
# ===============================
while (1) {
defined($jabber->Process(GRAIN)) or
die "The connection to the Jabber server was broken\n";
my $s = &set_status($rcx->Poll(9, SENSOR));
&set_presence($jabber, $s) if defined $s;
}
The while (1) is a bit of a giveaway. This script won't stop until you force it to stop, with a Ctrl-C. But that's essentially what we want. In the loop, we call the Process() method on our Jabber connection object in $jabber.
Process() is the equivalent of the JabberPy's process() method in the Python scripts. Process() waits around for up to the number of seconds specified as the single argument (or not at all if no argument is specified) for XML to appear on the stream connection from the Jabber server. If complete fragments do appear, callbacks, defined to the connection object, are called with the elements (<iq/>s, <message/>s and <presence/>s) that the fragments represent. This is in the same way as, for example, callbacks are called in Python scripts using the JabberPy libraries. The setup_Jabber(), coming next, is where the callback definition is made.
The Process() method returns undef if the connection to the Jabber server is terminated while waiting for XML. The undef value is dealt with appropriately by ending the script.
The GRAIN constant, set to one second in the script's setup section, is used to specify how long to wait. For the most part, we're not expecting to receive much incoming Jabber traffic. The occasional presence subscription (or unsubscription) request, perhaps (see later), but other than that, the only packets travelling over the connection to the Jabber server will be availability <presence/> packets representing coffee state changes, sent from the script. So normally, this delay of 1 second will take place. That's a comfortable polling interval for our light sensor too. So we do that within the same loop, happy in the knowledge that's it's most likely been a second since the last poll.
Calling the ActiveX control's Poll() again with the same arguments as before ("get a sensor value from the sensor attached to the SENSORth connector"), we pass the value to the set_status() to determine the coffee state. If the state was different from last time (if $s receives a value, and not undef), then we want to emit a <presence/> element to reflect that state. We achieve this by calling the set_presence() function, passing it the connection object and the state.
Here we define the setup_Jabber() function, which is called once to set up the connection to the Jabber server and authenticate with a pre-defined user.
# Set up Jabber client connection, sending intial presence
# --------------------------------------------------------
sub setup_Jabber {
my ($server, $port, $user, $pass, $resource, $initial_status) = @_;
my $connection = new Net::Jabber::Client;
# Connect
my $status = $connection->Connect( hostname => $server,
port => $port );
die "Cannot connect to Jabber server $server on port $port\n"
unless $status;
# Callbacks
$connection->SetCallBacks( presence => \&InPresence );
# Ident/Auth
my @result = $connection->AuthSend( username => $user,
password => $pass,
resource => $resource );
die "Ident/Auth failed: $result[0] - $result[1]\n"
if $result[0] ne "ok";
# Roster
$connection->RosterGet();
# Initial presence dependent upon initial status
&set_presence($connection, $initial_status);
return $connection;
}
First, we instantiate a new Net::Jabber::Client object. Net::Jabber distinguishes between client-based and component-based connections to Jabber; the component-based equivalent of this class is Net::Jabber::Component. The connection() method is passed arguments that specify the hostname and port of the Jabber server to connect to. It returns a zero status if the connection could not be made.
We can register handlers for Jabber elements that are received over the XML stream which is carried via the connection we've just made. Here we are only interested in incoming <presence/> elements—indeed, only those carrying presence subscription or unsubscription requests, as we'll see in the definition of the InPresence() function.
The single method SetCallBacks() does what the collective Jabber.Connection methods setPresenceHandler(), setMessageHandler(), and setIqHandler() do in a single call, taking a list of element types and subroutine references, in the form of a hash.
After registering our callback for <presence/> elements, it's time to authenticate, passing the username, password and resource that we defined in our list of constants at the start of the script. If the authentication is successful, the result of the call to the AuthSend() method is a single string with the value "ok". If not, that value is replaced with an error code and the descriptive text is available in a further string. (This is why we catch the results of a call in an array, called @result). The full list of Jabber error codes and texts are shown in Table 5-3.
Why RosterGet()? We're not subscribing to anyone, and we're not really interested in anything but the values we're polling from our brick. Yes, you guessed it—we want the script to receive and process presence subscription and unsubscription requests, but we aren't sent those by the JSM unless we've requested our roster beforehand. See the section called Request for roster in Chapter 7 for an explanation as to why.
Once we've kicked the JSM into sending any presence requests on to us, the job is almost done. The last thing to do in setting up our Jabber connection is to send our initial availability information. setup_Jabber() receives the initial coffee status as the last argument in the call (in $initial_status), which it now duly passes on to the function that sends a <presence/> element, set_presence(). Along with the initial coffee status, we also send the $connection object that represents the connection to the Jabber server that we've just established (and that is referred to outside of this function with the $jabber variable. This is so the set_presence() function can use the connection handle to send the element down the stream.
This function is used by setup_Jabber() to send the script's (and the coffee's) initial presence. It's also used within the main sensor poll / presence push loop to send further presence packets if the coffee's state changes.
sub set_presence {
my ($connection, $s) = @_;
my $presence = Net::Jabber::Presence->new();
my ($show, $status) = split("/", $status[$s], 2);
$presence->SetPresence( show => $show,
status => $status );
print $status, "\n";
$connection->Send($presence);
}
On receipt of the Jabber connection object and the status, which will be 0 (NOCOFFEE) or 1 (COFFEE), set_presence() constructs a new Net::Jabber::Presence object. This object represents a <presence/> element, upon which we can make method calls to hone the element as we wish. SetPresence() is one of these methods, with which we can set values for each of the <show/> and <status/> tags. We retrieve the values for each of these tags by pulling the strings from the appropriate member of the @status array, as described in the section called Setup.
We print the coffee's status (remember, this function is only called when the status changes, not every time the sensor is polled), and send our newly built <presence/> element off down the XML stream to the Jabber server by passing the presence object as an argument to the Send() method of the connection object in $connection. This works in the same way as the send() function in JabberPy, and the send() function in JabberBeans. And by the diffusive magic of the presence subscription model (see the section called Presence Subscription in Chapter 5), everyone who has subscribed to the script user's presence, and who is available, will receive the coffee status information.
Figure 8-4 shows the status information received in the WinJab client. The string sent in the <status/> tag is shown in the tooltip that appears when the mouse hovers over the "coffee" roster item.
Our presence handler, the callback subroutine InPresence(), exists for a single purpose: to honor requests for subscription and unsubscription to the script user's (and therefore the coffee's) presence. This callback is designed to work in the same way as the presenceCB() callback in the Python recipe described in the section called Presence-sensitive CVS notification in Chapter 7.
However, while the Python JabberPy library hands to the callbacks a Jabber.Connection object and the element to be handled, the Perl Net::Jabber library hands over a session ID and the element to be handled. The session ID is related to functionality for building Jabber servers, functionality that is not yet complete. We can and should ignore it for our script's purposes. What is important is the element to be handled, which appears as the second argument passed to the subroutine, which we collect into the $presence variable from $_[1].
What is common between the two libraries is that the element that is passed to be handled, as the subject, so to speak, of the callback, is an instance of the class that the callback represents. In other words, here we have a callback to handle <presence/> elements, and the element received is an instance of the Net::Jabber::Presence class (just as the element received by a JabberPy presence callback is an instance of the Jabber.Presence class).
# Handle presence messages
# ------------------------
sub InPresence
{
my $presence = $_[1];
my $from = $presence->GetFrom();
my $type = $presence->GetType();
if ($type eq "subscribe") {
print "Subscribe request ($from) ...\n";
$jabber->Send($presence->Reply(type => 'subscribed'));
}
if ($type eq "unsubscribe") {
print "Unsubscribe request ($from) ...\n";
$jabber->Send($presence->Reply(type => 'unsubscribed'));
}
}
With an object in $presence, we can get information from the element using data retrieval methods such as those used here: GetFrom() and GetType(), which extract the values from the from and type attributes of the <presence/> element respectively.
If the <presence/> element type represents a subscription request (type='subscribe'), we unquestioningly honor the request, by sending back an affirmative reply. The Reply() method of the presence object is one of a number of high-level functions that make it very comfortable to turn elements around and send them back. In this case, the method replaces the value of the <presence/>'s to attribute with the value of the from attribute, and preserves any id attribute. It also allows us to pass arguments as if we were calling the SetPresence() method described earlier. Rather than set the <show/> and <status/> tags as we did earlier in the set_presence() function, we merely set the element's type attribute to "subscribed" or "unsubscribed", depending on the request.
So with an incoming <presence/> element in $presence that looks like this:
<presence from='qmacro@jabber.org/office' type='subscribe'
to='coffee@merlix.dyndns.org' id='21'>
calling the Reply() method would cause the element in $presence to change to this: [3]
<presence to='qmacro@jabber.org/office' type='subscribed' id='21'>
Note that the script doesn't ask for a subscription to the user's presence in return. The script isn't interested whether or not the people that have subscribed to its presence are available or not. It's sole job in life is to emit coffee availability.
This function is called once every time the script is started, and is required to initialize the RCX:
sub setup_RCX {
my $sensor = shift;
my $rcx = Win32::OLE->new('SPIRIT.SpiritCtrl.1');
$Win32::OLE::Warn = 0;
$rcx->{ComPortNo} = 1;
$rcx->{InitComm};
$rcx->SetSensorType($sensor, 3);
$rcx->SetSensorMode($sensor, 2);
return $rcx;
}
A Win32::OLE object representing the RCX's ActiveX control "Spirit" is instantiated. A Win32::OLE function is used to suppress warnings, and the RCX is initialized by setting the COM port to COM1 and initializing the serial communiations on that port. The sensor type and mode are set for the light sensor attached to the connector identified by the value passed into the $sensor variable. Table 5-1 shows us that sensor type 3 represents "Light", and sensor mode 2 specifies a "Transitional" measurement mode, the upshot of which is that the values returned on a poll are all within a certain restricted range, which makes it easier to decide on the coffee or no-coffee status.
We return the Win32::OLE RCX object to be used elsewhere in the script for calibration and polling.
The calibrate() function, called if the script is started without the -l switch, simply prints a message, waits for the user to press Enter, and then goes into a a gentle loop, emitting whatever value was polled from the light sensor, so the user can determine the pivot point:
sub calibrate {
my $rcx = shift;
print <<EOT;
Calibration mode.
Note the sensor values and decide on a 'pivot' value
above which 'no coffee' is signified and below which
'coffee' is signified.
End the calibration mode with Ctrl-C.
Press Enter to start calibration...
EOT
<STDIN>;
while (1) {
print $rcx->Poll(9, SENSOR), " ";
sleep 1;
}
}
The output produced from this function can be seen in Figure 8-3.
The set_status() function simply receives the latest light value as polled from the sensor and compares it with the pivot value. If the status so determined (in $new_status) is different from the current status (in $current_status) the current status is updated and returned. Otherwise undef is returned:
sub set_status {
my $val = shift;
my $new_status = $val < $opts{'l'} ? COFFEE : NOCOFFEE;
if ($new_status != $current_status) {
$current_status = $new_status;
return $current_status;
}
else {
return undef;
}
}
If this function returns a status value, a new <presence/> element is generated and emitted by the script. Otherwise, there's no change ("the coffee's still there", or "there's still no coffee!") and nothing happens.
Here's the script in its entirety.
use Net::Jabber qw(Client);
use Win32::OLE;
use Getopt::Std;
use strict;
my %opts;
getopt('ls', \%opts);
use constant SERVER => "merlix.dyndns.org";
use constant PORT => 5222;
use constant USERNAME => "coffee";
use constant PASSWORD => "pass";
use constant RESOURCE => "perlscript";
use constant NOCOFFEE => 0;
use constant COFFEE => 1;
use constant SENSOR => defined($opts{'s'}) ? $opts{'s'} : 0;
use constant GRAIN => 1;
my $current_status = -1;
my @status;
$status[NOCOFFEE] = 'xa/coffeepot is empty';
$status[COFFEE] = '/coffee is available!';
my $rcx = &setup_RCX(SENSOR);
# Either calibrate if no parameters given, or
# run with the parameter given as -l, which will
# be taken as the pivot between coffee and no-coffee.
&calibrate($rcx) unless defined($opts{'l'});
# Determine initial status (will be either 0 or 1)
my $s = &set_status($rcx->Poll(9, SENSOR));
my $jabber = &setup_Jabber(SERVER, PORT, USERNAME, PASSWORD, RESOURCE, $s);
# Main loop: check Jabber and RCX
# ===============================
while (1) {
defined($jabber->Process(GRAIN)) or
die "The connection to the Jabber server was broken\n";
my $s = &set_status($rcx->Poll(9, SENSOR));
&set_presence($jabber, $s) if defined $s;
}
# Set up Jabber client connection, sending intial presence
# --------------------------------------------------------
sub setup_Jabber {
my ($server, $port, $user, $pass, $resource, $initial_status) = @_;
my $connection = new Net::Jabber::Client;
# Connect
my $status = $connection->Connect( hostname => $server,
port => $port );
die "Cannot connect to Jabber server $server on port $port\n"
unless $status;
# Callbacks
$connection->SetCallBacks( presence => \&InPresence );
# Ident/Auth
my @result = $connection->AuthSend( username => $user,
password => $pass,
resource => $resource );
die "Ident/Auth failed: $result[0] - $result[1]\n"
if $result[0] ne "ok";
# Roster
$connection->RosterGet();
# Initial presence dependent upon initial status
&set_presence($connection, $initial_status);
return $connection;
}
sub set_presence {
my ($connection, $s) = @_;
my $presence = Net::Jabber::Presence->new();
my ($show, $status) = split("/", $status[$s], 2);
$presence->SetPresence( show => $show,
status => $status );
print $status, "\n";
$connection->Send($presence);
}
# Handle presence messages
# ------------------------
sub InPresence
{
my $presence = $_[1];
my $from = $presence->GetFrom();
my $type = $presence->GetType();
if ($type eq "subscribe") {
print "Subscribe request ($from) ...\n";
$jabber->Send($presence->Reply(type => 'subscribed'));
}
if ($type eq "unsubscribe") {
print "Unsubscribe request ($from) ...\n";
$jabber->Send($presence->Reply(type => 'unsubscribed'));
}
}
sub setup_RCX {
my $sensor = shift;
my $rcx = Win32::OLE->new('SPIRIT.SpiritCtrl.1');
$Win32::OLE::Warn = 0;
$rcx->{ComPortNo} = 1;
$rcx->{InitComm};
$rcx->SetSensorType($sensor, 3);
$rcx->SetSensorMode($sensor, 2);
return $rcx;
}
sub calibrate {
my $rcx = shift;
print <<EOT;
Calibration mode.
Note the sensor values and decide on a 'pivot' value
above which 'no coffee' is signified and below which
'coffee' is signified.
End the calibration mode with Ctrl-C.
Press Enter to start calibration...
EOT
<STDIN>;
while (1) {
print $rcx->Poll(9, SENSOR), " ";
sleep 1;
}
}
sub set_status {
my $val = shift;
my $new_status = $val < $opts{'l'} ? COFFEE : NOCOFFEE;
if ($new_status != $current_status) {
$current_status = $new_status;
return $current_status;
}
else {
return undef;
}
}
| [1] | There's also a DC power socket for those of us without rechargeable batteries. |
| [2] | See the section called The Presence Element in Chapter 5 for details on the <presence/> element |
| [3] | Remember, the from attribute on elements originating from clients is set by the server, not by the client. |