yuno_refillToday at work we had “Innovation day” which is actually kinda cool. Basically, for the day, we can work on any project we like – anything at all. Generally, most people work on tools or enhancements to existing software (which generally works out very cool) but I wanted to do something totally different.

Enter – the coffee shamer. See, we have a problem around the office that there’s always someone who takes the last cup of coffee and doesn’t brew another pot. I suspect there are several offenders but catching them in the act is a challenge. I’d had an idea a few years ago of creating some kind of alarm to let everyone know when someone had taken the last cup but not put on another brew…I just didn’t know how to implement it. Until this year I found this blog post. This chap had used something called an Arduino to measure the weight of the pot and sound a buzzer. Ah ha! Engaging research mode, I read everything I could find out about Ardunios and knew I had to have one. So I ordered a kit from Canada Robotix which is a great starter kit for getting going with an Arduino plus a buzzer and a force resistor. I floated the idea around the office to a few people and we figured we’d go for it and make it.

So, I did a few basic experiments with the Arduino and did some more reading and gradually came up with the idea of what we’d do…we’d detect the level of coffee and sound a buzzer and blink some LEDs as in the sample blog post. Then, I was watching a YouTube tutorial on serial communications by Jeremy Blum and he used a ‘receiver’ written in a language called Processing. Showing how reasonably easy serial communication was, this led me to the next level – we’d take a picture of the guilty party and post it to a web page. Genius. Here’s what the basic Arduino side looked like with the force sensor in place under the jug

coffee_shamer

So, onto Innovation day and I briefed the team on the basic idea and we set to work. One quick side note. This would make an excellent Agile ‘lab’ project as we really used all of the agile principals – small teams, task breakdowns, estimation, rapid prototypes, strong communication, etc. After having a power bump, we also quickly learned “save often” but that’s another lesson 🙂 We split the work as follows:

Kgirl and I would work on the circuit and calibrating the Arduino.
Kguy would work on the receiver with processing and taking the images
Mguy would work on generating the “guilty party” web material

Things worked out pretty well. On the Arduino side, the main problems we had was that the force resistor I’d purchased was really too sensitive. It was meant to measure very small force measures (finger tips) rather than the weight of a pot. For example, an empty pot was showing a reading of 950 out of 1024 so almost at the top of the scale. This meant that even half filling the pot gave us a very small change in value. We also found that taking a single reading would bounce around a bit so we introduced a ‘smoothing’ read where we took 20 samples and averaged them 1ms apart. This made a huge difference in the stability of our readings. We also decided to only read every 5 seconds which helped too with the variability. Finally, on the Arduino side, we had to account for the fact that when a user was filling their cup, they were actually applying force to the resistor and hence affecting the reading. We found a 10s delay took care of this when in the FULL state.

The processing piece came together reasonably quickly but Mguy had some issues with the web side but it all came together in the end. Our demo had a small issue when the Mac we were using to be the serial receiver crashed but after that, it actually worked! One of the most interesting projects I’ve worked on and I learned a TON of new stuff. OK, maybe not all 100% related to work but definitely some interesting new skills.

Here’s the Arduino and Processing code we used. The Arduino code needs some cleanup but the basics are all there.

Arduino

int fsrAnalogPin = 0; // FSR is connected to analog 0

int greenLEDPin = 3;
int yellowLEDPin = 4;
int redLEDPin = 5;

// how long to sleep for before taking a second reading
// This is used when we detect EMPTY. In a real setting
// this would need to be long enough to allow a user to
// refill the pot
#define TIMETOLIVE 10000

// Serial messages
#define TAKEPICMSG 1
#define GUILTYMSG 2
#define GOODGUYMSG 3

// Threshold values
#define NOPOT 942
#define LOWPOT 950

// States
#define INIT 0
#define EMPTY 1
#define FULL 2

int fsrReading;
int fsrReading2;

int currentState = INIT;
int sentGuiltyMsg = 0;
int sentGoodGuyMsg = 0;

void setup(void) {
Serial.begin(9600); // We'll send debugging information via the Serial monitor
pinMode(greenLEDPin, OUTPUT);
pinMode(yellowLEDPin, OUTPUT);
pinMode(redLEDPin, OUTPUT);

}

void loop(void) {
fsrReading = readForceSensor(fsrAnalogPin);

if (currentState == INIT)
{
// This was helpful in troubleshooting when hooked
// up to the receiver
blinkLEDS();
}

if (fsrReading <= LOWPOT)
{
setLowLED();

if (currentState == INIT)
{
currentState = EMPTY;
} else if (currentState == FULL)
{
// reset the goodguy messsage flag
// which was set in the FULL state
sentGoodGuyMsg = 0;

// We may be in an alarm state here
sendMessage(TAKEPICMSG);

delay(TIMETOLIVE);
fsrReading2 = readForceSensor(fsrAnalogPin);

if (fsrReading2 = LOWPOT)
{
setFullLED();

sentGuiltyMsg = 0;

if (currentState == EMPTY && !sentGoodGuyMsg)
{
sendMessage(GOODGUYMSG);
sentGoodGuyMsg = 1;
}
currentState = FULL;
}
}

delay(5000);
}

// This takes 20 samples 1ms apart and averages them
// smoothes out our readings and makes a big difference
int readForceSensor(int sensorPin)
{
int numReadings = 20;
int readings[numReadings]; // the readings from the analog input
int index = 0; // the index of the current reading
int total = 0; // the running total
int average = 0; // the average

// init the array
for (int thisReading = 0; thisReading < numReadings; thisReading++)
readings[thisReading] = 0;

for (int thisReading = 0; thisReading = numReadings)
// ...wrap around to the beginning:
index = 0;

// calculate the average:
average = total / numReadings;
delay(1); // delay in between reads for stability
}
return average;
}

