Saturday, February 28, 2009

GUI Design and click-distance

I am not a GUI designer by trade, but I have done some GUI design both of web applications and normal GUIs. And I'd like to both rant a little and describe some of my GUI design thoughts.

First I think that we can all agree that many GUIs are poorly designed and therefore frustrating to use. And a lot has been written about how to clearly lay out information on the page, etc. A good layout is alway good, but especially for new users to the system. But what about the other 99% of the time the user uses the system? Once the user understands your GUI, how long it takes to physically move the mouse across the screen to use each widget required to get the job done becomes the determining factor as to whether your GUI is useable or not.

A lot less (if anything -- I can't find anything but I certainly haven't done a lit review on this!) has been written this -- about the kinematics of web design.

Wikipedia defines kinematics as studying motion without considering the circumstances leading to that motion. That is very similar to what I want to talk about -- which is the motion of the user's hands as he moves the mouse, moves from the mouse to the keyboard and so on. It doesn't really matter what the purpose of this motion is, other than to observe that good GUI design should attempt to minimize or eliminate the motion for common tasks.

Let me suggest a new metric called "click-distance". The purpose of click-distance is to create a quantitive measurement of how "far" the user has to move from his current GUI-state to get to his desired GUI state. So the metric will contain a lot more components then just clicking and moving the mouse, but I think that the term "click-distance" implies a good first order approximation.

One think that I don't like about the term is that perhaps even more important as "click-distance" is "click-time". That is, how long did it take to traverse the click-distance? But I think that "click-time" itself is not as important as "click-frustration" or how PISSED the user gets because he has to do something like individually select 500 files in 500 upload file dialog boxes instead of shift-clicking them all at once. But the further we go down this path, the less measurable the metric becomes.

Clearly by recording the sequence of input the user makes, we can infer something about the distance the user's hands travelled given a particular physical setup (desktop, laptop, or PDA/phone). And don't forget about left-handed people!

Click-distance should capture the following concepts.


Literal distance metrics:

1. Total distance the mouse cursor moved. Obvious!
2. Number of mouse-repositions. A person can move halfway across the screen in almost the time it takes to move just a little way. From experimenting on myself, it seems to take around a second for each individual mouse motion.
3. Number of uninterrupted mouse direction changes. A person generally moves the mouse in a straight line from its current position to the desired destination. If the mouse moves back and forth or around and around, that implies difficulty positioning -- that is, the GUI element is too small. From your high school calculus we know that the direction is changing along a particular axis when the velocity is zero...

Button press metrics:

4. Number of clicks and keys pressed.
5. Number of alternations between mouse press and key press. This metric is a rough estimate of how many times the user had to move his hand from the mouse to the keyboard, and is therefore a major contribution to click distance
6. ctrl-alt-shift-click Combinations.

Hard to measure metrics:

6. How often a certain sequence is repeated. Repetition is a huge source of user frustration (and is the source of carpel-tunnel syndrome) but conveniently is often the easiest to remove through a bit of GUI redesign. I think that it is so important that click-distance should be the square of the repetition, but only proportional to the rest of the metrics. However, it is difficult to programatically identify repetitive movements...

To make a quantitive formula, each of the terms described above should be multiplied by a constant adjustment factor that reflects the distance the hand must move. These constants might differ based on the physical layout being considered:

click-distance = A*mouseDist + B*mouseRepos + C*mouseVelZero + D*presses + E*mouseKeyAlternations + F*keyClickCombinations + (G*repetitions)^2

The idea is of course to mininize this click-distance for common tasks. Of course, you wouldn't really have to DO this math, just start thinking about your GUI in terms of how long it takes to do an operation and you'll be well along the path to good, useable design.


Here are some practical ideas that have emerged from my consideration of click-distance in GUI designs:

1. No modal dialog boxes!
They force the user to move the hand to the mouse, reposition, and click. For all "alerts", "errors", etc, use a "message bar" (preferably NOT hidden on the bottom of the screen) and graphics (like red text and animation) to draw attention.

They also position themselves "creatively" -- aka NOT where your mouse is!

Using a modal dialog box makes it hard to see the information behind it.

