Press any gadget's button from your phone with an optocoupler

Give an old battery gadget a Wi-Fi button without touching its electronics. An optocoupler presses the button with light, so the gadget and the ESP32 never share a wire. Big buttons on a phone page make it easy for someone...
  • Level:Intermediate
  • Duration: 1 h
  • Ages 12+
  • Platform: ESP32

Tools for this lesson

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

Press any gadget's button from your phone with an optocoupler – EEMENTOR STEM™ lesson

Lots of gadgets have small, stiff buttons: a remote-controlled toy, a battery fan, a fairy-light switch, the button on a talking toy. For someone with weak fingers or limited movement, pressing them can be hard or impossible. In this lesson the ESP32 presses the button for you, from a page on any phone or tablet with big, easy buttons. The clever part is an optocoupler: a tiny chip with an LED and a light sensor inside. The ESP32 switches the LED on, the light sensor sees it and "presses" the gadget's button. The two sides are connected only by light, so the gadget's circuit and the ESP32 never share a single wire.

What you'll learn

  • What an optocoupler is and why it keeps two circuits apart (galvanic isolation)
  • How to wire the PC817 optocoupler
  • How to "press" a button in another circuit without touching its electronics
  • How to make a phone page with big buttons that also works for press-and-hold
  • How to add a safety time limit in code

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • PC817 optocoupler
  • 1 resistor, 220 Ω
  • Practice gadget: the 9 V battery (ESP32 IoT Starter Kit) with a 9 V battery snap (in the UNO R3 Starter Kit, or from any electronics shop), 1 LED and 1 resistor, 560 Ω
  • Later: a battery gadget with a push button, and 2 wires to connect to it (ask an adult to help)
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Meet the optocoupler

The PC817 is a small chip with four legs. Look for the dot on top: that marks pin 1. With the dot at the bottom left, the legs are:

  • Pin 1 (bottom left, at the dot): A, the anode (+) of the LED inside
  • Pin 2 (bottom right): K, the cathode (−) of the LED
  • Pin 3 (top right): E, the emitter of the light sensor
  • Pin 4 (top left): C, the collector of the light sensor

When current flows through the LED (pins 1 and 2), the light sensor (a phototransistor) lets current flow from C to E. To the gadget, that looks like someone pressing its button.

Wiring

Plug the PC817 across the middle gap of the breadboard, with the dot at the bottom left. The ESP32 side is below the gap, and the practice gadget is above it, with its own battery on the top rails.

Breadboard wiring diagram: Press any gadget's button from your phone with an optocoupler
Breadboard view. Check each connection against the wiring table below.

Build it step by step

Plug in all the parts first, then add the wires in this order. In each picture the new wires are bright and the ones from earlier steps are faded. Click a picture to see it larger.

Step 1 of 4: GND from the ESP32 to the blue rail
Step 1. GND from the ESP32 to the blue rail
Step 2 of 4: Optocoupler: GPIO 26 through 220 Ω, K to the bottom − rail, E to the top − rail
Step 2. Optocoupler: GPIO 26 through 220 Ω, K to the bottom − rail, E to the top − rail
Step 3 of 4: Practice gadget battery: + and − to the top rails
Step 3. Practice gadget battery: + and − to the top rails
Step 4 of 4: Practice gadget: top + rail to the 560 Ω resistor, LED to C
Step 4. Practice gadget: top + rail to the 560 Ω resistor, LED to C
From To
PC817 Dot at bottom left: pins 1 and 2 in columns 30 and 31 (row f), pins 4 and 3 in columns 30 and 31 (row e)
ESP32 GPIO 26 Column 25, row j
Resistor 220 Ω Column 25, row h to column 30, row h (pin 1, A)
Column 31, row j (pin 2, K) Bottom − rail
ESP32 GND Bottom − rail
9 V battery + Top + rail (only the practice gadget uses the top rails)
9 V battery − Top − rail
Top + rail Column 42, row a
Resistor 560 Ω Column 42, row c to column 46, row c
Practice LED long leg (+) Column 46, row e
Practice LED short leg (−) Column 47, row e
Column 47, row c Column 30, row c (pin 4, C)
Column 31, row a (pin 3, E) Top − rail

No battery snap? For a first test, connect the top + rail to the ESP32's 3V3 pin and the top − rail to GND instead of the battery. It works the same, but the practice LED is dimmer, and the two sides then share the ESP32's power, so they are no longer isolated. Use a battery as soon as you can.

