Built-in project · Arduino Uno R3

Rescue beacon on an Arduino Uno

A white LED flashes SOS from a table of lengths, but only in the dark: a light sensor wakes it at dusk and a button forces it on. Plays the pattern without delay(). The whole build, an Arduino Uno and 5 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.

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

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

The circuit runs here once the page has loaded.

What you'll learn

  • Lights
  • Sensors
  • Choosing and reading a resistor
  • How an LED lights and why it needs a resistor
  • Reading a button
  • Difficulty: Beginner
  • Time: About 20 minutes
  • Board: Arduino Uno R3
  • Parts: 7

How it works

A white LED flashes SOS from a table of lengths, but only in the dark: a light sensor wakes it at dusk and a button forces it on. Plays the pattern without delay().

Arduino Uno R3 runs the sketch below. The circuit around it: Resistor, 220 Ω, LED, white, Pushbutton, Light sensor, Resistor, 10k Ω. pinMode sets BEACON as an output. digitalWrite drives BEACON LOW. digitalRead reads whether TEST is HIGH or LOW. analogRead measures the voltage on SENSOR as a number. Serial.begin opens the serial port at 115200 baud.

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
Resistor, 220 Ω pin 1Arduino Uno R3 pin 6
Pushbutton pin 1aArduino Uno R3 pin 2
Light sensor pin 2Arduino Uno R3 pin A0
Resistor, 10k Ω pin 1Arduino Uno R3 pin A0
LED, white pin AResistor, 220 Ω pin 2
LED, white pin CGround
Pushbutton pin 2bGround
Light sensor pin 15 V
Resistor, 10k Ω pin 2Ground

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
// Rescue beacon: a white LED that flashes SOS after dark, on an Uno.
//
//   A0      the middle of a divider: 5 V, an LDR, A0, 10k, GND
//   pin 6   the beacon LED, through 220 ohm to GND
//   pin 2   TEST button to GND (forces the beacon on, press again to release)
//
// In daylight the beacon sleeps, because a rescue signal nobody can see is a
// waste of battery. When the light drops it starts: three short flashes, three
// long, three short, a pause, and again. The TEST button turns it on whatever
// the light is, so you can see that it works.
//
// # A pattern is data
//
// Morse is not a program, it is a table. PATTERN lists how long each part of
// SOS lasts, in units of 200 ms, alternating ON, OFF, ON, OFF: the first
// number is a short flash, the second the gap after it, and so on. S is
// "1,1,1,1,1,3" (flash, gap, flash, gap, flash, and a longer gap that ends
// the letter), O is the same with 3s for the flashes. To send something else
// you change the table and not a line of code.
//
// # A player that never waits
//
// The sketch keeps one number, `step`, which is where in the table it is, and
// the time at which that step ends. loop() asks whether the time has come,
// and if it has, goes to the next step and flips the LED. Between those
// moments it is free to read the light sensor and the button, so the beacon
// can be switched off the instant it gets light, even half way through a
// flash. A version with delay() would have to finish the whole SOS first.
//
// # Light and dark with a gap between them
//
// Dusk is not a moment, and a cloud passing over the sensor should not start
// the beacon. Two thresholds, one for going dark and a higher one for coming
// back to light, keep it from flickering in between; the same idea as a
// thermostat's gap.

const uint8_t SENSOR = A0;
const uint8_t BEACON = 6;
const uint8_t TEST = 2;

const int DARK = 250;     // below this the beacon wakes (0 to 1023)
const int LIGHT = 500;    // above this it goes back to sleep
const unsigned long UNIT_MS = 200;
const unsigned long REST_MS = 5000;

// ON, OFF, ON, OFF ... in units: S (. . .), O (- - -), S (. . .)
const uint8_t PATTERN[] = {1, 1, 1, 1, 1, 3, 3, 1, 3, 1, 3, 3, 1, 1, 1, 1, 1, 7};
const uint8_t STEPS = sizeof(PATTERN);

bool night = false;
bool forced = false;
bool active = false;      // the beacon is running (dark, or forced)
uint8_t step = 0;
unsigned long stepEnd = 0;
bool resting = false;
unsigned long sent = 0;
bool lastReading = HIGH;
bool pressedState = HIGH;  // the button's debounced state
unsigned long changedAt = 0;

