How To: Interface a light color sensor with Sensorpedia and serve Atom! (guide 3)

We have previously documented how to interface two sensors: an LM34 temperature and a Maxbotix-EZ distance with Sensorpedia.  For this guide we will be switching things up a bit.  Instead of using a Make Controller to communicate with sensors, we will be using another popular DIY/tinkerer microcontroller: the Arduino (duemilanove). Instead of pushing our data to Twitter, we will now be generating an Atom feed and serving it ourselves.

The color sensor we will be working with is the ADJD-S371. First, a note on voltage requirements.  The color sensor is designed to operate at 3.3VDC with a maximum operating voltage of 3.6VDC.  We’ll want to be sure to use the 3.3[V] supply from the Arduino board. Proper calibration of the ADJD-S371 will be left as an exercise for the reader (a half ping-pong works reasonably well as an integrator) .

color_sensor_11

color_sensor

The wires were connected as follows:

  • SCL – serial clock to Arduino analog input 5
  • SDA – the serial data line to analog input 4
  • SLEEP was tied to GND
  • LED was connected to pin 1
  • XCLK – the external clock left floating

For this guide, we will be using C to program our Arduino and C# instead of Python for our server.

First we will need to read in our sensor and output its values over serial. We will be using the wonderful ADJD-S371 code published by Arduino user Marcus to output R,G,B tuples:

//ADJD settings
#define I2C_ADDRESS 0x74       // 7bit

#define REG_CAP_RED       0x06
#define REG_CAP_GREEN     0x07
#define REG_CAP_BLUE      0x08
#define REG_CAP_CLEAR     0x09

#define REG_INT_RED_LO    0x0A
#define REG_INT_RED_HI    0x0B
#define REG_INT_GREEN_LO  0x0C
#define REG_INT_GREEN_HI  0x0D
#define REG_INT_BLUE_LO   0x0E
#define REG_INT_BLUE_HI   0x0F
#define REG_INT_CLEAR_LO  0x10
#define REG_INT_CLEAR_HI  0x11

#define REG_DATA_RED_LO   0x40
#define REG_DATA_RED_HI   0x41
#define REG_DATA_GREEN_LO 0x42
#define REG_DATA_GREEN_HI 0x43
#define REG_DATA_BLUE_LO  0x44
#define REG_DATA_BLUE_HI  0x45
#define REG_DATA_CLEAR_LO 0x46
#define REG_DATA_CLEAR_HI 0x47
#define redPin 3
#define greenPin 5
#define bluePin 6
#define ledPin 11
#define clearCalPin 9

float redFactor=1;
float blueFactor=1;
float greenFactor=1;

//initial darkLevel;
int calibrationDarkness = 0;
byte calibrationRed = 5;
byte calibrationGreen = 5;
byte calibrationBlue = 5;

void setup(void)
{
  Serial.begin(9600);
  pinMode(redPin, OUTPUT);
  pinMode(greenPin, OUTPUT);
  pinMode(bluePin, OUTPUT);
  pinMode(ledPin, OUTPUT);
  pinMode(clearCalPin, INPUT);

  setupADJD();

  // sensor gain setting (Avago app note 5330)
  // CAPs are 4bit (higher value will result in lower output)
  set_register(REG_CAP_RED, 0x01);
  set_register(REG_CAP_GREEN, 0x01);
  set_register(REG_CAP_BLUE, 0x01);
  set_register(REG_CAP_CLEAR, 0x01);

  //calibrate led
  digitalWrite(redPin,HIGH);
  digitalWrite(bluePin,HIGH);
  digitalWrite(greenPin,HIGH);
  int ledGain = getColorGain();
  set_gain(REG_INT_RED_LO,ledGain);
  set_gain(REG_INT_GREEN_LO,ledGain);
  set_gain(REG_INT_BLUE_LO,ledGain);
  performMeasurement();
  int red=get_readout(REG_DATA_RED_LO);
  int green=get_readout(REG_DATA_GREEN_LO);
  int blue=get_readout(REG_DATA_BLUE_LO);
  digitalWrite(redPin,0);

  digitalWrite(bluePin,0);
  digitalWrite(greenPin,0);
  int m=2000; //bigger anyway
  m=min(m,red);
  m=min(m,green);
  m=min(m,blue);
  redFactor=((float)m*255.0)/(1000*(float)red);
  greenFactor=((float)m*255.0)/(1000*(float)green);
  blueFactor=((float)m*255.0)/(1000*(float)blue);

}