Never let a 9 V wire touch the ESP32 or the bottom rails: 9 V on any ESP32 pin destroys it. Don't connect the top rails to the bottom rails. The whole point is that the two circuits stay separate. The 9 V battery and its LED stand in for a real gadget while you test.

The sketch

#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>

const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const int OPTO_PIN = 26;                     // through 220 ohm to the optocoupler's pin 1
const unsigned long MAX_HOLD_MS = 10000;     // never hold the button longer than this

WebServer server(80);
bool pressed = false;
unsigned long pressedAt = 0, releaseAt = 0;  // releaseAt = 0: hold until told to let go
unsigned long presses = 0;

const char PAGE[] PROGMEM = R"rawliteral(
<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Gadget button</title>
<style>
 body{font-family:sans-serif;margin:0;padding:16px;background:#101828;color:#fff;text-align:center}
 button{display:block;width:100%;margin:14px 0;padding:34px 10px;font-size:30px;font-weight:bold;
   border:0;border-radius:24px;color:#101828;background:#FFD43B;touch-action:none;
   user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
 #hold{background:#7CE38B} #long{background:#9EC5FE}
 #st{font-size:22px;min-height:30px}
</style></head><body>
<h1>Gadget button</h1>
<button id="press">Press</button>
<button id="long">Long press (2 s)</button>
<button id="hold">Hold while I touch</button>
<p id="st">Ready</p>
<script>
var queue=Promise.resolve();          // one request at a time, in order
function send(path){
  queue=queue.then(function(){
    return fetch(path).then(function(r){return r.text();})
      .then(function(t){document.getElementById('st').textContent=t;})
      .catch(function(){document.getElementById('st').textContent='No connection';});
  });
}
document.getElementById('press').onclick=function(){send('/press?ms=250');};
document.getElementById('long').onclick=function(){send('/press?ms=2000');};
var h=document.getElementById('hold'), down=false;
h.oncontextmenu=function(e){e.preventDefault();};
h.addEventListener('pointerdown',function(e){e.preventDefault();down=true;send('/down');});
['pointerup','pointerleave','pointercancel'].forEach(function(n){
  h.addEventListener(n,function(){if(down){down=false;send('/up');}});
});
setInterval(function(){if(!down)send('/status');},2000);
</script></body></html>
)rawliteral";

void press(bool on) {
  if (on && !pressed) { presses++; pressedAt = millis(); }
  pressed = on;
  digitalWrite(OPTO_PIN, on ? HIGH : LOW);
}

String status() {
  return String(pressed ? "Pressing" : "Released") + " (" + presses + " presses)";
}

void setup() {
  Serial.begin(115200);
  pinMode(OPTO_PIN, OUTPUT);
  press(false);
  presses = 0;
  WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print('.'); }
  Serial.print(" open http://");
  Serial.println(WiFi.localIP());
  if (MDNS.begin("gadget")) Serial.println("or http://gadget.local");

  server.on("/", []() { server.send_P(200, "text/html", PAGE); });
  server.on("/press", []() {
    unsigned long ms = constrain(server.arg("ms").toInt(), 50, (long)MAX_HOLD_MS);
    press(true);
    releaseAt = millis() + ms;
    server.send(200, "text/plain", "Pressing for " + String(ms) + " ms");
  });
  server.on("/down", []() { press(true); releaseAt = 0; server.send(200, "text/plain", status()); });
  server.on("/up", []() { press(false); server.send(200, "text/plain", status()); });
  server.on("/status", []() { server.send(200, "text/plain", status()); });
  server.begin();
}

void loop() {
  server.handleClient();
  unsigned long now = millis();
  if (pressed && releaseAt != 0 && (long)(now - releaseAt) >= 0) {
    press(false);
    Serial.println(status());
  }
  if (pressed && now - pressedAt > MAX_HOLD_MS) {   // safety: the phone may have lost Wi-Fi
    press(false);
    Serial.println("Let go after the time limit");
  }
}

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

Try it

  1. Clip the 9 V battery on. The practice LED stays dark: the optocoupler is "not pressed".
  2. Upload the sketch and open the Serial Monitor at 115200 baud. Open the address it shows on a phone on the same Wi-Fi, or try http://gadget.local.
  3. Tap Press: the practice LED flashes for a quarter of a second. Tap Long press: it stays on for 2 seconds.
  4. Hold your finger on Hold while I touch: the LED stays on until you lift your finger, or for 10 seconds at most.
  5. Now the test that shows the isolation: unclip the 9 V battery and press again. Nothing lights, even though the ESP32 is working. The gadget side only gets its power from its own battery.

