Technology

Off-The-Shelf Hacker: Customize Your Gauges with Processing Code

4 Apr 2018 3:00am, by

Last week we discussed reading multiple sensors with an Arduino Pro Mini and then sending the data to the Raspberry Pi for display on multiple analog gauges.

This week we’ll look at pulling the data into the Pi and developing the multi-gauge setup with the Processing programming language. We’ll talk about how to break up the serial line input text into corresponding values to drive two onscreen gauges. I’ll also show you a way to make the movable gauge pointers without a lot of code.

Much like using the Arduino IDE, on the Pi to program the Pro Mini, you can use the Processing IDE, right on the Pi to take care of the code for the gauges. Pairing an Arduino and a Raspberry Pi (with a monitor and keyboard/mousepad) unleashes a potent portable development/usage environment. The Arduino and Processing code for this article were both developed with that setup.

Examining the Code

To review, the Arduino Pro Mini reads the ultrasonic range finder and coverts the distance into a text string. The Arduino code also simulates a variable value for a light level photocell. Both values are sent from the Arduino to the Pi over the serial line as a simple stream of text.

Since I’m not tweaking the Arduino code, at this point, I just powered the Pro Mini on and developed the Processing code inputting the data from the wired serial line. No need to use the FTDI cable for this job.

You’ll probably still want to run the stty command line before starting the Processing IDE, just to make sure you can read the serial data stream.

stty -raw -icrnl -F /dev/ttyAMA0 115200

Remember that ttyAMA0 is the standard wired serial port name for the Raspberry Pi.

Here’s a screenshot of some sample serial data from the Pro Mini.

Notice that the data stream is just two values separated by a comma and an invisible line feed at the end. The first value is the data from the ultrasonic rangefinder and the other is the “simulated” data from a light level sensing photocell. You could add any number of values to the data stream.

Now that we know what the input looks like, let’s see how the Processing code works.

/*
 * Steampunk Gauge 
 * 
 * Reads serial input stream and displays the values on analog gauges
 * Revs up pointers at the beginning and turns gauges red. Uses graphics files
 * for gauge face and pointers. Translates/rotates graphic instead
 * of using code to manually construct pointer. 
*/

PFont f;
PImage g1photo, g2photo, p1photo, p2photo;
float g1x, g1y, g2x, g2y;

String angle = "0";              // angle in degrees
String ivalue = "0";             // input line
float gaugeaddangle = 275.0;     // gauge additional angle to max value 
float gaugestartangle = 135.0;   // gauge angle from 3:00 zero position 
float maxinputvalue = 157.5;     // max limit of measurement
float angle1f = 0.0;             // angle of pointer for gauge 1
float angle2f = 0.0;             // angle of pointer for gauge 2
float segLength = 95;            // pointer length from center to max point
int revup = 0;                   // initial rev-up value when you pin the needle
float dangervalue = 130;         // if input is above this number the gauge turns red
BufferedReader datafile1;        // input file from the Arduino
BufferedReader startupfile;      // rev-up file that pins needle at startup 

void setup() {
   
  size(600, 300);                // gauge window size
  g1photo = loadImage("/home/pi/gauge10-b.png");      // gauge #1 face graphic file
  g2photo = loadImage("/home/pi/gauge10-c.png");      // gauge #2 face graphic file
  p1photo = loadImage("/home/pi/clock-pointer.gif");  // gauge #1 pointer graphic file
  p2photo = loadImage("/home/pi/s-pointer.gif");      // gauge #2 pointer graphic file
  
  f = createFont("Penhurst", 16, true);
  
  
  strokeWeight(2);     // line weight
  stroke(0,0,0);       // line color (RGB)
  
  g1x = 150;           // gauge #1 pointer x origin
  g1y = height * 0.5;  // gauge #1 pointer y origin
  g2x = 373;           // gauge #2 pointer x origin
  g2y = 73;            // gauge #2 pointer y origin
  
  datafile1 = createReader("/dev/ttyAMA0");
  // connect to the serial input data file
  // if gauges are not visible, execute stty first on the command line
  startupfile = createReader("/home/pi/processing-3.3/gauge-test2.txt");
  // connect to the rev-up file
  
}

