Building an Arduino based capacitive touch kitchen timer – Part 3

This part of the Arduino based capacitive touch kitchen timer series discusses the software that will make the capacitive kitchen timer come alive. It also talks about how I implemented the capacitive touch ring and some experiments I conducted to ensure good gesture detection and user experience.

Functional description

Before I show you the code, let’s go over a short functional description that explains how I planned the software and this product to work:

When powered on, the device starts up in idle mode (mode zero). In this mode, the displays are off. When a user activates the center button, the device switches to mode one (set minutes). In this mode, the MCU listens for inputs on the ring buttons and tries to determine the direction. The users can input a number between 0 and 59. The user confirms the minutes by pressing the center button again. The device then switches to mode two and asks for the seconds. This procedure is the same as for the minutes. The range is, again, between 0 and 59. The seconds don’t affect the minutes set previously, which means that going past 59 seconds, for example, won’t increase the minutes.

When the user presses the center button while the device is in mode two, the clock goes into countdown mode. If more than zero minutes are remaining, the device first displays the minutes, then flashes both DPs, then shows the seconds, then flashes the right DP and then it repeats this process. If the minutes reached zero, the timer continuously shows the seconds. Once the minutes and seconds both reach zero, the timer beeps until the user presses the center button to stop the beeping. It then goes back to mode zero (idle).

If the user presses and holds the center button for approximately two seconds, the timer aborts the current action, and it goes back to mode zero (idle). The timer, however, should always remember the last inputs (minutes and seconds) and recall them when setting the time.

Testing a prototype

But before I could implement the previously discussed functionality, I had to come up with a way to detect a user’s finger. For that, the finished device needs some metal objects that form one side of the capacitor that senses the user input. At first, I thought about adding some small metal strips or something like washers. That, however, turned out not to be precise enough. Furthermore, it would make it more difficult to design and assemble the case of the device.

I also wanted the conductive pads to be relatively large, and the entire device should be easy to assemble, which would not be the case when using metal strips or washers. Therefore, I opted to design a small PCB that contains thick traces that act as one side of the capacitive touch sensor. When a user gets close to the PCB, the finger acts as the second plate. Having the traces on a PCB also means that assembling the device becomes much easier in the end. However, I first produced a simple prototype at home that looks like this:

Figure 1: The top and bottom side of the capacitive touch ring prototype PCB. Note the wide traces. The capacitive sensing works better the more metal the first plate of the capacitor contains. The PCB doesn’t contain any other components other than the traces. I also soldered wires directly to the metal plates and connected them to the circuit (which I discussed in part two of this series).

I etched the PCB at home, and I didn’t care about the size and shape of the prototype for now. The final version of the timer will contain a circular PCB that will fit in the case perfectly. Anyway, for now, this prototype is enough to test whether the first design and the software work. I quickly put together a test script that determines which of the five conductive pads senses the user’s finger. Using the Arduino IDE, I was able to create the following output:

Figure 2: The output of the test script. Each color represents a different conductive pad.

As figure 2 shows, the capacitive sensor reliably detects when a user activates one of the touchpads. In this test, I made sure not to directly touch the pads on the prototype board, as they will be completely covered in the final version. Therefore, I wanted to make sure that the detection still works before going any further. Everything worked as expected, and so I continued with the implementation.

Demo of the firmware functions

We’re now almost ready to look at the code! But before we do that, have a look at the following short video that demonstrates how the firmware works:

Note that it still has troubles detecting gestures from time to time. However, I think that’s due to the long wires attached to the prototype PCB. I should be able to resolve these minor issues by tweaking the threshold and cool-down values in the final version. Anyway, now it’s finally time to have a look at the code!

Implementation

The source code of this project turned out to be very long, and I won’t discuss every single line of code in this article. Instead, I uploaded the code to this GitHub repo, where you are free to download and modify it as you like! As usual, you are free to share the code with others as long as you include a link to this series and you don’t use the code in a commercial product!

Anyway, I made sure to include many comments, and I’ll go over the most important parts here. Feel free to leave a comment or reach out to me if you still have questions regarding the code! The most important part of the firmware is the detectFinger function that detects the most recent input gesture and maps it to an internal value:

