Daphne 2.0 a Houseplant on Twitter
Back in the old days, (mid 1990s) when the Internet was still a new thing for most people, there was this guy named Paul who hooked his hot tub to the Net. From a command prompt anywhere in the world you could type:
"finger hottub@hamjudo.com"
and this is what you would get in return:
"Paul's hottub is warm at about 99 degrees Fahrenheit.
It is nice outside at about 63 degrees Fahrenheit.
The ozone generator is working. The cover is closed.
The backup battery is OK at 10.5 volts (this will still work down to 6 volts)"
The hot tub, a coffee pot, a few Coke machines and of course a toaster were the early precursors to the myriad of things that are connected to the Internet today. In fact, we almost seem to be at the point where if a thing is not somehow connected to the Internet, then we are surprised.
Since I was wanting to explore the concept of serial communications, specifically a PC polling an Arduino for information, I decided to follow in Paul's footsteps and learn by putting something on the Internet. The result is Daphne 2.0, my house plant on Twitter. "Daphne" being both the name of the project and the name of the Dieffenbachia house plant who volunteered for this assignment.
As it turned out, this project was more about the software than the hardware.
Daphne's Twitter page is here.
General Description
With a multitude of different configurations and design available, I decided to stick to my "serial polling, tethered Arduino" plan and keep things as simple as possible.
Daphne Hardware
The hardware for Daphne is very simple; an Arduino, three sensors and a LED for blinking. Since this is just a temporary project I didn't even bother building an enclosure for the Arduino. I used one of my Bare-Bones Arduino clone boards and simply mounted it on a standard breadboard for stability. Power is supplied by the FTDI USB-to-TTL-Serial cable connected to the PC (more on PC-Arduino connections later).
The three things most important to Daphne were the things that were going to be monitored, temperature, light and moisture.
Sensors
For the temperature I used my favorite LM34 precision Fahrenheit temperature sensor. It has a range of -50F to +300F, output voltage linearly proportional to the temperature and can be operated without any trimming components. As the schematic below shows, connect one pin to +5v, one to ground and the third goes to the analog input A0 on the Arduino. This configuration provides the basic range of +5F to +300F. Can't get much simpler!
Light was measured using a photo-resistor (photocell) which reads about .5 ohms in full sun and about 40K ohms in darkness. The sensor was built by soldering a 10K ohm resistor in series with the photocell and adding a connection between the two that would be the analog input A1 to the Arduino. (see schematic).
Moisture is measured using two stainless steel probes in series with a 10k ohm resistor, and tapping in between the resistor and probs for analog input A2. The two probes are inserted into the plant's soil about 3 cm apart. The more moisture, the more current passes through the soil between these probes.
Each sensor needed +5v, a ground connection and an analog output line, so three wires ran from each sensor to to the Arduino. Fortunately the Bare-Bones Arduino is configured to make it easy for 3-wire connections into the analog inputs.
Other than an LED on pin D13 and its current limiting resistor, thats all there is to the hardware.
Daphne Software
*Note: Blogger does not allow file hosting so the code is listed at the bottom of this discussion.
The software consists of two separate applications, Arduino software and PC software. Both are discussed below.
Daphne / Arduino
The Arduino application in this project is simply an analog sensor manager. Its sole purpose is to provide the raw data from a particular sensor when asked. Looking through the code from the top you can see some constants for analog pin assignments, and some float variables are created and initialized. Then some variables to manage the blinking LED are set. This blinking is interesting because it does not use the delay() routine which can interfere with serial communications. A more detailed description of that blink code can be found here at the The Blink Without Delay page on the Arduino site.
The sampleSensor() routine takes an analog pin as input, samples the voltage a number of times with a slight delay in between samples and then returns the average value on that pin. The averaging reduces the data jitter found on many different types of sensors.
updateFeedback() is part of the Blink Without Delay code.
setup() is simple, just assign the LED pin and set the serial rate. The serial rate needs to be the same as your daphne PC application rate. For this simple program and the small amount of data received and transmitted, 9600 is plenty fast. In reality the sampleSensor() routine is where the slowdown occurs, not in the the serial communication.
The main loop() of the application works as follows: Arduino listens to its serial port and when data is available, it assigns that data to the sensorID variable. In this application Arduino is expecting either the characters "R", "a", "b" or "c". Anything else and Arduino just returns an "X", meaning it didn't understand what was asked. "R" means, Arduino, are you ready?, and is used by the PC program to determine if the Arduino is connected and powered on. If ready, Arduino returns a "Y". "a", "b", "c" are requests for the analog sensor data on analog pins 0, 1 and 2 respectively. For example, if Arduino sees an "a" on the serial input it calls sampleSensor(in_a) which gets and returns an average reading of A0 input.
Finally a call to updateFeedback() blinks the LED.
Thats it for the Arduino code. Very simple and easy to expand or modify. More analog sensors could be added or digital sensors as well.
Daphne / PC
Many different software applications can communicate with an Arduino.
Since this was a hardware and software education project for me I chose to write the PC portion of the Daphne project in Python and run it on a Ubuntu PC. The general logic of the Daphne PC application is this:
1) Daphne, are you available? 2) If yes, then get all sensor data. (If Daphne not available, then print error message and End) 3) Once sensor data has been gathered, process that data. 4) Build some descriptions for each sensor. 5) Build a tweet 6) If in debug mode, print data to screen. If not in debug mode, post the tweet up to Twitter. 7) End
The main() routine is where program logic starts. debugMode can be set to True or False depending on where you want to display the data. A data object called sensor_data is created. This object encapsulates all the raw and processed data within one run of the application. As an object, the data is then easier to pass around to different subroutines.
getSensorValues() takes the data object as a parameter, and for each sensor, sends out a serial request to the Arduino. the raw data is then stored in the data object.
processSensorvalues() is where the raw sensor data from the Arduino is massaged to become temperature, light and moisture data. The LM34 temperature sensor uses the formula ((s_data.tempF_Val / 1024) * 5) * 100 where s_data.tempF_Val is the raw Arduino data, 1024 is the maximum number of units that Arduino can slice the input voltage into, 5 is the maximum input voltage (5v) and 100 gets the value out of a decimal and turns it into a temp.
Temperature C is then computed. Then both the raw light value and the raw moisture value are subtracted from 1023 (max units again). This subtraction is done to generate a number that increases when light or moisture increases and decreases when light or moisture decrease. In other words, a light value of 835 represents more light falling on Daphne than a light value of 427. This subtraction in software is used to 'flip' the numbers because of the physical placement of the 10k ohm resistor in both the light and moisture sensor. If the resistors were on the ground side rather than the +5v side, then the subtraction would not be needed.
These calculated values are stored in the data object as well as scrapeData which is just a string of data in square brackets. More on this later.
Next, buildSensorDescriptions() is called. This is the "fuzzy logic" portion of Daphne where I did a little testing to decide what the sensor values meant to Daphne. Temperature was easy, above 80 is "toasty", around 50 is "cool", etc. Light and moisture were determined by logging the extremes (full sun, total darkness, 100% water, no water at all) and splitting and assigning names like "bright" and "moist" to sub ranges. Not completely precise but that fuzzy logic for you.
buildTwitterTweet() puts all the pieces of information into a string that will fit into the required Twitter string length.
Finally, if debugMode is True, then the information is just printed locally to the PC screen, otherwise sendTwitterInformation() is called and the tweet is posted. If you use the code, provide your own user name and password.
The result on Daphne_Plant's Twitter page is something like this:
"The temperature is a toasty 81.1F degrees (27.3C), the light is bright and my soil is moist. [tlm,81.1,981.0,721.0]"
The information in the square brackets is the scrapeData mentioned earlier. It represents the temperature, light, moisture data values and is included so I can scrape the data off all the Twitter posts with a different Python application, effectively using Twitter as my data storage device (but thats a different blog entry sometime.)
PC to Arduino Connections
This was always to be a tethered project from the start, meaning the Arduino was going to only be responsible for answering polls and providing data serially, through some form of connection to a PC. Daphne has been tested in three different tether configurations; PC to Bare-Bones with FTDI USB-to-TTL-Serial cable, PC to Diecimila Arduino with USB cable and PC to Diecimila Arduino via Ethernet using XPort Direct shield and XPort module.
On order are a set of XBee's and shields. Who knows, maybe awireless Daphne project update be available someday.
Daphne Arduino Code
/*
Daphne_v2_0
* Manages 3 analog sensors as a polled response.
* Includes blinking LED as ready state feedback.
* Returns raw sensor data, the calling software is responsible
for interpretation of the raw data
Hardware base: Arduino AtMega168
This Arduino software is released to the public
domain "as is". The author makes no warranty
expressed or implied and assumes no responsibilities.
Feel free to fold, bend, spindle or mutilate as you see fit.
*/
//################################################################
// analog sensor input pin assignments
const int in_a = 0;
const int in_b = 1;
const int in_c = 2;
// feedback blinking LED
const int feedbackLED = 13;
int feedbackLED_state = LOW;
long feedbackPreviousMillis = 0;
// once per second blink
long feedbackInterval = 1000;
float sampleCount = 5.0;
float a_val = 0;
float b_val = 0;
float c_val = 0;
//-----------------------------------------------------------------------
int sampleSensor(int sensorPin) {
// samples the sensor n times for an average reading
float s = 0.0;
for(int i=0; i s += analogRead(sensorPin);
delay(200);
}
return s / sampleCount;
}
//-----------------------------------------------------------------------
void updateFeedback() {
// feedback LED blinker
if (millis() - feedbackPreviousMillis > feedbackInterval) {
feedbackPreviousMillis = millis();
if (feedbackLED_state == LOW)
feedbackLED_state = HIGH;
else
feedbackLED_state = LOW;
// set the LED with the ledState of the variable:
digitalWrite(feedbackLED, feedbackLED_state);
}
}
//-----------------------------------------------------------------------
void setup() {
pinMode(feedbackLED, OUTPUT);
Serial.begin(9600);
}
//-----------------------------------------------------------------------
void loop() {
byte sensorID;
// loop until a request comes in
if (Serial.available() > 0) {
sensorID = Serial.read();
switch(sensorID) {
case 'R': // are you Ready arduino ?
Serial.println('Y'); // respond, Yes
break;
case 'a': // analog 0
a_val = sampleSensor(in_a);
Serial.println(a_val);
break;
case 'b': // analog 1
b_val = sampleSensor(in_b);
Serial.println(b_val);
break;
case 'c': // analog 2
c_val = sampleSensor(in_c);
Serial.println(c_val);
break;
default:
Serial.println("X");
}
}
updateFeedback();
}
Daphne Python Code
#!/usr/bin/python
#
# Daphne_v2_0
#
# * Manages 3 analog sensors as a polled response.
# * Includes blinking LED as ready state feedback.
# * Returns raw sensor data, the calling software is responsible
# for interpretation of the raw data
#
#
# This Python software is released to the public
# domain "as is". The author makes no warranty
# expressed or implied and assumes no responsibilities.
# Feel free to fold, bend, spindle or mutilate as you see fit.
#
################################################################
import os
import sys
import time
import serial
import twitter
import simplejson
usbport = '/dev/ttyUSB0'
ser = serial.Serial(usbport, 9600, timeout=500)
#------------------------------------------------------------------------------
class SensorDataPackage:
# data description
d_date = "YY/MM/DD"
d_time = "Time"
d_tempf = "Temp(F)"
d_tempc = "Temp(C)"
d_light = "Light"
d_moisture = "Moisture"
d_twitter = "Tweet"
# date and time strings
ds = ""
ts = ""
tempF_raw = 0 # temperature
light_raw = 0 # luminosity
moisture_raw = 0 # moisture
tempF_Val = 0
tempC_Val = 0
light_Val = 0
moisture_Val = 0
tweet_Val = ""
tempDescription = ""
lightDescription = ""
moistureDescription = ""
scrapeData = ""
#------------------------------------------------------------------------------
def arduinoAvailable():
# are you available?
ser.write('R')
answer = ser.readline()
#print answer
if answer.strip() == 'Y':
return True
else:
return False
#----------------------------------------------------------------------------
def getSensorValues(s_data):
s_data.ds = time.strftime("%y/%m/%d", time.localtime()) # date
s_data.ts = time.strftime("%H:%M:%S", time.localtime()) # time
# temperature
ser.write('a')
s_data.tempF_raw = ser.readline().strip()
# light
ser.write('b')
s_data.light_raw = ser.readline().strip()
# moisture
ser.write('c')
s_data.moisture_raw = ser.readline().strip()
#----------------------------------------------------------------------------
def processSensorValues(s_data):
#value to temperature F
s_data.tempF_Val = float(s_data.tempF_raw)
s_data.tempF_Val = ((s_data.tempF_Val / 1024) * 5) * 100;
# for debugging
s_data.tempC_Val = 5.0/9.0 * (s_data.tempF_Val - 32)
#luminosity (sensor produces the inverse, so reverse it here) {lower number = darker}
s_data.light_Val = float(s_data.light_raw)
s_data.light_Val = 1023 - s_data.light_Val
#soil moisture level (sensor produces the inverse, so reverse it here) {lower number = dryer}
s_data.moisture_Val = float(s_data.moisture_raw)
s_data.moisture_Val = 1023 - s_data.moisture_Val
# build scrape data
s_data.scrapeData = "[tlm," + '%.1f'%(s_data.tempF_Val) + "," + '%.1f'%(s_data.light_Val) + "," + '%.1f'%(s_data.moisture_Val) + "]"
#----------------------------------------------------------------------------
def buildSensorDescriptions(s_data):
# temperature
if s_data.tempF_Val >= 80:
s_data.tempDescription = "toasty"
elif ((s_data.tempF_Val >= 70) and (s_data.tempF_Val <= 79.9)):
s_data.tempDescription = "pleasant"
elif ((s_data.tempF_Val >= 60) and (s_data.tempF_Val <= 69.9)):
s_data.tempDescription = "cool"
elif ((s_data.tempF_Val >= 50) and (s_data.tempF_Val <= 59.9)):
s_data.tempDescription = "cool"
elif ((s_data.tempF_Val >= 40) and (s_data.tempF_Val <= 49.9)):
s_data.tempDescription = "cold"
else:
s_data.tempDescription = "freezing"
# light
if s_data.light_Val >= 1010:
s_data.lightDescription = "intense"
elif ((s_data.light_Val >= 950) and (s_data.light_Val <= 1009.9)):
s_data.lightDescription = "bright"
elif ((s_data.light_Val >= 500) and (s_data.light_Val <= 949.9)):
s_data.lightDescription = "fading"
elif ((s_data.light_Val >= 200) and (s_data.light_Val <= 499.9)):
s_data.lightDescription = "dim"
else:
s_data.lightDescription = "off"
# moisture
if s_data.moisture_Val >= 1000:
s_data.moistureDescription = "wet"
elif ((s_data.moisture_Val >= 700) and (s_data.moisture_Val <= 999.9)):
s_data.moistureDescription = "moist"
else:
s_data.moistureDescription = "Dry! Hello, I said DRY!!!"
#----------------------------------------------------------------------------
def screenPrintInformation(s_data):
# data dump to screen
timeNow = time.strftime("%H:%M:%S", time.localtime())
print
print s_data.d_time, ":", "\t", "\t", timeNow
print s_data.d_tempf, ":", "\t", '%.1f'%(s_data.tempF_Val), "\t", "\t", s_data.tempDescription
print s_data.d_tempc, ":", "\t", '%.1f'%(s_data.tempC_Val)
print s_data.d_light, ":", "\t", '%.1f'%(s_data.light_Val), "\t", "\t", s_data.lightDescription
print s_data.d_moisture, ":", "\t", '%.1f'%(s_data.moisture_Val), "\t", "\t", s_data.moistureDescription
print "Scrape data", ":", "\t\t", s_data.scrapeData
print s_data.d_twitter, ":", "\t", s_data.tweet_Val
print "Length of tweet is: ", len(s_data.tweet_Val)
print
#----------------------------------------------------------------------------
def buildTwitterTweet(s_data):
#tempDesc = "Currently it is a " + s_data.tempDescription + " " + '%.1f'%(s_data.tempF_Val) + "(F) degrees, "
tempFDesc = "The temperature is a " + s_data.tempDescription + " " + '%.1f'%(s_data.tempF_Val) + "F degrees "
tempCDesc = "(" + '%.1f'%(s_data.tempC_Val) + "C), "
lightDesc = "the light is " + s_data.lightDescription
moistureDesc = " and my soil is " + s_data.moistureDescription + ". "
#s_data.tweet_Val = tempDesc + lightDesc + moistureDesc
s_data.tweet_Val = tempFDesc + tempCDesc + lightDesc + moistureDesc + s_data.scrapeData
#----------------------------------------------------------------------------
def sendTwitterInformation(s_data):
try:
api = twitter.Api(username="your_name", password="your_password")
api.PostUpdate(s_data.tweet_Val)
#print "tweet ok"
except ValueError:
print "Error"
#----------------------------------------------------------------------------
def main():
# True for screen dump, False for Twitter post
debugMode = True
sensor_data = SensorDataPackage()
if arduinoAvailable():
#print "Arduino is available"
getSensorValues(sensor_data)
processSensorValues(sensor_data)
buildSensorDescriptions(sensor_data)
buildTwitterTweet(sensor_data)
if debugMode:
screenPrintInformation(sensor_data)
else:
sendTwitterInformation(sensor_data)
else:
print "No response, Arduino may not be connected."
sys.exit(0)
#-----------------------------------------------------------------------------
# >>> entry point when application starts
if __name__ == '__main__':
main()
else:
sys.exit(0)