User Interface Design Part 6: … and the rest.

Now that we’ve designed our interface by designing our visual language and defining the nouns and verbs, we’ve laid out the screens using that visual interface, and built some of the basic building blocks: the code to draw our interface elements and the code to manage our screen, all that is left to do is to build the individual screens.

Rather than describe every single screen in our system, I’ll describe the building of one of the screens: the rest are built more or less the same way, and you can see how this all works in the source code.


When the user presses the fan button or presses one of the temperature buttons, we drop the user into the temperature setting page.

Temperatures

This page allows the user to set the fan, turn off the unit, and set the temperature.

Now recall that our user interface is segregated in three major blocks of code: the model, view and controller:

Model View Controller

From a code perspective, we manipulate the GThermostat object, the model component which is used to directly control the HVAC hardware. Our code also handles the drawing of our layout and setting up the hit locations which represent the areas where the user can tap our screen.

Constructing the page object

Our temperature page, then, is very simple. We need to extend the UI page code we wrote yesterday, initialize our page with the appropriate layout parameters. We then need to draw the contents when the contents need to be redrawn, and we need to handle tap events.

class AdaTempPage: public AdaUIPage
{
    public:
                        AdaTempPage();
                        
        virtual void    drawContents();

        virtual void    handleEvent(uint8_t ix);
};

Our constructor, likewise, is very simple, since we’re doing most of the work in AdaUIPage:

AdaTempPage::AdaTempPage() : AdaUIPage(&ATemp)
{
}

Literally all we’re doing is passing in a set of globals so our base class can know the title of our page, the label to use for the back button, the location of the rectangles that represent our buttons:

static const AdaUIRect ATempRects[] PROGMEM = {
    { 117,  88,  40,  37 },       // Heat ++
    { 117, 126,  40,  37 },       // Heat --
    { 231,  88,  40,  37 },       // Cool ++
    { 231, 126,  40,  37 },       // Cool --
    {  64, 195,  63,  37 },       // Auto
    { 148, 195,  63,  37 },       // On
    { 229, 195,  63,  37 }        // Off
};

static const AdaPage ATemp PROGMEM = {
    string_settings, string_back, NULL, ATempRects, 7
};

Where string_settings and string_back was declared in a separate “AdaStrings.h” header:

/* AdaStrings.h */

extern const char string_settings[];
extern const char string_back[];

and

/* AdaStrings.cpp */

const char string_settings[] PROGMEM = "SETTINGS";
const char string_back[] PROGMEM = "\177DONE";

Note: Because we reuse these strings, rather than use the F(“SETTINGS”) directive I’ve elected to move all the strings to a central file. This prevents the same string from being created multiple times in memory, wasting precious program memory space.

Drawing the page contents

We create two support routines which help us draw our buttons. The reason why we locate our button drawing and fan light drawing code in separate routines is so as to reduce flicker on the screen.

We could, when the user presses a button, call invalidateContents, a method we created previously to mark the content area as needing redrawing. However, this causes an unacceptable flashing of the screen. So instead, we move the code which draws the temperature area and draws the fan lights–that way we only erase and redraw the portion of the screen that needs redrawing. In this way we reduce the flickering on our screen.

static void DrawHeatCool(uint16_t xoff, uint8_t temp)
{
    char buffer[8];

    GC.setFont(&Narrow75D);
    GC.setTextColor(ADAUI_RED,ADAUI_BLACK);

    FormatNumber(buffer,temp);
    GC.drawButton(RECT(xoff,88,70,75),buffer,66);
}

static void DrawFan(uint8_t fan)
{
    GC.setTextColor(ADAUI_BLACK,(fan == ADAHVAC_OFF) ? ADAUI_GREEN : ADAUI_DARKGRAY);
    GC.drawButton(RECT(209,195,19,37));

    GC.setTextColor(ADAUI_BLACK,(fan == ADAHVAC_FAN_AUTO) ? ADAUI_GREEN : ADAUI_DARKGRAY);
    GC.drawButton(RECT( 44,195,19,37));

    GC.setTextColor(ADAUI_BLACK,(fan == ADAHVAC_FAN_ON) ? ADAUI_GREEN : ADAUI_DARKGRAY);
    GC.drawButton(RECT(128,195,19,37));
}

Notice in both cases we make extensive use of our new user interface code. We even use it to draw our temperature–even though the background of the “button” is black. It may be slightly faster to explicitly draw “fillRect” with black and to call the GC.print() method to draw our temperature–but it would cause other libaries to be loaded into memory.

And memory usage in our thermostat is tight. Which means sometimes we reuse what we have rather than link against what may be nicer.

Now that we have these support routines, drawing our buttons and controls is simple:

void AdaTempPage::drawContents()
{
    char buffer[8];

    // Draw temperatures
    DrawHeatCool( 43,GThermostat.heatSetting);
    DrawHeatCool(157,GThermostat.coolSetting);
    
    // Draw buttons
    GC.setFont(&Narrow25D);
    GC.setTextColor(ADAUI_BLACK,ADAUI_BLUE);
    GC.drawButton(RECT(117,88,40,37), (const __FlashStringHelper *)string_plus,28,KCornerUL | KCornerUR,KCenterAlign);
    GC.drawButton(RECT(117,126,40,37),(const __FlashStringHelper *)string_minus,28,KCornerLL | KCornerLR,KCenterAlign);

    GC.drawButton(RECT(231,88,40,37), (const __FlashStringHelper *)string_plus,28,KCornerUL | KCornerUR,KCenterAlign);
    GC.drawButton(RECT(231,126,40,37),(const __FlashStringHelper *)string_minus,28,KCornerLL | KCornerLR,KCenterAlign);
    
    // Draw state buttons
    GC.drawButton(RECT( 32,195,11,37),KCornerUL | KCornerLL);
    GC.drawButton(RECT( 64,195,63,37),(const __FlashStringHelper *)string_auto,28);
    GC.drawButton(RECT(148,195,60,37),(const __FlashStringHelper *)string_on,28);
    GC.drawButton(RECT(229,195,60,37),(const __FlashStringHelper *)string_off,28,KCornerUR | KCornerLR);
    
    DrawFan(GThermostat.fanSetting);
}

