Lesson 4: Driven to Abstraction

In previous lessons, you controlled the Modulo board itself. In this lesson, you’ll go beyond the Modulo and interact with the larger world: you’ll use the Afero Platform and a smartphone to control a robot!

As you make your way through this lesson, keep in mind that for very simple products/projects, ASR with its GPIO pins may provide all the computing power and control interfaces you require. But the Afero Platform has been built for much more than that:

  • ASR provides a quick and powerful way to add communication between any device and the network.
  • The Afero Platform supports scalable controls, with a “language” based on simple attributes.
  • The Afero Profile Editor allows you to define the nature and scope of attributes required to support your product.
  • These attributes are not only the atomic control instructions, they are foundational to the Afero mobile app UI and the usage data collected to help you support your users.

What You’ll Learn

In this lesson, you’ll learn how to use attributes in two different ways, one simple and the other more complex:

  • Simple Attribute Use - The attribute values you expose to the user in the mobile app UI are used to control the target device directly. There is a 1:1 correspondence between an exposed attribute value and the target device’s behavior.
  • Complex Attribute Use - The attribute values exposed in the UI are not used to control a target device directly, but instead are used in calculations that determine the values of a second set of attribute values, which do control the target device. These secondary attributes are not displayed in the UI. (Of course you could design a UI that gives the user access to every attribute and force your user to perform every calculation and adjustment individually. However, providing a higher-level UI often has clear usability advantages.)

We’ll be making two device profiles, each with its own attributes, mobile app UIs, and program logic. Each profile will define a different style of robot controller: the tank-style illustrating the simple attribute use case and the car-style illustrating the complex attribute use case. Remember, both profiles will control the same robot – there’s no difference in the hardware. The point of having two versions is to illustrate two levels of complexity you might encounter in your project designs.

More About the Tank-Style Controller

The first profile drives the robot using tank-style controls: one slider controls the right wheel, and another slider controls the left:

In this tank-style UI, the user selects the attribute values in the mobile app, and those values are applied directly to each of the two servos, thus determining the robot’s movement. Creating this UI version also demonstrates the use of the attrSetHandler() callback.

More About the Car-Style Controller

The second profile uses controls more like an automobile: there’s a single steering slider, an accelerator slider, and a menu control for the “transmission”:

In this car-style UI, the attributes directly affecting the target device aren’t exposed to the user. Instead, the UI provides an “abstract” interface to a set of secondary attributes. The primary attributes (those set by the user) are used by a program running in the MCU to calculate secondary values, which will drive the robot. So, for example, we have a single “steering” attribute that is used to calculate values of two other attributes, which are used in turn to control the servos. This version of the robot controller also demonstrates the use of the attrNotifyHandler() and the attrSetHandler() callbacks.

Let’s Get Set Up

Now that you know where we’re headed in this lesson, it’s time to get set up. Make sure that:

  • You’ve worked through Tutorial Lessons 1-3. We won’t repeat set-up steps here that you would have performed previously.
  • You’ve downloaded, installed, and signed in to the Afero mobile app and the Afero Profile Editor.
  • You have an Afero Modulo board.
  • You have a Parallax Boe-Bot. The Boe-Bot should already have been assembled per the instructions that came with it, and the lesson will be considerably more fun if you’ve got batteries on hand to run the ‘bot! (Boe-Bot is a trademark of Parallax Inc.)
  • You have an Arduino Uno, plus an Afero Plinto shield. This lesson can be performed with other MCU configurations, but we’ll be showing how you can incorporate the Plinto into a Boe-Bot, so if you use something else you’ll need to handle the physical steps on your own.
  • You have the Arduino IDE (1.8 or later) up and running.

The use of a Boe-Bot is not required; we only pick it for this example because it’s reasonably popular, we have one here in the lab, and it’s easy to work with. Feel free to build your robot based on the principles covered here and whatever hardware you have available — anything that uses some flavor of Arduino to control a couple of continuous-rotation servos should be easy for you to adapt to Afero.


Let’s Begin the Lesson!

This lesson has the following steps:

In steps 4 and 6, we’ll highlight a few sections of the Arduino code, but you are strongly encouraged to read through the sketches in their entirety to understand how the robot is being controlled.

Step 1. Assemble the Robot

