Build a friendship lamp that glows when a friend taps theirs

Touch the metal plate on your printed lamp and your friend's lamp lights up in your colour, wherever they live. The lamps talk through ntfy, and a phone can send a glow too.
  • Level:Advanced
  • Duration: 1 h 10 min
  • Ages 12+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a friendship lamp that glows when a friend taps theirs – EEMENTOR STEM™ lesson

A friendship lamp is a lamp you give to someone far away. When you touch yours, theirs lights up in your colour, and when they touch theirs, yours lights up in their colour. It says "I'm thinking of you" without any words. The two lamps send their taps through ntfy, a free message service, so they work across town or across the world, as long as both have Wi-Fi. If you only have one kit, a friend can make your lamp glow from the ntfy app on their phone.

What you'll learn

  • How to send a message to an internet service with an HTTPS POST
  • How to ask a service for new messages without missing any or seeing one twice
  • How to read JSON with the ArduinoJson library
  • How to fade an LED out slowly with millis()

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • Touch sensor module (45-in-1 Sensor Kit)
  • RGB LED module (45-in-1 Sensor Kit)
  • 3 resistors, 220 Ω (ESP32 IoT Starter Kit)
  • A 2.4 GHz Wi-Fi network
  • The ntfy app on a phone (optional)
  • The printed parts from the 3D printing box below (lamp base and shade), or the cardboard version described there
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Wiring

Breadboard wiring diagram: Build a friendship lamp that glows when a friend taps theirs
Breadboard view. Check each connection against the wiring table below.
From To
Touch module DO ESP32 GPIO 33
Touch module + and G 3V3 and GND
Touch module AO Not used
ESP32 GPIO 25 220 Ω resistor, then RGB module R
ESP32 GPIO 26 220 Ω resistor, then RGB module G
ESP32 GPIO 27 220 Ω resistor, then RGB module B
RGB module − GND

Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.

Touch the metal tip: the small LED on the touch module should light. If it stays on all the time, turn its screw a little until it only lights while you touch.

Choose a topic

Install the ntfy app and subscribe to a long, random topic name such as eementor-lamp-k4w9p2, as in the motion alert lesson. Topics on the free server are public, so never use a name that is easy to guess, and never send personal details through it.

Both lamps must use exactly the same topic, but each one gets its own MY_NAME and MY_COLOUR.

The sketch

In the Arduino IDE, open Library Manager and install the ArduinoJson library by Benoit Blanchon.

Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TOPIC = "eementor-lamp-k4w9p2";   // the same on both lamps
const char* MY_NAME = "Mia";                  // different on each lamp
const char* MY_COLOUR = "#FF2E88";            // the colour your friend's lamp shows
const unsigned long CHECK_EVERY = 15000;      // ask ntfy every 15 s
const unsigned long GLOW_MINUTES = 10;

const int TOUCH_PIN = 33;
const int RED_PIN = 25, GREEN_PIN = 26, BLUE_PIN = 27;

String lastId = "";                 // the newest message we have seen
long glowR = 0, glowG = 0, glowB = 0;
unsigned long glowStart = 0;
bool glowing = false;
unsigned long lastCheck = 0, lastTouch = 0;

void setColour(long r, long g, long b) {
  analogWrite(RED_PIN, r);
  analogWrite(GREEN_PIN, g);
  analogWrite(BLUE_PIN, b);
}

// "#FF2E88" -> red 255, green 46, blue 136
void glow(String hex) {
  if (hex.length() != 7 || hex[0] != '#') hex = "#FFFFFF";
  long v = strtol(hex.substring(1).c_str(), nullptr, 16);
  glowR = (v >> 16) & 255;
  glowG = (v >> 8) & 255;
  glowB = v & 255;
  glowStart = millis();
  glowing = true;
}

void fade() {
  if (!glowing) return;
  unsigned long total = GLOW_MINUTES * 60000;
  unsigned long gone = millis() - glowStart;
  if (gone >= total) {
    glowing = false;
    setColour(0, 0, 0);
    return;
  }
  long left = (total - gone) / 1000;          // seconds of glow left
  long all = total / 1000;
  setColour(glowR * left / all, glowG * left / all, glowB * left / all);
}