void loop() {

  digitalWrite(redPin,0);
  digitalWrite(bluePin,0);
  digitalWrite(greenPin,0);

  int clearGain = getClearGain();
  set_gain(REG_INT_CLEAR_LO,clearGain);
  int colorGain = getColorGain();
  set_gain(REG_INT_RED_LO,colorGain);
  set_gain(REG_INT_GREEN_LO,colorGain);
  set_gain(REG_INT_BLUE_LO,colorGain);

  int cc = 0;
  int red=0;
  int green=0;
  int blue=0;
  for (int i=0; i<4 ;i ++) {
    performMeasurement();
    cc +=get_readout(REG_DATA_CLEAR_LO);
    red +=get_readout(REG_DATA_RED_LO);
    green +=get_readout(REG_DATA_GREEN_LO);
    blue +=get_readout(REG_DATA_BLUE_LO);
  }
  cc/=4;
  red /=4;
  green /=4;
  blue /=4;

  float rv = (float)red*redFactor;
  float gv = (float)green*greenFactor;
  float bv = (float)blue*blueFactor;
  int r = rv;
  int g = gv;
  int b = bv;
  analogWrite(redPin,r);
  analogWrite(bluePin,g);
  analogWrite(greenPin, b);

  Serial.print(r);
  Serial.print(",");
  Serial.print(g);
  Serial.print(",");
  Serial.println(b);

  //delay(60000);
  delay(1000);
}

int getClearGain() {
  int gainFound = 0;
  int upperBox=4096;
  int lowerBox = 0;
  int half;
  while (!gainFound) {
    half = ((upperBox-lowerBox)/2)+lowerBox;
    //no further halfing possbile
    if (half==lowerBox) {
      gainFound=1;
    }
    else {
      set_gain(REG_INT_CLEAR_LO,half);
      performMeasurement();
      int halfValue = get_readout(REG_DATA_CLEAR_LO);

      if (halfValue>1000) {
        upperBox=half;
      }
      else if (halfValue<1000) {
        lowerBox=half;
      }
      else {
        gainFound=1;
      }
    }
  }
  return half;
}

int getColorGain() {
  int gainFound = 0;
  int upperBox=4096;
  int lowerBox = 0;
  int half;
  while (!gainFound) {
    half = ((upperBox-lowerBox)/2)+lowerBox;
    //no further halfing possbile
    if (half==lowerBox) {
      gainFound=1;
    }
    else {
      set_gain(REG_INT_RED_LO,half);
      set_gain(REG_INT_GREEN_LO,half);
      set_gain(REG_INT_BLUE_LO,half);
      performMeasurement();
      int halfValue = 0;

      halfValue=max(halfValue,get_readout(REG_DATA_RED_LO));
      halfValue=max(halfValue,get_readout(REG_DATA_GREEN_LO));
      halfValue=max(halfValue,get_readout(REG_DATA_BLUE_LO));

      if (halfValue>1000) {
        upperBox=half;
      }
      else if (halfValue<1000) {
        lowerBox=half;
      }
      else {
        gainFound=1;
      }
    }
  }
  return half;
}

The above Arduino code provides us with R,G,B tuples. We will now use a C# application to read in these values from the serial port, generate an Atom feed, and serve it up. We will use the open source WebFeeds library to generate an Atom document to hold our sensor data.  You can download the Visual Studio solution and pre-compiled DLL from the aforementioned website. This server is quite generic, and could easily serve any other sort of data from a serial port–just change the feed metadata.

//We'll need a few things
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.IO;
using System.IO.Ports;
using System.Net;
using System.Xml;
using System.Xml.Serialization;
using System.Runtime.InteropServices;
using System.Threading;
using WebFeeds;
using WebFeeds.Feeds;
using WebFeeds.Feeds.Atom;

