How To: Interface a light color sensor with Sensorpedia and serve Atom! (guide 3) 5 Comments
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) .


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:

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.
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???
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…