Use Handclap to Control Something With ESP32

by SPLatManOz in Circuits > Arduino

2684 Views, 22 Favorites, 0 Comments

Use Handclap to Control Something With ESP32

clapper mic eps32.jpg
Clapper recording 210830

Note: This Instructable is to share my experience with others with some electronics and Arduino programming knowledge, not for noobs.

As part of a larger project I wanted to explore using hand claps as a simple control (on/off for an LED light, as it happens). I was designing a PCB, and incorporated a cheap hobby grade electret microphone insert with a simple LM393 comparator and a tiny bit of hysteresis simply feeding an interrupt input on an ESP32 dev board. The general idea is that as the mic picks up a clapping sound it will generate a bunch of interrupts. The ESP32 notes the timing of each burst (clap) and then analyses it to extract pre-defined patterns.

The result is a system that can decode a sequence of N fast claps framed by a slow "start" clap and a slow "stop" clap (yes, kind of inspired by asynchronous (UART) data, but with a variable number of data bits).

The generic function I wound up with returns the number of claps. It is not perfect; the user ("clapper"?) must be able to maintain reasonable timing, and it is not impervious to random ambient noise. So it's not suitable for a bomb trigger! In fact don't use this for anything where a false decode could have dangerous consequences!


Anatomy of a Clap

3 claps.jpg
One clap.jpg
burst.jpg

I spent many hours with my oscilloscope simply studying handclaps and taps on the desk (part of my intended application might involve taps on the unit or its mounting surface, rather than acoustic claps).

Picture 1 above shows three claps at what I'd call fast but not very fast. They tell me the speed at which a person is likely to clap at - individual claps will come say 150mS to 600mS apart. It also becomes apparent that it's the gaps between claps that need to be analysed.

Picture 2 is (more or less) one single clap, while picture 3 is expanded enough to indicate the timings of individual interrupts i.e. input transitions.

From the experiments I was able to set upper and lower time limits for various parts of a clap sequence, which the program can use to pick out and analyse important features of a clap pattern.

Handling Multiple "commands"

A lot of effort went into trying to securely differentiate different clap patterns, say Morse codes (S.O.S. . . . _ _ _ . . . , V for Victory . . . _ etc) or maybe the Shave and Haircut door knock pattern https://www.youtube.com/watch?v=zWbjP_ahuB4 . What I found was that, in addition to requiring a separate Finite State Machine (FSM) for each pattern, or a complicated generic template driven FSM, some short patterns were not very robust against ambient sounds.

So instead I went for what I'll call Start - N - Stop encoding.

|___|_|_|_|___|
1   2 3 4 5   6

In the above each pipe | represents a clap. The long gap between the 1st and 2nd claps is the "start". Claps 2, 3, 4, and 5 are the "code", and the last clap with its long preceding gap is the "stop". Without that the sequence will fail. So for the above N = 4.

My test indicate that the coding is reasonably secure against false triggers for N = 3 or more. The greater N is, the less susceptible to false triggers.

Do not use this in any application where a false trigger could have bad consequences!

Schematic

clapper schematic.jpg

I'll show just the schematic of the analogue to digital front-end. I extracted this from a much larger schematic for my overall project. The LM393 is powered off 5V (pin 8) and ground (pin 4). It's a good idea to ground pins 5 and/or 6 if you are not using the second comparator in the chip.

The 5V supply is simply (in my case) the USB power that powers the ESP32. You may need to make alternative arrangements, though anything up to 10V (a typical electret microphone rating) should work.

R1 and C1 reduce any noise in the 5V supply.

R3, R6 and C3 produce a clean mid-rail (2.5V) reference. R5 and R7 bias the two LM393 inputs to that voltage.

The microphone output is ac coupled to pin 2 of the comparator via C2.

R2 is returned to the 3.3V output of the ESP32, ensuring the ESP32 only ever get fed 3.3V max.

R8 and R7 ensure a small amount of hysteresis. It could possibly be more (R7 larger), but I frankly didn't explore that. In hindsight I'd probably add 100K in series with pin 3 of the comparator, to balance the source resistances on the two input pins.

The output of the comparator (pin 1) goes to an ESP32 input configured as an interrupt pin. I used D39.

My Code

My code is below. You need a certain skill level to integrate it into your own application.

/* Aiming for sophisticated clapper switch for control of light strip with ESP32. Started Aug 2021
    The general idea is that the microphone via an LM393 generates interrupts. One clap will
    generate a whole bunch of 'em.

    After trying to implement arbitrary patterns like common Morse codes or
    "shave and a hair cut" - https://www.youtube.com/watch?v=zWbjP_ahuB4
    I concluded that an easier and slightly more secure way was to have patterns of the
    general form start-count-stop where start is two widely spaces clap, count is 1 or more faster
    claps and stop is a final slow clap. So for count = 5, the pattern would be
       !   ! ! ! ! !   !
       0   1 2 3 4 5   6

   There are layered FSM/tasks. From "bottom" up ....

   >  The ISR will capture the millis() time of every interrupt
      to a global variable (absolutely minimum processing in the ISR)

   >  A clap discriminator task looks for bursts of interrupts, lasting
      between minClapDuration and maxClapDuration, and with a minimum minClapGapInterval 
      silence before the next one. It reports each validated clap by its starting system 
      time millis(). The starting time is the first interrupt.

   >  A pattern recogniser clapPattern() that looks for and reports clap sequences that 
      comply with the start-count-stop pattern.
*/