int detectFinger()
{
  int dir = 0;

  if(millis() - lastDirectionDetection > FINGER_DETECTION_COOLDOWN)
  {
    long total1 = cs_1.capacitiveSensor(30);
    long total2 = cs_2.capacitiveSensor(30);
    long total3 = cs_3.capacitiveSensor(30);
    long total4 = cs_4.capacitiveSensor(30);
    long total5 = cs_5.capacitiveSensor(30);

    // The user pressed the center button
    // prioritize this touch button over all other ones on the touch wheel
    // it's extremely unlikely that the user activates this button by accident.
    // it is, however, likely that the user's finger gets close enough to the ring buttons
    // to falsely activate one of them when pressing the center button
    if(total5 > CENTER_BUTTON_THRESHOLD)
    {
      // This button is now activated once the user's finger moves away from the center button
      // before a certain amount of time has passed. This allows the software to detect both, a
      // short press and the user pressing the center button continuously.
      if(active_sensor != 5)
      {
        // Reset the counter and flag that determine whether the user holds down the button
        center_button_active_for = 0;
        center_button_held_detected = false;
      }
      else
      {
        // Increase a variable that counts how long the user holds down the center button
        // If that value gets greater than a certain threshold,
        // change the direction to three (center button held down).
        // return 0 if the counter is less than the threshold value.
        center_button_active_for += 1;
        dir = (center_button_active_for >= CENTER_BUTTON_HELD_THRESHOLD) ? 3 : 0;
      }
      
      active_sensor = 5;
    }
    // The following else/if blocks detect the other four ring buttons.
    // This is a quick and dirty solution, but it works for now...
    else if(total1 > TOUCH_SENSOR_THRESHOLD)
    {
      if(active_sensor == 2 || (active_sensor == -1 && last_active_sensor == 2))
        dir = -1;
  
      if(active_sensor == 4 || (active_sensor == -1 && last_active_sensor == 4))
        dir = 1;
  
      active_sensor = 1;
    }
    else if(total2 > TOUCH_SENSOR_THRESHOLD)
    {
      if(active_sensor == 1 || (active_sensor == -1 && last_active_sensor == 1))
        dir = 1;
  
      if(active_sensor == 3 || (active_sensor == -1 && last_active_sensor == 3))
        dir = -1;
  
      active_sensor = 2;
    }
    else if(total3 > TOUCH_SENSOR_THRESHOLD)
    {
      if(active_sensor == 2 || (active_sensor == -1 && last_active_sensor == 2))
        dir = 1;
  
      if(active_sensor == 4 || (active_sensor == -1 && last_active_sensor == 4))
        dir = -1;
  
      active_sensor = 3;
    }
    else if(total4 > TOUCH_SENSOR_THRESHOLD)
    {
      if(active_sensor == 3 || (active_sensor == -1 && last_active_sensor == 3))
        dir = 1;
  
      if(active_sensor == 1 || (active_sensor == -1 && last_active_sensor == 1))
        dir = -1;
  
      active_sensor = 4;
    }
    else
    {
      // This else gets activated once the user moves his/her finger away from any button
      // If the finger touched the center button before being moved away and the button
      // was not held down for too long, then return two (center button pressed for a short time).
      if(active_sensor == 5 && !center_button_held_detected)
        dir = 2;

      // No currently active button
      active_sensor = -1;
    }

#if DEBUG > 1
    if(last_active_sensor != active_sensor)
    {
      Serial.print("Active sensor = ");
      Serial.println(active_sensor);
    }
    if(dir == 3 && !center_button_held_detected)
    {
      Serial.println("Center button held down!");
    }
#endif

    // This is a second de-bounce measure. Checking the button state in too short intervals often leads
    // to imprecise measurements and false activations. Therefore, make sure to wait for a few milliseconds
    // before reading the state again. The next two lines set values for that mechanism.
    lastDirectionDetection = millis();
    last_active_sensor = active_sensor;

    // Only send the 'center button held down' return value once. If the direction is currently three and the
    // MCU detected that the user holds down the button, return 0.
    dir = (dir == 3 && center_button_held_detected) ? 0 : dir;

    // Update the center button held down detection flag.
    center_button_held_detected = center_button_held_detected || (dir == 3);

#if DEBUG > 1
  if(lastDir != dir)
  {
    Serial.print("Direction = ");
    Serial.println(dir);
    lastDir = dir;
  }
#endif
  }

  // Finally: return the detected direction value
  return dir;
}

