Touch Input

As I make progress through my bottom-up designed game, it is now time to look at how the player will respond to the game by repeating the light sequence. In the game console, the four light regions are physical buttons that the user can press. The closest approximation to that I could come up with on the Circuit Playground Express board is to use the capacitive touch sensors.

This board comes with seven capacitive touch sensors, labeled A1 through A7. You can actually see the labels printed on the board itself. You may notice that there is also an A0 pad, but unfortunately that one is not a capacitive touch pad.

The following diagram shows how I'm going to map these seven touch sensors to the four light regions in the game:

gaming_cpx-touch.jpg
Touch pads mapped to each light region

You can see that the yellow color is at a disadvantage compared to the other regions because it only has one touch pad versus two pads each for the others. Unfortunately this is something that cannot be helped, because the design of this board has an asymmetric distribution of touch pads. If you feel it is more important to have perfectly symmetrical inputs, then you can just not use pads A3, A4 and A7, which would leave all regions with a single touch pad positioned exactly as the yellow one.

The cpx module provides support for checking the state of each pad. For example, to check the state of pad A1, you can use the expression cpx.touch_A1. This is easily tested in a REPL session:

Download: file
>>> from adafruit_circuitplayground.express import cpx
>>> cpx.touch_A1
False

A value of False indicates that the pad in question is currently not being touched. Now go ahead and touch the A1 pad with a finger and then repeat the above statement:

Download: file
>>> from adafruit_circuitplayground.express import cpx
>>> cpx.touch_A1
True

Back when I needed to map region numbers to colors or to sounds, I used lists. Now I need to map capacitive touch pads to region numbers, which is a little bit different. Since the pads have names and not numbers, this time I'm going to specify this mapping using a Python dictionary:

Download: file
PAD_REGION = {
    'A1': 0,  # yellow region
    'A2': 2,  # red region
    'A3': 2,  # red region
    'A4': 3,  # green region
    'A5': 3,  # green region
    'A6': 1,  # blue region
    'A7': 1,  # blue region
}

With this data structure, once I know which pad is being touched, I can easily find the region number it corresponds to. I can now construct a function, which I'm going to call read_region() that returns the number of the region being touched by the player, or None if none of the pads are touched. Here is a first implementation of this function:

Download: file
def read_region():
    if cpx.touch_A1:
        return PAD_REGION['A1']
    elif cpx.touch_A2:
        return PAD_REGION['A2']
    elif cpx.touch_A3:
        return PAD_REGION['A3']
    elif cpx.touch_A4:
        return PAD_REGION['A4']
    elif cpx.touch_A5:
        return PAD_REGION['A5']
    elif cpx.touch_A6:
        return PAD_REGION['A6']
    elif cpx.touch_A7:
        return PAD_REGION['A7']

Simple, right? Now I can replace the light demo code at the bottom of code.py with a slightly more interesting piece of code that waits for the user to touch one of the pads, and then lights the appropriate region:

Download: file
while True:
    region = read_region()
    if region is not None:
        light_region(region)

This simple code snippet shows how powerful bottom-up development is. Since all the basic blocks of this game have now been implemented, it takes just four lines of code to make the board respond to user input in a similar way to the Simon game!

The read_region() function is currently implemented in a very simplistic way, it just returns the region being touched or None, which means that it does not wait for the player to touch a pad. An interesting improvement would be to have this function wait for the player to select a pad for a given amount of time. This can be accomplished by wrapping the chain of if statements that do the pad checking logic in a while-loop, and having the loop exit only after the specified time has passed without the player having chosen a region:

Download: file
def read_region(timeout=30):
    val = 0
    start_time = time.time()
    while time.time() - start_time < timeout:
        if cpx.touch_A1:
            val = PAD_REGION['A1']
        elif cpx.touch_A2:
            val = PAD_REGION['A2']
        elif cpx.touch_A3:
            val = PAD_REGION['A3']
        elif cpx.touch_A4:
            val = PAD_REGION['A4']
        elif cpx.touch_A5:
            val = PAD_REGION['A5']
        elif cpx.touch_A6:
            val = PAD_REGION['A6']
        elif cpx.touch_A7:
            val = PAD_REGION['A7']
    return val

Here I have expanded the function to take a timeout argument which specifies for how many seconds the function should wait for user input, with a default of 30 seconds. Inside the function I use the time.time() function to get the current time in seconds, and then on each loop iteration I check how many seconds have passed by substracting that time from an updated reading of time.time(). If this difference is larger than the requested timeout, then the loop exits, and the function returns None. But if, on the other side, the player touches one of the pads, then the function immediately returns the region touched.

The benefit of adding this loop is that now the game logic (that I have yet to write!) does not need to get complicated with keeping track of time while it waits for player input. With this solution, if read_region() returns None it means that the player took too long to answer and has lost the game.

To make sure that you have the correct code, below you can see a complete copy of code.py with all the work done so far:

Download: file
import time
from adafruit_circuitplayground.express import cpx

cpx.pixels.brightness = 0.1  # adjust NeoPixel brightness to your liking

REGION_LEDS = (
    (5, 6, 7),  # yellow region
    (2, 3, 4),  # blue region
    (7, 8, 9),  # red region
    (0, 1, 2),  # green region
)
 
REGION_COLOR = (
    (255, 255, 0),  # yellow region
    (0, 0, 255),    # blue region
    (255, 0, 0),    # red region
    (0, 255, 0),    # green region
)

REGION_TONE = (
    252,  # yellow region
    209,  # blue region
    310,  # red region
    415,  # green region
)

PAD_REGION = {
    'A1': 0,  # yellow region
    'A2': 2,  # red region
    'A3': 2,  # red region
    'A4': 3,  # green region
    'A5': 3,  # green region
    'A6': 1,  # blue region
    'A7': 1,  # blue region
}

def light_region(region, duration=1):
    # turn the LEDs for the selected region on
    for led in REGION_LEDS[region]:
        cpx.pixels[led] = REGION_COLOR[region]
    
    # play a tone for the selected region
    cpx.start_tone(REGION_TONE[region]);
    
    # wait the requested amount of time
    time.sleep(duration)
    
    # stop the tone
    cpx.stop_tone()

    # turn the LEDs for the selected region off
    for led in REGION_LEDS[region]:
        cpx.pixels[led] = (0, 0, 0)

def read_region(timeout=30):
    val = 0
    start_time = time.time()
    while time.time() - start_time < timeout:
        if cpx.touch_A1:
            val = PAD_REGION['A1']
        elif cpx.touch_A2:
            val = PAD_REGION['A2']
        elif cpx.touch_A3:
            val = PAD_REGION['A3']
        elif cpx.touch_A4:
            val = PAD_REGION['A4']
        elif cpx.touch_A5:
            val = PAD_REGION['A5']
        elif cpx.touch_A6:
            val = PAD_REGION['A6']
        elif cpx.touch_A7:
            val = PAD_REGION['A7']
    return val

while True:
    region = read_region()
    if region is not None:
        light_region(region)
This guide was first published on Jul 24, 2019. It was last updated on Jul 24, 2019. This page (Touch Input) was last updated on Sep 19, 2019.