Most Arduino guides start with one module and explain everything about it.
That is useful for learning a component. It is not useful for designing an interactive device.
An interactive device lives in a place. A place has people. People do things. The device must respond to those things.
This guide flips the question. Instead of "what does this module do?" it asks: what happens in this room, and which module makes that happen automatically?
Topics covered: HC-SR04 distance sensing, WS2812B addressable LEDs, HC-SR501 PIR motion detection, KY-038 sound sensing, DHT22 environmental monitoring, threshold-based logic, relay control.
What You'll Need
- HC-SR04 ultrasonic sensor (×1)
- WS2812B NeoPixel LED strip (30-LED, ×1)
- HC-SR501 PIR motion sensor (×1)
- KY-038 sound sensor module (×1)
- DHT22 temperature and humidity sensor (×1)
- 5V relay module (×1) — for fan/heater control
- Arduino Nano or Uno (×1)
- ESP32 dev board (×1, for Wi-Fi projects)
- USB cable, breadboard, jumper wires
Scene 1: The Living Room — Proximity-Responsive Ambient Lighting
Goal: The LED strip brightens and shifts color as someone walks closer to the sofa.
The living room is where an interactive device becomes part of the home. The interaction needs to feel natural — not a button press, not a voice command. Just presence and distance.
WS2812B addressable LEDs give you per-LED color and brightness control. HC-SR04 measures distance to the nearest person. Together they create proximity-responsive lighting without any touch.
Hardware
- Arduino Nano
- HC-SR04 ultrasonic sensor
- WS2812B LED strip (30 LEDs)
- 5V 2A power supply for LEDs
- Breadboard and jumper wires
How It Works
HC-SR04 emits a 40kHz ultrasonic pulse and measures the return time. Divide by 2 (round trip) and multiply by the speed of sound to get distance in centimeters.
WS2812B uses a single-wire protocol. Each LED has a built-in driver chip. You send color values as a serial stream and each LED captures its own 24-bit color value, then forwards the rest. That is why you can control hundreds of LEDs with one Arduino pin.
Wiring
Arduino HC-SR04
Pin 7 ────── Echo
Pin 8 ────── Trig
5V ────── VCC
GND ────── GND
Arduino WS2812B Strip
Pin 6 ────── Din
5V ────── VCC (via 5V 2A supply)
GND ────── GND (shared with Arduino)
Code
// WF1 Run #032 - Scene 1: Proximity-Responsive Living Room Lighting
#include <Adafruit_NeoPixel.h>
#include <NewPing.h>
#define TRIG_PIN 8
#define ECHO_PIN 7
#define LED_PIN 6
#define LED_COUNT 30
#define MAX_DISTANCE 200 // cm
#define NEAR_DISTANCE 50 // cm — full brightness zone
#define FAR_DISTANCE 150 // cm — minimum brightness zone
NewPing sonar(TRIG_PIN, ECHO_PIN, MAX_DISTANCE);
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
void setup() {
Serial.begin(115200);
strip.begin();
strip.show(); // Initialize all pixels to 'off'
}
void loop() {
delay(50); // Wait 50ms between pings (29ms minimum)
unsigned int distance = sonar.ping_cm();
if (distance == 0) {
distance = MAX_DISTANCE; // No object detected — treat as far
}
// Map distance to brightness
int brightness;
if (distance <= NEAR_DISTANCE) {
brightness = 255;
} else if (distance >= FAR_DISTANCE) {
brightness = 30;
} else {
brightness = map(distance, NEAR_DISTANCE, FAR_DISTANCE, 255, 30);
}
// Map distance to color temperature (warm white near, cool blue far)
uint8_t r = map(distance, 0, FAR_DISTANCE, 255, 100);
uint8_t g = map(distance, 0, FAR_DISTANCE, 200, 180);
uint8_t b = map(distance, 0, FAR_DISTANCE, 150, 255);
uint32_t color = strip.Color(r * brightness / 255,
g * brightness / 255,
b * brightness / 255);
// Fill entire strip with proximity-based color
for (int i = 0; i < LED_COUNT; i++) {
strip.setPixelColor(i, color);
}
strip.show();
Serial.print("Distance: ");
Serial.print(distance);
Serial.print(" cm → Brightness: ");
Serial.println(brightness);
delay(100);
}
Interaction Design Note
The threshold between FAR_DISTANCE (150cm) and NEAR_DISTANCE (50cm) maps to a natural human behavior zone — someone across the room vs. someone on the sofa. The warm-to-cool color shift reinforces the physical sense of proximity.
Scene 2: The Workshop — Motion-Triggered Task Lighting with Sound Awareness
Goal: The work light turns on when someone enters the garage, and stays on only if the space is actually occupied — not just briefly passing through.
The workshop is a transitional space. People enter carrying things, leave, come back. Motion sensors alone fail here — a PIR sensor triggers on any warm body passing through, including someone just walking past the open door.
The solution: motion detection plus distance confirmation plus ambient sound awareness. If the ultrasonic sensor detects a person AND the sound sensor reads below the ambient threshold (not someone using power tools), activate the light.
Hardware
- Arduino Nano
- HC-SR501 PIR motion sensor
- HC-SR04 ultrasonic sensor
- KY-038 sound sensor module
- 5V relay module
- LED shop light (powered via relay)
- External 5V power supply
How HC-SR501 Works
The PIR sensor detects infrared radiation differences between a warm body and the room temperature background. It has a Fresnel lens that focuses infrared onto a split detector. When the two halves see different heat signatures, the sensor triggers.
The sensor has two potentiometers: sensitivity (how far it detects, up to ~7m) and delay (how long the output stays HIGH after trigger, 5s–300s).
How KY-038 Works
The KY-038 has a digital output (D0) that goes HIGH when sound exceeds a threshold set by the onboard potentiometer, and an analog output (A0) that gives a raw voltage proportional to sound pressure level.
For this scene, we use A0 to read ambient sound level. If the workshop is noisy (power tools), we suppress the occupancy confirmation to avoid false positives.
Wiring
Arduino HC-SR501 PIR
Pin 2 ────── OUT
5V ────── VCC
GND ────── GND
Arduino HC-SR04
Pin 7 ────── Trig
Pin 8 ────── Echo
5V ────── VCC
GND ────── GND
Arduino KY-038
A0 ────── A0
5V ────── VCC
GND ────── GND
Arduino Relay Module
Pin 4 ────── IN
5V ────── VCC
GND ────── GND
Code
// WF1 Run #032 - Scene 2: Workshop Motion-Triggered Task Light
#include <NewPing.h>
#define PIR_PIN 2
#define TRIG_PIN 7
#define ECHO_PIN 8
#define SOUND_PIN A0
#define RELAY_PIN 4
#define MAX_DISTANCE 150 // cm
#define OCCUPY_DISTANCE 80 // cm — person is inside workshop
#define SOUND_THRESHOLD 400 // Below this = quiet workshop
#define OCCUPY_TIMEOUT 30000 // ms — light stays on 30s after last detection
unsigned long lastMotionTime = 0;
bool lightOn = false;
NewPing sonar(TRIG_PIN, ECHO_PIN, MAX_DISTANCE);
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // Relay NC — light off at startup
}
void loop() {
int pirState = digitalRead(PIR_PIN);
int distance = sonar.ping_cm();
int soundLevel = analogRead(SOUND_PIN);
unsigned long now = millis();
if (pirState == HIGH && distance > 0 && distance < OCCUPY_DISTANCE) {
// Motion detected AND person is inside workshop threshold
if (soundLevel < SOUND_THRESHOLD) {
// Workshop is quiet — confirmed occupancy
lastMotionTime = now;
if (!lightOn) {
digitalWrite(RELAY_PIN, HIGH);
lightOn = true;
Serial.println("Light ON — workshop occupied");
}
} else {
Serial.println("Motion detected but workshop noisy — suppressing");
}
}
// Turn off light if no confirmed occupancy within timeout
if (lightOn && (now - lastMotionTime > OCCUPY_TIMEOUT)) {
digitalWrite(RELAY_PIN, LOW);
lightOn = false;
Serial.println("Light OFF — workshop empty");
}
delay(100);
}
Scene 3: The Display Case — Presence-Activated Museum Lighting
Goal: The LED lighting inside a glass display case activates only when a visitor is standing in front of it, with no touch required.
Display cases have a specific problem: the exhibit needs to look pristine when no one is there, but the lighting should communicate "this is worth looking at" when someone approaches. A motion-activated case light creates a museum experience that draws attention without manual interaction.
Hardware
- Arduino Nano
- HC-SR501 PIR sensor
- WS2812B LED ring (12 LEDs)
- Frosted glass display case (any glass cabinet)
- 5V 2A power supply
Code
// WF1 Run #032 - Scene 3: Display Case Presence-Activated Lighting
#include <Adafruit_NeoPixel.h>
#define PIR_PIN 2
#define LED_PIN 6
#define LED_COUNT 12
#define DISPLAY_ON_COLOR strip.Color(220, 240, 255) // Cool museum white
#define DISPLAY_DIM_COLOR strip.Color(30, 35, 40) // Barely visible standby
Adafruit_NeoPixel ring(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
bool displayLit = false;
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
ring.begin();
ring.setBrightness(255);
ring.fill(DISPLAY_DIM_COLOR);
ring.show();
}
void loop() {
int pirState = digitalRead(PIR_PIN);
if (pirState == HIGH && !displayLit) {
// Fade in to full brightness over 2 seconds
for (int brightness = 0; brightness <= 255; brightness += 5) {
ring.setBrightness(brightness);
ring.fill(DISPLAY_ON_COLOR);
ring.show();
delay(40); // ~2 second fade-in
}
displayLit = true;
Serial.println("Display ON");
} else if (pirState == LOW && displayLit) {
// Fade out to standby over 3 seconds
for (int brightness = 255; brightness >= 0; brightness -= 3) {
ring.setBrightness(brightness);
ring.fill(DISPLAY_ON_COLOR);
ring.show();
delay(50); // ~3 second fade-out
}
ring.fill(DISPLAY_DIM_COLOR);
ring.setBrightness(255);
ring.show();
displayLit = false;
Serial.println("Display standby");
}
delay(200);
}
Interaction Design Note
The slow fade-in (2 seconds) and fade-out (3 seconds) are not decorative — they are functional. Abrupt lighting changes distract from the exhibit. Gradual transitions signal intentionality. The visitor perceives the lighting as responding to their presence, not reacting to a sensor.
Scene 4: The Music Corner — Sound-Reactive LED Ambient Strip
Goal: The LED strip changes color and intensity in response to the music playing in the room — without microphones or cables.
This is a common goal with a common failure mode: the sensor picks up its own LED glow. The solution is to physically separate the sound sensor from the LED light source, or to mount the sensor facing away from the LEDs.
Hardware
- Arduino Nano
- KY-038 sound sensor
- WS2812B LED strip (60 LEDs)
- External 5V 3A power supply
Code
// WF1 Run #032 - Scene 4: Music Corner Sound-Reactive LED Strip
#include <Adafruit_NeoPixel.h>
#define SOUND_PIN A0
#define LED_PIN 6
#define LED_COUNT 60
#define NOISE_FLOOR 10 // Sensor baseline noise
#define MUSIC_MIN 100 // Minimum level that triggers reaction
#define MUSIC_MAX 600 // Maximum mapped level
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
void setup() {
Serial.begin(115200);
strip.begin();
strip.show();
}
void loop() {
int rawSound = analogRead(SOUND_PIN);
int soundLevel = rawSound - NOISE_FLOOR;
if (soundLevel < 0) soundLevel = 0;
if (soundLevel < MUSIC_MIN) {
// Quiet — idle breathing effect
breatheEffect(50); // Slow, dim idle
} else {
// Active music — map level to brightness and hue
uint8_t brightness = map(soundLevel, MUSIC_MIN, MUSIC_MAX, 100, 255);
brightness = constrain(brightness, 0, 255);
uint32_t color = getMusicColor(soundLevel);
strip.setBrightness(brightness);
for (int i = 0; i < LED_COUNT; i++) {
strip.setPixelColor(i, color);
}
strip.show();
Serial.print("Sound: ");
Serial.print(soundLevel);
Serial.print(" → Brightness: ");
Serial.println(brightness);
}
delay(50);
}
uint32_t getMusicColor(int level) {
// Hue sweep based on music intensity
uint16_t hue = map(level, MUSIC_MIN, MUSIC_MAX, 0, 65535);
hue = constrain(hue, 0, 65535);
return strip.ColorHSV(hue, 255, 255);
}
void breatheEffect(uint8_t brightness) {
// Sine wave breathing at ~0.5 Hz
uint8_t breath = (sin(millis() / 1000.0 * PI) + 1) * 0.5 * brightness;
uint32_t dimBlue = strip.Color(0, 20, breath);
for (int i = 0; i < LED_COUNT; i++) {
strip.setPixelColor(i, dimBlue);
}
strip.show();
delay(50);
}
Critical Installation Note
Mount the KY-038 facing away from the LED strip. If the sound sensor is behind the LEDs, it will detect the LED's own heat output and stay permanently triggered. Place it on the front edge of the enclosure, pointing toward the listener.
Scene 5: The Greenhouse — Environmental Monitor with Automatic Fan Control
Goal: The system reads temperature and humidity every 30 seconds. If either exceeds a set threshold, it turns on a ventilation fan and displays a warning on the LCD.
The greenhouse problem is persistence. Plants do not respond to momentary spikes — they respond to sustained conditions. This scene uses a state machine to distinguish between a brief reading and a real problem that requires action.
Hardware
- Arduino Nano
- DHT22 temperature and humidity sensor
- 16×2 LCD I2C display
- 5V relay module
- 5V exhaust fan
- 5V 2A power supply
How DHT22 Works
The DHT22 uses a single-wire bidirectional serial protocol. It sends a 40-bit data packet: 16-bit humidity, 16-bit temperature, and 8-bit checksum. The Arduino library handles the protocol — you just call readTemperature() and readHumidity().
The sensor requires at least 2 seconds between readings. Do not poll it faster or you will get NaN values.
Wiring
Arduino DHT22
Pin 2 ────── DATA
5V ────── VCC
GND ────── GND
Arduino LCD I2C
A4 (SDA) ───── SDA
A5 (SCL) ───── SCL
5V ────── VCC
GND ────── GND
Arduino Relay
Pin 4 ────── IN
5V ────── VCC
GND ────── GND
Code
// WF1 Run #032 - Scene 5: Greenhouse Environmental Monitor
#include <DHT.h>
#include <LiquidCrystal_I2C.h>
#define DHT_PIN 2
#define RELAY_PIN 4
#define DHTTYPE DHT22
#define TEMP_THRESHOLD 32.0 // °C — fan turns on above this
#define HUM_THRESHOLD 85.0 // % — fan turns on above this
#define READING_INTERVAL 30000 // ms between readings
#define FAN_RUN_TIME 600000 // ms — fan runs 10 minutes once triggered
DHT dht(DHT_PIN, DHTTYPE);
LiquidCrystal_I2C lcd(0x27, 16, 2);
enum State { IDLE, WARNING, FAN_ON };
State systemState = IDLE;
unsigned long lastReadingTime = 0;
unsigned long fanStartTime = 0;
void setup() {
Serial.begin(115200);
dht.begin();
lcd.init();
lcd.backlight();
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // Fan off at startup
displayReading(0, 0); // Init display
}
void loop() {
unsigned long now = millis();
if (now - lastReadingTime >= READING_INTERVAL) {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
if (isnan(temp) || isnan(hum)) {
Serial.println("DHT22 read error — skipping");
lastReadingTime = now;
return;
}
lastReadingTime = now;
displayReading(temp, hum);
Serial.print("Temp: "); Serial.print(temp);
Serial.print("°C | Humidity: "); Serial.print(hum);
Serial.println("%");
// State transitions
if (temp > TEMP_THRESHOLD || hum > HUM_THRESHOLD) {
if (systemState == IDLE) {
systemState = WARNING;
Serial.println("State: WARNING");
}
} else {
if (systemState == FAN_ON && (now - fanStartTime > FAN_RUN_TIME)) {
systemState = IDLE;
digitalWrite(RELAY_PIN, LOW);
Serial.println("State: IDLE — fan off");
}
}
}
// Handle WARNING state — start fan if conditions persist
if (systemState == WARNING) {
digitalWrite(RELAY_PIN, HIGH);
fanStartTime = now;
systemState = FAN_ON;
Serial.println("State: FAN_ON");
lcd.setCursor(0, 1);
lcd.print("FAN: ON ");
}
}
void displayReading(float temp, float hum) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("T:");
lcd.print(temp, 1);
lcd.print((char)223); // Degree symbol
lcd.print("C H:");
lcd.print(hum, 1);
lcd.print("%");
if (temp > TEMP_THRESHOLD || hum > HUM_THRESHOLD) {
lcd.setCursor(0, 1);
lcd.print("!! THRESHOLD !!");
}
}
Troubleshooting Table
| Problem | Cause | Fix |
|---|---|---|
| HC-SR04 reads 0 constantly | No object in range | Wrap in if (distance == 0) distance = MAX_DISTANCE
|
| WS2812B LEDs all flicker | Power supply cannot deliver enough current | Use a separate 5V supply for LEDs, share ground with Arduino |
| HC-SR501 never triggers indoors | Lens is indoors, Fresnel pattern needs open space | Adjust sensitivity pot to max, point toward open area |
| KY-038 sound sensor always HIGH | Potentiometer threshold set too low | Turn the onboard potentiometer clockwise to increase threshold |
| DHT22 returns NaN | Polling too fast (needs 2s minimum) | Add delay(2000) between reads, or use DHT library's begin()
|
| Relay clicks but load does not activate | Relay is NC (normally closed) | Check: COM/NO wiring — use NO for Arduino-controlled loads |
| LCD shows nothing | I2C address wrong | Run I2C scanner, common addresses are 0x27 and 0x3F |
| LED breathing effect looks jittery |
delay() inside loop blocks timing |
Replace delay() with non-blocking millis() timing |
Start Here
Affiliate disclosure: As an Amazon Associate, I earn from qualifying purchases.
The right parts make the difference:
- HC-SR04 ultrasonic sensor on Amazon — Distance sensing for presence detection and proximity lighting
- WS2812B LED strip 30-LED on Amazon — Addressable LEDs for ambient lighting and visual feedback
- HC-SR501 PIR motion sensor on Amazon — Motion detection for hands-free activation
- DHT22 temperature humidity sensor on Amazon — Environmental monitoring for climate control
- Arduino Nano compatible board on Amazon — Compact controller for sensor and LED projects
Next Step: From Scene to Sensor, Without Writing Code
If this guide gave you ideas for your own setup — but you are not sure which sensors and outputs work best for your specific space — I can help you map that out.
I offer a personalized interactive device design guide at Fiverr:
👉 https://www.fiverr.com/phd_hfchang/generate-an-arduino-interactive-prototypef
What you get:
- A custom guide based on your actual scene (not generic recommendations)
- Sensor selection matched to user behavior and physical constraints
- Interaction logic without needing to write code from scratch
- Testing methodology with pass/fail criteria for each output
Tags: Arduino HC-SR04 WS2812B HC-SR501 DHT22 Interactive Devices Home Automation Ambient Lighting Sensor Projects






Top comments (0)