Thursday, March 5, 2009

Duinoscope v2.0


I revised my free Arduino oscilloscope software in a couple of ways: I changed the Arduino side to use the StandardFirmata sketch (one less thing to maintain, and quite a bit faster), and made a number of improvements to the software. In particular, I added support for digital inputs, so Duinoscope is now capable of being a primitive logic analyzer.

The biggest limitation right now is the low sampling rate. I'm going to try a couple of approaches to improve this, but it's already quite useful in handling low frequency signals. I used it to debug my rotary encoder project based on a PS/2 mouse.

Here it is, in all its glory: load it into Processing, follow the embedded instructions, and enjoy!

/*
* Duinoscope: the free Arduino-based Oscilloscope/Logic Analyzer, V2.0
*
* Requires StandardFirmata to be loaded into a connected Arduino.
*
* Hereby placed into the Public Domain.
*
* 2009 by Shawn Vincent <svincent@svincent.com>
* http://www.svincent.com/
*
* Inspired by source code by an anonymous poster at
* http://accrochages.drone.ws/en/node/90
*
* There are a lot of features that this program *could* have, but
* doesn't. It is not a high end tool, but it's free, and it's a
* single file Processing sketch. What can you ask for? Hopefully
* you'll find it as useful as I have.
*
* The biggest limitation is that the sampling rate is limited by
* Firmata and the refresh rate of the Processing sketch. The latter
* is not too hard to deal with, I plan to investigate if the Firmata
* limitation is hard to resolve.
*
*/
import processing.serial.*;
import cc.arduino.*;

/*
Instructions for use

1. Build two fonts:
Courier-48.vlw and Courier-14.vlw
using Tools>Create Font... in the Processing IDE.

Place these fonts in the data/ directory of the sketch.

2. Install the StandardFirmata sketch onto your Arduino.

This sketch can be loaded in the standard Arduino IDE
using File>Sketchbook>Examples>Library-Firmata>StandardFirmata

Upload this sketch to your Arduino using File>Upload to I/O Board

3. Configure as needed

In the Configuration section below, add or remove elements to the
'data' array to configure the scope elements to display and which
pins you would like to monitor.

Advanced: it is possible to do different processing on the inputs
before display. A sample OhmStream that calculates resistance of
a resistive sensor using a voltage divider is provided. Others
could be added easily.

3. Run Duinoscope in Processing and enjoy.


*/


// -----------------------------------------------------------------------------
// ---- Configuration
// -----------------------------------------------------------------------------

/**
* This array defines the data sources read from the Arduino. Each of
* them represents a graph on the display.
*
* You can modify this if you like, to change colors or labels, or
* more dramatically, to change the type of data source (so that you
* can see values in Ohms rather than Volts, etc).
*
*/
final DataStream[] data = new DataStream[] {
new VoltStream("analog 0", analog(0), color(255,0,0)),
new VoltStream("analog 1", analog(1), color(0,255,0)),
new VoltStream("analog 2", analog(2), color(0,0,255)),
// new VoltStream("analog 3", analog(3), color(255,255,0)),
// new VoltStream("analog 4", analog(4), color(255,0,255)),
// new VoltStream("analog 5", analog(5), color(0,255,255)),

// new VoltStream("digital 0", digital(0), color(127,0,0)),
// new VoltStream("digital 1", digital(1), color(0,127,0)),
// new VoltStream("digital 2", digital(2), color(0,0,127)),
new VoltStream("digital 3", digital(3), color(127,127,0)),
// new VoltStream("digital 4", digital(4), color(127,0,127)),
// new VoltStream("digital 5", digital(5), color(0,127,127)),

// new VoltStream("digital 6", digital(6), color(255,127,127)),
// new VoltStream("digital 7", digital(7), color(127,255,127)),
// new VoltStream("digital 8", digital(8), color(127,127,255)),
// new VoltStream("digital 9", digital(9), color(255,255,127)),
// new VoltStream("digital 10", digital(10), color(255,127,255)),
// new VoltStream("digital 11", digital(11), color(127,255,255)),

// new VoltStream("digital 12", digital(12), color(255,0,0)),
// new VoltStream("digital 13", digital(13), color(0,255,0)),

};

public static AnalogPin analog(int pin) { return new AnalogPin(pin); }
public static DigitalPin digital(int pin) { return new DigitalPin(pin); }

// -----------------------------------------------------------------------------
// ---- Global state
// -----------------------------------------------------------------------------

/**
* The connection to the Arduino
**/
Arduino arduino;

/**
* Large font used to show the current value of a data source.
*
* Make sure you have these fonts made for Processing. Use Tools...Create Font.
**/
static PFont bigFont;

