knock sensor/Using it/09. A secret knock
Using it · 09 of 10

A secret knock

Shave and a haircut, two bits: seven knocks, and a TK01 XL LED that lights for three seconds when it hears them. The sketch divides every gap between knocks by the longest gap it heard, so the rhythm has to be right and the speed does not matter. An interrupt with a 100 ms hold-off collects the knocks; 1.5 s of silence ends a try.

A rhythm, not a speed

Nobody knocks the same rhythm at the same speed twice. So the lock cannot compare times; it compares shapes. It divides every gap between two knocks by the longest gap in the same try. Knocked slowly or quickly, "shave and a haircut, two bits" comes out as the same six numbers: a half, a quarter, a quarter, a half, one, and a half.

A rhythm, not a speed
Try
Knocks
7 of 7
Tolerance
0.1
Lock
opens
Open. Every gap, divided by the longest gap in this try, is within 0.1 of the secret's. The sketch never looks at how fast you knocked, only at the rhythm, so the same knock works slow or quick, and a wobble of a few tens of milliseconds does not matter.

Pick a try. The secret, at a steady pace with a few tens of milliseconds of wobble, opens. Quicker, the same rhythm with every beat a third shorter, opens too: dividing by the longest gap takes the speed out. Even knocks, seven of them evenly spaced, have the right count and every gap equal to the longest, so every bar is 1 and most miss their bands. One short fails before any bar is drawn: six knocks, and the secret has seven.

The tries in the figure are made up; the verdicts are the sketch's own matches(), run on them.

Collecting the knocks

The interrupt from the last article does the collecting. Instead of a count, it keeps the time of each knock, and the hold-off still keeps the ringing out: every entry in the list is a knock, not a pulse. That matters more here than anywhere, because one stray pulse would add a gap a few milliseconds long and wreck the shape.

The shortest gap in the rhythm is one beat. With the hold-off at 100 ms, a beat has to be longer than that, so knock at anything slower than about ten beats a second, which is every natural pace.

Deciding when a try is over

The sketch does not check after seven knocks. It waits until there has been no knock for 1.5 s, then checks whatever it heard. So an extra knock counts against you, and knocking at random until seven happen to fit does not work.

It then checks the count, divides, compares, prints the count and the verdict, and on a match lights the TK01 for three seconds:

7 knocks: wrong
7 knocks: open

The first line is what a hurried try looks like. The count was right, and one gap was off by more than a tenth of the longest.

The code

The interrupt from the last article, now keeping the time of each knock instead of a count. When the knocking has stopped for END_MS, loop() checks the count, divides each gap by the longest, compares it with the secret and lights the TK01 on a match.

knock_secret.ino
/*
  Knock Sensor - a secret knock                          TK28 / /p/tk28

  Wiring, the TK28. Count from the square pad, switch at the top,
  header at the bottom:

    GND    -> GND
    VCC    -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
              (during a knock, SIGNAL gives your pin whatever VCC is)
    NC     -> nothing   (unconnected on the board)
    SIGNAL -> D2 on an Uno, GPIO 25 on an ESP32, GPIO 4 on an
              ESP32-S3, GP15 on a Raspberry Pi Pico

  The TK01 XL LED, counted the same way:

    GND    -> GND
    NC     -> nothing   (both of its NC pins)
    SIGNAL -> D9 on an Uno, GPIO 4 on an ESP32, GPIO 5 on an
              ESP32-S3, GP14 on a Raspberry Pi Pico

  Arduino IDE
    Tools > Board                 your board, e.g. ESP32S3 Dev Module
    Tools > Port                  the one that appears when you plug in
    Tools > USB CDC On Boot       Enabled   (ESP32-S3 only)
    No library needed.
*/

// The GPIO number SIGNAL is wired to.
// Uno: 2. ESP32: 25. ESP32-S3: 4. Pico: 15.
const int KNOCK_PIN = 4;
// The TK01's SIGNAL. Uno: 9. ESP32: 4. ESP32-S3: 5. Pico: 14.
const int LED_PIN = 5;