All we do is draw our temperature, our four buttons (the “+” and “-” under our temperatures), and the four regions that define the fan at the bottom.

Handling Events

The cornerstone of our “control” code, the thing that translates user actions with changes in our model, is contained in our handleEvent method. We have seven areas the user can tap, and we handle each of those cases in our code. Rather than duplicate the entire method here–the whole thing is on GitHub–I’ll just talk about one of these cases.

        case AEVENT_FIRSTSPOT+4:
            GThermostat.fanSetting = ADAHVAC_FAN_AUTO;
            DrawFan(GThermostat.fanSetting);
            break;

The constant “AEVENT_FIRSTSPOT+4” refers to the fifth item in the list:

static const AdaUIRect ATempRects[] PROGMEM = {
    { 117,  88,  40,  37 },       // Heat ++
    { 117, 126,  40,  37 },       // Heat --
    { 231,  88,  40,  37 },       // Cool ++
    { 231, 126,  40,  37 },       // Cool --
    {  64, 195,  63,  37 },       // Auto
    { 148, 195,  63,  37 },       // On
    { 229, 195,  63,  37 }        // Off
};

And this is the area drawn by our drawContents code:

void AdaTempPage::drawContents()
{
...    
    // Draw state buttons
    GC.drawButton(RECT( 32,195,11,37),KCornerUL | KCornerLL);
    GC.drawButton(RECT( 64,195,63,37),(const __FlashStringHelper *)string_auto,28);
    GC.drawButton(RECT(148,195,60,37),(const __FlashStringHelper *)string_on,28);
    GC.drawButton(RECT(229,195,60,37),(const __FlashStringHelper *)string_off,28,KCornerUR | KCornerLR);
...
}

Now when our button is tapped on, the location is detected, and we receive a call to handleEvent with the constant AEVENT_FIRSTSPOT+4.

And when we do, we want to turn the thermostat on to “AUTO”:

            GThermostat.fanSetting = ADAHVAC_FAN_AUTO;

Our thermostat code will then use this setting to make decisions in the future about turning on and off the fan as the temperature rises and falls. But because the model code is contained elsewhere, it’s not our problem. We simply tell the thermostat code what to do; it figures out how to do it.

Then we redraw our fan control lights to let the user know the setting was changed:

            DrawFan(GThermostat.fanSetting);

And that’s it. There is no step 3.

We do this for the rest of our event messages as well. One proviso is that we don’t allow the user to set the temperature below 50 or above 90, and we require temperatures to be 5 degrees apart.

And that’s it.


That’s our thermostat code. Well, there are a bunch of other screens–but all of them more or less follow the same pattern as the code above: we draw the screen, we listen for events, we respond to those events by making the appropriate changes in our model code.

There may be a few thousand lines of page handling code–but they all do the same thing.

And that’s the beauty of good design: it simplifies your code. If all the controls look the same, you can use the same routine to draw the controls. If all your controls behave the same way, you can reuse the same code to handle its behavior.

This simplicity makes the thermostat code quick for any user to understand and use. And while we may have started with the goal of an “LCARS”-like Star Trek-like interface, what we got was something that is pretty simple and nearly invisible. It’s an interface most users would not question if they encountered it.

The overall source kit is at GitHub if you want to test the code yourself.


Wrapping up.

In this six part series we discussed the importance of building a good user interface by carefully considering the visual language. We touched upon discovery–the ability of the user to discover how to use your interface by the use of “affordances”: using elements which behave consistently and which seem apparent to the user. We also touched upon consistency: making sure that when you design your visual language you stick with the design.

We briefly touched upon the components of your visual language: the “nouns” (the things you are manipulating), and the “verbs” (the actions you are taking with your nouns). We touched on the importance of visually separating “nouns” and “verbs” even in a simple interface like a thermostat.

And we spent most of our time putting this into practice: by first taking interface inspirations from a science fiction show to come up with our interface elements, then by putting together those elements in a consistent way to design the screens of our thermostat.

When putting together our code, we discussed the importance of consistency and how it reduces our work when putting together user interface drawing code: by using the same style of button everywhere we only have to write the code for drawing that button once.

We then touched upon the Model-View-Controller paradigm, and put together the “model” which reflected the “nouns” of our user interface and provided interfaces which allowed us to take action against our model–the “verbs” of our interface.

We built our screen control code which handles the common actions–commonality made possible by the consistency of our design. And we finally showed how to put together one of the screens–a process made extremely easy by having a good user interface design. We even touched briefly on times when our user interface guidelines had to be violated for simplicity–and how these exceptions should be rare and only done when necessary.


Hopefully you’ve learned something. Or at the very least, you can see some of the cool things you can do with your Arduino.

Published by

William Woody

I'm a software developer who has been writing code for over 30 years in everything from mobile to embedded to client/server. Now I tinker with stuff and occasionally help out someone with their startup.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s