Assembling our robot is straightforward:

  1. Get the following together:
    • A Modulo
    • A Plinto carrier board
    • An assembled Boe-Bot
    • Four ½”-long spacers and four ¾”-long 4-40 mounting bolts to attach the Board of Education® to the Boe-Bot chassis. (Board of Education is a registered trademark of Parallax Inc.)
  2. Unplug the servo cables of the Boe-Bot from the servo connectors on the Board of Education.
  3. Unscrew the mounting bolts that attach the Board of Education to the Boe-Bot.
  4. Detach the Arduino from the underside of the BoE.
  5. Plug the Plinto carrier board onto the top of the Arduino board.
  6. Plug the Modulo onto the Plinto.
  7. Connect the Arduino-Plinto-Modulo assembly back onto the Board of Education.
  8. Reattach the BoE to the Boe-Bot, using the ½” spacers and ¾" bolts to raise the Board of Education (which now includes not only the Arduino, but also the Plinto and Modulo) above the chassis:

Step 2. Wire the Robot

We won’t be using the same pins for the servos as the standard Boe Bot, so we can’t use the servo connectors on the Board of Education.

  1. We’ll need to hook up a few wires. Color’s not critical, of course, but we suggest you gather:
    • Two 2” lengths of wire with RED insulation
    • Two 2” lengths of wire with BLACK insulation
    • One 2” length of wire with BLUE insulation
    • One 2” length of wire with YELLOW insulation
  2. Using the RED wires, connect each of the red servo leads (the center of the servo connectors) to the “5V” headers at the upper-right of the breadboard on the BOE.
  3. Using the BLACK wires, connect each of the black servo leads to the “GND” headers at the lower-left of the breadboard on the BoE.
  4. Run the YELLOW wire from the DIGITAL PIN 3 socket on the BoE, to the WHITE lead on the servo connector coming from the LEFT servo on the Boe-Bot.
  5. Run the BLUE wire from the DIGITAL PIN 4 socket on the BoE, to the WHITE lead on the servo connector coming from the RIGHT servo on the Boe-Bot.
    Servo Connector Wiring

That does it for the physical assembly and wiring of the robot!

Step 3. Create the Device Profile for the Tank-Style Controller

For this step, you can use our pre-built device profile, or create the profile yourself using the Afero Profile Editor. Both methods are described below.

Use the Pre-Built Tank-Style Profile

  1. Be sure you’ve got the latest afLib from http://github.com/aferodeveloper/afLib.
  2. Load the profile in Afero Profile Editor:
    1. From the Profile Editor start page, select theOpen Icon Open icon.
    2. In the "Open" dialog, navigate to <your sketchbook location>/libraries/afLib/examples/boebot_tank/.
    3. Open the profile that is appropriate for your Modulo: The directory named "profile" contains a profile for the Modulo-1; the directory named "profile2" is for the Modulo-2.
  3. Ensure your robot has power (power to servos is not needed yet), and then publish the profile to the Modulo.

Create the Tank-Style Profile from Scratch

Instead of using the pre-built device profile, you can use the Afero Profile Editor to create the device profile from scratch:

  1. Start a new project – be sure to select the correct module type.
  2. Define a new device type. Name it something like “TankControls”, and give it a cool icon.
  3. At the top of the Attributes window is the MCU Protocol drop-down menu; enable the MCU by selecting SPI.
  4. Define two MCU attributes:
    • Servo_Right: SINT16, Default Value 1500, Writeable
    • Servo_Left: SINT16, Default Value 1500, Writeable

    Note that the default values for the servo attributes (1500) are based on the servos that came with our Boe-Bot. Those servos have a range from 1300 to 1700, with 1500 representing STOP. If your servos are different, alter the default value accordingly.

  5. Define two UI controls:
    • Slider, for Servo_Right, Default Label “Right”, min 1300, max 1700, step 1
    • Slider, for Servo_Left, Default Label “Left”, min 1300, max 1700, step 1
  6. Define one UI Control Group:
    • Add the “Right” slider at top.
    • Below that, the “Left” slider.
  7. Publish your new profile to your Modulo

Step 4. Program the MCU for the Tank-Style Controller

You will find the pre-built boebot_tank.ino Arduino sketch in <your sketchbook location>/libraries/afLib/examples/boebot_tank/boebot_tank.ino. Open the sketch in the Arduino IDE, upload it onto your robot, power up (including servo power), launch the Afero mobile app on your mobile device, and give it a test drive!

