automatic-wiper-arduino-nano

Rain Sensing Wiper using Arduino Nano with OLED — Step-by-Step Guide Leave a comment

automatic-wiper-arduino-nano

This post contains affiliate links. If you buy through Amazon links, we earn a small
commission at no extra cost to you. We always list Electronixity first.

Ever wondered how a car’s automatic wiper knows exactly when it starts raining? This
rain sensing wiper project recreates that behaviour on a desktop scale using an Arduino Nano, a
rain sensor, an MG90 servo motor, and a
0.96″ OLED display. When the rain sensor detects moisture, a servo-driven
wiper arm starts sweeping back and forth while the OLED animates raindrops and a matching
wiper blade in real time. The moment it’s dry again, the wiper parks itself and the OLED
switches to a cheerful sun animation. It’s an intermediate build — budget 45–60 minutes for
wiring and code — and a great demonstration of automatic, sensor-driven control for STEM and
engineering mini-projects.

Features & What You’ll Learn

  • Digital rain sensing and state-change detection
  • Driving a servo and an OLED animation in perfect sync
  • Non-blocking timing with millis() instead of delay()
  • Simple animated graphics on an SSD1306 OLED (drops, wiper arm, sun rays)
  • Difficulty level: Intermediate
  • Estimated build time: 45–60 minutes

Bill of Materials (BOM)

Here’s everything you need for this build. We’ve linked each part to Electronixity so you
can order them in one cart, with Amazon India as a backup option.

Circuit Diagram & Pin Connections

The rain sensor outputs a simple digital HIGH/LOW signal, the
OLED runs over I2C, and the MG90 servo drives the physical
wiper arm.

Component PinArduino Nano Pin
OLED VCC5V
OLED GNDGND
OLED SDAA4
OLED SCLA5
Rain Sensor VCC5V
Rain Sensor GNDGND
Rain Sensor D0D12
MG90 Servo SignalD3
MG90 Servo VCC5V (external supply recommended)
MG90 Servo GNDGND
automatic-wiper-arduino-circuit-diagram

Step-by-Step Assembly

  1. Wire the OLED to the Nano’s I2C pins (A4/A5), same as any standard

    SSD1306 module.
  2. Connect the rain sensor‘s digital output (D0) to pin D12. This board is

    most commonly active LOW — meaning it reads LOW when wet, HIGH when dry.
  3. Attach a small wiper-arm shape to the MG90 servo horn and connect its

    signal wire to D3.
  4. Install the Adafruit GFX and Adafruit SSD1306

    libraries from the Arduino IDE Library Manager before uploading.
  5. Place a few drops of water on the rain sensor’s exposed plate to test detection once

    everything is wired.

Adjust the sensitivity potentiometer on the rain sensor board (if present) so a small
amount of water reliably triggers a state change without false positives from humidity.

Code

This sketch checks the rain sensor every 200 ms, switches between a “wet” and “dry” state,
and animates the OLED accordingly — falling raindrops with a sweeping wiper line when wet, or
a rotating sun icon when dry. The MG90 servo always mirrors the wiper angle drawn on screen.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Servo.h>

// -------------------- OLED CONFIG --------------------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// -------------------- PINS --------------------
const int RAIN_PIN  = 12;   // Rain sensor DIGITAL output
const int SERVO_PIN = 3;    // MG90 signal

// Digital rain sensor is usually ACTIVE LOW (0 = rain, 1 = dry)
const int RAIN_ACTIVE_LEVEL = LOW;

// -------------------- RAIN STATE LOGIC --------------------
enum RainState : uint8_t { RAIN_DRY, RAIN_WET };
RainState rainState = RAIN_DRY;

// -------------------- TIMING --------------------
unsigned long lastSensorReadMs = 0;
const unsigned long SENSOR_INTERVAL_MS = 200;

unsigned long lastAnimMs = 0;
unsigned long animIntervalMs = 60;

// -------------------- RAIN ANIMATION --------------------
struct Drop { int x; int y; };
const int MAX_DROPS = 16;
Drop drops[MAX_DROPS];

void initDrops() {
  for (int i = 0; i < MAX_DROPS; i++) {
    drops[i].x = random(0, SCREEN_WIDTH);
    drops[i].y = random(-SCREEN_HEIGHT, 0);
  }
}

void updateDrops(int activeDrops) {
  for (int i = 0; i < activeDrops; i++) {
    int speed = 2;
    drops[i].y += speed;
    if (drops[i].y > SCREEN_HEIGHT) {
      drops[i].y = random(-20, 0);
      drops[i].x = random(0, SCREEN_WIDTH);
    }
  }
}

// -------------------- WIPER ANIMATION + SERVO --------------------
Servo wiperServo;
const int WIPER_MIN_ANGLE = 40;
const int WIPER_MAX_ANGLE = 140;
int wiperAngle = WIPER_MIN_ANGLE;
int wiperDir   = 1;

// -------------------- SUN ANIMATION --------------------
int sunPhase = 0;

// -------------------- RAIN STATE --------------------
void updateRainStateDigital(int level) {
  RainState newState = (level == RAIN_ACTIVE_LEVEL) ? RAIN_WET : RAIN_DRY;

  if (newState != rainState) {
    rainState = newState;
    if (rainState == RAIN_DRY) {
      wiperAngle = WIPER_MIN_ANGLE;
      wiperServo.write(wiperAngle);
    }
  }
}