const unsigned long HOLD_OFF_MS = 100;  // longer than the spring rings
const unsigned long END_MS = 1500;      // this much silence ends a try
const unsigned long OPEN_MS = 3000;     // how long the LED stays on
const float TOLERANCE = 0.1;            // of the longest gap

// Shave and a haircut, two bits: the gaps between 7 knocks, in beats.
const int SECRET[] = {2, 1, 1, 2, 4, 2};
const int GAPS = 6;
const int MAX_KNOCKS = 10;

#ifndef IRAM_ATTR
#define IRAM_ATTR               // only the ESP32 cores need it
#endif

volatile unsigned long knockAt[MAX_KNOCKS];
volatile int knocks = 0;
volatile unsigned long lastKnock = 0;

// Runs the instant SIGNAL goes HIGH. Short, and no printing.
void IRAM_ATTR onKnock() {
  unsigned long now = millis();
  if (now - lastKnock < HOLD_OFF_MS) return;   // still ringing
  lastKnock = now;
  if (knocks < MAX_KNOCKS) knockAt[knocks] = now;
  knocks++;
}

bool matches(int n) {
  if (n != GAPS + 1) return false;            // wrong count
  unsigned long longest = 0;
  for (int i = 0; i < GAPS; i++) {
    unsigned long gap = knockAt[i + 1] - knockAt[i];
    if (gap > longest) longest = gap;
  }
  for (int i = 0; i < GAPS; i++) {
    float heard = (knockAt[i + 1] - knockAt[i]) / (float)longest;
    float wanted = SECRET[i] / 4.0;           // 4: SECRET's longest
    if (fabs(heard - wanted) > TOLERANCE) return false;
  }
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(KNOCK_PIN, INPUT);    // the block has its own pull-down
  pinMode(LED_PIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(KNOCK_PIN), onKnock, RISING);
}

void loop() {
  noInterrupts();               // copy them in one piece
  int n = knocks;
  unsigned long last = lastKnock;
  interrupts();

  if (n > 0 && millis() - last > END_MS) {     // the knocking stopped
    bool open = matches(n);
    Serial.print(n);
    Serial.println(open ? " knocks: open" : " knocks: wrong");
    if (open) {
      digitalWrite(LED_PIN, HIGH);
      delay(OPEN_MS);
      digitalWrite(LED_PIN, LOW);
    }
    noInterrupts();
    knocks = 0;                 // ready for the next try
    interrupts();
  }
}

SECRET is in beats, and only the ratios matter: {2, 1, 1, 2, 4, 2} divided by its longest gap, 4, is what the heard gaps are compared with. Change it to your own rhythm, keeping MAX_KNOCKS above its length, and change the 4 to your longest gap.

When it does not work

I knock the rhythm and it says wrong every time.

Look at the count it prints first. Too few means a knock was too soft to reach the spring, or two came within the 100 ms hold-off: knock firmly and not too fast. Too many means the spring rang past the hold-off on a hard knock; raise HOLD_OFF_MS to 150. With the count right, raise TOLERANCE to 0.15 and try again.

It opens for rhythms that are nearly right.

Lower TOLERANCE. At 0.1, each gap may be off by a tenth of the longest gap, which at a relaxed speed is a few tens of milliseconds. At 0.05 it takes a steady hand. It is a toy lock either way: anyone who hears the rhythm has the key.

It answers before I have finished.

The try ends after END_MS of silence, 1.5 s. The longest gap in the rhythm is four beats; at a slow pace, a beat of 400 ms, that gap is 1.6 s and the sketch decides in the middle of it. Knock a little faster or raise END_MS to 2500.

Why wait for silence instead of checking after the seventh knock?

So that an eighth knock counts against you. If the sketch checked at seven, anyone could knock twenty times and it would open as soon as seven of them happened to fit.

Where this goes next

The short list of reasons, in the order to check them.

When a knock does nothing

Edit this page — content/books/knock-sensor/a-secret-knock.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

Knock Sensor

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum