Part 5: PC GUI Demo

In the previous section we obtained some useful output, and confirmed that our XYZ Pad basically works as expected.

But staring at a stream of text, it's hard to develop any detailed sense for how the XYZ Pad responds, or identify performance aspects which can be improved upon. For more insight, here's a graphic demo app that we put together in Processing, which you will need to download if you'd like to run the demo.


Here's a screenshot >>


To get the example Arduino sketch talking with the graphic application, you'll need to make one edit in xyzdefines.h.

Find these lines:

//#define TERMINAL_OUTPUT 1
#define PROCESSING_OUTPUT 1


And swap the commenting, like so:

#define TERMINAL_OUTPUT 1
//#define PROCESSING_OUTPUT 1


Good to go! Processing code and a downloadable project .zip are posted below. For a list of hotkeys and some tips to optimize performance, check the included text file.


Download Code: XYZPadTutorialVisualizer.zip

Code:
/**********************************************************************************************************
* Project: XYZPadTutorialVisualizer.pde
* By: Chris Wittmier @ Sensitronics LLC
* LastRev: 03/29/2015
* Description: General 4-Wire XYZ pad tester / visualizer
**********************************************************************************************************/
import processing.serial.*;


/**********************************************************************************************************
* ADJUSTABLE PARAMETERS
**********************************************************************************************************/
//PHYSICAL DIMENSIONS
float PAD_WIDTH_INCHES = 6.0; //physical dimensions of pad. Can specify larger/smaller than actual if 1:1 mapping is not desired.
float PAD_HEIGHT_INCHES = 6.0;

//DISPLAY_PARAMETERS
int PIXELS_PER_INCH = 100; //adjust for monitor, typical pitch is ~100PPI
int MAXIMUM_POINT_DIAMETER_PIXELS = 100; //for a point input at maximum force, a circle with this diameter will be drawn
boolean SHOW_GRIDLINES = true; //if true, faint 1x1 inch gridlines will be overlayed

//COMM SETTINGS
int SERIAL_BAUD_RATE = 115200;
boolean AUTO_CONNECT_HIGHEST_PORT = true; //if true, open highest number serial port at startup. Leave true, no manual connect yet.


/**********************************************************************************************************
* CONSTANTS
**********************************************************************************************************/
int SERIAL_BUFFER_SIZE = 8;

final int MODE_NONE = 0;
final int MODE_X_EDIT = 1;
final int MODE_Y_EDIT = 2;
final int MODE_DUAL_EDIT = 3;
final int MODE_Z_EDIT = 4;

final int CONSOLE_X_POSITION = 10;
final int CONSOLE_Y_POSITION = 20;
final int CONSOLE_TEXT_SIZE = 14;

/**********************************************************************************************************
* GLOBALS
**********************************************************************************************************/
Serial sPort;
int[] serial_buffer = new int[SERIAL_BUFFER_SIZE];
int[] current_message = new int[SERIAL_BUFFER_SIZE - 2];
boolean message_ready_flag = false;

int canvas_width_pixels;
int canvas_height_pixels;
float x_scale_factor;
float y_scale_factor;
float force_scale_factor;
int operation_mode = MODE_NONE;

float x_pre_offset = 0;
float y_pre_offset = 0;
float x_pre_scale = 1;
float y_pre_scale = 1;
float force_pre_scale = 1;
int force_pre_thresh = 3;


/**********************************************************************************************************
* setup()
**********************************************************************************************************/
void setup()
{
canvas_width_pixels = (int) (PAD_WIDTH_INCHES * PIXELS_PER_INCH);
canvas_height_pixels = (int) (PAD_HEIGHT_INCHES * PIXELS_PER_INCH);
x_scale_factor = (float) canvas_width_pixels / 1023.0;
y_scale_factor = (float) canvas_height_pixels / 1023.0;
force_scale_factor = (((float) MAXIMUM_POINT_DIAMETER_PIXELS) / 1023.0) / 2.0;
size(canvas_width_pixels, canvas_height_pixels);

drawBackground();

if(AUTO_CONNECT_HIGHEST_PORT)
{
sPort = new Serial(this, Serial.list()[(Serial.list().length) - 1], SERIAL_BAUD_RATE);
}
}


/**********************************************************************************************************
* draw()
**********************************************************************************************************/
void draw()
{
if((message_ready_flag) || (operation_mode != MODE_NONE))
{
drawBackground();
}
if(message_ready_flag)
{
parseReport(current_message);
message_ready_flag = false;
}
if(operation_mode != MODE_NONE)
{
drawConsole();
}
}


/**********************************************************************************************************
* drawConsole()
**********************************************************************************************************/
void drawConsole()
{
fill(255, 255, 255);
textSize(CONSOLE_TEXT_SIZE);
switch(operation_mode)
{
case MODE_X_EDIT:
text("X Scale: " + x_pre_scale + " X Offset: " + x_pre_offset, CONSOLE_X_POSITION, CONSOLE_Y_POSITION);
break;
case MODE_Y_EDIT:
text("Y Scale: " + y_pre_scale + " Y Offset: " + y_pre_offset, CONSOLE_X_POSITION, CONSOLE_Y_POSITION);
break;
case MODE_DUAL_EDIT:
text("X Scale: " + x_pre_scale + " X Offset: " + x_pre_offset + " Y Scale: " + y_pre_scale + " Y Offset: " + y_pre_offset, CONSOLE_X_POSITION, CONSOLE_Y_POSITION);
break;
case MODE_Z_EDIT:
text("Force Scale: " + force_pre_scale + " Force Threshold: " + force_pre_thresh, CONSOLE_X_POSITION, CONSOLE_Y_POSITION);
break;
default:
break;
}
}


/**********************************************************************************************************
* keyPressed()
**********************************************************************************************************/
void keyPressed()
{
if(key == CODED)
{
switch(keyCode)
{
case UP:
adjustScale(1, operation_mode);
break;
case DOWN:
adjustScale(-1, operation_mode);
break;
case LEFT:
adjustOffset(-1, operation_mode);
break;
case RIGHT:
adjustOffset(1, operation_mode);
break;
case 33:
adjustOffset(50, operation_mode);
break;
case 34:
adjustOffset(-50, operation_mode);
break;
default:
if(operation_mode != MODE_NONE)
{
operation_mode = MODE_NONE;
drawBackground();
}
break;
}
}
else
{
switch(key)
{
case 'x':
case 'X':
operation_mode = MODE_X_EDIT;
break;
case 'y':
case 'Y':
operation_mode = MODE_Y_EDIT;
break;
case 'b':
case 'B':
operation_mode = MODE_DUAL_EDIT;
break;
case 'z':
case 'Z':
operation_mode = MODE_Z_EDIT;
break;
default:
if(operation_mode != MODE_NONE)
{
operation_mode = MODE_NONE;
drawBackground();
}
break;
}
}
}


/**********************************************************************************************************
* adjustScale()
**********************************************************************************************************/
void adjustScale(float step, int opmode)
{
if(opmode == MODE_NONE)
{
return;
}
else if(opmode == MODE_X_EDIT)
{
x_pre_scale += step;
}
else if(opmode == MODE_Y_EDIT)
{
y_pre_scale += step;
}
else if(opmode == MODE_DUAL_EDIT)
{
x_pre_scale += step;
y_pre_scale += step;
}
else if(opmode == MODE_Z_EDIT)
{
force_pre_scale += step;
}
if(x_pre_scale < 1)
{
x_pre_scale = 1;
}
if(y_pre_scale < 1)
{
y_pre_scale = 1;
}
if(force_pre_scale < 1)
{
force_pre_scale = 1;
}
}


/**********************************************************************************************************
* adjustOffset()
**********************************************************************************************************/
void adjustOffset(float step, int opmode)
{
if(opmode == MODE_NONE)
{
return;
}
else if(opmode == MODE_X_EDIT)
{
x_pre_offset += step;
}
else if(opmode == MODE_Y_EDIT)
{
y_pre_offset += step;
}
else if(opmode == MODE_DUAL_EDIT)
{
x_pre_offset += step;
y_pre_offset += step;
}
else if(opmode == MODE_Z_EDIT)
{
force_pre_thresh += step;
}
if(force_pre_thresh < 0)
{
force_pre_thresh = 0;
}
}