Connect a real gadget

Only use a gadget that runs on batteries or a USB power bank, never anything that plugs into a mains socket. The PC817 can switch up to 35 V and 50 mA, which is plenty for the button of a battery gadget. Ask an adult to help, because you need to open the gadget and solder or clip two wires to its button.

  1. Take out the gadget's batteries and open it. Find the push button you want to "press" and its two contacts on the circuit board.
  2. Solder or clip a thin wire to each contact, and lead the wires out of the case.
  3. Put the batteries back. With a multimeter, measure the voltage between the two wires: the one that is more positive goes to C (pin 4) and the other to E (pin 3). No multimeter? Try one way; if pressing does nothing, swap the wires. On gadgets up to 6 V, the wrong way round does no harm, it just doesn't work. For higher voltages, measure first.
  4. Unclip the 9 V battery, remove the practice gadget's wires from the optocoupler, and connect the gadget's two wires to columns 30 and 31 in the top half instead.

Some gadgets don't respond: for example when their "button" is part of a grid of keys, like a keyboard or a TV remote, or when it switches a large current directly. Then try a different gadget, or ask an adult to check the circuit.

What you should see

Phone page with big Press, Long press (2 s) and Hold while I touch buttons, and how long the practice LED stays on for each, plus the Serial Monitor at 115200 baud.

How it works

Inside the PC817 optocoupler: an LED shines across an isolation gap onto a light sensor, next to the /press?ms=, /down and /up requests and the 10 s safety limit.

Light instead of a wire. GPIO 26 drives the optocoupler's LED through the 220 Ω resistor: (3.3 V − 1.2 V) ÷ 220 Ω = about 10 mA. Inside the chip, the light falls on a phototransistor, which then lets current flow between C and E, just like the two contacts of a pressed button. Engineers call this galvanic isolation: the two sides share no electrical connection, so the gadget's voltages can never reach the ESP32, and the ESP32 doesn't care what the gadget's circuit looks like. Optocouplers are used in phone chargers, in the controllers of washing machines and in medical equipment, wherever two circuits have to talk but must stay apart.

Three ways to press. The page sends small web requests, one after another, so they always arrive in the right order. Every 2 seconds it also asks for the status, so it shows "Released" after the safety limit too. /press?ms=250 presses for a fixed time, and the ESP32 lets go by itself when releaseAt is reached. /down and /up follow your finger: the page listens for pointerdown when you touch, and pointerup or pointerleave when you lift your finger or slide off the button. That's how the button can be held for as long as your finger stays on it, up to the 10-second safety limit.

A safety limit. What if the phone loses Wi-Fi while you are holding the button? The "up" message would never arrive, and the gadget would be pressed forever. So the ESP32 lets go by itself after MAX_HOLD_MS, 10 seconds. Good devices never trust the network completely.

Big buttons help everyone. The page uses huge buttons with strong colours and a large text that says what happened. People with shaky hands, poor eyesight or limited movement can use it, and so can everyone else. Designers call this accessible design.

Try this next

  • Add a second optocoupler on another GPIO pin and press two different buttons, for example "on" and "speed" on a fan.
  • Add a timer to the page: "press in 10 minutes", so a fan or a toy switches itself off later.
  • Connect a large, soft accessibility switch or a big arcade button to the ESP32, so it can be pressed with a hand, an elbow or a foot as well as from the phone.
  • Combine it with Blynk or voice control so the gadget can be pressed from anywhere.

Troubleshooting

  • The practice LED never lights: check the 9 V battery and that the optocoupler's dot is at the bottom left. C (pin 4) must go to the LED's short leg and E (pin 3) to the top − rail.
  • The practice LED is always on: a wire connects column 47 straight to the − rail, or the optocoupler is turned the wrong way.
  • The page doesn't open: the phone must be on the same Wi-Fi as the ESP32. Type the number from the Serial Monitor with http:// in front if gadget.local doesn't work.
  • 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.
  • The real gadget doesn't react: swap the two wires on C and E. If it still doesn't react, its button may work in a way an optocoupler can't copy; try another gadget.

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