/**
* Smaller font used to show additional information about the data source.
**/
static PFont smallFont;

// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------

/**
* setup: called once
**/
void setup()
{
// Connect via Firmata
String serialPortName = Arduino.list()[0];
try {
arduino = new Arduino(this, serialPortName);
} catch (RuntimeException ex) {
throw new RuntimeException
("Could not find an Arduino on "+serialPortName, ex);
}

// force all digital pins to be input pins.
for (int i=0;i<14;i++)
arduino.pinMode(i, Arduino.INPUT);

// set the size of the window to 80% of the screen
// Note: DO NOT USE P2D RENDERER. For some unknown reason, it causes
// analogRead() and digitalRead() to return spurious values. Grrr.
// http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1236214804/0
size(screen.width*8/10, screen.height*8/10);

// load the fonts
bigFont = loadFont("Courier-48.vlw");
smallFont = loadFont("Courier-14.vlw");
}

// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------

/**
* draw: called to render each frame.
**/
void draw()
{
// once per frame, read stuff from the Arduino.
for (int i=0; i<data.length; i++) {
data[i].readData(arduino);
}

// clear screen to black.
background(0);

// render the graphs
for (int i=0; i<data.length; i++) {
data[i].drawGraph(this,
0, height/data.length*i,
width, height/data.length);
}
}


// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------

static abstract class Pin {

protected final int pin;

public Pin(int _pin) { pin = _pin; }

public abstract int read(Arduino arduino);
}

static class AnalogPin extends Pin {
public AnalogPin(int _pin) { super(_pin); }
public int read(Arduino arduino) {
int r = arduino.analogRead(pin);
if (r < 0 || r > 1023)
System.out.println("Bad value a"+pin+": "+r);
return r;
}
}

static class DigitalPin extends Pin {
public DigitalPin(int _pin) { super(_pin); }
public int read(Arduino arduino) {
return arduino.digitalRead(pin) * 1023;
}
}


abstract static class DataStream {
final int PrevValuesCount = 1000;

private final String name;
private final Pin pin;


private int val = -1;
private int min = -1;
private int max = -1;
private int total = -1;
private int count = 0;

// cyclic buffer
private final int[] prevValues = new int[PrevValuesCount];
private int prevValuesNext = 0;

private final color displayColor;

PImage prevImg;

public DataStream(String _name, Pin _pin, color _displayColor) {
name = _name;
pin = _pin;
displayColor = _displayColor;
for (int i=0; i<PrevValuesCount; i++)
prevValues[i] = -1; // initialize to undefined.
}

public String getName() { return name; }
public color getColor() { return displayColor; }

public void add(int v) {

// add value to cyclic buffer
prevValues[prevValuesNext] = v;
prevValuesNext = (prevValuesNext+1) % PrevValuesCount;

val = v;

if (min == -1 || v < min) min = v;
if (max == -1 || v > max) max = v;
total += v;
count++;
}

public int getPrevRaw(int i) {
int idx = (prevValuesNext-i) % PrevValuesCount;
if (idx < 0) idx += PrevValuesCount; // normalize negative mod stuff
return prevValues[idx];
}

public abstract String getUnit();


public int getNowRaw() { return val; }
public float getNowCooked() { return cook(getNowRaw()); }

public int getMinRaw() { return min; }
public float getMinCooked() { return cook(getMinRaw()); }

public int getMaxRaw() { return max; }
public float getMaxCooked() { return cook(getMaxRaw()); }

public int getAvgRaw() { if (count == 0) return -1; return total / count; }
public float getAvgCooked() { return cook(getAvgRaw()); }

abstract protected float cook(int v);


static float toVolts(int analogPinValue, float minValue, float maxValue) {
return round(map(analogPinValue, 0, 1023, minValue, maxValue), 10.0);
}

void readData(Arduino arduino) {
add(pin.read(arduino));
}

/**
* Render the current graph for the given data stream.
**/
void drawGraph(PApplet applet, int x, int y, int width, int height) {

// calculate some stuff useful throughout this method.
final int labelWidth = 300;
final int bottomPx = y + height;

// hack: don't draw right to the top.
height -= 1;

// Draw out the detail data smallish.
drawTextualSummary(applet, x+width-labelWidth, y);

int graphWidth = width-labelWidth;


// draw the max/min/avg line - kindof cute feature
{
// max
applet.stroke(50, 0, 0); // dark red
int maxV = getMaxRaw()*height/1024;
if (maxV >= 0)
applet.line(x, bottomPx-maxV-1, x+graphWidth, bottomPx-maxV-1);

// avg
applet.stroke(0, 50, 0); // dark green
int avgV = getAvgRaw()*height/1024;
if (avgV >= 0)
applet.line(x, bottomPx-avgV-1, x+graphWidth, bottomPx-avgV-1);

// min
applet.stroke(0, 0, 50); // dark blue
int minV = getMinRaw()*height/1024;
if (minV >= 0)
applet.line(x, bottomPx-minV-1, x+graphWidth, bottomPx-minV-1);
}

// draw the actual graph line
applet.stroke(getColor());
int prevValuesCount = graphWidth - 20;
int prevRaw = getPrevRaw(1);
for (int i=2; i<prevValuesCount; i++) {

// now we know 'curr' and prev'
int currRaw = getPrevRaw(i);

// if the value exists, draw the line.
if (currRaw >= 0) {
// scale to the size of the widget
int prevV = prevRaw*height/1024;
int currV = currRaw*height/1024;

applet.line(x+i-1, bottomPx-prevV-1, x+i, bottomPx-currV-1);
}

// keep track of prev
prevRaw = currRaw;
}

// draw lines between each graph
applet.stroke(70, 70 ,70);
applet.line(x, bottomPx, x+width-1, bottomPx);
}

/**
*
**/
void drawTextualSummary(PApplet applet, int x, int y)
{
int now = getNowRaw();
float nowV = getNowCooked();

int min = getMinRaw();
float minV = getMinCooked();

int max = getMaxRaw();
float maxV = getMaxCooked();

int avg = getAvgRaw();
float avgV = getAvgCooked();

applet.textFont(smallFont);

int smallOffset = 5;

int curY = y;

// draw the name of the data source in the data source color (legend)
applet.fill(getColor());
applet.text(getName(), x+smallOffset, curY+=14);

// draw the rest of the values in white.
applet.fill(255);
applet.text("min: " + pad(concise(minV), 7) + getUnit()+" " + min,
x+smallOffset, curY += 14);
applet.text("max: " + pad(concise(maxV), 7) + getUnit()+" " + max,
x+smallOffset, curY += 14);
applet.text("avg: " + pad(concise(avgV), 7) + getUnit()+" " + avg,
x+smallOffset, curY += 14);

// Draw out the current cooked values biggish
applet.textFont(bigFont);
applet.fill(getColor());
applet.text(pad(concise(nowV),7) + getUnit(),
x+smallOffset, curY+=48);
}

}

