Built-in project · Arduino Uno R3

Bidirectional visitor counter on an Arduino Uno

Two IR sensors at a doorway tell people coming in from going out, an I2C LCD shows IN, OUT and inside, and a relay lights the room while anyone is in. The whole build, an Arduino Uno and 6 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 25 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $39. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Telling direction from the order two sensors trigger
  • Counting a person once with a wait-until-clear step
  • Timing out a half-finished event
  • Switching a lamp with a relay
  • Showing a live count on an I2C LCD
  • Difficulty: Intermediate
  • Time: About 25 minutes
  • Board: Arduino Uno R3
  • Parts: 11

How it works

Two infrared obstacle sensors stand across a doorway: A on pin 2 on the outside and B on pin 3 on the inside. Each pulls its output LOW while something is in front of it. Someone coming in breaks A then B. Someone going out breaks B then A.

The sketch remembers which beam broke first, counts the person when the other one breaks, then waits for both to be clear. So one person is counted once. If nothing follows within PASS_MS (1.5 seconds), the person turned back and is forgotten.

An LCD shows IN, OUT and how many are inside. A relay on pin 7 powers a lamp while the count is above zero.

Parts list

11 parts, plus the jumper wires. They retail for about $39; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Arduino Uno R3 pin GNDGround
Relay module pin INArduino Uno R3 pin 7
Obstacle sensor, FC-51 (2) pin OUTArduino Uno R3 pin 3
Obstacle sensor, FC-51 (1) pin OUTArduino Uno R3 pin 2
Character LCD with I2C backpack pin SDAArduino Uno R3 pin A4
Character LCD with I2C backpack pin SCLArduino Uno R3 pin A5
Obstacle sensor, FC-51 (1) pin GNDGround
Obstacle sensor, FC-51 (1) pin VCC5 V
Obstacle sensor, FC-51 (2) pin GNDGround
Obstacle sensor, FC-51 (2) pin VCC5 V
Relay module pin GNDGround
Relay module pin VCC5 V
Filament lamp pin 1Relay module pin NO
DC jack and adapter pin V+Relay module pin COM
DC jack and adapter pin V-Ground
Filament lamp pin 2Ground
Character LCD with I2C backpack pin GNDGround
Character LCD with I2C backpack pin VCC5 V

The note on the canvas

Word for word, as it sits beside the circuit in the editor.

Try it
1. Press Run. The LCD reads IN 0, OUT 0, Inside: 0.
2. Walk in: drag sensor A (outside) close, then sensor B (inside), then move both back to nothing. IN goes up and the lamp lights.
3. Walk out: B first, then A. When the room is empty the lamp goes off.
4. A then nothing is someone who turned back, and is not counted.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Bidirectional visitor counter: two IR obstacle sensors across a doorway
// tell people coming in from people going out, an I2C LCD shows the count,
// and a relay keeps the room light on while anyone is inside.
//
//   sensor A   the outer FC-51 (the corridor side): OUT on pin 2, VCC 5V, GND
//   sensor B   the inner FC-51 (the room side):     OUT on pin 3, VCC 5V, GND
//   LCD        16x2 with an I2C backpack at 0x27: SDA A4, SCL A5, VCC 5V, GND
//   relay      IN on pin 7 (high trigger), VCC 5V, GND
//              COM to the 12 V adapter's +, NO to the lamp, the lamp's other
//              lead to the adapter's - (which also joins the Uno's GND)
//
// HOW IT WORKS
//   An FC-51 pulls OUT LOW while something is in front of it. Someone coming
//   in breaks A first and then B; someone going out breaks B first and then
//   A. The sketch remembers which beam broke first, counts the person when
//   the other one breaks, and then waits for both beams to be clear before
//   it looks for the next person, so one person is counted once.
//
//   A beam that breaks and nothing follows within PASS_MS (someone who
//   looked in and turned back) is forgotten. Going out of an empty room is
//   not counted, so the count inside never goes below zero.
//
//   The light is on while the room count is above zero and goes off when the
//   last person leaves.
//
// WHAT TO CHANGE
//   PASS_MS   how long a person may take from one beam to the other
//   Mount the two sensors a hand's width apart, at waist height, so one
//   person cannot break both at the same instant.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

const int SENSOR_A = 2;   // outside
const int SENSOR_B = 3;   // inside
const int RELAY_PIN = 7;

const unsigned long PASS_MS = 1500;

int entered = 0;
int left = 0;
int inside = 0;

char first = 0;           // 'A' or 'B': the beam that broke first, or 0
unsigned long firstAt = 0;
bool waitClear = false;

bool blocked(int pin) {
  return digitalRead(pin) == LOW;
}

void show() {
  lcd.setCursor(0, 0);
  lcd.print("IN ");
  lcd.print(entered);
  lcd.print("  OUT ");
  lcd.print(left);
  lcd.print("     ");
  lcd.setCursor(0, 1);
  lcd.print("Inside: ");
  lcd.print(inside);
  lcd.print(inside > 0 ? "  light" : "       ");
  digitalWrite(RELAY_PIN, inside > 0 ? HIGH : LOW);
}

void report(const char *what) {
  Serial.print(what);
  Serial.print("  in ");
  Serial.print(entered);
  Serial.print("  out ");
  Serial.print(left);
  Serial.print("  inside ");
  Serial.println(inside);
}

void setup() {
  Serial.begin(115200);
  pinMode(SENSOR_A, INPUT);
  pinMode(SENSOR_B, INPUT);
  digitalWrite(RELAY_PIN, LOW);
  pinMode(RELAY_PIN, OUTPUT);
  lcd.init();
  lcd.backlight();
  show();
  Serial.println("visitor counter ready");
}

void loop() {
  bool a = blocked(SENSOR_A);
  bool b = blocked(SENSOR_B);

  if (waitClear) {
    if (!a && !b) waitClear = false;
  } else if (first == 0) {
    if (a && !b) first = 'A';
    else if (b && !a) first = 'B';
    if (first) firstAt = millis();
  } else if (first == 'A' && b) {
    entered++;
    inside++;
    report("in");
    show();
    first = 0;
    waitClear = true;
  } else if (first == 'B' && a) {
    if (inside > 0) {
      left++;
      inside--;
      report("out");
      show();
    } else {
      Serial.println("out ignored: nobody was inside");
    }
    first = 0;
    waitClear = true;
  } else if (millis() - firstAt > PASS_MS) {
    first = 0;  // turned back: forget it
    waitClear = true;
  }
  delay(5);
}

Code walkthrough

The key functions of the sketch, and what each one does.

blocked sketch.ino

Returns true when a sensor pin reads LOW. The FC-51 sensors pull the pin LOW when something is in front of them, so this turns that into a plain yes or no.

bool blocked(int pin) {
  return digitalRead(pin) == LOW;
}

show sketch.ino

Rewrites both LCD lines with IN, OUT and Inside. Extra spaces clear old digits. It also sets the relay pin HIGH when inside is above zero, so the lamp follows the count.

void show() {
  lcd.setCursor(0, 0);
  lcd.print("IN ");
  lcd.print(entered);
  lcd.print("  OUT ");
  lcd.print(left);
  lcd.print("     ");
  lcd.setCursor(0, 1);
  lcd.print("Inside: ");
  lcd.print(inside);
  lcd.print(inside > 0 ? "  light" : "       ");
  digitalWrite(RELAY_PIN, inside > 0 ? HIGH : LOW);
}

report sketch.ino

Prints one line to the Serial Monitor with the event name and the three counts. loop calls it for each person counted.

void report(const char *what) {
  Serial.print(what);
  Serial.print("  in ");
  Serial.print(entered);
  Serial.print("  out ");
  Serial.print(left);
  Serial.print("  inside ");
  Serial.println(inside);
}

loop sketch.ino

A small set of steps. First wait for both beams to clear, then note which beam breaks first, then count when the other breaks. An exit with nobody inside is ignored, so the count never goes below zero.

void loop() {
  bool a = blocked(SENSOR_A);
  bool b = blocked(SENSOR_B);

  if (waitClear) {
    if (!a && !b) waitClear = false;
  } else if (first == 0) {
    if (a && !b) first = 'A';
    else if (b && !a) first = 'B';
    if (first) firstAt = millis();
  } else if (first == 'A' && b) {
    entered++;
    inside++;
    report("in");
    show();
    first = 0;
    waitClear = true;
  } else if (first == 'B' && a) {
    if (inside > 0) {
      left++;
      inside--;
      report("out");
      show();
    } else {
      Serial.println("out ignored: nobody was inside");
    }
    first = 0;
    waitClear = true;
  // ... 6 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • Entries are counted as exits This project
    Swap which sensor is on pin 2 and which is on pin 3, or swap SENSOR_A and SENSOR_B.
  • A slow walker is never counted This project
    Raise PASS_MS.
  • The LCD stays blank This project
    SDA goes to A4 and SCL goes to A5. Check VCC and GND.
  • It detects nothing or always detects. Range depends on the pot and the surface, and black or shiny things reflect poorly. Obstacle sensor
    Turn the pot until the OUT LED switches at your distance. Test with a pale, matte surface. OUT is LOW when something is seen, so read it as active low.
  • Bright sunlight or an incandescent lamp makes it fire at random, because they give off infrared light. Obstacle sensor
    Shade the sensor or use it indoors. Power it from 3.3 V to 5 V, join the grounds, and keep it from facing other IR parts.
  • The relay does not click, or the board resets when it switches. Relay module
    A relay coil needs more current than a pin can give. Use a module with a transistor driver, and power VCC from 5 V. Share ground between the module and the Arduino.
  • The relay works backwards: on when you expect off. Relay module
    Many modules are active LOW: the input must go LOW to turn the relay on. Set the pin HIGH at startup so the load does not switch on during boot.
  • Board gets too much voltage and overheats or dies. DC jack and adapter
    Check the adapter voltage before plugging in. Boards like the Uno want 7 to 12 V on the jack. A 5 V part wired to a 12 V supply can be destroyed.

Questions and answers

How does it tell someone coming in from someone going out?

By which beam breaks first. Coming in breaks the outer sensor A and then B; going out breaks B and then A.

Can one person be counted twice?

No. After counting someone, the sketch waits until both beams are clear before it looks for the next person.

What if someone looks in and turns back?

A beam that breaks with nothing following within PASS_MS (1.5 s) is forgotten, so they are not counted.

Can the count inside go negative?

No. Going out of an empty room is ignored, and the light turns off when the count reaches zero.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.