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

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.
1 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
/* * 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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.