DEV Community

張旭豐
張旭豐

Posted on

5 Arduino Modules That Transform How Your Interactive Device Feels — Organized by Scene

Common Arduino modules for interactive devices

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.

Living room scene with proximity-responsive LED strip on wall behind 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)
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Workshop scene with motion-triggered LED shop light and PIR sensor visible

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Museum display case scene with presence-activated LED lighting and visitor viewing exhibit

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Music corner scene with sound-reactive LED strip and person listening to music

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Greenhouse scene with environmental monitor, exhaust fan, and DHT22 sensor visible

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
Enter fullscreen mode Exit fullscreen mode

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 !!");
  }
}
Enter fullscreen mode Exit fullscreen mode

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:


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)