void sendTap() {
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;
  http.begin(client, String("https://ntfy.sh/") + TOPIC);
  http.addHeader("Title", MY_NAME);
  http.addHeader("Tags", "sparkling_heart");
  int status = http.POST(MY_COLOUR);
  http.end();
  Serial.printf("Tap sent (%d)\n", status);
}

void checkMessages() {
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;
  // only messages after the last one we saw (or from the last minute, at start)
  String since = lastId.length() ? lastId : "1m";
  http.begin(client, String("https://ntfy.sh/") + TOPIC + "/json?poll=1&since=" + since);
  int status = http.GET();
  if (status == 200) {
    String body = http.getString();
    int start = 0;
    while (start < (int)body.length()) {      // one JSON message per line
      int end = body.indexOf('\n', start);
      if (end < 0) end = body.length();
      JsonDocument msg;
      if (!deserializeJson(msg, body.substring(start, end))) {
        lastId = msg["id"].as<String>();
        String from = msg["title"] | "";
        String text = msg["message"] | "";
        if (from != MY_NAME) {                // not our own tap
          Serial.println("Glow from " + (from.length() ? from : String("the app")) + ": " + text);
          glow(text);
        }
      }
      start = end + 1;
    }
  }
  http.end();
}

void setup() {
  Serial.begin(115200);
  pinMode(TOUCH_PIN, INPUT);
  pinMode(RED_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(BLUE_PIN, OUTPUT);
  WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" connected.");
  glow(MY_COLOUR);                            // hello: 3 seconds in your own colour
  setColour(glowR, glowG, glowB);
  delay(3000);
  glowing = false;
  setColour(0, 0, 0);
}

void loop() {
  if (digitalRead(TOUCH_PIN) == HIGH && millis() - lastTouch > 5000) {   // at most one tap per 5 s
    lastTouch = millis();
    setColour(255, 255, 255);                 // a white flash says "sent"
    sendTap();
    if (!glowing) setColour(0, 0, 0);         // flash over
  }
  if (millis() - lastCheck >= CHECK_EVERY) {
    lastCheck = millis();
    checkMessages();
  }
  fade();
}

client.setInsecure() keeps the connection encrypted but skips checking the server's certificate. That keeps the sketch short and is fine for a learning project. For a device you leave running for years, load the server's root certificate with client.setCACert() instead.

Try it

  1. Open the Serial Monitor at 115200 baud. After connecting, the lamp glows in its own colour for three seconds.
  2. Touch the plate: the lamp flashes white and the Serial Monitor says "Tap sent". The ntfy app shows the tap with a heart.
  3. In the ntfy app, open the topic and send the message #00C2FF. Within 15 seconds the lamp glows light blue and slowly fades out over 10 minutes.
  4. Build the second lamp with the same topic but another name and colour, and give it to your friend.

How it works

A tap is an HTTPS POST to the topic's address. The message is your colour, and the Title header carries your name. ntfy keeps the message for a while and passes it on to everyone who asks.

Each lamp asks ntfy every 15 seconds: "anything new?". The since part of the address makes that question precise. ntfy gives every message an id, and the lamp remembers the newest one it has seen, so the next answer contains only messages that came after it. The answer has one JSON object per line, and ArduinoJson turns each line into fields you can read by name. Messages with your own name are skipped, otherwise your lamp would light up for its own taps.

The fade uses millis() instead of delay(): every pass through loop() works out how much of the glow time is left and sets the brightness to match, so the lamp can still notice a touch while it fades.

Try this next

  • Make a double tap send a different message, like "good night", that shows as a slow pulse.
  • Use the touch module's AO pin to make a long touch brighter than a short one.
  • Add a third lamp for another friend, each with its own colour.

Troubleshooting

  • The lamp never reacts to the app: check that the topic in the sketch and in the app are exactly the same, and that the message starts with # and has six hex digits.
  • It sends taps by itself: the touch module is too sensitive. Turn its screw until its LED is off when nobody touches it.
  • A colour looks wrong: check the three RGB wires; the module's pin order differs between makers.
  • It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.
  • A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit