Skip to content
Ahmed Dabak edited this page Jul 25, 2026 · 1 revision

Touch

Introduction

Touch is the TTP223 capacitive touch pad -- or any push button, which behaves identically. It is a facade over the TouchSensor module.

if (Touch::isTouched()) Mouth::show(Icon::Happy);
if (Touch::wasTapped()) Gestures::play(Gesture::Happy);

Nothing needs enabling. The sensor is a plain digital pin, so both calls are safe before the hardware is wired.

Held or tapped

The two calls answer genuinely different questions, and picking the wrong one is the most common bug on this page:

  • isTouched() -- is the pad being held right now? True for as long as contact lasts. A one-second touch in a fast loop() makes it true hundreds of times in a row.
  • wasTapped() -- has a new touch happened since the last call? True exactly once per touch, because it detects the state change rather than the state.
void loop() {
  if (Touch::isTouched()) Mouth::show(Icon::Heart);   // while held
  else                    Mouth::show(Icon::Smile);   // when released
}

That is the right use of isTouched(): a display that follows the finger. It is the wrong call for anything that should happen once.

Once per touch

Anything that toggles, counts, or starts a performance wants wasTapped():

bool dancing = false;

void loop() {
  if (Touch::wasTapped()) {
    dancing = !dancing;
    Mouth::show(dancing ? Icon::Happy : Icon::Line);
  }
  if (dancing) Legs::jitter(1);
}

wasTapped() must be called every loop() iteration to work. It compares the pin against what it saw last time, so a call that happens only inside an if branch will miss touches and report changes late.

Note A long blocking call -- Legs::walkForward(3), a gesture, a melody -- is time in which nobody is reading the pin. A touch that starts and ends inside one of those is lost. This is the same blocking trade-off described on Eyes, and non-blocking motion is on the Roadmap.

Momentary and toggle sensors

TTP223 boards ship in two modes, and kits are inconsistent about which you get:

  • Momentary -- the output is HIGH only while you touch it. This is the default assumption.
  • Toggle -- the output flips state on every touch and stays there until the next one.

In toggle mode isTouched() reports the latched state, not whether a finger is present, which makes it nearly meaningless. wasTapped() detects a change either way, so it is the call that works on both kinds of board.

Declare which one you have, so the framework agrees with your hardware:

OttoConfig cfg;
cfg.touch.toggleMode = true;
OttoFlow::start(cfg);

The setting changes what counts as one touch. In momentary mode only the rising edge counts, so pressing and releasing fires wasTapped() once. In toggle mode every change counts, because on that board a touch is a change.

If you are unsure which board you have, run watch touch in the Serial Console and touch the pad twice: a momentary board returns to its resting state when you let go, a toggle board does not.

Debouncing and the first call

Two details of the implementation are worth knowing:

  • 50 ms debounce. Changes closer together than that are ignored, which suppresses the electrical chatter of a mechanical button. A deliberate double-tap is far slower than this and comes through fine.
  • The first call primes, and always returns false. It learns the resting state of your pin rather than guessing, so a sensor that idles HIGH does not fire a phantom tap at startup.

Wiring and the pin

The default pin is A0, used as a digital input:

OttoConfig cfg;
cfg.touch.pin        = A0;     // the default
cfg.touch.toggleMode = false;  // the default
OttoFlow::start(cfg);

Any pin that can do digitalRead() works -- except A6 and A7 on the Nano, which are analog-input only. If you also fitted a photoresistor, note that the official single-sensor examples put that on A0 as well; the two cannot share a pin, so move one of them (see Light).

A plain push button works in place of the touch pad: wire it so the pin reads HIGH when pressed, and leave toggleMode false.

Going lower: the TouchSensor module

bool held   = TouchSensor::isTouched();   // raw pin state
bool tapped = TouchSensor::wasTapped();   // debounced state change

The module is what Touch forwards to, one to one -- there is no extra capability down here, only the hardware-truth name. Reach for it in generic code that should not read as a body part.

See also Sensors for the other senses, and Examples for a touch-driven sketch.

Clone this wiki locally