/**********************************************************************************************************
* drawBackground()
**********************************************************************************************************/
void drawBackground()
{
background(0, 0, 0);

if(SHOW_GRIDLINES)
{
strokeWeight(1);
stroke(85, 85, 85);
for(int i = PIXELS_PER_INCH; i < canvas_width_pixels; i += PIXELS_PER_INCH)
{
line(i, 0, i, canvas_height_pixels);
}
for(int i = PIXELS_PER_INCH; i < canvas_height_pixels; i += PIXELS_PER_INCH)
{
line(0, i, canvas_width_pixels, i);
}
}
}


/**********************************************************************************************************
* serialEvent() - trigger on each received byte, shift buffer left and read current byte into end of buffer
**********************************************************************************************************/
void serialEvent(Serial sPort)
{
while(sPort.available() > 0)
{
for(int i = 1; i < SERIAL_BUFFER_SIZE; i ++)
{
serial_buffer[i - 1] = serial_buffer[i];
}
int got_int = (int) sPort.read();
got_int = got_int & 0x00FF;
serial_buffer[SERIAL_BUFFER_SIZE - 1] = got_int;
if((serial_buffer[SERIAL_BUFFER_SIZE - 2] == 255) && (serial_buffer[SERIAL_BUFFER_SIZE - 1] == 255))
{
for(int i = 0; i < SERIAL_BUFFER_SIZE; i ++)
{
if(i < SERIAL_BUFFER_SIZE - 2)
{
current_message[i] = serial_buffer[i];
}
serial_buffer[i] = 0x00;
}
if(message_ready_flag == false)
{
message_ready_flag = true;
}
}
}
}


/**********************************************************************************************************
* parseReport()
**********************************************************************************************************/
void parseReport(int[] report)
{
int x_pos, y_pos, force;
x_pos = ((report[0] << 8) & 0xFF00) | (report[1] & 0x00FF);
y_pos = ((report[2] << 8) & 0xFF00) | (report[3] & 0x00FF);
force = ((report[4] << 8) & 0xFF00) | (report[5] & 0x00FF);
drawPointForce(x_pos, y_pos, force);
}


/**********************************************************************************************************
* drawPointForce()
**********************************************************************************************************/
void drawPointForce(int x_pos, int y_pos, int force)
{
int center_x, center_y, force_radius;
float float_x, float_y, float_force;
if(force >= force_pre_thresh)
{
float_x = ((float) x_pos + x_pre_offset) * x_pre_scale;
float_y = ((float) y_pos + y_pre_offset) * y_pre_scale;
float_x = ((float) float_x) * x_scale_factor;
float_y = ((float) float_y) * y_scale_factor;
center_x = (int) float_x;
center_y = (int) float_y;
float_force = ((float) force) * force_pre_scale;
float_force = ((float) float_force) * force_scale_factor;
force_radius = ceil(float_force);
ellipseMode(RADIUS);
fill(255, 0, 0);
ellipse(center_x, center_y, force_radius, force_radius);
//println(center_x + " " + center_y + " " + force_radius);
}
}




/**********************************************************************************************************
* CLASSES
**********************************************************************************************************/



Conclusions

We'll leave some experimentation and observation open to the reader / builder. This has been a relatively basic example, so there's a lot of room for some clever improvements in your own force-sensing designs.

For example, here's one issue you may have noted: If you apply a force in the X/Y+ corner and move to the X/Y- corner (maintaining the same force), you'll observe a gradual change in Z-Axis sensitivity.

If you consider where we're driving and measuring the XYZ Pad, you can probably see why this occurs (and maybe come up with some ideas to compensate). Resistive touchscreens exhibit similar characteristics - the geometric algorithms used in touchscreen / touchpad controller ICs make for some worthwhile reading.


Well, that's all for this tutorial, we look forward to seeing your force-sensing projects in action! Comments or suggestions? Feel free to drop us a line.