Does not scale: doing 200 operations that contain a modal dialog requires 200 reposition-click-reposition operations.

The only time a modal dialog should be used is in TRUELY irrevocable operations. Like formatting your hard drive.

Of course, the corrolary to this is that your program should not HAVE any truely irrevocable operations.

2. Keep it all in one window.
Use tabs, etc. They much shorter in mouse distance to switch between than windows that have to be sized and positioned.

3. Design with REAL data, not your unit-test toys.
You really ought to try the GUI with real-world examples, that contain lots of data. Doing so is the best way to discover repetition.

4. Allow multi-select absolutely whenever possible.
Creatively apply what this might mean in cases where you are not sure... implementing one of 3 possibilities at least fixed 1/3 of the problem...

5. Keep values populated in fields, and don't force a history selection from the selection box.

This will dramatically reduce click-distance for unanticipated repetitive tasks. Look, this is about click-frustration. If a person is entering 1000 people's addresses over the whole USA, it is not so bad to type the city and state. But if the person is the tax accountant for a particular town and so the city and state is almost always the same it can be very tiresome.

Its best to have an easy way to delete the field in one go like most boxes have. Or even better try a tiny "pin value" icon button near the field. If the "pin value" button is depressed, the last value entered is remembered for the next time.

6. Selection boxes suck!
Clearly the problem is that the user was probably typing, so now he must move from the keyboard to the mouse. Click to open the selection, all kinds of tricky clicking (especially since the selection box sometimes closes itself if the mouse wanders too far) to scroll down, and finally click to select. It is major mouse distance.

Here's one example that would save millions of people 10 seconds per day and massively reduce office rage:

Its much harder to find "New York" in a list of states 52 states then to just type "NY"!

Especially when some of you only show us 3 states at a time!

7. For the prime function, click distance should be <= 1.

Word processors allow you to add lots of letters to a document with 1 keypress per letter. They are not so good at saving 10 different copies of the document. You have to pop up 10 modal dialog boxes. File managers let you copy 100 documents to 10 different places with a mouse distance of about 20 (1 selection operation + 10*(mouse position + key:ctrl-v)). And so on...


Ok. I think that's enough examples. There are lots more. As you think of examples, please add them as comments!

Friday, February 27, 2009

First Arduino shield boards arrived!

I designed an Arduino shield to provide a lot of outputs using very few pins on the Arduino... it uses 2 M5451 35 segment LED display driver chips.




Here's the board with all the parts soldered on.









Here is a angled view. On the front you can see a lot of pads. The idea was to allow the user to choose which Arduino pins should control the board by only bridging some of the gaps. However it turns out to be very difficult to solder tiny wires across these gaps.

Any ideas on how to do this better?





Here it is shown lighting up all the LEDs I happened to have lying around.











Here is the bottom view showing the Arduino connected.











A youtube video about it:

Tuesday, February 24, 2009

constant current LED driver thoughts (LM334)


I was experimenting on how to mount surface mount LEDs in wood and made this little arch to test out ideas.

All the LEDs are in series. But the question was how to drive them? I could connect them to a wall wart transformer and put in a resistor, but that would mean that the resistance of the resistor would have to be tuned to the wall wart transformer. Since I was just going to grab a transformer from my junk pile of dead electronics I didn't want to do that. Instead I settled on this constant current circuit shown in discover circuits:

http://www.discovercircuits.com/DJ-Circuits/constantcurretled1.htm


This circuit is really strange because the current from the LED goes through R1 along with the current from the LM334's current selection. Was it a typo? Should the LED current really just go straight to ground?

Also, I popped up the LM334 spec. The LM334 basically has 3 pins: Positive, ground, and a third "current selection" pin. You just connect a resistor across the current selection pin and the ground pin to choose what current you want. So it turned out that the LM334 is a constant current driver itself, so why have the extra transistor?

And In fact, the naked LM334 circuit what I ended up using. But the issue kept bothering me and so I finally decided to think about it.

I decided to try 3 simple circuits. Let's call the simple one "A":