When using the tank-style controller, note that the value of the Left- and Right-Servo attributes are set directly by and displayed on the slider controls. To drive the robot forward, set both attributes to value >1500; to reverse, the attributes should be <1500, and to turn you set the sliders to spin the servos at different speeds and/or different directions.

When you’re done exploring this control UI, come back and we’ll examine the code:

Let’s start with the declaration of the standard setup() function. If you’ve worked with Arduino before, this won’t contain much new: we start by initializing the Serial console and specify pins to control each servo. Then we instantiate afLib — the Afero software library that provides the interface between MCU code and the attributes in ASR. This library is always needed for a project that will use MCU attributes. Finally, we store the start time so we can count down a few seconds of delay while the system gets ready.

void setup() {
    Serial.begin(9600);
    // Wait for Serial to be ready…
    while (!Serial) {
        ;
    }

    // Set the pins for the servos
    leftWheel.attach(3);
    rightWheel.attach(4);

    // Initialize the afLib
    afTransport *theSPI = new ArduinoSPI(CS_PIN);
    aflib = iafLib::create(digitalPinToInterrupt(INT_PIN), ISRWrapper, attrSetHandler, attrNotifyHandler, &Serial, theSPI);

    // We allow the system to get ready before sending commands
    start = millis() + STARTUP_DELAY_MILLIS;
}

Next, examine the loop() function. Note especially that we call afLib->loop(). It’s critical that you call this function in your MCU loop whenever you use afLib — this gives afLib processing time, without which nothing else will run correctly.

void loop() {

    switch (state) {

    case STATE_INIT:
        if (millis() > start ) {
            state = STATE_RUNNING;
        }
        break;

    case STATE_RUNNING:
        break;
    }

    aflib->loop();
}

One more aspect of our loop() code is worth mentioning: we’ve structured the code as a state machine, and the MCU loop simply repeatedly runs a switch on a variable that holds the current state, and executes the code appropriate for that state. For the tank-style controller example, we only have two states (“STATE_INIT”, “STATE_RUNNING”), and the use of a state machine is a bit overkill, but we did it so we could point it out as a very common and useful design pattern. It provides a handy way to run different code during different phases of execution, while looping so that we can repeatedly call afLib’s loop().

Having looked at setup() and loop(), we have yet to see any code that actually drives the robot! Where’s the good stuff?

Recall from Lesson 3 that when the user makes a change in the app UI, the Afero Cloud gets a message that the value of an attribute should be changed. The Cloud sends a message to ASR to change the attribute value, after which the attrSetHandler() callback runs. This makes attrSetHandler() the perfect place for our code that sets servo speeds based on the attributes.

