Sending and Receiving Second Life Data to Pachube

Basic Approach

If you're already familiar with LSL you may only need to know the following. To transfer data between second life and pachube we use the following code to define two keys inside a timer event.

timer (){

key http_request_id_put = llHTTPRequest(http://www.pachube.com/api/FEED_NUMBER.csv?key=API_NUMBER, [ HTTP_METHOD, "PUT"], csv); /

key http_request_id_get = llHTTPRequest(http://www.pachube.com/api/FEED_NUMBER.csv?key=API_NUMBER, [], "");    

}

After this we use an http_response() event to determine the type of information. If you haven't coded in LSL before and this doesn't mean anything, everything is explained below and packaged into a working example.

The aim of this tutorial is to set out how to write a simple script in LSL, the Second Life Scripting language that connects to a pachube feed. The script will gather a number of variables from second life and add them to a pachube feed. It will also display a single variable read from another feed (in this tutorial that will be datasteam 38, from feed 1136 which happens to be the wind direction as measured in Plymouth).

Understanding Programming in Second Life with LSL

I'm going to assume at the start of this tutorial that you don't have any prior knowledge of second life scripting. This tutorial will be quite hard if you haven't had any experience of second life at all, so I suggest spending a few minutes familiarizing yourself with the interface and controls.

There are lots of tutorials available to start to learn to script in second life, many are located, alongside the essential language documentation at the LSL Portal of the second life wiki (http://wiki.secondlife.com/wiki/LSL_Portal).

To create scripts in Second Life you first need to teleport to a sandbox area, this is an area where you can create objects freely. Scripts are introduced into the second life environment by being attached to objects. To create a script you must first create an object. Right click on the environment when you have teleported to a sandbox region of second life and hit create. This will bring up a panel where you can add an object.

Its probably best to keep this object simple, for this tutorial I made a cone.

After we have this object, we can click on the content tab and start adding scripts to it. The default script for a new object is a simple "Hello World" script, so once that has loaded in the text editor we can delete it.

Time to Start Adding Code

To being we need to specify all the global variables we're going to use. Firstly we're going to add the variables for connecting to pachube. You will need to add your pachube api and the number of the feed you can write to.

//these variables are concatenated later to make the urls second life receives data from and writes data to., e.g. (http://www.pachube.com/api/WRITE_FEED.csv?key=YOUR_API_KEY)

string server = "http://www.pachube.com/api/";
string api_key = "YOUR_PACHUBE_API_KEY"; // your API key, found at pachube.com under your login settings


integer write_feed = YOUR_FEED_NUMBER; //
integer receive_feed = 1136; // this is the feed you wish to take data from, in this case building sensor data from i-DAT in plymouth
string data_type = ".csv"; // this tutorial parses data from second life as .csv. Parsing as .xml is also possible with additional syntax.
integer datastream_id = 38;

These next variables specify keys and variables used by second life. Their purpose will be clearer in a bit.

// keys to define incoming and outgoing information
key http_request_id_put;
key http_request_id_get;

float http_period = 60.0;

//these are used later for finding the postion of the nearest avatar;
vector lastAvPos = <0,0,0>;
integer touchCount = 0;
string lastAvName = "";
string soughtName = "";
float distanceToNearestAvatar = 99999;
string nearestAvatarName = "";

Lets Get Things Talking

To make things happen in second life we have to create events, which are contained by a state. In this script all our events will take place in a single state called default. All the following code will be contained in state as shown below.

default
{

//Events Go Here

}

The state_entry and touch_start Events

The first thing the state needs is a event explaining what happens when the object appears. In this case we tell the object to listen on specific chat channels,establish how often it will talk to pachube and remind the user of the basic instructions.

// state_entry is what happens when the object created enters the second life environment

state_entry()
    {
        // this code is telling the object we created to listen on channels one and two. You can communicate with by speaking on these channels.
        // listen on channel zero for any chat spoken by anyone. replace NULL_KEY with llGetOwner() to listen only to the object owner.
        llListen(14,"",NULL_KEY,"");
        llListen(15,"",NULL_KEY,"");
       
llSetTimerEvent(http_period); // generate a timer event every n secs

        llSensorRepeat("", NULL_KEY, AGENT, 20, PI, 15);
        llOwnerSay("say a number on channel 1 to set feed_id, and on channel 2 to set datastream id.  use channel 3 to set avatar name to monitor.  making requests every "+(string)http_period);

    }

The next part is an event which describes what happens when the object is touched.

// touch_start is the process that starts when the object created is touched

    touch_start(integer total_number)
    {
        soughtName = llDetectedName(0);
        llWhisper(0,"touched! sought avatar is now: "+soughtName);
        touchCount = touchCount + 1;

    }

Contacting Pachube

The next event is the timer event. In this event the script establishes some of the variables we wish to send to pachube. It also gets gets all the details for contacting both the feed we intend to write to and the one we intend to read from.

timer()
    {
        vector pos = llGetPos();  // this gets the vector position of the object
        vector windVec = llWind(<0,0,0>);   // this gets the second life wind vector
        float windMagnitude = llVecMag(windVec); // this works out the magniture of the second life wind
        float x = lastAvPos.x; // this finds the x, y, and z positions of the last avatar to touch the object
        float y = lastAvPos.y;
        float z = lastAvPos.z;

        string feed_url = server + (string)write_feed + data_type + "?key=" + api_key; // this line generates the url of the feed your going to write to (e.g http://www.pachube.com/api/WRITE_FEED.csv?key=YOUR_API_KEY )
        string remote_url = server + (string)receive_feed + data_type +  "?key=" + api_key; // and again for the incoming feed


        // The string csv is the data being written to pachube as comma seperated variables
        string csv =
        (string)x+","+
        (string)y+","+
        (string)z+","+
        (string)lastAvName + ","+
        (string)touchCount + ","+
        (string)windVec.x+ ","+
        (string)windVec.y + ","+
        (string)windVec.z + ","+
        (string)windMagnitude+ ","+
        (string)distanceToNearestAvatar + ","+  // we calculate this below
        (string)nearestAvatarName + ","+
        (string)pos.x + ","+
        (string)pos.y + ","+
        (string)pos.z;

       http_request_id_put = llHTTPRequest(feed_url, [ HTTP_METHOD, "PUT"], csv); // this sends the data to the server url

       http_request_id_get = llHTTPRequest(remote_url, [], "");     // and for the incoming data feed


    }

Handling the Response

The next event handles the response from pachube. If second life receives incoming data from pachube it will parse it and tell the creator of the object about the requested variable. Otherwise second life assumes that the incoming data is the status of the server for the out going data. In this case second life announces the status above the object.

http_response(key request_id, integer status, list metadata, string body)
    {

        if (request_id == http_request_id_get){ // if the request_id matches that of incoming information list variables

            list remote_variables = llParseString2List(body,[","],[]);

            integer v = llList2Integer(remote_variables,6);
       //llSetText("received remote data: " + body + "last variable is: " + (string) v, <0,0,1>, 1);  // this line will produce text above the object showing all incoming data
       
       llOwnerSay ("The Variable Requested is " + (string) v);
        } else {

       llSetText("updated pachube, status: " + (string) status, <0,0,1>, 1);    // otherwise give the status of the out-going connection to pachube    }

}






}

A Little Extra Code

This chunk of code uses a sensor event (which returns the number of avatars in the surrounding area) to work out which avatar is closest. This is included to show an example of how information might be gathered from the Second Life Universe.

    sensor(integer total_number)  //
    {
   
      float distanceToNearestAvatarSoFar = 9999999;                      // define the distance to the nearest avatar as large
      string nearestAvatarNameSoFar = "";
      integer i;
      for (i = 0; i < total_number; i++)                                 // starts a loop of the number of avatars detected
      {
        key id=llDetectedKey(i);                                           // finds their key
        vector size = llGetAgentSize(id);                                // works out their vector size using the key
        integer agentInfo = llGetAgentInfo(id);
        vector avCentre = llDetectedPos(i);
        float distanceToThisAvatar = llVecDist(llGetPos(), avCentre);    // works out the distance to avatar i
 
        if (distanceToThisAvatar < distanceToNearestAvatarSoFar)         // checks to see whether avatar i is the the closest
        {   
            distanceToNearestAvatarSoFar = distanceToThisAvatar;        // changes the distance to the nearest avatar to match the new closer avatar
            nearestAvatarNameSoFar = llDetectedName(i);                    //  stores the name of the nearest avatar
        }
        vector upPos = avCentre + <0, 0, (size.z / 2.0)>;
        vector downPos = avCentre - <0, 0, (size.z / 2.0)>;

        if (llDetectedName(i)==soughtName)
        {
            lastAvName = soughtName;
            lastAvPos = avCentre;
        }
    }

    distanceToNearestAvatar = distanceToNearestAvatarSoFar;            // all the avatars have been scanned so this stores the final location and name
    nearestAvatarName = nearestAvatarNameSoFar;
}

Changing the Feed Your Getting Data From

Lastly this listen() event allows the owner of the object (in this case the person who created it) to talk to it through different chat channels. For this script the object listens for integers. Messages it receives on channel 14 set the feed number the object is listening to and messages on channel 15 set the data feed.
The script displays its updates in the chat box.

    // the listen() event allows communication with the object
            //


            // on channel one: set the patchube feed you want to receive data from
            // on channel two: set the data stream within that feed to obtain updates for one variable

           
    // in second life you change the chat channel you're on by prefixing the message with a forward slash and a number
            // e.g  To say "Hello World", on channel two: "/2 Hello World!"
           
    listen(integer channel, string name, key id, string message)
    {

        llOwnerSay("got msg on channel " + (string)channel + " val: " + message);
        if (channel == 14)
        {
            integer newval = (integer)message;
            receive_feed = newval;
            llSay(0,"Receiving Data from Feed "+(string)receive_feed);
        }
        if (channel == 15)
        {
            integer newval = (integer)message;
            datastream_id = newval;
            llSay(0,"Datastream: "+(string)datastream_id);
        }

    }

Everything Finished

If all that code is added to the cone, then you should end up with an object that looks and talks like this:

All the Code Together

If you want to copy all the code in one go, take it from here.

// keys to define incoming and outgoing information
key http_request_id_put;
key http_request_id_get;

//last position of sought avatar
vector lastAvPos = <0,0,0>;
integer touchCount = 0;
string lastAvName = "";
string soughtName = "";
float distanceToNearestAvatar = 99999;
string nearestAvatarName = "";

//these varaibles are concatenated later to make the urls second life receieves data from and writes data to., e.g. (http://www.pachube.com/api/WRITE_FEED.csv?key=YOUR_API_KEY)

string api_key = "YOUR_PACHUBE_API_KEY"; // your API key, found at pachube.com under your login settings

string server = "http://www.pachube.com/api/";
integer write_feed = 1149;
integer receive_feed = 1044; // this is the feed you wish to take data from, in this case building sensor data from i-DAT in plymouth
string data_type = ".csv"; // this tutorial parses data from second life as .csv. Parsing as .xml is also possible with additional syntax.
integer datastream_id = 10;





default
{
   
// state_entry is what happens when the object created enters the second life environment

state_entry()
    {
        // this code is telling the object we created to listen on channels one and two. You can communicate with by speaking on these channels.
        // listen on channel zero for any chat spoken by anyone. replace NULL_KEY with llGetOwner() to listen only to the object owner.
        llListen(14,"",NULL_KEY,"");
        llListen(15,"",NULL_KEY,"");
       
        float http_period = 60.0;
        llSetTimerEvent(http_period); // generate a timer event every n secs

        llSensorRepeat("", NULL_KEY, AGENT, 20, PI, 15);
        llOwnerSay("say a number on channel 1 to set feed_id, and on channel 2 to set datastream id.  use channel 3 to set avatar name to monitor.  making requests every "+(string)http_period);

    }


// touch_start is the process that starts when the object created is touched

    touch_start(integer total_number)
    {
        soughtName = llDetectedName(0);
        llWhisper(0,"touched! sought avatar is now: "+soughtName);
        touchCount = touchCount + 1;

    }

    //called repeatedly by the LSL engine if you register a timer event
    //here we will make an http request to get some info from neill's feed-simplifying proxy
    //we also piggyback some data to send (avatar coords) as request params on the same request (so update rates of both are tightly coupled for now)
    timer()
    {
        vector pos = llGetPos();                   // this gets the vector position of the object
        vector windVec = llWind(<0,0,0>);         // this gets the second life wind vector
        float windMagnitude = llVecMag(windVec);  // this works out the magniture of the second life wind
        float x = lastAvPos.x;                      // this finds the x, y, and z positions of the last avatar to touch the object
        float y = lastAvPos.y;
        float z = lastAvPos.z;

        string feed_url = server + (string)write_feed + data_type + "?key=" + api_key; // this line generates the url of the feed your going to write to (e.g http://www.pachube.com/api/WRITE_FEED.csv?key=YOUR_API_KEY )
        string remote_url = server + (string)receive_feed + data_type +  "?key=" + api_key; // and again for the incoming feed


        // The string csv is the data being written to pachube as comma seperated variables
        string csv =
        (string)x+","+
        (string)y+","+
        (string)z+","+
        (string)lastAvName + ","+
        (string)touchCount + ","+
        (string)windVec.x+ ","+
        (string)windVec.y + ","+
        (string)windVec.z + ","+
        (string)windMagnitude+ ","+
        (string)distanceToNearestAvatar + ","+             // we calculate this below
        (string)nearestAvatarName + ","+
        (string)pos.x + ","+
        (string)pos.y + ","+
        (string)pos.z;

       http_request_id_put = llHTTPRequest(feed_url, [ HTTP_METHOD, "PUT"], csv); // this sends the data to the server url

       http_request_id_get = llHTTPRequest(remote_url, [], "");              // and for the incoming data feed


    }

    http_response(key request_id, integer status, list metadata, string body) // this event handles the response from Pachube
    {

        if (request_id == http_request_id_get){                                    // if the request_id matches that of incoming information list variables

            list remote_variables = llParseString2List(body,[","],[]);

            integer v = llList2Integer(remote_variables,datastream_id);
       //llSetText("received remote data: " + body + "last variable is: " + (string) v, <0,0,1>, 1);  // this line will produce text above the object showing all incoming data
       
       llOwnerSay ("The Variable Requested is " + (string) v);
        } else {

       llSetText("updated pachube, status: " + (string) status, <0,0,1>, 1);    // otherwise give the status of the out-going connection to pachube    }

}

}   //////////


//  This section uses a sensor event, which returns the number of local avatars detected to find out who is closest

    sensor(integer total_number)  //
    {
   
      float distanceToNearestAvatarSoFar = 9999999;                      // define the distance to the nearest avatar as large
      string nearestAvatarNameSoFar = "";
      integer i;
      for (i = 0; i < total_number; i++)                                 // starts a loop of the number of avatars detected
      {
        key id=llDetectedKey(i);                                           // finds their key
        vector size = llGetAgentSize(id);                                // works out their vector size using the key
        integer agentInfo = llGetAgentInfo(id);
        vector avCentre = llDetectedPos(i);
        float distanceToThisAvatar = llVecDist(llGetPos(), avCentre);    // works out the distance to avatar i
 
        if (distanceToThisAvatar < distanceToNearestAvatarSoFar)         // checks to see whether avatar i is the the closest
        {   
            distanceToNearestAvatarSoFar = distanceToThisAvatar;        // changes the distance to the nearest avatar to match the new closer avatar
            nearestAvatarNameSoFar = llDetectedName(i);                    //  stores the name of the nearest avatar
        }
        vector upPos = avCentre + <0, 0, (size.z / 2.0)>;
        vector downPos = avCentre - <0, 0, (size.z / 2.0)>;

        if (llDetectedName(i)==soughtName)
        {
            lastAvName = soughtName;
            lastAvPos = avCentre;
        }
    }

    distanceToNearestAvatar = distanceToNearestAvatarSoFar;            // all the avatars have been scanned so this stores the final location and name
    nearestAvatarName = nearestAvatarNameSoFar;
}
     /////



    // the listen() event allows communication with the object
            //


            // on channel one: set the patchube feed you want to receive data from
            // on channel two: set the data stream within that feed to obtain updates for one variable

           
    // in second life you change the chat channel you're on by prefixing the message with a forward slash and a number
            // e.g  To say "Hello World", on channel two: "/2 Hello World!"
           
    listen(integer channel, string name, key id, string message)
    {

        llOwnerSay("got msg on channel " + (string)channel + " val: " + message);
        if (channel == 14)
        {
            integer newval = (integer)message;
            receive_feed = newval;
            llSay(0,"Receiving Data from Feed "+(string)receive_feed);
        }
        if (channel == 15)
        {
            integer newval = (integer)message;
            datastream_id = newval;
            llSay(0,"Datastream: "+(string)datastream_id);
        }

    }
}

Re: Sending and Receiving Second Life Data to Pachube

This worked perfectly and incredibly easily for me, and I'm pretty bad at examples generally.

Thanks very much! I now have a working system, and am playing with the script, modifying for my own purposes. I'm very excited about what I can do with all of this!!