So basically the current from the LEDs runs through the LM334, and so, regardless of the input voltage, is regulated by the size of the resistor connected between pins 2 and 3.

I guess the problem is that all the current is passing through the LM334...







And then a "simplified" discovercircuits, call it "B":


The LM334 is used as suggested by the spec. But the LM334 only controls the current that flows out of the base of the transistor, so current through the transistor and LED will be about 100 times (or whatever the amplification of your transistor is) what is going through the LM334.

This turns out to be very useful if you want to use the relatively low current LM334 chip to control a much larger current. Or if you only have larger value transistors lying around.





This one from discovercircuits "C":

As I previously said, "C" is really strange because the current from the LED goes through R1 along with the current from the LM334's current selection.

The LM334 holds is middle pin at 68mV above the bottom. So without doing any math, you can see that current going through the LED wil have to "share space" through the resistor. As the current through the LED rises, the current sensed by the LM334 goes down. This therefore is equivalent (to the LM334) like putting a bigger resistor across its pins. End result: the LM334 lets a lower current pass through itself (i.e. out of the transistor's base). You actually end up with a stable situation.

As with the second circuit (B), the advantage is that load is going through the transistor, not the LM334. An additional advantage is that the resistor values to make it all work are similar to that in the LM334 spec, and you don't have a 100x multiplication of the current (which may or may not be an advantage)

Another disadvantage of B is that it is very sensitive to the exact amplification (beta) of the transistor. But I guess transistor betas used to vary pretty widely within the same part due to small fabrication differences (I am not sure how accurate they are nowadays)... So while B is great for home use where you can test the exact beta of the transistor and choose a resistor to match, this circuit might be a bad idea to use in a production circuit board.

However, a big disadvantage of C is that the load (from the LED) is going through the resistor, so you do waste more power than in B and most importantly will have to use a high-watt (bulky, expensive, don't have it in your basement) resistor. Or of course a bunch of normal resistors in parallel...but that is also pretty ugly.

Sunday, February 22, 2009

Arduino and M5451 -- Control 35 LEDs, motors, etc!

I've been fooling around with an Ardunio, which is a small embedded microprocessor and discovered that its a great way for software engineers to learn some hardware engineering. It is fairly limited in its outputs though, so I bought a couple of M5451 chips (futureelectronics.com MM5451YN for small quantities). This is nice because it is a 40 pin DIP device that plugs right into a breadboard, and is intended to drive LED displays.

But it does not HAVE to be used for LED displays -- I'm thinking of it as 35 constant-current switches that can be conveniently controlled (on/off) by 2 Arduino digital pins (clock and serial data). And if you want to change the current (of all pins) you can use a third pin to connect a "brightness" pin on the 5451 to a PWM on the Arduino.

So this is a *LOT* cooler than the chips that drive 8x8 arrays of LEDs by strobing them (MAX7219, search for "Arduino 8x8 matrix") because those chips flash the LEDs. So they really can't be used for other applications (like driving motors, or turning on a big-current transistor switch), and the LEDs are in theory not as bright because they are not on the entire time. In theory, I should be able to drive 2 of these 8x8 matrixes with one M5451 with 3 outputs left over, or one 16x16 (256 LEDS!) display (if I can get my hands on one).

Also, the cool thing about the M5451 being a constant current driver is that you don't need a current-limiting resistor inline with each LED. And you can connect several LEDs in series without worrying about V=IR math.

But let me emphasize for the beginning hardware DIYer, its important to NOT think of the outputs as logic, just think of them as switches. Basically you connect the M5451 to ground, and connect your load (let's just use LEDs, for example) between the + and it. So the M5451 must be on the cathode, minus, ground side -- whatever you call it -- of your LED. This is IMPORTANT, since if you buy something like RGB LEDs you want to get common ANODE. Remember, relative to the LED, the anode side is where conventional current (+) goes IN.

Of course, I bought the common cathode RGB LEDs (*sigh*).

So here's a picture of a M5451 on a breadboard with a boarduino and with a bunch of LEDs:


Ignore the upper part of the breadboard. all the activity is on the bottom. Also ignore the square silver chip and transistors on the far right. That's just stuff hanging around from another project!

The boarduino is on the bottom left. The 2 pins used are the green and yellow wires on the top side (the blue and short green wires are just power and ground).

You can see all the LEDs bridging from the + rail to the pins on the M5451 (middle)


I wrote a bit of code to drive the M5451 (see below). You can turn on each M5451 switch independently, control the brightness via one of the Arduino's PWM ports (PWM means pulse-width-modulation -- the Arduino toggles the line between 5 and 0 volts rapidly and so that it seems like the line actually has a voltage which is the weighted average of the time the line is 5 vs. 0 -- i.e. a simulated analog voltage).

On top of that, I made a class that is similar to PWM; it toggles the M5451 lines rapidly so the LEDs seem to change brightness.

Here is a video. To make it come out, I have a halogen desk lamp shining directly on the board from about a foot away. About halfway through I turned off the light so you could see the LED brightness in normal room light! Check out the video here:

video


Here's the code:


/*
* M5451 LED driver chip
* Author: G. Andrew Stone
* Public Domain
*/

int myClockPin = 2; // Arduino pin that goes to M5451 clock
int mySerDataPin = 3; // Arduino pin that goes to M5451 data

void setup() // run once, when the sketch starts
{
}


#define M5451_NUMOUTS 35
#define M5451_CLK 0
class M5451
{
public:
byte clockPin;
byte brightPin;
byte serDataPin;
M5451(byte clockPin,byte serDataPin,byte brightPin);

void set(unsigned long int a, byte b=0);
void setBrightness(byte b);

private:
void mydelay(int clk);
};

void M5451::setBrightness(byte b)
{
if (brightPin < 0xff)
analogWrite(brightPin,b);
}

#define MaxBrightness 4096 //256
class FlickerBrightness:public M5451
{
public:
FlickerBrightness(byte clkPin, byte dataPin, byte brightnessPin);

void shift(int amt=1)
{
offset+=amt;
if (offset>=M5451_NUMOUTS) offset -=M5451_NUMOUTS;
else if (offset< 0) offset +=M5451_NUMOUTS;
}
void loop(void);

int brightness[M5451_NUMOUTS];
int bresenham[M5451_NUMOUTS];
int iteration;
int offset;
};

FlickerBrightness::FlickerBrightness(byte clkPin, byte dataPin,byte brightnessPin):M5451(clkPin,dataPin,brightnessPin)
{
for (int i=0;i < M5451_NUMOUTS;i++)
{
brightness[i] = 0;
bresenham[i] = 0;
}

iteration = 0;
offset = 0;
}

void FlickerBrightness::loop(void)
{
int i;
byte pos;
unsigned long int a=0;
byte b=0;
boolean lvl=false;

for (i=0,pos=offset;i < M5451_NUMOUTS;i++,pos++)
{
if (pos>=M5451_NUMOUTS) pos=0;
bresenham[i] += brightness[pos];
if (bresenham[i]>=MaxBrightness)
{
bresenham[i] -= MaxBrightness;
lvl = true;
}
else lvl = false;

if (i<32) a = (a<<1)|lvl;
else b = (b<<1)|lvl;
}
iteration++;
if (iteration > MaxBrightness) iteration = 0;

set(a,b);
}


M5451::M5451(byte clkPin, byte dataPin, byte brightnessPin)
{
int i;

clockPin = clkPin;
serDataPin = dataPin;
brightPin = brightnessPin;

pinMode(clkPin, OUTPUT); // sets the digital pin as output
pinMode(serDataPin, OUTPUT); // sets the digital pin as output
pinMode(brightPin,OUTPUT);

// Clear out the device so we can clock in items
digitalWrite(serDataPin,LOW);
for (i=0;i< M5451_NUMOUTS+2;i++)
{
mydelay(M5451_CLK);
digitalWrite(clockPin,HIGH);
mydelay(M5451_CLK);
digitalWrite(clockPin,LOW);
}
}

void M5451::mydelay(int clk)
{
int i;
for (i=0;i< clk;i++);
//delay(clk);
}

void M5451::set(unsigned long int a, byte b)
{
int i;

// Write the initial "start" signal
digitalWrite(clockPin,LOW);
digitalWrite(serDataPin,LOW);
mydelay(M5451_CLK);
digitalWrite(clockPin,HIGH);
mydelay(M5451_CLK);
digitalWrite(clockPin,LOW);
mydelay(M5451_CLK/2);
digitalWrite(serDataPin,HIGH);
mydelay(M5451_CLK/2);
digitalWrite(clockPin,HIGH);
mydelay(M5451_CLK);
digitalWrite(clockPin,LOW);

// Write the bits
for (i=0;i< M5451_NUMOUTS;i++)
{
int serDataVal;
if (i<32) serdataval =" (a&1);">>=1;}
else { serDataVal = (b&1); b>>=1;}
mydelay(M5451_CLK/2);
digitalWrite(serDataPin,serDataVal);
mydelay(M5451_CLK/2);
digitalWrite(clockPin,HIGH);
mydelay(M5451_CLK);
digitalWrite(clockPin,LOW);
}
}

void loop() // run over and over again
{
unsigned long int j;
int i;
FlickerBrightness leds(myClockPin,mySerDataPin,9);

leds.setBrightness(255);

for (i=3;i>=0;i--)
{
for (j=0;j<35;j++)
{
leds.set(1L<< j,(j>=32) ? 1L<<(j-32):0);
delay(10*i);
}
}

// Proportional fading
if (1) for (j=0;j<200;j++)
{
for (i=0;i< M5451_NUMOUTS;i++)
{
int k = 1<<(j%13);
if ((i&3)<2)
{
if (leds.brightness[i] < 35) leds.brightness[i] = 35;
else leds.brightness[i] += leds.brightness[i]>>2;
}
else
{
if (leds.brightness[i] < 35) leds.brightness[i] = MaxBrightness;
else leds.brightness[i] -= leds.brightness[i]>>2;
}
}
for (i=0;i<100;i++) leds.loop();
}

leds.set(0xffffffff,0xff); /* ALL ON */
delay(250);
leds.set(0,0); /* ALL OFF */
delay(250);

// Linear per-LED brightness method
if (1) for (j=0;j<4096;j++)
{
for (i=0;i< M5451_NUMOUTS;i++)
{
int k = j*10;
if (i&1)
{
leds.brightness[i] = abs((k&(MaxBrightness*2-1))-MaxBrightness);
}
else
leds.brightness[i] = MaxBrightness - abs((k&(MaxBrightness*2-1))-MaxBrightness);
}
for (i=0;i<10;i++) leds.loop();
}


// ALL FADE using M5451 Brightness feature
leds.set(0xffffffff,0xff); /* ALL ON */
for (j=1;j<5;j++)
{
for (i=0;i<256;i++)
{
leds.setBrightness(i&255);
delay(j);
}
for (i=255;i>=0;i--)
{
leds.setBrightness(i&255);
delay(j);
}
}

leds.setBrightness(255);

leds.set(0xffffffff,0xff); /* ALL ON */
delay(250);
leds.set(0,0); /* ALL OFF */
delay(250);


// MARQUEE

for (i=0;i< M5451_NUMOUTS;i++) // Clear all LEDs to black
{
leds.brightness[i]=0;
}

// Turn on a couple to make a "comet" with dimming tail
leds.brightness[0] = MaxBrightness-1;
leds.brightness[1] = MaxBrightness/2;
leds.brightness[2] = MaxBrightness/4;
leds.brightness[3] = MaxBrightness/8;
leds.brightness[4] = MaxBrightness/16;
leds.brightness[5] = MaxBrightness/64;
leds.brightness[6] = MaxBrightness/100;

for (j=0;j<100;j++)
{
leds.shift(1);
for (i=0;i<150;i++) leds.loop();
}

for (j=0;j<100;j++)
{
leds.shift(-1);
for (i=0;i<100;i++) leds.loop();
}


if (1)
{
leds.set(0xffffffff,0x7);
delay(1000);
//leds.set(0xf0f0f0f0,0x7);
//delay(10000);
//leds.set(0x11111111,0x1);
//delay(10000);
}

}