void setFullLED()
{
digitalWrite(greenLEDPin,HIGH);
digitalWrite(yellowLEDPin,LOW);
digitalWrite(redLEDPin,LOW);
}

void setHalfLED()
{
digitalWrite(greenLEDPin,LOW);
digitalWrite(yellowLEDPin,HIGH);
digitalWrite(redLEDPin,LOW);
}

void setLowLED()
{
digitalWrite(greenLEDPin,LOW);
digitalWrite(yellowLEDPin,LOW);
digitalWrite(redLEDPin,HIGH);
}

// Write the code over the serial interface
void sendMessage(int msgID)
{
Serial.write(msgID);
}

//Super cool light show on initialization
void blinkLEDS() {
digitalWrite(redLEDPin, HIGH);
delay(100);
digitalWrite(redLEDPin, LOW);
delay(100);
digitalWrite(yellowLEDPin, HIGH);
delay(100);
digitalWrite(yellowLEDPin, LOW);
delay(100);
digitalWrite(greenLEDPin, HIGH);
delay(100);
digitalWrite(greenLEDPin, LOW);
delay(100);
digitalWrite(redLEDPin, HIGH);
delay(100);
digitalWrite(redLEDPin, LOW);
delay(100);
digitalWrite(yellowLEDPin, HIGH);
delay(100);
digitalWrite(yellowLEDPin, LOW);
delay(100);
digitalWrite(greenLEDPin, HIGH);
delay(100);
digitalWrite(greenLEDPin, LOW);
delay(2000);
}

And here’s the Processing code. NOTE that the ID of the serial port is hard-coded and must be set accordingly.


import processing.video.*;
import processing.serial.*;

import java.io.File;

final Integer SERIAL_PORT_ID = 5; // TODO Specify correct port-id
final String FINAL_IMAGES_DIR_NAME = "Images";

Serial port;
Capture cam;
Integer imageNum = 0;
String lastImageName = null;
Integer lastCmd = 0;
String sketchDirPath = null;

void setup() {
sketchDirPath = sketchPath("");

println("Sketch directory: " + sketchDirPath);

size(640, 480);
startCamera();
initializeSerialPort();
}

void startCamera() {
String[] cameras = Capture.list();

if (cameras.length == 0) {
println("There are no cameras available for capture.");
exit();
} else {
// The camera can be initialized directly using an
// element from the array returned by list():
cam = new Capture(this, cameras[0]);
cam.start();

println("Using camera: " + cameras[0]);
}
}

void initializeSerialPort() {
println("Available serial ports: ");
println(Serial.list());

port = new Serial(this, Serial.list()[SERIAL_PORT_ID], 9600);
}

void draw() {
processLastCmd();
}

void processLastCmd() {
switch(lastCmd) {
case 1: // Take an image, might be guilty or not guilty
takeImage();
break;
case 2: // Last image was definitely of a guilty individual!
markLastImageAsGuilty();
break;
case 3: // Take a new image and this guy is not guilty
takeImageOfNonGuilty();
break;
default:
// println("Unrecognized command read from serial-port: " + lastCmd);
}

lastCmd = 0;
}

// notification that there is something to read on the serial port
void serialEvent(Serial port) {
lastCmd = port.read();
}

void takeImage() {
while(!cam.available()) {
println("Waiting for camera to become available...");

try {
Thread.sleep(300);
}
catch(Exception e) {
println(e);
}
}

lastImageName = "Image-" + (++imageNum) + ".png";

cam.read(); // read frame into memory
image(cam, 0, 0); // have to display image so save() can capture and save it
save(lastImageName); // save to temporary storage on disk

println("Saved un-judged imaged to disk: " + lastImageName);
}

void markLastImageAsGuilty() {
moveLastImageToFinalLocation();

println("Marked image " + lastImageName + " as guilty and moved!");
}

void takeImageOfNonGuilty() {
takeImage();
moveLastImageToFinalLocation();

println("Non-guilty image moved: " + lastImageName + ".");
}

void moveLastImageToFinalLocation() {
File oldImageFile = new File(sketchDirPath + "/" + lastImageName);
File newImageFile = new File(sketchDirPath + "/" + FINAL_IMAGES_DIR_NAME + "/" + oldImageFile.getName());

try {
if(! oldImageFile.renameTo(newImageFile)) {
println("Failed to move " + oldImageFile + " to " + newImageFile + "!!!!!");
}
}
catch(Exception e) {
println("Failed to move " + oldImageFile + " to " + newImageFile + "!!!!!");
}
}