attrSetHandler() is called when a client changes an attribute.
bool attrSetHandler(const uint8_t requestId,
               const uint16_t attributeId,
               const uint16_t valueLen,
               const uint8_t *value) {

    int valAsInt = *((int *)value);

    if (DEBUG) {
        Serial.print("attrSetHandler() attrId: ");
        Serial.print(attributeId);
        Serial.print(" value: ");
        Serial.println(valAsInt);
    }

    switch (attributeId) {
    case AF_SERVO_RIGHT:
        updateRightServoSpeed(revSpeed(valAsInt);
        break;

    case AF_SERVO_LEFT:
        updateLeftServoSpeed(valAsInt);
        break;
    }
    return true;
}

Our attrSetHandler() definition does the following:

  • Cast the “value pointer” argument as pointer to an 8-bit integer, and dereference it to get the integer value to which the attribute should be set.
  • Switch on the attribute ID, and call the appropriate function to set the speed of either the right or left servo.

At this point it’s important to mention one detail about the Boe-Bot: the Boe-Bot uses a pair of identical servos to drive the right and left wheels. However, since the servos are mounted in opposite orientations, the servos must run in opposite directions to make both wheels turn the same direction.

Those servos accept speed settings from 1300 to 1700, where 1300 means “maximum reverse”, 1700 means “maximum forward”, and 1500 is “stop”, and we have defined our attributes and sliders accordingly. But remember: when the left servo is running at 1700, the right servo must run at 1300 in order to drive the robot forward.

We could have let the user deal with sliders that move in opposite directions, but that would be an awkward interface design. Instead, we created a small function, revSpeed(), that calculates the “reversed” speed value for a given slider setting, and we apply that calculation to the right wheel. The user driving the robot will never know what’s really going on from the UI, but if you watch the serial console output, you’ll be able to see it clearly:

// Function provides "reversed" value of the supplied speed.
// Determines the equivalent right-wheel speed, given a left-wheel speed
// Needed because identical servos are mounted in opposite directions on robot
int revSpeed(int forSpeed) {
    return SERVO_MAX_LIMIT - (forSpeed - SERVO_MIN_LIMIT);
}

In a way, revSpeed() provides a small example of the central point of this lesson: the difference between an attribute used directly and one used indirectly. Using the tank-style controller, the user seems to have direct control over the attribute value, and that specific value is applied to the item under control. That is, when the user drags the left wheel slider to “1700”, a value of 1700 is exactly what’s applied to the servo. On the other hand, the right wheel is driven through a minor indirection: its direction is reversed, and the attribute value the user sets is not the value the servo gets.

We’ll now move on to the car-style controller, where none of the attributes set in the UI directly set the value of a servo. In the car-style controller, the values shown in the UI do set attribute values, but those attributes do not have direct impact on the servos; instead, they are inputs to calculations that set other attribute values to do the work of changing servo speed.

Step 5. Create the Device Profile for the Car-Style Controller

As above, you can use our pre-built device profile, or create the profile yourself using the Afero Profile Editor. Both methods are described below.

Use the Pre-Built Car-Style Profile

  1. Load the profile in Afero Profile Editor:
    1. From the Profile Editor start page, select theOpen Icon Open icon.
    2. In the Open dialog, navigate to <your sketchbook location>/libraries/afLib/examples/boebot_car/.
    3. Open the profile that is appropriate for your Modulo: the directory named "profile" contains a profile for the Modulo-1; the directory named profile2 is for the Modulo-2.
  2. Ensure your robot has power (power to servos is not needed yet), and then publish the profile to the Modulo.

Create the Car-Style Profile from Scratch

Instead of using the pre-built device profile, you can use the Afero Profile Editor to create the device profile from scratch:

  1. Start a new project. As always, select the module type you will use in your robot.
  2. Define a new device type.
  3. At the top of the Attributes window is the MCU Protocol drop-down menu; enable the MCU by selecting SPI.
  4. Define five MCU attributes:
    • Servo1: SINT16, Default Value 1500, Writeable
    • Servo2: SINT16, Default Value 1500, Writeable
    • Accel_Attr: SINT16, Default Value 0, Writeable
    • Steer_Attr: SINT16, Default Value 0, Writeable
    • Transmission_Attr: SINT16, Default Value 0, Writeable
  5. Define three UI controls:
    • Slider, for Accel_Attr, Default Label “Accelerator”, min 0, max 200, step 1
    • Slider, for Steer_Attr, Default Label “Steering”, min -100, max 100, step 1
    • Menu, for Transmission_Attr, Default Label “Transmission”, with three value options:
      • Value -1, Label “Reverse”
      • Value 0, Label “Park”
      • Value 1, Label “Forward”

      Note that the default values for the servo attributes (1500) are based on the servos that came with our Boe-Bot. Those servos have a range from 1300 to 1700, with 1500 representing STOP. If your servos are different, alter the default value accordingly.

  6. Define one UI Control Group
    • Add the “Steering” slider at top.
    • Below that, the “Accelerator” slider.
    • At the bottom, the “Transmission” menu control.
  7. Publish the profile to your Modulo.

Step 6. Program the MCU for the Car-Style Controller

You will find the pre-built boebot_car.ino Arduino sketch in <your sketchbook location>/libraries/afLib/examples/boebot_tank/boebot_car.ino. Open the sketch in the Arduino IDE, upload it onto your robot, power up (including servo power), launch the Afero mobile app on your mobile device, and try it out. Remember that at the level of the servos doing the work, everything is the same as the tank-style controller version, but the UI is obviously very different, as are the underlying attributes.

When you’re done exploring this control UI, come back and we’ll examine the code.

You’ll notice that setup() for the car-style is quite similar to setup() for the tank-style: the only difference is that in the car-style we call initVars(), which initializes several variables related to robot speed and direction. In the tank-style version, these variables didn’t even exist. Why the difference?

In the tank-style code, the value of the two attributes Servo_Right and Servo_Left were always reflected in the UI, and were used directly to set the servo speeds. This meant that no other variables were required to store information about navigation. That’s not true for the car-style controller. In the car-style controller code, we need to store our own copies of the current control values and we need to store copies of those values as they were before the most recent UI change. This information is required to calculate the effects of a particular change in the UI, as we’ll see in a moment.

As for encapsulating the initialization of all these variables into a function: we do it because it turns out that this initialization is the same as resetting all controls, which is exactly what we want to do when the user switches the Transmission control to Park. We put it all into a function so we can easily call it from multiple places.

Next, we can see that loop() for the car-style controller has an important difference from that for the tank-style… we’ve added a state:

void loop() {

    switch (state) {
    case STATE_INIT:
        // We allow the system to get ready before we start firing commands at it
        if (millis() > start) {
            state = STATE_RUNNING;
        }
        break;

    case STATE_RUNNING:
        break;

    case STATE_UPDATE_ATTRS:
        // Could simply call setAttribute16() and ignore return value, 
        // but we'll be more robust and retry until success
        while (aflib->setAttribute16(AF_SERVO1, currentRightSpeed) != afSUCCESS) {
            aflib->loop();
        }
        while (aflib->setAttribute16(AF_SERVO1, currentLeftSpeed) != afSUCCESS) {
            aflib->loop();
        }
        if (currentTransVal == TRANS_PARK) {
            // If we're parked, make sure the UI gets reset
            while (aflib->setAttribute16(AF_ACCEL_ATTR, 0) != afSUCCESS) {
                aflib->loop();
            }
            while (aflib->setAttribute16(AF_STEER_ATTR, 0) != afSUCCESS) {
                aflib->loop();
            }
        }
        state = STATE_RUNNING;
        break;
    }

    aflib->loop();
}

The added state, STATE_UPDATE_ATTRS, is right at the heart of how differently the car-style controller operates. Upon entry to this state, we explicitly call setAttribute() for each of the servo attributes. In the tank-style controller, we never made any setAttribute() calls; all the attribute value changes were controlled by the Afero Cloud in response to UI selections by the user. Let’s explore this difference by tracing what happens when a user changes the Accelerator control in the car-style controller UI.

When the Accelerator control is changed, the mobile client communicates that update to the Afero Cloud. The Cloud informs ASR that attribute AF_ACCEL_ATTR should be updated, and when it is, attrSetHandler() is called. So far, everything seems much like the tank-style controller… except that our attrSetHandler() function for the car-style controller does not have cases for AF_SERVO_LEFT or _RIGHT, and does not have any calls to set servo speed. In other words, in the car-style controller, servo speeds are not directly set by user actions in the UI, the way they were in the tank-style controller.

Instead, in attrSetHandler(), in the case for AF_ACCEL_ATTR, we call carUpdateAccelerator(). This function combines the new value of the accelerator attribute in conjunction with the last saved attribute value (lastAccelVal) to determine the required value change for the accelerator attribute. This change, together with the current value of the Transmission attribute and the current speed of each servo, is used to calculate a new speed for each servo. When the calculation is complete, the state is changed to STATE_UPDATE_ATTRS. As we saw above, this state causes setAttribute() to be called for each servo attribute and then resets the state to STATE_RUNNING.

While we're here, let’s note one technique to boost robustness: when we call setAttribute16() in STATE_UPDATE_ATTRS, rather than just making the call and proceeding, we run the call in a loop, testing for success. When we do this, we must remember to include a call to aflib->loop() within our test-loop, in order for afLib to run.

As a result of the setAttribute() call, the attrNotifyHandler() callback runs, and that is where the servo speeds are changed.

// This is called when either an Afero attribute has been changed via setAttribute 
// or in response to a getAttribute call.
void attrNotifyHandler(const uint8_t requestId,
                       const uint16_t attributeId,
                       const uint16_t valueLen,
                       const uint8_t *value) {
    int valAsInt = *((int *)value);

    if (DEBUG) {
        Serial.print("attrNotifyHandler() attrId: ");
        Serial.print(attributeId);
        Serial.print(" value: ");
        Serial.println(valAsInt);
    }

    switch (attributeId) {
    case AF_SERVO1:
        updateRightServoSpeed(valAsInt);
        break;

    case AF_SERVO2:
        updateLeftServoSpeed(valAsInt);
        break;
    }
    ...
}

Wrapping It Up

When the user makes a change in the car-style controller UI, the new attribute value is used to calculate some other values, and then setAttribute() is called to set attributes to those values. The setAttribute() calls result in attrNotifyHandler() callbacks firing, and servo speeds are updated there. This is quite different from the tank-style controller, where a UI change essentially updates a servo value directly (in attrSetHandler()).

You’ll probably find both approaches useful in your projects.