void stopBeacon() {
  active = false;
  resting = false;
  step = 0;
  digitalWrite(BEACON, LOW);
}

void startBeacon(unsigned long now) {
  active = true;
  resting = false;
  step = 0;
  stepEnd = now + PATTERN[0] * UNIT_MS;
  digitalWrite(BEACON, HIGH);
}

void setup() {
  Serial.begin(115200);
  pinMode(BEACON, OUTPUT);
  pinMode(TEST, INPUT_PULLUP);
  Serial.println("rescue beacon ready");
}

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

  // light and dark, with a gap
  int light = analogRead(SENSOR);
  if (!night && light < DARK) {
    night = true;
    Serial.println("dark: beacon on");
  } else if (night && light > LIGHT) {
    night = false;
    Serial.println("daylight: beacon asleep to save the battery");
  }

  // the test button toggles an override. A reading counts once it has held
  // for 40 ms, which rides out the chatter of the contacts on the way down
  // and on the way back up.
  bool reading = digitalRead(TEST);
  if (reading != lastReading) {
    lastReading = reading;
    changedAt = now;
  }
  if (reading != pressedState && now - changedAt > 40) {
    pressedState = reading;
    if (pressedState == LOW) {
      forced = !forced;
      Serial.println(forced ? "test: beacon forced on" : "test: released");
    }
  }

  bool want = night || forced;
  if (want && !active) startBeacon(now);
  if (!want && active) stopBeacon();

  if (active && now >= stepEnd) {
    if (resting) {
      startBeacon(now);
    } else {
      step++;
      if (step >= STEPS) {
        sent++;
        Serial.print("SOS #");
        Serial.print(sent);
        Serial.println(" sent");
        resting = true;
        digitalWrite(BEACON, LOW);
        stepEnd = now + REST_MS;
      } else {
        digitalWrite(BEACON, (step % 2 == 0) ? HIGH : LOW);
        stepEnd = now + PATTERN[step] * UNIT_MS;
      }
    }
  }
}

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() {
  Serial.begin(115200);
  pinMode(BEACON, OUTPUT);
  pinMode(TEST, INPUT_PULLUP);
  Serial.println("rescue beacon 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();

  // light and dark, with a gap
  int light = analogRead(SENSOR);
  if (!night && light < DARK) {
    night = true;
    Serial.println("dark: beacon on");
  } else if (night && light > LIGHT) {
    night = false;
    Serial.println("daylight: beacon asleep to save the battery");
  }

  // the test button toggles an override. A reading counts once it has held
  // for 40 ms, which rides out the chatter of the contacts on the way down
  // and on the way back up.
  bool reading = digitalRead(TEST);
  if (reading != lastReading) {
    lastReading = reading;
    changedAt = now;
  }
  if (reading != pressedState && now - changedAt > 40) {
    pressedState = reading;
    if (pressedState == LOW) {
      forced = !forced;
      Serial.println(forced ? "test: beacon forced on" : "test: released");
    }
  }
  // ... 25 more lines in the editor

Troubleshooting

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

  • The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • The LED does not light, or it lights once and then dies. LED
    LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin.
  • A blue or white LED stays dark on 3.3 V. LED
    Blue and white LEDs drop about 3 V, so 3.3 V leaves almost nothing for the resistor. Use a 5 V supply, or a smaller resistor and a red or green LED.
  • The input reads random HIGH and LOW when the button is not pressed. Pushbutton
    A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW.
  • The button seems always pressed, or never changes. Pushbutton
    A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected.
  • The reading barely changes, or sits stuck near 0 or the top of the range. Light sensor
    An LDR is a resistor, so use it in a divider with a fixed resistor, about 10k, and read the middle node. Without the divider the analog pin sees nothing useful.
  • Readings go the wrong way: higher in the dark. Light sensor
    Which side the LDR sits on decides the direction. LDR on the supply side gives a higher reading in the light. LDR on the ground side gives a lower reading in the light. Swap them if needed.

Change it and keep it

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