Built-in project · Arduino Uno R3

Lie detector, science fair style, on an Arduino Uno

Skin response and heart rate against a calm baseline on a ten-segment stress bar. A science demo of how the body reacts, not a real lie detector. The whole build, an Arduino Uno and 14 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 40 minutes to build and run. Runs in your browser. Free, and no account needed.

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

The circuit runs here once the page has loaded.

What you'll learn

  • Measuring skin conductance with a GSR sensor
  • Detecting a heartbeat from an infrared signal
  • Comparing live readings with a baseline
  • Driving a ten-segment LED bar from ten pins
  • Why a stress reading is not proof of lying
  • Difficulty: Intermediate
  • Time: About 40 minutes
  • Board: Arduino Uno R3
  • Parts: 16

How it works

This is a science demo of the body's stress response, not a real lie detector. A galvanic skin response sensor on A0 measures how well your skin conducts. Sweat raises it. A MAX30102 sensor on I2C detects each heartbeat with infrared light.

At startup, and whenever you press the button on A3, the sketch spends 10 seconds recording a calm baseline of skin conductance and heart rate. The bar graph fills as a progress meter during that time.

After that, the ten-segment bar on pins 2 to 11 shows the change from baseline. Each 5% more conductance adds a segment, and each 3 beats a minute above baseline adds another. It prints readings every half second.

Parts list

16 parts, plus the jumper wires. They retail for about $42; 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
MAX30102 pulse oximeter pin SCLArduino Uno R3 pin SCL
MAX30102 pulse oximeter pin SDAArduino Uno R3 pin SDA
MAX30102 pulse oximeter pin GNDArduino Uno R3 pin GND
Resistor, 220 Ω (10) pin 1Arduino Uno R3 pin 11
Resistor, 220 Ω (9) pin 1Arduino Uno R3 pin 10
Resistor, 220 Ω (8) pin 1Arduino Uno R3 pin 9
Resistor, 220 Ω (7) pin 1Arduino Uno R3 pin 8
Resistor, 220 Ω (6) pin 1Arduino Uno R3 pin 7
Resistor, 220 Ω (5) pin 1Arduino Uno R3 pin 6
Resistor, 220 Ω (4) pin 1Arduino Uno R3 pin 5
Resistor, 220 Ω (3) pin 1Arduino Uno R3 pin 4
Resistor, 220 Ω (2) pin 1Arduino Uno R3 pin 3
Resistor, 220 Ω (1) pin 1Arduino Uno R3 pin 2
Galvanic skin response sensor pin VCCArduino Uno R3 pin IOREF
MAX30102 pulse oximeter pin VINArduino Uno R3 pin 3V3
LED bar graph, 10 segments pin C10Arduino Uno R3 pin GND
LED bar graph, 10 segments pin C9Arduino Uno R3 pin GND
LED bar graph, 10 segments pin C8Arduino Uno R3 pin GND
LED bar graph, 10 segments pin C7Arduino Uno R3 pin GND
LED bar graph, 10 segments pin C6Arduino Uno R3 pin GND
LED bar graph, 10 segments pin C5Arduino Uno R3 pin GND
LED bar graph, 10 segments pin C4Arduino Uno R3 pin GND
LED bar graph, 10 segments pin C3Arduino Uno R3 pin GND
LED bar graph, 10 segments pin C2Arduino Uno R3 pin GND
LED bar graph, 10 segments pin C1Arduino Uno R3 pin GND
Pushbutton pin 2aArduino Uno R3 pin GND
Pushbutton pin 2bArduino Uno R3 pin GND
Galvanic skin response sensor pin GNDArduino Uno R3 pin GND
Galvanic skin response sensor pin SIGArduino Uno R3 pin A0
Pushbutton pin 1aArduino Uno R3 pin A3
Pushbutton pin 1bArduino Uno R3 pin A3
Resistor, 220 Ω (1) pin 2LED bar graph, 10 segments pin A1
Resistor, 220 Ω (2) pin 2LED bar graph, 10 segments pin A2
Resistor, 220 Ω (3) pin 2LED bar graph, 10 segments pin A3
Resistor, 220 Ω (4) pin 2LED bar graph, 10 segments pin A4
Resistor, 220 Ω (5) pin 2LED bar graph, 10 segments pin A5
Resistor, 220 Ω (6) pin 2LED bar graph, 10 segments pin A6
Resistor, 220 Ω (7) pin 2LED bar graph, 10 segments pin A7
Resistor, 220 Ω (8) pin 2LED bar graph, 10 segments pin A8
Resistor, 220 Ω (9) pin 2LED bar graph, 10 segments pin A9
Resistor, 220 Ω (10) pin 2LED bar graph, 10 segments pin A10

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
// Lie detector, science fair style: skin response and heart rate against a
// calm baseline, on a ten-segment stress bar.
//
// A science demo, not a lie detector. Nothing can tell a lie from the
// truth by measuring the body: sweat and heart rate rise with surprise,
// effort, laughter or a cold room just as readily as with a fib. What this
// shows, honestly, is the body's stress response, and how it changes from a
// calm baseline.
//
// The skin sensor is a Grove GSR module with two finger straps. Its reading
// x converts to resistance with the module's own formula,
//   R = (1024 + 2x) * 10 k / (512 - x)
// and conductance, in microsiemens, is one over that. Sweatier skin conducts
// more.
//
// Wiring (Arduino Uno):
//   GSR SIG -> A0 (the module runs from 5 V)
//   LED bar segments 1 to 10 -> D2 to D11, each through 220 ohms
//   calibrate button -> A3, to ground
//   MAX30102 on I2C: SDA A4, SCL A5, VIN 5 V