In summary, this function makes five calls to the external capacitive touch library, and then it inspects the return values of these calls. If one of the values exceeds a certain threshold, then the program assumes that the user wants to make an input. The capacitive touchpads of the ring and the center button have different thresholds. I decided to implement this because it was too easy to falsely activate one of the ring buttons when reaching for the center button (you have to place your finger over at least one of the pads in the ring to activate the one in the center). That’s also the reason why the program prioritizes the center button over all the other ones. These measures ensure that the center button is only activated when the user really wants that. Once the user activates the center button, the program ignores the other ones as long as the center button stays active.

The function additionally contains four if/else blocks (one for each of the ring buttons). Note that this is a quick and dirty way of implementing it, but it should work for now, and I might update it later. Anyway, each if/else block checks which of the ring buttons is currently active. If it finds one, it checks whether one of the other ring buttons was active just before the current one got activated. If that’s the case, it inspects which other button was active before, and the program then determines the direction in which the user swiped his or her finger. Let’s take a closer look at one of the if/else blocks:

else if(total1 > TOUCH_SENSOR_THRESHOLD)
{
  if(active_sensor == 2 || (active_sensor == -1 && last_active_sensor == 2))
    dir = -1;

  if(active_sensor == 4 || (active_sensor == -1 && last_active_sensor == 4))
    dir = 1;
  
  active_sensor = 1;
}

This block gets activated whenever the user places his finger on pad number one (see figure 2). When this event occurs, the program checks which pad it previously detected as the active one (by retrieving the value stored in the active_sensor variable). If that value is two, then the user swiped from pad number two (in the last cycle) to pad number one (this cycle) and, therefore, wants to reduce the current value on the screen. The other direction works in a similar way. Note how there is a second condition that might apply. If the active_sensor is set to -1 (no finger detected in the last cycle), then the program also checks the last_active_sensor to determine the direction. I did this because it’s possible that the system falsely detects that the user lifted his finger away from the pads when transitioning from one of the capacitive pads to the other. Either way, the program sets the currently detected pad as the active one.

The loop method calls the detectFinger function in each iteration:

// Determine the action that the user performed.
// 0  = no action
// 1  = count up
// -1 = count down
// 2  = center button pressed
// 3  = center button held down
int action = detectFinger();

And it then carries out an action depending on the mode (see above) and the detected action. There’s nothing too exciting going on in this function. However, I’d like to point out a few things that I learned while debugging the program:

  • The LedControl library, used for sending values to the MAX7219 chip, seems to have problems updating the screens too often. I didn’t experience this problem with the Arduino itself. However, the screens seem to flicker, and the ATMega328 MCU resets itself constantly when updating the displays in each iteration of the loop method. Therefore, I added a short cool-down period when updating the seconds. Doing so is not a problem in this application, as it’s more than enough to refresh the displays roughly twice a second. Note that this might also be a problem with the MAX7219 chip, and adding a capacitor close to its power pins might also help resolve this issue.
  • You must add a properly sized capacitor close to the power pins of the atmega328P supply to ensure reliable operation. If you don’t, the MCU tends to reboot when the voltage dips below a certain threshold due to increased current draw (e.g., when displaying two numbers that utilize more LED segments than others). I’ll add these capacitors in a later revision.
  • It’s unnecessary to call the clearDisplay function before sending new characters to the MAX7219 chip. Doing so in every iteration of the loop, however, causes a notable flicker on the seven-segment displays. Therefore, the setup method of the sketch is the only one that calls the clearDisplay function.
  • It’s possible to clear the screen without actually calling the clearDisplay function by sending whitespace characters to each of the seven-segment displays.

Other than that, the firmware program contains three more helper functions. The most interesting one is the following one:

void displayRemainingTime(int remainingMinutes, int remainingSeconds)
{
  // Show the current time on the displays
  // When the remaining minutes are greater then zero, first show the minutes,
  // then flash both DPs, then show the seconds, then flash the right DP, and then
  // wait for a while before repeating this process
  if(remainingMinutes > 0)
  {
    // Display the minutes
    if(countDownState == 0 && (millis() - lastCountDownStateSwitch > COUNTDOWN_PAUSE_TIME))
    {
      displayValue(remainingMinutes);
      lastCountDownStateSwitch = millis();
      countDownState = 1;

#if DEBUG > 0
      Serial.print(remainingMinutes);
#endif
    }
    // Flash both DPs
    else if(countDownState == 1 && (millis() - lastCountDownStateSwitch > COUNTDOWN_DISPLAY_TIME))
    {
      flashDPs(true, true);
      lastCountDownStateSwitch = millis();
      countDownState = 2;

#if DEBUG > 0
    Serial.print("..");
#endif
    }
    // Display the remaining seconds
    else if(countDownState == 2 && (millis() - lastCountDownStateSwitch > COUNTDOWN_DP_TIME))
    {
      displayValue(remainingSeconds);
      lastCountDownStateSwitch = millis();
      countDownState = 3;

#if DEBUG > 0
      Serial.print(remainingSeconds);
#endif
    }
    // Flash the right DP
    else if(countDownState == 3 && (millis() - lastCountDownStateSwitch > COUNTDOWN_DISPLAY_TIME))
    {
      flashDPs(false, true);
      lastCountDownStateSwitch = millis();
      countDownState = 4;
      
#if DEBUG > 0
    Serial.println(".");
#endif
    }
    // Turn the display off for a short time
    else if(countDownState == 4 && (millis() - lastCountDownStateSwitch > COUNTDOWN_DP_TIME))
    {
      countDownState = 0;
    }
  }
  else
  {
    // Constantly display the seconds when the remaining minutes are below zero
    if(millis() - lastCountDownStateSwitch > COUNTDOWN_PAUSE_TIME)
    {
      displayValue(remainingSeconds);
      lastCountDownStateSwitch = millis();
    }
  }
}

This function also keeps an internal state that allows it to know what part of the animation to display next. Remember that when more than zero minutes are remaining, the timer should first flash the minutes, then flash both DPs, then show the seconds, then only light up the right DP before repeating the process. The countDownState variable determines in which state of this animation the clock currently is. Other than that, there’s nothing too special going on in this function. The other two helpers (displayValue and flashDPs) do the rest of the work.

Possible variations and things to consider

When I tested this program with the prototype board, I found it quite hard to activate the center button without getting too close to the other buttons located at the edge. Therefore, it could make sense to use a different button layout.

Furthermore, it’s somewhat annoying to draw out an entire circle with your finger only to add another second to the timer. Incrementing the timer in steps of five or ten seconds makes more sense (which I ended up doing).

Besides that, I think I’ll build another version later that doesn’t utilize the ring design. Two buttons (one for incrementing the count and one to decrement it) make more sense, and this setup is probably also faster and easier to use.

Another idea that came to my mind is to use some sort of virtual slider. So, instead of using touch buttons and only detecting the presence of a finger, it would be cool to use a longer strip and detect the position of the finger along that strip. That would allow users to swipe left and right to set the time (similar to the old iOS lock screen (“swipe to unlock”)).

You could add a pause function to stop the timer and allow the users to resume it after a while. Currently, it’s only possible to abort the timer by holding down the center button for more than approximately two seconds.

I also though about adding SMD LEDs to the capacitive ring PCB, but I ended up not doing it for now. The LEDs could, however, also be controlled by the existing seven-segment controller IC, which means that they won’t occupy additional GPIO pins of the microcontroller.

Currently, the firmware doesn’t deal with the millis() timer rolling over. This is eventually going to be a problem, and I’ll update the firmware accordingly when releasing the last article of this series.

Sources

Capacitive sensing for dummies – instructables.com
Timing Rollover – arduino.cc

Table of contents

Part 1: Project Idea and the theory behind capacitive sensing
Part 2: The circuit and a custom PCB
Part 3: The software (You are here)
Part 4: The case design and an upcoming revision
Part 5: The finished product, lessons learned

4 thoughts on “Building an Arduino based capacitive touch kitchen timer – Part 3

Leave your two cents, comment here!

Fill in your details below or click an icon to log in:

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.