void draw() {
    
  background(255);        // make background white (255)
  image(g1photo,0,0);     // gauge #1 face graphic
  image(g2photo,300,0);   // gauge #2 face graphic
 
  textFont(f, 16);        // gauge #1 digital text readout set up
  textAlign(CENTER);
  fill(0);
   
  try {
    if (revup == 0) {     // display rev up code on script start
        ivalue = startupfile.readLine();
        if (ivalue == null) {
             revup = 1;
             ivalue = "20,20";
             }
        } 
        else {
             ivalue = datafile1.readLine();   // after startup pull in serial data
        }
  }

  catch (IOException e) {
    e.printStackTrace();
    ivalue = null;
  }
  if (ivalue == null) {
    // Stop reading because of an error or file is empty
    noLoop();  
  }
  
  if (ivalue != null) {
    
    println(ivalue);                       // ivalue check out
    String[] pieces = split(ivalue, ',');  // breaks serial input line into pieces
    String g1value = pieces[0];
    String g2value = pieces[1];
    print(g1value);
    print("+");
    println(g2value);

    // anglef = radians(135) + radians(270);
    // input values to gauge angle conversions
    angle1f = (radians(gaugestartangle) + (float(g1value) * radians(gaugeaddangle/maxinputvalue)));
    angle2f = (radians(gaugestartangle) + (float(g2value) * radians(gaugeaddangle/maxinputvalue)));
    text(int(g1value), 150, 275);      // print the digital value on gauge #1
    
    if (int(g1value) > dangervalue){   // turns gauge red when over reving set value
      tint(255, 0, 0);
      } else {
      noTint();
    }
}
  // push and pop to display pointer values on gauge
  pushMatrix();
  segment1(g1x, g1y, angle1f);
  popMatrix();
  pushMatrix();
  segment2(g2x, g2y, angle2f);
  popMatrix();
    
}

void segment1(float x, float y, float a) {    // gauge #1 pointer move
  translate(x, y);
  rotate(a);                            // rotate pointer
  image(p1photo, -77, -94, 200, 150);   // pointer graphic image
}

void segment2(float x, float y, float a) {    // gauge #1 pointer move
  translate(x, y);
  rotate(a);                            // rotate pointer         
  image(p2photo, -41, -35, 100, 75);    // pointer graphic image
}

Much of the code was covered in a previous article. I’ve added quite a few comments to help readers understand how the existing and new functionality works.

Breaking up the input file is new. Scroll down to the “String[] pieces” line. This function takes an input line and breaks it up into character values, separated by the comma. The values are assigned to an array. We have only two input values in the text stream from the serial line, so we have two elements, [0] and [1] in the array. Additional sensor input values would increment the array accordingly. Those array elements are used to drive the code that cranks the pointers around the gauge face. Note in Processing, we can blatantly convert a character value to a float value using the float() function.

In a nutshell, adding new gauges basically means duplicating the angle#f and the segment functions then tweaking the appropriate setup initializations. You also have to account for additional input values in the input text stream.

Let’s move on to building those good-looking, low-effort gauge pointers.

Replacing Pointer Code with a Graphic

Much like ditching the notion of using manual code to ‘build’ a gauge with a dial face graphic, we can also streamline swinging the pointer around, with a picture too.

Here’s an old code snippet showing how to construct a pointer with line segments and code.

void segment(float x, float y, float a) {
  translate(x, y);
  rotate(a);
  line(0, 0, segLength, 0);
  line(segLength-15, 7, segLength, 0);
  line(segLength-15, -7, segLength, 0);
  line(0, 0, -50, 0);
  line(-30, 0, -40, -10);
  line(-30, 0, -40, 10);
  line(-40, 0, -50, -10);
  line(-40, 0, -50, 10);
  line(-50, 0, -60, -10);
  line(-50, 0, -60, 10);
}

That’s a lot of work to edit the values back and forth, hit “Run” (in the Processing IDE) then adjust the numbers to get a nice looking pointer.

Instead, look at the segment part of the new code. Here you’ll just see a reference to an image. The image variable connects to a .png or .gif file of the gauge pointer. For the large gauge, I took a picture with my Galaxy 8+ super-phone, of a DIY clock minute hand, pulled it into the GIMP and scaled the output file (png or gif) to 200×150 pixels. The dial face file, “/home/pi/gauge10-b.png” is scaled to 300×300 pixels.

The small gauge uses a dial face file called “/home/pi/gauge10-c.png” and is scaled to 150×150 pixels. This explains the half size in comparison to the larger left-hand gauge. A picture of a Phillips head screwdriver was made the same way, although scaled down to 100×75 pixels. Dial placement was handled earlier in the code with the g1x, g1y, g2x and g2y variables.

It seems a little tedious to do it this way but isn’t that bad once you get the hang of it. You can design any kind of dial face or gauge pointer at any scale and place it anywhere on your gauge dashboard, with this technique. If you don’t like the steampunk theme, pick something else, take some pics and you’re in business. It sure beats trying to build some lame “code” pointer and then spend hours modding it to look half-way right.

What do you think?

Large gauge with the clock pointer, a small gauge with screwdriver pointer

What’s Next?

I think this will wrap up the gauges for a while. I’m using the steampunk conference badge for a tech talk at OSCON in July, so probably need to halt tweaking and get everything stable. The hardware should work when we step up to the podium. I need to work on the startup and shutdown scripts, get everything looking consistent and do adequate testing.

Look for a different Off-The-Shelf Hacker topic next week.


A digest of the week’s most important stories & analyses.

View / Add Comments

Please stay on topic and be respectful of others. Review our Terms of Use.