#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"

const uint8_t GSR_PIN = A0;
const uint8_t FIRST_BAR = 2;   // segments on D2 to D11
const uint8_t CALIBRATE = A3;  // the button, to ground
const unsigned long CALIBRATE_MS = 10000;

MAX30105 heart;
const byte RATE_SIZE = 4;      // beats averaged
byte rates[RATE_SIZE];
byte rateSpot = 0;
byte ratesSeen = 0;
unsigned long lastBeat = 0;
int bpm = 0;

float baseSkin = 0;    // microsiemens, the calm baseline
int baseBpm = 0;
bool calibrating = false;
bool calibrated = false;
unsigned long calibrateAt = 0;
float skinSum = 0;
long bpmSum = 0;
int samples = 0;
bool buttonDown = false;
unsigned long printAt = 0;

// Skin conductance in microsiemens, or 0 with no fingers on the straps.
float readSkin() {
  int x = analogRead(GSR_PIN);
  if (x >= 505) return 0;  // open straps read 512
  float ohms = (1024.0 + 2.0 * x) * 10000.0 / (512.0 - x);
  return 1000000.0 / ohms;
}

void showBar(int segments) {
  for (uint8_t i = 0; i < 10; i++) {
    digitalWrite(FIRST_BAR + i, i < segments ? HIGH : LOW);
  }
}

// The SparkFun example's way: a beat is a rise in the infrared reflection,
// and the gap between two beats is the heart rate, averaged over four.
void readHeart() {
  long ir = heart.getIR();
  if (ir < 50000) {  // no finger on the sensor
    bpm = 0;
    ratesSeen = 0;
    return;
  }
  if (checkForBeat(ir)) {
    unsigned long now = millis();
    unsigned long gap = now - lastBeat;
    lastBeat = now;
    float rate = 60000.0 / gap;
    if (rate > 30 && rate < 220) {
      rates[rateSpot++] = (byte)rate;
      rateSpot %= RATE_SIZE;
      if (ratesSeen < RATE_SIZE) ratesSeen++;
      int sum = 0;
      for (byte i = 0; i < ratesSeen; i++) sum += rates[i];
      bpm = sum / ratesSeen;
    }
  }
}

void startCalibration() {
  calibrating = true;
  calibrateAt = millis();
  skinSum = 0;
  bpmSum = 0;
  samples = 0;
  Serial.println("Calibrating: sit still and breathe slowly for 10 seconds.");
}

void readButton() {
  bool down = digitalRead(CALIBRATE) == LOW;
  if (down && !buttonDown) startCalibration();
  buttonDown = down;
}

void setup() {
  Serial.begin(115200);
  for (uint8_t i = 0; i < 10; i++) pinMode(FIRST_BAR + i, OUTPUT);
  pinMode(CALIBRATE, INPUT_PULLUP);
  if (!heart.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("MAX30102 not found: check its wiring.");
  }
  heart.setup();
  heart.setPulseAmplitudeRed(0x0A);  // a dim red lamp to say it is on
  heart.setPulseAmplitudeGreen(0);
  Serial.println("Science fair stress meter ready.");
  startCalibration();
}

void loop() {
  readHeart();
  readButton();
  float skin = readSkin();

  if (calibrating) {
    // Ten seconds of quiet readings make the baseline. The bar fills as a
    // progress meter while it does.
    unsigned long spent = millis() - calibrateAt;
    if (skin > 0 && millis() - printAt >= 200) {
      printAt = millis();
      skinSum += skin;
      bpmSum += bpm;
      samples++;
    }
    showBar(spent * 10 / CALIBRATE_MS);
    if (spent >= CALIBRATE_MS) {
      calibrating = false;
      if (samples == 0) {
        Serial.println("No fingers on the straps. Press the button to try again.");
        return;
      }
      calibrated = true;
      baseSkin = skinSum / samples;
      baseBpm = bpmSum / samples;
      Serial.print("Baseline: skin ");
      Serial.print(baseSkin, 2);
      Serial.print(" uS");
      Serial.print(", heart ");
      Serial.print(baseBpm);
      Serial.print(" bpm");
      Serial.println(". Now ask a question.");
    }
    return;
  }

  if (millis() - printAt < 500) return;
  printAt = millis();

  if (!calibrated) return;
  if (skin == 0) {
    showBar(0);
    Serial.println("skin no fingers on the straps");
    return;
  }
  // Stress is a change from the baseline, not a level: one person's calm is
  // another's excited. The scale is ours, not a medical one: every 5% more
  // conductance is one segment
  float skinChange = (skin - baseSkin) * 100.0 / baseSkin;
  float score = skinChange / 5.0;
  // and every 3 beats a minute above the baseline is another.
  int heartChange = bpm > 0 && baseBpm > 0 ? bpm - baseBpm : 0;
  score += heartChange / 3.0;
  int segments = constrain((int)(score + 0.5), 0, 10);
  showBar(segments);
  Serial.print("skin ");
  Serial.print(skin, 2);
  Serial.print(" uS (");
  if (skinChange >= 0) Serial.print('+');
  Serial.print(skinChange, 0);
  Serial.print("%)");
  Serial.print("  heart ");
  if (bpm > 0) {
    Serial.print(bpm);
    Serial.print(" bpm (");
    if (heartChange >= 0) Serial.print('+');
    Serial.print(heartChange);
    Serial.print(")");
  } else {
    Serial.print("--");
  }
  Serial.print("  stress ");
  Serial.print(segments);
  Serial.println("/10");
}