public class VoltStream extends DataStream {
private final float minValue;
private final float maxValue;

public VoltStream(String _name, Pin _id, color _displayColor) {
this(_name, _id, _displayColor, 0, 5);
}

public VoltStream(String _name, Pin _id, color _displayColor,
float _min, float _max) {
super(_name, _id, _displayColor);
minValue = _min;
maxValue = _max;
}

public String getUnit() { return "V"; }


protected float cook(int v) { return toVolts(v, minValue, maxValue); }

}

public class OhmStream extends DataStream {
private final float otherResistorOhms;

public OhmStream(String _name, Pin _id, color _displayColor,
float _otherResistorOhms) {
super(_name, _id, _displayColor);
otherResistorOhms = _otherResistorOhms;
}

public String getUnit() { return "ohm"; }


protected float cook(int v)
{
float vOut = toVolts(v, 0, 5);
float vIn = 5.0;


/*
Voltage divider theory says:

vOut = R2/(R1+R2)*vIn

Through simple transformations, we can get:

R1 = R2 * (vIn - vOut) / vOut
*/

return otherResistorOhms * (vIn - vOut) / vOut;
}

}


// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------



static String concise(float v) {
if (v == 0 || v == Float.NaN
|| v == Float.NEGATIVE_INFINITY || v == Float.POSITIVE_INFINITY)
return String.valueOf(v);
else if (v>1000000)
return round(v/1000000.0, 10.0)+"M";
else if (v>1000)
return round(v/1000.0, 10.0)+"K";
else if (v<0.001)
return round(v*1000.0, 10.0)+"m";
else if (v<0.000001)
return round(v*1000000.0, 10.0)+"u";
else
return String.valueOf(v);
}

static float round(float v, float factor) {
return round(v*factor)/factor;
}

static String pad(String s, int size) {
if (s.length() >= size)
return s;
return " "
.substring(0,size-s.length()) + s;
}

1 Comments:

Anonymous Anonymous said...

Coul you please put the pde file, I don´t know processing and I have problems when paste the code it's don´t compile on my processing environment.
Thanks, great work.

3:08 AM  

Post a Comment

Subscribe to Post Comments [Atom]

<< Home