#define maxIntGapTime 50        // Max gap between interrupts within one clap
#define maxClapGapTime 1000     // Max gap (silence) between claps
#define minClapGapInterval 150  // Min silence time between successive claps
#define maxClapDuration 100     // The longest time I'd expect a burst of interrupts from a single clap to last
#define minClapDuration 10      // The shortest time I'd expect a burst of interrupts from a single clap to last
#define dwellAfterNoise 5000    // Require this much silence after a failed decode before even considering accepting a clap
#define minStart 600            // min duration of start/stop interval
#define maxData  400            // Max duration of a data (one of n) interval
#define minData  150            // Min duration of a data (one of n) interval

// Pay partiucular attention to dwellAfterNoise. If a decode fails you may need some solid silence before
// the system will accept anything new.

#define micInterruptIn 39  // <<<<<<<  Change to whichever input you are using
#define LED_BUILTIN 2  //  2 for ESP32 dev board

unsigned long volatile g_newIntTime; // One global variable required to connect ISR to foreground.

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115400); delay(200);
  Serial.println("");
  Serial.println("Hello");
  Serial.println(F(" Compiled " __DATE__ " at " __TIME__ " File: " __FILE__ )); // Tags when last compiled.
  pinMode (LED_BUILTIN, OUTPUT);
  pinMode(micInterruptIn, INPUT);
  attachInterrupt(digitalPinToInterrupt(micInterruptIn), clapInt, CHANGE); // Define interrupt handler (ISR) and mode
}

void loop() {
  // This forms the basis of a "superloop" multitasking structure. So long as clapPattern()
  // is called frequently, with no blocking instructions anywhere in the loop, it should work
  // For a good tutorial on superloops, see https://learn.adafruit.com/multi-tasking-the-arduino-part-1
  
  int p = clapPattern();    // Test of a clap pattern has been detected
  if (p != 0) {
    Serial.print("===========  Pattern "); Serial.println(p);
    // Insert your code here to react to various values of p, for example a switch/case
  }
}

int clapPattern() { // Returns the number of claps in a validated pattern. This is the "top level" decoder

  /* This is a Finite State Machine (FSM). If that's new to you, see
   https://www.instructables.com/id/Finite-State-Machine-on-an-Arduino/ 

   You will notice I don;t give state symbolic names. That's because I am Old School, and often
   design FSMs with a bubble diagram. Number fit in small circles much better than long names!
   Also, some FSM may have a lot of minor transition states, and naiming them all would
   simply be a PITA!
  */
  
  /* This pattern detector looks out for:
       a start "bit", = long interval
       n short intervals
       a stop "bit", = long interval
  */
  
  static unsigned long lastClap;
  static int state = 0;
  static int count = 0;

  unsigned long i = clapAt();  // returns non-zero for each detected interval. Zero if silence

  if (i == 0) {  // Silence. Nothing to do here
    return 0;
  }

  int interval = i - lastClap; // Time since the previous clap
  lastClap = i;

  if (interval > maxClapGapTime) {  // too long ... reset
    //   Serial.println("Reset 0");
    state = 0;
    return 0;
  }

  switch (state) {
    case 0: // Looking for start bit
      if (interval > minStart) {
        count = 0;
        state = 1;
        Serial.print("S ");
      }
      return 0;

    case 1:  // counting bits or hitting stop bit
      if (interval > minStart) { // Stop bit?
        state = 0;
        Serial.println("S ");
        return ++count; // bingo!  Added one for the start bit
      }
      if (interval > minData && interval < maxData) { // data bit?
        count++;
        Serial.print(count);        Serial.print(" ");
        return 0;  // keep counting
      }
      state = 0; // Got an invalid interval ... abort
      Serial.println("X");
      return 0;
  } // state
}

unsigned long clapAt() { //  Pick out individual claps (lowest level), return millis() of start of clap
  static unsigned long lastInt = 0;
  static unsigned long firstInt = 0;
  static byte state = 0;
  unsigned long intGapTime;

  intGapTime = g_newIntTime - lastInt ;  // time since last interrupt
  lastInt = g_newIntTime;   // Remember for next time

  switch (state) {
    case 0:   // Idle
      if (intGapTime > 0) { // Had an interrupt
        firstInt = g_newIntTime;
        state = 1;
        return 0;
      }
      return 0;

    case 1:   // In a burst, could be a genuine clap, but still short
      if (intGapTime > 0) { // Had an interrupt{
        if (g_newIntTime - firstInt > minClapDuration) { // Met _minimum_ burst length test
          state = 2;
          return 0;
        }
      }
      if (millis() - g_newIntTime > maxIntGapTime) { // No interrupt: End of the burst
        state = 0;
        //       Serial.println("Too short");
        return 0;
      }
      return 0;

    case 2:   // In a burst, could be a genuine clap, waiting to see if it's too long or not
      if (intGapTime > 0) { // Had an interrupt
        if (g_newIntTime - firstInt > maxClapDuration) { // Failed _maximum_ burst length test
          state = 3;  // Go and wait for it to get quiet again!
          return 0;
        }
        return 0; // Just idling through minimum clap duration
      }
      // No interrupt. Could be a desirable silence gap marking the end of a genuine clap.
      if (g_newIntTime - firstInt > minClapDuration) { // Met _minimum_ burst length test
        state = 4;
        return firstInt;
      }
      return 0; // Just idlig toward maximum clp duration

    case 3:   //  In a too long burst, waiting for it to settle before resetting to idle state ("re-arming")
      if (intGapTime > 0) { // Had an interrupt
        return 0;
      }
      if (millis() - lastInt > dwellAfterNoise) {
        state = 0;
        return 0;
      }
      return 0;
      
    case 4:   //  Timing gap after a confirmed gap
      if (intGapTime > 0) { // Had an interrupt. Tail end burst after a clap
        return 0;
      }
      if (millis() - lastInt > minClapGapInterval) {
        state = 0;
        return 0;
      }
      return 0;
  }
}

void clapInt() {  // ISR: Quickly grab a time stamp and get out
  g_newIntTime = millis();
}