Code walkthrough

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

readSkin sketch.ino

Reads A0 and turns it into conductance in microsiemens. A reading of 505 or more means the straps are open, so it returns 0. Otherwise it computes the skin resistance from the module's formula and returns one million divided by that. Sweatier skin gives a bigger number.

// Skin conductance in microsiemens, or 0 with no fingers on the straps.
float readSkin() {
  int x = analogRead(GSR_PIN);
  if (x >= 505) return 0;  // open straps read 512
  float ohms = (1024.0 + 2.0 * x) * 10000.0 / (512.0 - x);
  return 1000000.0 / ohms;
}

readHeart sketch.ino

Reads the infrared level. Below 50000 there is no finger, so the heart rate is cleared. When checkForBeat sees a beat, the time since the last beat gives a rate. Rates between 30 and 220 are kept, and the last four are averaged into bpm.

// The SparkFun example's way: a beat is a rise in the infrared reflection,
// and the gap between two beats is the heart rate, averaged over four.
void readHeart() {
  long ir = heart.getIR();
  if (ir < 50000) {  // no finger on the sensor
    bpm = 0;
    ratesSeen = 0;
    return;
  }
  if (checkForBeat(ir)) {
    unsigned long now = millis();
    unsigned long gap = now - lastBeat;
    lastBeat = now;
    float rate = 60000.0 / gap;
    if (rate > 30 && rate < 220) {
      rates[rateSpot++] = (byte)rate;
      rateSpot %= RATE_SIZE;
      if (ratesSeen < RATE_SIZE) ratesSeen++;
      int sum = 0;
      for (byte i = 0; i < ratesSeen; i++) sum += rates[i];
      bpm = sum / ratesSeen;
    }
  }
}

loop sketch.ino

First it reads the heart sensor, the button and the skin. During calibration it sums samples for 10 seconds and then stores baseSkin and baseBpm. After that it scores the change from baseline, sets the bar with showBar, and prints skin, heart and stress out of 10.

void loop() {
  readHeart();
  readButton();
  float skin = readSkin();

  if (calibrating) {
    // Ten seconds of quiet readings make the baseline. The bar fills as a
    // progress meter while it does.
    unsigned long spent = millis() - calibrateAt;
    if (skin > 0 && millis() - printAt >= 200) {
      printAt = millis();
      skinSum += skin;
      bpmSum += bpm;
      samples++;
    }
    showBar(spent * 10 / CALIBRATE_MS);
    if (spent >= CALIBRATE_MS) {
      calibrating = false;
      if (samples == 0) {
        Serial.println("No fingers on the straps. Press the button to try again.");
        return;
      }
      calibrated = true;
      baseSkin = skinSum / samples;
      baseBpm = bpmSum / samples;
      Serial.print("Baseline: skin ");
      Serial.print(baseSkin, 2);
      Serial.print(" uS");
  // ... 47 more lines in the editor

Troubleshooting

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

  • The monitor says No fingers on the straps. This project
    The skin sensor must read a closed circuit during calibration. Check the sensor signal on A0, then press the button to try again.
  • The bar sits at 0 or 10 all the time. This project
    The baseline was taken while you were not calm. Press the button and sit still for the 10 seconds.
  • The monitor says MAX30102 not found. This project
    Check its SDA and SCL wires and its power and ground.
  • Segments are dim, uneven, or the chip is damaged. LED bar graph
    Every segment is its own LED and needs its own current-limiting resistor, about 220 to 330 ohms at 5 V. One shared resistor makes brightness change with how many are lit.
  • The bar lights in reverse order or a segment never lights. LED bar graph
    Check pin 1 and the orientation. Anode and cathode sides are on opposite rows. Some bars are numbered the other way around, so test with one segment at a time.
  • 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 reading is noisy or unrelated to skin. Galvanic skin response sensor
    Strap the electrodes on two fingers snugly and keep still. Poor contact changes the signal. Let it settle for 30 seconds or more after putting it on.

Change it and keep it

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