StairSense is an experimental project of mine that’s meant you lift up your boring staircase!
More seriously, it provides functional and adaptive lighting which is cool during the day and enhances safety at night while still being cool. B-) It can also detect if, God forbid, someone falls on the stairs.
It combines distributed computing, wireless microcontroller coordination via MQTT and ultrasonic sensing!
It consists of a couple of microcontrollers (ESP32s) that are each responsible for a section of the staircase. For example, MCU0 is responsible for steps 0 to 4, MCU1 for steps 5 to 9 and so on…

Each microcontroller is connected to a couple of HC-SR04 ultrasonic sensors and WS2812B addressable LED strips. A pair of ultrasonic sensor and LED strip for each step.
The ultrasonic sensor is used to detect if someone is goes there or if something is on the step. It’s a cheap, low-maintenance, easy-to-set-up and non-invasive. It even works in the dark!
The only limitation is that you need an atmosphere, otherwise the sound wave wouldn’t have a medium to travel!
And of course, the pièce de resistance is the addressable LED strip for each step. It’s really the showpiece, as this what people will see when they climb the stairs. It’s also the most “expensive” component in the system.
The microcontrollers cost about $8 and the ultrasonic sensors go for about $2 each (and could even be cheaper, if bought in bulk). A 1-metre LED strip cost about $12.
It’s addressable which means any individual pixel could be controlled! This gives rise to so many possibilties and lighting patterns.
All the prices listed are in Canadian Dollars (CAD).

There is also a server in the equation. It helps with coordination and edge computing tasks like fall detection.
I am still experimenting with the communication protocol.
I was initially using ESP-NOW for communication among the microcontrollers, as a true peer-to-peer system. However, I later decided to include a server to offload a great deal of the work off of the MCUs. In my opinion, this decision also improved maintainability. I am using Go for the server.
It uses the ultrasonic sensor, on each step. When the system boots up, it automatically calibrates itself and establishes a threshhold.

And if someone goes by, their foot being in front of the sensor, decreases that distance. The system knows that there is something on that step.

I am still working on it. What you are seeing on this page is just prototyping. I will update the page as I make more progress. The software is closed source for now.
Until next time, here a some of “cool” features in this prototype.
Every time the system boots up, it automatically calibrates itself.
After the system is calibrated, it tries to connect to the Wi-Fi network and the server. (You can see that by then end, with the blue flashing LED on the microcontroller.)
When luminosity drops below a threshold or after a configured time interval, Night Mode is enabled. In this mode, StairSense maintains a low idle illumination (only a few LEDs light up) and brightens as someone approaches. The LEDs ramp up smoothly to avoid sudden glare.
Both static patterns (like the blue color pattern) and dynamic ones (like the moving rainbox pattern) are both supported!
TRIG line with more than one HC-SR04The HC-SR04 sensors were not designed to be driven by a singular shared TRIG line. During my testing, it worked fine. A distinct ECHO line is still needed for each. Below is the simplified, but comprehensive logic being used.
void getDistances()
{
// 1. Trigger all sensors at once
// TRIGGER_PIN is shared among the sensor
digitalWrite(TRIGGER_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIGGER_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIGGER_PIN, LOW);
// 2. Wait for the echoes in parallel (non blocking)
for (int i = 0; i < 3; i++)
{
echoStart[i] = 0;
echoEnd[i] = 0;
started[i] = false;
ended[i] = false;
}
startTime = micros();
while (true)
{
unsigned long now = micros();
if (now - startTime > TIMEOUT)
break;
// Check each echo pin
if (!started[0] && digitalRead(ECHO_PIN_1) == HIGH)
{
echoStart[0] = now;
started[0] = true;
}
if (started[0] && !ended[0] && digitalRead(ECHO_PIN_1) == LOW)
{
echoEnd[0] = now;
ended[0] = true;
}
if (!started[1] && digitalRead(ECHO_PIN_2) == HIGH)
{
echoStart[1] = now;
started[1] = true;
}
if (started[1] && !ended[1] && digitalRead(ECHO_PIN_2) == LOW)
{
echoEnd[1] = now;
ended[1] = true;
}
if (!started[2] && digitalRead(ECHO_PIN_3) == HIGH)
{
echoStart[2] = now;
started[2] = true;
}
if (started[2] && !ended[2] && digitalRead(ECHO_PIN_3) == LOW)
{
echoEnd[2] = now;
ended[2] = true;
}
// Break if all echoes received
if (ended[0] && ended[1] && ended[2])
break;
}
// 3. Calculate distances
for (int i = 0; i < 3; i++)
{
if (started[i] && ended[i] && echoEnd[i] > echoStart[i])
{
unsigned long duration = echoEnd[i] - echoStart[i];
distances[i] = duration / 58.0; // in cm
}
else
{
distances[i] = -1; // timeout or invalid
}
}
}
This is not officially supported in the sensor’s datasheet. In my testing, it worked pretty well. Considering the setup (each step is staggered and above each other), cross-talk is unlikely to occur.