A tip-over alarm
Stand the block on edge on the wall of a box, header edge down, and upright becomes a reading: header. The sketch filters out knocks, and if the box reads anything else for more than a second it lights a TK01 and keeps it lit, even after the box is stood back up, until someone resets the board.
Stand it on edge
Flat on a table, the block has no resting reading: the roller stays wherever it last rolled. So this build does not lay it flat. It stands on edge, fixed to the inside wall of a box, with the header edge pointing down.
Now gravity always has an opinion. While the box is upright, the roller sits
on the header side and joins D and A, and the scan reads header. Tip the
box onto its left or right side and the reading becomes left or right;
turn it upside down and it becomes top. Anything but header means the
box is not the way up it should be.
A clock, and a latch
The sketch is the last article's filtered scan with two things added. When
the believed reading stops being header, it notes the time. If it is still
not header TIP_MS later, one second here, the alarm comes on. The filter
has already thrown away knocks, and the one-second wait throws away a box
that was bumped and rocked back.
Then the alarm stays on. Once alarmOn is set, nothing in the loop clears
it, so a box that fell over and was quietly stood back up still says so.
Pressing the board's reset button clears it. The timing in the figure is
illustrative; the rule is the sketch's.
What it cannot see
The switch only feels tilts in the plane of the board. A box that falls
straight forwards or backwards, away from the wall the block is on, lays the
board flat, and a flat board keeps its last reading: header. The alarm
never comes on.
If that direction matters, mount a second block on a wall at right angles to the first, and alarm on either. Each needs four pins of its own.
The wiring
The block is wired as in the first read. The alarm is a TK01 XL LED on a fifth pin: D9 on an Uno, GPIO 4 on an ESP32, GPIO 15 on an ESP32-S3, GP15 on a Pico. Its first blink shows the wires. Without one, watch the serial monitor, knock the box once, then tip it over for a couple of seconds:
left
tipped over: alarm
headerThe knock never appears. LED_PIN is the only pin the sketch ever drives
HIGH, and it is not one of the tilt lines.
The code
Watches a box with the block on its wall, header edge down. It filters every scan, and once the box has read anything but header for TIP_MS it lights a TK01 XL LED on a second pin and leaves it lit. No library, no long delay().
/*
4-Direction Tilt Sensor - a tip-over alarm TK19 / /p/tk19
Wiring. Count from the square pad on the TinkerBlock board, parts
up, header at the bottom:
GND -> GND
VCC -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
A -> D2 on an Uno, GPIO 25 on an ESP32, GPIO 4 on an
ESP32-S3, GP10 on a Raspberry Pi Pico
B -> D3, GPIO 26, GPIO 5, GP11 (same order)
C -> D4, GPIO 27, GPIO 6, GP12
D -> D5, GPIO 32, GPIO 7, GP13
The block stands on edge on the box's wall, header edge down.
A TK01 XL LED on a second pin is the alarm (optional):
GND -> GND
SIGNAL -> D9 on an Uno, GPIO 4 on an ESP32, GPIO 15 on an
ESP32-S3, GP15 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 numbers A, B, C and D are wired to.
// Uno: 2 3 4 5. ESP32: 25 26 27 32. ESP32-S3: 4 5 6 7. Pico: 10 11 12 13.
const int PIN_A = 4;
const int PIN_B = 5;
const int PIN_C = 6;
const int PIN_D = 7;
// The GPIO number the TK01's SIGNAL is wired to.
// Uno: 9. ESP32: 4. ESP32-S3: 15. Pico: 15.
const int LED_PIN = 15;
const int STABLE_READS = 5; // 5 scans, 10 ms apart: 50 ms
const int SCAN_MS = 10;
const unsigned long TIP_MS = 1000;
const int NO_SIDE = 0;
const int LEFT_DOWN = 1;
const int TOP_DOWN = 2;
const int RIGHT_DOWN = 3;
const int HEADER_DOWN = 4;
const char* SIDE_NAME[] = {"none", "left", "top", "right", "header"};
const int UPRIGHT = HEADER_DOWN; // standing on edge, header down
void releaseAll() {
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_C, INPUT_PULLUP);
pinMode(PIN_D, INPUT_PULLUP);
}
void holdLow(int pin) {
digitalWrite(pin, LOW); // LOW first, then output
pinMode(pin, OUTPUT);
}
bool readsLow(int pin) {
return digitalRead(pin) == LOW;
}
int scanTilt() {
releaseAll();
holdLow(PIN_A);
delayMicroseconds(10);
bool ab = readsLow(PIN_B); // left
bool da = readsLow(PIN_D); // header
releaseAll();
holdLow(PIN_C);
delayMicroseconds(10);
bool bc = readsLow(PIN_B); // top
bool cd = readsLow(PIN_D); // right
releaseAll();
if (ab) return LEFT_DOWN;
if (da) return HEADER_DOWN;
if (bc) return TOP_DOWN;
if (cd) return RIGHT_DOWN;
return NO_SIDE;
}
int candidate = UPRIGHT;
int count = 0;
int believed = UPRIGHT;
bool tipped = false;
unsigned long tippedAt = 0;
bool alarmOn = false;
void setup() {
Serial.begin(115200);
releaseAll();
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
}
void loop() {
int side = scanTilt();
if (side == candidate) {
if (count < STABLE_READS) count++;
} else {
candidate = side;
count = 1;
}
if (count >= STABLE_READS && candidate != believed) {
believed = candidate;
Serial.println(SIDE_NAME[believed]);
}
unsigned long now = millis();
if (believed == UPRIGHT) {
tipped = false; // upright again: stop the clock
} else if (!tipped) {
tipped = true; // just tipped: start the clock
tippedAt = now;
}
if (tipped && !alarmOn && now - tippedAt >= TIP_MS) {
alarmOn = true; // latched: only a reset clears it
digitalWrite(LED_PIN, HIGH);
Serial.println("tipped over: alarm");
}
delay(SCAN_MS);
}The alarm depends on time, not on one reading: a tip starts a clock, standing up stops it. Once alarmOn is set nothing in loop() clears it, so the alarm outlives the tip; only a reset does. LED_PIN is the only pin the sketch ever writes HIGH, and it is not a tilt line.
The same alarm in MicroPython: filter every scan, start a clock when the box stops reading header, and light the TK01 once it has been tipped for TIP_MS.
"""
4-Direction Tilt Sensor - tip-over alarm, MicroPython TK19 / /p/tk19
Wiring. Count from the square pad on the TinkerBlock board, parts
up, header at the bottom:
GND -> GND
VCC -> 3V3 (never 5V beside a 3.3 V board)
A -> GPIO 25 on an ESP32, GPIO 4 on an ESP32-S3, GP10 on a Pico
B -> GPIO 26, GPIO 5, GP11 (same order)
C -> GPIO 27, GPIO 6, GP12
D -> GPIO 32, GPIO 7, GP13
The block stands on edge on the box's wall, header edge down.
A TK01 XL LED on a second pin is the alarm (optional):
GND -> GND
SIGNAL -> GPIO 4 on an ESP32, GPIO 15 on an ESP32-S3, GP15 on a 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 numbers A, B, C and D are wired to.
# ESP32: 25 26 27 32. ESP32-S3: 4 5 6 7. Pico: 10 11 12 13.
PIN_A, PIN_B, PIN_C, PIN_D = 4, 5, 6, 7
# The GPIO number the TK01's SIGNAL is wired to.
# ESP32: 4. ESP32-S3: 15. Pico: 15.
LED_PIN = 15
STABLE_READS = 5 # 5 scans, 10 ms apart: 50 ms
SCAN_MS = 10
TIP_MS = 1000
UPRIGHT = "header" # standing on edge, header down
a = Pin(PIN_A, Pin.IN, Pin.PULL_UP)
b = Pin(PIN_B, Pin.IN, Pin.PULL_UP)
c = Pin(PIN_C, Pin.IN, Pin.PULL_UP)
d = Pin(PIN_D, Pin.IN, Pin.PULL_UP)
led = Pin(LED_PIN, Pin.OUT, value=0)
def release_all():
for p in (a, b, c, d):
p.init(Pin.IN, Pin.PULL_UP)
def hold_low(p):
p.init(Pin.OUT, value=0)
def scan_tilt():
release_all()
hold_low(a)
time.sleep_us(10)
ab = b.value() == 0
da = d.value() == 0
release_all()
hold_low(c)
time.sleep_us(10)
bc = b.value() == 0
cd = d.value() == 0
release_all()
if ab:
return "left"
if da:
return "header"
if bc:
return "top"
if cd:
return "right"
return "none"
candidate = UPRIGHT
count = 0
believed = UPRIGHT
tipped_at = None # None while upright
alarm_on = False
while True:
side = scan_tilt()
if side == candidate:
count = min(count + 1, STABLE_READS)
else:
candidate = side
count = 1
if count >= STABLE_READS and candidate != believed:
believed = candidate
print(believed)
now = time.ticks_ms()
if believed == UPRIGHT:
tipped_at = None # upright again: stop the clock
elif tipped_at is None:
tipped_at = now # just tipped: start the clock
if (tipped_at is not None and not alarm_on
and time.ticks_diff(now, tipped_at) >= TIP_MS):
alarm_on = True # latched: only a reset clears it
led.value(1)
print("tipped over: alarm")
time.sleep_ms(SCAN_MS)time.ticks_ms() wraps round, so the sketch compares times with ticks_diff() rather than subtracting. The alarm latches: stop the script with Ctrl-C in Thonny, or reset the board, to clear it.
When it does not work
The block is not reading header at rest. Check that it stands on edge with the header edge pointing down, square to the floor, so gravity holds the roller on that side. Watch the serial monitor: the sketch starts out believing header and prints only changes, so a side printed straight after start-up means the block is not standing header-down.
It cannot. The switch only feels tilts in the plane of the board. Falling forwards or backwards lays the board flat, and a flat board keeps its last reading, which is header. Mount a second block on a wall at right angles to the first to cover that direction.
Press your board's reset button, or power it off and on. The sketch latches the alarm on purpose, so a box that was tipped and stood back up still says so. To clear it from a button instead, reset alarmOn and turn LED_PIN off when the button is pressed.
Yes. The sketch drives LED_PIN HIGH for the alarm, so anything that switches on with a HIGH works on that pin unchanged; the active buzzer block is one. A passive buzzer needs tone() instead of digitalWrite().
The short list of reasons, and why the dark LEDs are usually not one of them.
When a tilt reads nothing →Edit this page — content/books/four-direction-tilt-sensor/a-tip-over-alarm.mdx
Questions about this product
See what other owners have asked, and read their solutions.
4-Direction Tilt 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.