namespace LightSensorAtomServer{
    class MainProgram {

        // Create the serial port with basic settings
        private SerialPort port = new SerialPort("COM16", 9600, Parity.None, 8, StopBits.One); //the serial port we will use to read in data. note that the COM port must be that which our data is coming in on. something like COM5 for windows, or /dev/ttyUSB0 for POSIX

        static void Main(string[] args){
            new MainProgram(); //instantiate the program
        }

        MainProgram(){
            SerialPortStart(); //initialize the serial port
            startServer(); //start up our HTTP server, which will in turn be responsible for calling methods to read in data, generate an Atom feed, and serve it
        }

        public void startServer(){
            HttpListener listener = new HttpListener(); //create a new HTTP listener
            listener.Prefixes.Add("http://*:80/"); //make it listen to all requests on the standard web port, 80
            listener.Start(); //start listening
            Console.WriteLine("Listening...");
            for (; ; ) //run the server forever
            {
                HttpListenerContext context = listener.GetContext(); //gives us access to information about requests
                new Thread(new Worker(context, writeFeed(ReadData())).ProcessRequest).Start(); //make a new thread to respond, pass it the context AND the feed we created. Note that we are reading in data and then writing a feed. Effect: We wait for the latest data before serving
            }
        }

        class Worker{
            private HttpListenerContext context; //a ListenerContext local to this class and thus thread
            private byte[] byteFeedArray; //this will hold our feed output as a byte array

            public Worker(HttpListenerContext context, byte[] byteFeedArray){//set our instance variables according to the parameters passed in
                this.context = context; //set our HTTPListenerContext
                this.byteFeedArray = byteFeedArray; //set our data feed to the output generated above
            }

            public void ProcessRequest(){ //answer our web request
                string msg = context.Request.HttpMethod + " " + context.Request.Url;
                Console.WriteLine(msg); //output the request to the console so we can spot strange requests
                if (context.Request.HttpMethod == "GET") { //if they are requesting the data with a GET request
                    context.Response.ContentType = "application/xhtml+xml"; //since we are outputting Atom, set the MIME type of the data output to XML
                    context.Response.ContentLength64 = byteFeedArray.Length; //set our content length
                    context.Response.OutputStream.Write(byteFeedArray, 0, byteFeedArray.Length); //write out our entire feed byte array
                    context.Response.OutputStream.Close(); //close the request, we're done serving to this request
                } else {} //we're not allowing other HTTP verbs
            }
        }

        public byte[] writeFeed(string data) { //this function is responsible for actually creating and outputting the byte array of our feed from data and stored variables
            byte[] byteFeedArray; //a byte array to store our finished feed array
            int streamPosition; //We will store the output of WebFeeds in a MemoryStream, and use this streamPosition variable to help us keep track of where we are in reading it out
            AtomFeed sensorFeed = new AtomFeed(); //This AtomFeed object is the real deal. It will be the one serialized and outputted to our final byte array
            string feedTitle   = "Color Light Sensor Data Feed"; //set the title for our feed
            string feedSubtitle = "A series of RGB values."; //set the subtitle for our feed
            string feedAuthorName = "Chris Tomkins-Tinch"; //set the feed author name
            string feedAuthorUri = "http://www.christomkinstinch.com";
            double feedLat = 35.9302; //set the feed lat
            double feedLon = -84.3112; //...and lon

            string entryTitle = "Light Color Data"; //set the entry title (we'll only have one for now)
            string entryContent = data; //set the data to that passed in as a parameter
            double entryLat = 35.9302; //set the entry lat
            double entryLon = -84.3112;//...and lon

            sensorFeed = generateFeed(System.Guid.NewGuid().ToString(), feedTitle, feedSubtitle, feedAuthorName, feedAuthorUri, feedLat.ToString(), feedLon.ToString());//pass all of the above local variables to the function that will output a new AtomFeed with all of the needed metadata, set our local sensorFeed to this output

            XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces();//make a Serializer so we can add some namespaces to our feed
            namespaces.Add("georss", "http://www.georss.org/georss"); //and the georss namespace
            sensorFeed.AddNamespaces(namespaces); //add this namespaces to the sensor feed

            sensorFeed.Entries.Add(generateEntry(System.Guid.NewGuid().ToString(), entryTitle, entryContent, entryLat.ToString(), entryLon.ToString()));//pass all of the above local variables to the function that will output a new AtomEntry with all of the needed elements and added to the feed we made above

            MemoryStream memoryStream = new MemoryStream(); //make a new MemoryStream to hold the output of the XML Serializer
            FeedSerializer.SerializeXml(sensorFeed, memoryStream, null, true); //serialize our AtomFeed (sensorFeed) and put the output into our memory stream

            memoryStream.Seek(0, SeekOrigin.Begin);// prepare to start navigating and outputting from our memory stream. set the position to the beginning of the stream.

            byteFeedArray = new byte[memoryStream.Length]; //initialize our feed byte array to be the same length as our memory stream
            streamPosition = memoryStream.Read(byteFeedArray, 0, 20);// Read the first 20 bytes from the stream.

            while (streamPosition < memoryStream.Length) {// Read the remaining bytes, byte by byte.
                byteFeedArray[streamPosition++] = Convert.ToByte(memoryStream.ReadByte());
            }

            return byteFeedArray; //output the final byte array.  this contains the serialized xml of our AtomFeed which has been converted from its memory stream location to a byte array. now we can serve it.
        }

        public AtomFeed generateFeed(string feedUuid, string feedTitle, string feedSubtitle, string feedAuthorName, string feedAuthorUri string feedLat, string feedLon){
            AtomFeed feed = new AtomFeed(); //an AtomFeed from the JSONFX WebFeeds lib
            XmlDocument xmlDoc = new XmlDocument(); //an XmlDocument so we can make XML elements

            feed.Updated = new AtomDate(DateTime.UtcNow); //set the feed updated time to the current time
            feed.Title = feedTitle; //set the feed title to that specified by the parameter
            feed.ID = feedUuid; //set the feed universally unique identifier to that specified by the parameter
            AtomPerson author = new AtomPerson(); //make a new AtomPerson construct
            author.Name = feedAuthorName; //add the author name specified in the parameter to the author construct
            author.Uri = feedAuthorUri; //add the author URL specified in the parameter to the author construct
            feed.Authors.Add(author); //add the author construct to the feed. this will generate the author element and child elements of name, url, etc.
            AtomLink feedLink = new AtomLink("http://www.example.com/sensor/feed/url");//add a link construct to our feed. this represents where we can go to retrieve this feed
            feed.Links.Add(feedLink); //add the feed link to our feed
            feed.Categories.Add(new AtomCategory("light-color")); //add a category construct
            feed.Categories.Add(new AtomCategory("color"));  // and another
            feed.Categories.Add(new AtomCategory("arduino"));// and one more

            XmlElement georssPoint = xmlDoc.CreateElement("georss", "point", "http://www.georss.org/georss"); //make a new XML element using the XmlDocument we defined above. specify its namespace as georss, and add the georss namespace to that link. WebFeeds will take care of consolidating like namespaces and adding them to the feed level of the outputted document
            georssPoint.InnerText = feedLat + " " + feedLon; //make a space delimited string to represent our location, per the GeoRSS simple spec
            feed.ElementExtensions.Add(georssPoint); //add our newly minted GeoRSS XmlElement to our feed

            return feed; //return our feed. we aren't done with it yet, but now we have added all of the feed-level metadata
        }

