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.
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: openThe 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 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.
The same lock in MicroPython. on_knock appends the time of each knock to a list, with the hold-off. When the knocking has stopped for END_MS, the main loop takes the list, checks it and lights the TK01 on a match.
"""
Knock Sensor - a secret knock, MicroPython TK28 / /p/tk28
Wiring, the TK28. Count from the square pad, switch at the top,
header at the bottom:
GND -> GND
VCC -> 3V3 (never 5V: during a knock, SIGNAL gives your pin VCC)
NC -> nothing (unconnected on the board)
SIGNAL -> 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 -> GPIO 4 on an ESP32, GPIO 5 on an ESP32-S3,
GP14 on a Raspberry Pi Pico
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Save it to the board as main.py to run it on every power-up.
Nothing to install: machine and time are built in.
"""
from machine import Pin
import time
# The GPIO number SIGNAL is wired to. ESP32: 25. ESP32-S3: 4. Pico: 15.
KNOCK_PIN = 4
# The TK01's SIGNAL. ESP32: 4. ESP32-S3: 5. Pico: 14.
LED_PIN = 5
HOLD_OFF_MS = 100 # longer than the spring rings
END_MS = 1500 # this much silence ends a try
OPEN_MS = 3000 # how long the LED stays on
TOLERANCE = 0.1 # of the longest gap
SECRET = [2, 1, 1, 2, 4, 2] # shave and a haircut, two bits
MAX_KNOCKS = 10
knock = Pin(KNOCK_PIN, Pin.IN) # no pull: the block has its own
led = Pin(LED_PIN, Pin.OUT, value=0)
times = []
last_knock = time.ticks_add(time.ticks_ms(), -HOLD_OFF_MS)
def on_knock(pin): # short, and no printing
global last_knock
now = time.ticks_ms()
if time.ticks_diff(now, last_knock) < HOLD_OFF_MS:
return # still ringing
last_knock = now
if len(times) < MAX_KNOCKS:
times.append(now)
def matches(t):
if len(t) != len(SECRET) + 1:
return False # wrong count
gaps = [time.ticks_diff(t[i + 1], t[i]) for i in range(len(SECRET))]
longest = max(gaps)
for heard, wanted in zip(gaps, SECRET):
if abs(heard / longest - wanted / max(SECRET)) > TOLERANCE:
return False
return True
knock.irq(trigger=Pin.IRQ_RISING, handler=on_knock)
while True:
quiet = time.ticks_diff(time.ticks_ms(), last_knock)
if times and quiet > END_MS: # the knocking stopped
heard = times
times = [] # ready for the next try
ok = matches(heard)
print(len(heard), "knocks:", "open" if ok else "wrong")
if ok:
led.value(1)
time.sleep_ms(OPEN_MS)
led.value(0)
time.sleep_ms(10)The main loop swaps in a fresh list before checking the old one, so a knock during the check starts the next try instead of spoiling this one. ticks_diff does every subtraction, because ticks_ms wraps round.
When it does not work
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.
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.
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.
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.
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
Questions about this product
See what other owners have asked, and read their solutions.
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.