Light curtain on an Arduino Uno
Two E18-D80NK beams guard a servo press: start works only with the beams clear, a broken beam on the down stroke retracts the ram and latches a lockout that needs a reset, and a fail-safe e-stop stops everything. The whole build, an Arduino Uno and 9 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 35 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $51. On Mokxi they're free.
What you'll learn
- Sensors
- Motors
- Displays
- Lights
- Controllers
- Difficulty: Intermediate
- Time: About 35 minutes
- Board: Arduino Uno R3
- Parts: 14
How it works
Two E18-D80NK beams guard a servo press: start works only with the beams clear, a broken beam on the down stroke retracts the ram and latches a lockout that needs a reset, and a fail-safe e-stop stops everything.
Arduino Uno R3 runs the sketch below. The circuit around it: Traffic light module, Hobby servo, Buzzer, Slide switch, Arcade button, IR proximity switch, E18-D80NK. pinMode sets BEAM_A_PIN as an input with its pull-up on. digitalRead reads whether b.pin is HIGH or LOW. delay waits 20 milliseconds. Serial.begin opens the serial port at 9600 baud. millis reads the time since power-up, so the sketch can time things without stopping.
Parts list
14 parts, plus the jumper wires. They retail for about $51; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Traffic light module
- 1 × Hobby servoIn Halloween Prop Kit, Christmas Lights Kit, Starter Projects Kit.
- 3 × Power, 5 V
- 1 × BuzzerIn Starter Kit, Rocket Flight Computer Kit.
- 1 × Slide switchIn Logic Lab Kit, Spaceship Panel Kit.
- 2 × Arcade buttonIn Claw Machine Kit, Fight Stick Kit, Pinball Table Kit.
- 2 × IR proximity switch, E18-D80NK
- 1 × Character LCD with I2C backpackIn Claw Machine Kit, Vending Machine Kit, Rhythm Game Kit.
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 pin | Connects to |
|---|---|
| Traffic light module pin R | Arduino Uno R3 pin 13 |
| Traffic light module pin Y | Arduino Uno R3 pin 12 |
| Traffic light module pin G | Arduino Uno R3 pin 11 |
| Hobby servo pin PWM | Arduino Uno R3 pin 9 |
| Buzzer pin 1 | Arduino Uno R3 pin 8 |
| Slide switch pin 2 | Arduino Uno R3 pin 6 |
| Arcade button (1) pin 1 | Arduino Uno R3 pin 5 |
| Arcade button (2) pin 1 | Arduino Uno R3 pin 4 |
| IR proximity switch, E18-D80NK (1) pin OUT | Arduino Uno R3 pin 3 |
| IR proximity switch, E18-D80NK (2) pin OUT | Arduino Uno R3 pin 2 |
| Character LCD with I2C backpack pin VCC | Arduino Uno R3 pin 5V |
| Character LCD with I2C backpack pin GND | Arduino Uno R3 pin GND |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 pin A4 |
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 pin A5 |
| Traffic light module pin GND | Ground |
| Hobby servo pin GND | Ground |
| Hobby servo pin VCC | 5 V |
| Buzzer pin 2 | Ground |
| Slide switch pin 1 | Ground |
| Arcade button (1) pin 2 | Ground |
| Arcade button (2) pin 2 | Ground |
| IR proximity switch, E18-D80NK (1) pin VCC | 5 V |
| IR proximity switch, E18-D80NK (1) pin GND | Ground |
| IR proximity switch, E18-D80NK (2) pin VCC | 5 V |
| IR proximity switch, E18-D80NK (2) pin GND | Ground |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Light curtain: two infrared beams guard a press, and a hand in the beams
// stops it. A fault latches until someone clears it on purpose.
//
// 2, 3 two E18-D80NK beams across the press opening, OUT (brown on 5 V,
// blue on GND): low while something is in that beam
// 4 the green start button, its other side on GND
// 5 the reset button, its other side on GND
// 6 the e-stop switch: closed to GND while it is safe to run
// 8 the active buzzer's plus leg, its other leg on GND
// 9 the press ram, a servo (0 = up, 90 = down)
// 13/12/11 the stack light: red, yellow, green
// LCD SDA -> A4 SCL -> A5 VCC -> 5 V GND -> GND
//
// An interlock is a rule the machine checks before and during every move:
//
// READY ram up, green light. Start with both beams clear -> DOWN.
// Start with a beam blocked is refused.
// DOWN the ram travels down over STROKE_MS, waits, then -> UP. A
// broken beam on the way down -> LOCKOUT.
// UP the ram returns. Done -> READY, one more cycle.
// LOCKOUT ram up at once, red light flashing, buzzer pulsing. Only the
// reset button gets out, and only with both beams clear.
// STOPPED the e-stop is open: ram up, red light. It needs the e-stop
// closed, both beams clear and a press of reset.
//
// The e-stop is fail-safe: the pin's pull-up reads HIGH when the switch is
// open AND when its wire is cut, so a broken wire stops the press too. A
// fault never clears itself; a person has to look, clear it and press reset.
//
// Press Run, press start, and drag a sensor's target close while the ram
// is coming down.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "mokxi_servo.h"
const int BEAM_A_PIN = 2;
const int BEAM_B_PIN = 3;
const int START_PIN = 4;
const int RESET_PIN = 5;
const int ESTOP_PIN = 6;
const int BUZZER_PIN = 8;
const int RAM_PIN = 9;
const int RED_PIN = 13;
const int YELLOW_PIN = 12;
const int GREEN_PIN = 11;
const unsigned long STROKE_MS = 2000; // ram travel, top to bottom
const unsigned long DWELL_MS = 300; // ram held down
const unsigned long RETURN_MS = 800; // ram back up before the next cycle
const unsigned long PULSE_MS = 250; // lockout flash and beep
const unsigned long DEBOUNCE_MS = 30; // a button steady this long
const int RAM_UP = 0;
const int RAM_DOWN = 90;
enum State { READY, DOWN, UP, LOCKOUT, STOPPED };
const char* const NAMES[] = {"READY", "DOWN", "UP", "LOCKOUT", "STOPPED"};
LiquidCrystal_I2C lcd(0x27, 16, 2);
Servo ram(RAM_PIN);
struct Button {
int pin;
bool down;
bool raw;
unsigned long changedAt;
};
Button startButton = {START_PIN, false, false, 0};
Button resetButton = {RESET_PIN, false, false, 0};
State state = READY;
unsigned long enteredAt = 0;
unsigned long cycles = 0;
bool lastClear = true;
// True once for each press.
bool pressed(Button& b, unsigned long now) {
bool raw = digitalRead(b.pin) == LOW;
if (raw != b.raw) {
b.raw = raw;
b.changedAt = now;
}
if (raw != b.down && now - b.changedAt >= DEBOUNCE_MS) {
b.down = raw;
return raw;
}
return false;
}
bool beamsClear() {
return digitalRead(BEAM_A_PIN) == HIGH && digitalRead(BEAM_B_PIN) == HIGH;
}
bool estopOpen() {
return digitalRead(ESTOP_PIN) == HIGH;
}
// The LCD takes a few milliseconds a character, so it is written only when
// something changes, and always after the outputs have been set.
void showInfo() {
lcd.setCursor(0, 1);
if (state == LOCKOUT || state == STOPPED) {
lcd.print(lastClear ? "Press reset " : "Clear the beams ");
} else if (!lastClear) {
lcd.print("Beam blocked ");
} else {
lcd.print("Cycles ");
lcd.print(cycles);
lcd.print(" ");
}
}
void show() {
lcd.setCursor(0, 0);
lcd.print("Press ");
lcd.print(NAMES[state]);
lcd.print(" ");
showInfo();
}
// Every output, from the state and the ram's angle.
void outputs(int ramAngle) {
bool flash = (millis() - enteredAt) % (2 * PULSE_MS) < PULSE_MS;
ram.write(ramAngle);
digitalWrite(RED_PIN, state == STOPPED || (state == LOCKOUT && flash));
digitalWrite(YELLOW_PIN, state == DOWN || state == UP);
digitalWrite(GREEN_PIN, state == READY);
digitalWrite(BUZZER_PIN, state == LOCKOUT && flash);
}
void enter(State next) {
state = next;
enteredAt = millis();
// Every state starts with the ram up, and the outputs go before anything
// slow, so a lockout pulls the ram back on the pass that sees the beam.
outputs(RAM_UP);
Serial.print("state ");
Serial.println(NAMES[next]);
lastClear = beamsClear();
show();
enteredAt = millis();
}
void setup() {
pinMode(BEAM_A_PIN, INPUT_PULLUP);
pinMode(BEAM_B_PIN, INPUT_PULLUP);
pinMode(START_PIN, INPUT_PULLUP);
pinMode(RESET_PIN, INPUT_PULLUP);
pinMode(ESTOP_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RED_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
Serial.begin(9600);
Serial.println("Light curtain");
ram.begin();
ram.write(RAM_UP);
lcd.init();
lcd.backlight();
// An e-stop that is open at power up holds the press stopped.
enter(estopOpen() ? STOPPED : READY);
}
void loop() {
unsigned long now = millis();
bool clear = beamsClear();
bool start = pressed(startButton, now);
bool reset = pressed(resetButton, now);
unsigned long t = now - enteredAt;
int ramAngle = RAM_UP;
// The e-stop wins over everything, in every state.
if (estopOpen() && state != STOPPED) {
Serial.println("e-stop");
enter(STOPPED);
t = 0;
}
switch (state) {
case READY:
if (start) {
if (clear) {
enter(DOWN);
t = 0;
} else {
Serial.println("start refused: beam blocked");
}
}
break;
case DOWN:
if (!clear) {
// The ram goes back up on this very pass.
Serial.println("beam broken: lockout");
enter(LOCKOUT);
break;
}
ramAngle = t >= STROKE_MS ? RAM_DOWN : (int)((long)RAM_DOWN * t / STROKE_MS);
if (t >= STROKE_MS + DWELL_MS) {
enter(UP);
ramAngle = RAM_UP;
}
break;
case UP:
if (t >= RETURN_MS) {
cycles++;
Serial.print("cycle ");
Serial.println(cycles);
enter(READY);
}
break;
case LOCKOUT:
case STOPPED:
if (reset) {
if (estopOpen()) {
Serial.println("reset refused: e-stop open");
} else if (!clear) {
Serial.println("reset refused: beam blocked");
} else {
Serial.println("reset");
enter(READY);
}
}
break;
}
outputs(ramAngle);
if (clear != lastClear) {
lastClear = clear;
showInfo();
}
delay(20);
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Runs once when the board starts. It gets the pins and libraries ready.
void setup() {
pinMode(BEAM_A_PIN, INPUT_PULLUP);
pinMode(BEAM_B_PIN, INPUT_PULLUP);
pinMode(START_PIN, INPUT_PULLUP);
pinMode(RESET_PIN, INPUT_PULLUP);
pinMode(ESTOP_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RED_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
Serial.begin(9600);
Serial.println("Light curtain");
ram.begin();
ram.write(RAM_UP);
lcd.init();
lcd.backlight();
// An e-stop that is open at power up holds the press stopped.
enter(estopOpen() ? STOPPED : READY);
}loop sketch.ino
Runs over and over for as long as the board has power. This is where the project does its work.
void loop() {
unsigned long now = millis();
bool clear = beamsClear();
bool start = pressed(startButton, now);
bool reset = pressed(resetButton, now);
unsigned long t = now - enteredAt;
int ramAngle = RAM_UP;
// The e-stop wins over everything, in every state.
if (estopOpen() && state != STOPPED) {
Serial.println("e-stop");
enter(STOPPED);
t = 0;
}
switch (state) {
case READY:
if (start) {
if (clear) {
enter(DOWN);
t = 0;
} else {
Serial.println("start refused: beam blocked");
}
}
break;
case DOWN:
if (!clear) {
// ... 41 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- One color never lights, or all three light together. Traffic light module
Check each wire goes to its own pin labeled R, Y, G, and that GND is connected. A pin driven high lights its LED. Resistors are already on the board, so do not add more. - Lights seem dim or the wrong color comes on in a sequence. Traffic light module
Confirm the pins in your code match the wires, and that the module gets the right supply voltage. Keep each state's delay long enough to see, and turn the old color off before the next. - The servo jitters, twitches or resets the board when it moves. Hobby servo
Power it from its own 5 V supply, not the Arduino 5 V pin, and join the grounds. A hobby servo can pull several hundred milliamps. Add a 100 uF to 470 uF capacitor across its supply. - The servo hums at the end of its travel or will not reach full angle. Hobby servo
Stay inside the pulse range, about 1000 to 2000 us for most hobby servos. Pushing past the end stop makes it strain. Calibrate the min and max pulse for your servo. - The buzzer makes a constant tone, or you cannot change its pitch. Buzzer
An active buzzer has its own oscillator, so it only beeps on and off. A passive buzzer needs tone() to set pitch. Check which type you have. - The buzzer is silent or very quiet. Buzzer
Check polarity: the plus mark goes to the pin or supply. For loud buzzers, drive them through a transistor instead of straight from a pin. - The input reads random values in one position. Slide switch
A switch only connects or disconnects. Add a pull-up or pull-down resistor, or use INPUT_PULLUP, so the pin has a defined level when the switch is open. - The load never turns on when the switch is flipped. Slide switch
Use the center (common) leg and one outer leg. The unused outer leg goes nowhere. Check that the switch is in series with the load.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.