// -------------------- STEP ANIMATIONS --------------------
void stepAnimations() {
  if (rainState == RAIN_WET) {
    updateDrops(MAX_DROPS);

    wiperAngle += wiperDir * 3;
    if (wiperAngle >= WIPER_MAX_ANGLE) {
      wiperAngle = WIPER_MAX_ANGLE;
      wiperDir   = -1;
    } else if (wiperAngle <= WIPER_MIN_ANGLE) {
      wiperAngle = WIPER_MIN_ANGLE;
      wiperDir   = 1;
    }

    wiperServo.write(wiperAngle); // servo mirrors the drawn angle
  } else {
    sunPhase = (sunPhase + 1) % 16;
  }
}

// -------------------- DRAW HELPERS --------------------
void drawWiper() {
  int pivotX = SCREEN_WIDTH / 2;
  int pivotY = SCREEN_HEIGHT - 4;
  int length = 40;

  float rad = wiperAngle * PI / 180.0;
  int endX = pivotX + (int)(length * cos(rad));
  int endY = pivotY - (int)(length * sin(rad));

  display.drawLine(pivotX, pivotY, endX, endY, WHITE);
  display.drawLine(pivotX - 1, pivotY, endX - 1, endY, WHITE);
}

void drawSun() {
  int cx = SCREEN_WIDTH - 26;
  int cy = 22;
  int radius = 9;

  display.fillCircle(cx, cy, radius, WHITE);

  for (int i = 0; i < 8; i++) {
    float baseAngleDeg = i * 45;
    float angleDeg = baseAngleDeg + sunPhase * 4;
    float rad = angleDeg * PI / 180.0;

    int innerR = radius + 2;
    int outerR = radius + 7;

    int x1 = cx + (int)(innerR * cos(rad));
    int y1 = cy + (int)(innerR * sin(rad));
    int x2 = cx + (int)(outerR * cos(rad));
    int y2 = cy + (int)(outerR * sin(rad));

    display.drawLine(x1, y1, x2, y2, WHITE);
  }
}

void drawScene() {
  display.clearDisplay();

  int wx = 4, wy = 18;
  int ww = SCREEN_WIDTH - 8;
  int wh = SCREEN_HEIGHT - 22;
  display.drawRoundRect(wx, wy, ww, wh, 4, WHITE);

  if (rainState == RAIN_WET) {
    for (int i = 0; i < MAX_DROPS; i++) {
      int x = drops[i].x;
      int y = drops[i].y;
      if (x >= wx + 2 && x <= wx + ww - 4 &&
          y >= wy + 2 && y <= wy + wh - 4) {
        display.drawLine(x, y, x, y + 3, WHITE);
      }
    }
    drawWiper();
  } else {
    drawSun();
  }

  display.display();
}

void setup() {
  Serial.begin(9600);
  pinMode(RAIN_PIN, INPUT);

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 init failed!"));
    while (true) {}
  }

  wiperServo.attach(SERVO_PIN);
  wiperServo.write(WIPER_MIN_ANGLE);

  randomSeed(analogRead(A1));
  initDrops();
  drawScene();
}

void loop() {
  unsigned long now = millis();

  if (now - lastSensorReadMs >= SENSOR_INTERVAL_MS) {
    lastSensorReadMs = now;
    int level = digitalRead(RAIN_PIN);
    updateRainStateDigital(level);
  }

  if (now - lastAnimMs >= animIntervalMs) {
    lastAnimMs = now;
    stepAnimations();
    drawScene();
  }
}

Code Walkthrough

The sketch uses millis() instead of delay() so the rain sensor can
be polled every 200 ms while the OLED animation keeps redrawing every 60 ms, independently.
updateRainStateDigital() only acts when the rain state actually changes — going
from dry to wet or back — rather than running logic on every loop. When it’s wet, the wiper
angle drawn on screen is sent straight to the physical MG90 servo via
wiperServo.write(wiperAngle), so the OLED graphic and the real wiper arm always
move in sync. When it dries out, the wiper parks at WIPER_MIN_ANGLE and the screen
switches to the rotating sun animation.

Output / Results

In dry conditions, the OLED shows a “windshield” frame with a small animated sun in the
corner. As soon as water touches the rain sensor, raindrops start falling inside the frame and
the wiper line — along with the real servo arm — begins sweeping back and forth, just like a
car’s automatic wiper system.

Troubleshooting

  • Wiper never starts even when wet: Your rain sensor board may be active HIGH instead of active LOW — change RAIN_ACTIVE_LEVEL to HIGH and retest.
  • Wiper triggers randomly with no water: Adjust the sensitivity trimmer on the rain sensor module, or check for a loose ground connection causing noisy readings.
  • OLED freezes during the wet animation: Make sure the servo isn’t starving the Nano of power — use a separate 5V source for the MG90.
  • Servo doesn’t move at all: Confirm the signal wire is on D3 and that the Servo library was included before any other library that uses Timer1.

Extensions & Next Steps

Ready to go further? Try these extensions:

  • Add a buzzer alert that beeps once when rain is first detected, useful for a smart window-closing system.
  • Log rain events with timestamps to track rainfall patterns over a week.
  • Combine this with a DHT22 sensor to also display humidity alongside the rain status.

Related Tutorials

Leave a Reply

Your email address will not be published. Required fields are marked *