        public AtomEntry generateEntry(string entryUuid, string entryTitle, string entryContent, string entryLat, string entryLon) {//Make and return a simple Atom entry
            AtomEntry entry = new AtomEntry(); //an AtomEntry from the JSONFX WebFeeds lib
            XmlDocument xmlDoc = new XmlDocument(); //an XmlDocument so we can make XML elements

            entry.Updated = new AtomDate(DateTime.UtcNow); //set the entry updated time to the current time
            entry.Title = entryTitle;//set the entry title to that specified by the parameter
            entry.ID = entryUuid;//set the feed universally unique identifier to that specified by the parameter
            entry.Content = entryContent;//set the feed content to that specified by the parameter, in this case our data

            XmlElement georssPoint = xmlDoc.CreateElement("georss", "point", "http://www.georss.org/georss");//make a new XML element using the XmlDocument we defined above. specify its namespace as georss, and add the georss namespace to that link. WebFeeds will take care of consolidating like namespaces and adding them to the feed level of the outputted document
            georssPoint.InnerText = entryLat + " " + entryLon;//make a space delimited string to represent our location, per the GeoRSS simple spec
            entry.ElementExtensions.Add(georssPoint);//add our newly minted GeoRSS XmlElement to our entry

            return entry;//return our entry object. now it can be added to a feed
        }

        public void SerialPortStart() {
            port.Open();//begin communication
        }

        public string ReadData() {
            char tempChar; //a local byte variable to hold output from the serial buffer
            string serialOutputString = ""; //a local variable to hold the string we will buid from the serial output

            port.DiscardInBuffer();//wipe the serial buffer so we know this is a new reading

            while (serialOutputString.IndexOf("\n") == -1){ //wait for an end-of-line character, so we know our next reading is complete
                tempChar = (char)port.ReadChar(); //read in a char
                serialOutputString = serialOutputString + tempChar; //and add the byte to our string
                Console.WriteLine(serialOutputString);
            }

            serialOutputString = port.ReadLine();

            serialOutputString = serialOutputString.Replace("\n", "").Replace("\r", ""); //once we're here we have reached an end-of-line and are now our of the loop. strip the newline characters
            Console.WriteLine(serialOutputString); //write the values to the console since they're nice to see.
            return serialOutputString; //return the serial output. we're finished reading from the serial port for now
        }
    }
}

We can go to http://localhost/ and view the resulting XML:



	35.9302 -84.3112
	
		Chris Tomkins-Tinch
		http://www.christomkintinch.com/
	
	
	
	

	07d9f330-3b50-436a-a28d-b41ba0b52f03

	2009-02-20T19:02:29Z
	
		35.9302 -84.3112
		270d3cd7-23c7-4785-baf0-45b528ecda4c

		2009-02-19T19:02:29Z
		127,127,127
	

Once this is deployed to a publicly-accessible web server, users will be able to subscribe to the feed at the server’s IP address. This Atom feed will be understood by any modern browser or feed reader:

color_sensor_feed_result

An Atom feed like this permits users to subscribe to your data feed using standard feed reading applications such as Google Reader, Bloglines, NewsGator, and others. Users can also take advantage of existing open source and commercial feed parsing libraries and utilities to parse this information and use it in their own applications. Sensorpedia makes it easy to index and discover new Atom data sources.

This entry was posted in Uncategorized. Bookmark the permalink.

5 Responses to How To: Interface a light color sensor with Sensorpedia and serve Atom! (guide 3)

  1. Pingback: Sensorpedia » Blog Archive » How To: Interface a light color sensor with Sensorpedia and serve Atom! (guide 3) « Arduino Blogs

  2. Pingback: Sensorpedia » Blog Archive » How To: Interface an irradiance sensor with Sensorpedia (guide 4)

  3. Pingback: Sensorpedia » Blog Archive » Sensorpedia student returns to map sensors, work on mobile apps

  4. Matt says:

    Chris,

    Stumbled upon sensorpedia search for an answer to my problems with the code you borrowed from Arduino user Marcus to output R,G,B tuples:

    Basically just like his example code your doesn’t compile…

    In function ‘void setup()’:
    error: ‘setupADJD’ was not declared in this scope In function ‘void loop()’:
    In function ‘int getClearGain()’:
    In function ‘int getColorGain()’:

    is what the compiler gives back as an error. Were you able to use this code verbatim for this project???

  5. Chris says:

    Matt,

    You’re absolutely right about “setupADJD();” not being declared or defined in the setup loop.

    It has been over a year since I worked on this project, and unfortunately I do not have the color sensor in front of me.

    Just looking at the code, I wonder if a line comment may have been lost in posting it to the web for the “setupADJD();” line…

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>