Community project

Make Me Flliper Zero

ESP32
Photo of Make Me Flliper Zero
Generated with AI

Sven Celikovic

Published September 4, 2026

This project builds a handheld multi-protocol scanner and emulator powered by an ESP32, capable of capturing and replaying infrared signals, reading NFC tags, RFID cards, iButton keys, and monitoring sub-GHz radio traffic. The device combines a 128x64 OLED display, dual-axis joystick control, and storage on microSD card to create a portable tool for exploring wireless protocols and RF devices.

The guide provides a complete wiring diagram connecting all sensors and modules to the ESP32, a full parts list with recommended suppliers, Arduino firmware that handles IR capture/playback, NFC polling, RFID reading, and radio monitoring through an intuitive menu system, and step-by-step assembly instructions from component layout through case closure and first-time operation.

Wiring diagram

Wiring diagram for Make Me Flliper Zero

Gather all the parts

QtyComponent
1

SSD1306 OLED

0.96 inch, 128x64

0.96 inch 128x64 OLED display with I2C interface

1

VS1838B IR Receiver

38 kHz

Three-pin 38 kHz infrared receiver module that demodulates remote-control IR bursts into an active-low digital output. Use a 3.3 V supply for direct MCU-safe wiring when the module variant supports it; otherwise level-shift the output.

1

KY-023 Dual Axis Joystick Module

KY-023 thumb joystick

Dual-axis analog joystick breakout (PSP/PS2-style thumbstick) with two perpendicular 10 kOhm potentiometers and an integrated push-button. Outputs analog voltages on VRx and VRy proportional to stick position, plus an active-low digital switch (SW) that is pulled LOW when the stick is pressed. Operates 3.3 V to 5 V; on 5 V boards the VRx/VRy swing matches the wider ADC range. SW should be read with INPUT_PULLUP.

1

Push Button

momentary

Momentary push button switch

1

PN532 NFC Module (I2C Mode)

PN532 I2C NFC reader

NXP PN532-based NFC/RFID reader-writer module supporting ISO 14443A/B, MIFARE, NTAG213, NTAG215, and NTAG216 tags. Supports three interface modes (I2C, SPI, HSU/UART) selected by on-board DIP switches. I2C mode: both DIP switches set to ON (SW1=ON, SW2=ON). I2C address 0x24. Operates at 3.3 V, compatible directly with ESP32 without level shifting. On-board pull-up resistors provided for I2C lines.

1

CC1101 Sub-GHz RF Module

433 MHz CC1101 (match your local legal band)

Texas Instruments CC1101-based Sub-1 GHz RF transceiver module operating across 300–928 MHz (315/433/868/915 MHz ISM bands). SPI interface (4-wire + 2 GDO pins). 3.3V supply and logic. Supports OOK, ASK, FSK, GFSK, MSK modulations. Used for signal sniffing, replay attacks, and Flipper Zero-equivalent Sub-GHz functionality on ESP32. All SPI devices on the VSPI bus (MOSI=GPIO23, MISO=GPIO19, SCK=GPIO18) with a dedicated CS pin.

1

MicroSD Card Module

3.3V SPI MicroSD module

SPI-based microSD card adapter module for SPI-capable microcontrollers. Uses MOSI, MISO, SCK, and CS plus power and ground. Many low-cost modules include a 3.3 V regulator and level shifting for 5 V MCU boards, while bare breakouts should be powered and signalled at 3.3 V.

1

RDM6300 125 kHz RFID Reader

125 kHz reader with antenna

A read-only module that displays the identifier of compatible low-frequency tags you own.

1

DS1990A iButton Probe

1-Wire iButton contact probe

A metal contact probe that reads the serial number of iButton keys you own.

1

940 nm Infrared LED

940 nm

An invisible-light LED that sends a remote command selected by the owner.

1

NPN Transistor (2N2222 / BC547)

2N2222

General-purpose NPN bipolar junction transistor (BJT) in TO-92 package. Used as a low-side switch to drive loads (e.g. buzzers) whose current exceeds ESP32 GPIO source capability (~12 mA). A ~1 kΩ base resistor is placed between the ESP32 GPIO and the transistor base. The collector connects to the load negative terminal (buzzer –), and the emitter connects to GND. When the GPIO goes HIGH (3.3 V), the transistor saturates and switches the load on. Compatible with both 2N2222/2N2222A and BC547/BC547B variants. Add a flyback diode (e.g. 1N4148) across inductive load terminals.

1

Resistor

1 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

100 Ω

Through-hole resistor (current-limiting in series with an LED)

1

LiPo 3.7V 1000mAh Battery

3.7 V, 1000 mAh

Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging.

1

TP4056 Li-Ion/LiPo charger module with protection

Protected USB-C TP4056 module

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

Boost Converter

Adjustable boost set to 5.0 V

Small adjustable MT3608-style DC-DC boost converter module for stepping a lower DC input up to a higher rail such as 5V, 9V, or 12V. It is a power-path module with VIN/VOUT terminals, not a GPIO peripheral.

1

Resistor

10 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

20 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

4.7 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Handheld ABS Project Enclosure

Approx. 160 × 95 × 35 mm, non-metallic

A small plastic case that holds the board, battery, screen, joystick, readers, and openings for the sensors.

Assemble it in 8 steps

1. Lay out the handheld parts

Place the ESP32, screen, joystick, clear button, infrared receiver, NFC board, RFID reader, iButton probe, radio board, microSD board, and battery parts where they will fit in the plastic case. Keep the NFC board, RFID antenna, infrared receiver, infrared LED, iButton probe, microSD slot, and joystick facing their planned case openings.

  • Use a non-metallic plastic case; metal can greatly reduce NFC, RFID, and radio range.
  • Do a dry fit before drilling or gluing anything.
  • Do not connect the battery while arranging parts; loose wires can short and heat up.

2. Wire the 3.3 volt parts

Connect OLED VCC, infrared receiver VCC, joystick VCC, PN532 VCC, CC1101 VCC, and microSD VCC to ESP32 3V3 (power). Connect each of their GND pins to ESP32 GND (ground). Connect OLED SDA and PN532 SDA together to GPIO21 (data), and OLED SCL and PN532 SCL together to GPIO22 (clock). Set both PN532 I2C switches to ON before powering it.

  • Keep the shared screen and NFC wires short.
  • The printed VCC, GND, SDA, and SCL labels—not the physical left-to-right order—tell you where each wire goes.
  • Use 3V3, never 5V, for the PN532, CC1101, joystick, OLED, and microSD module. A 5V signal can damage ESP32 inputs.

3. Wire the controls and infrared receiver

Connect infrared receiver OUT to GPIO27 (signal). Connect joystick VRx to GPIO32 (left-right signal), VRy to GPIO33 (up-down signal), and SW to GPIO14 (press signal). Place the clear button across the breadboard center gap; connect one side to GPIO26 (signal) and the opposite side to GND (ground).

  • The joystick press and clear button use the board's built-in pull-up, so they need no extra resistor.
  • If a button always reads as pressed, use a leg on the opposite side of the button.
  • Check the infrared receiver's printed VCC, GND, and OUT labels carefully; different receiver boards put those pins in different physical orders.

4. Wire the microSD card and receive-only radio

Connect microSD MOSI and CC1101 MOSI together to GPIO23 (data), both MISO pins to GPIO19 (data), and both SCK pins to GPIO18 (clock). Connect CC1101 CSN to GPIO4 (its private select signal) and microSD CS to GPIO13 (its private select signal). Fit the antenna supplied for your locally legal receive frequency to the CC1101 board.

  • The three shared wires are normal; the two separate CS wires let the ESP32 choose one board at a time.
  • Use the project only to receive signals you are entitled to examine.
  • Do not transmit with the CC1101 in this build. Radio rules and permitted frequencies vary by location.

5. Wire the low-frequency tag readers

Connect the RDM6300 VCC to the regulated 5V supply (power) and its GND to shared GND (ground). Build its safety divider: RDM6300 TX to the 10 kΩ resistor, join that resistor's far end to one end of the 20 kΩ resistor, connect that joined point to GPIO16 (signal), and connect the other 20 kΩ resistor end to GND. Connect iButton probe DATA to GPIO17 (signal) and GND to shared GND; connect the 4.7 kΩ resistor between GPIO17 and 3V3 (pull-up).

  • The two RFID resistors reduce the reader's 5V data output to a safe 3.3V-level signal for the ESP32.
  • This reads tag identifiers only; do not use it to copy or impersonate credentials.
  • Never connect the RDM6300 TX wire directly to GPIO16; its 5V output can damage the ESP32.

6. Build the infrared sending stage

Connect GPIO25 to one end of the 1 kΩ resistor, then connect its other end to the 2N2222 BASE (control). Connect the transistor EMITTER to GND (ground). Connect the infrared LED cathode, the short leg, to the transistor COLLECTOR. Connect 3V3 through the 100 Ω resistor to the infrared LED anode, the long leg (power).

  • Aim the LED through a small hole in the case.
  • The infrared light is invisible; test only with an appliance you own.
  • Do not reverse the LED or transistor connections. Reversed parts may not work and can become damaged.

7. Build the rechargeable power chain

Connect battery positive to TP4056 B+ (power) and battery negative to TP4056 B- (ground). Connect TP4056 OUT+ to boost converter VIN+ (power) and OUT- to VIN- (ground). Adjust the boost converter to exactly 5.0 V with a meter before connecting it. Connect boost VOUT+ to the ESP32 VIN/5V pin (power) and boost VOUT- to ESP32 GND (ground). The TP4056 IN+ and IN- go only to its own USB charging connector.

  • Charge through the TP4056 USB-C connector, not through the ESP32 USB connector.
  • Tie and insulate all battery wires so they cannot pull loose in the case.
  • A lithium battery can overheat or catch fire if shorted, punctured, or charged incorrectly. Stop immediately if it swells, gets hot, or smells unusual.
  • Do not connect the ESP32 USB cable and battery boost output at the same time unless you add proper power isolation.

8. Close the case and use the menus

Secure boards with standoffs or non-conductive foam, keep antennas away from battery wires, and close the case without pinching leads. The left-right joystick movement selects IR, NFC, 125 kHz RFID, iButton, or Sub-GHz receive mode. In IR mode, point a remote you own at the receiver to save its last command; press the joystick to send that saved command to your own appliance. Press the clear button to clear the screen.

  • Use only tags, remotes, equipment, and signals you own or are authorized to test.
  • The ESP32 DevKit USB connection is for charging/programming and serial communication; this board cannot safely provide native USB keyboard or device impersonation.
  • Keep NFC/RFID and radio features read-only. Do not use this project to clone credentials, bypass access controls, or interfere with radio systems.

Review all connections

1. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

2. Connections between "ir_receiver_1" and "ESP32"

Functionir_receiver_1ESP32
powerVCC3V3
groundGNDGND
digitalOUTGPIO 27

3. Connections between "joystick_1" and "ESP32"

Functionjoystick_1ESP32
powerVCC3V3
groundGNDGND
adcVRxGPIO 32
adcVRyGPIO 33
digitalSWGPIO 14

4. Connections between "button_clear" and "ESP32"

Functionbutton_clearESP32
groundGNDGND
digitalSIGNALGPIO 26

5. Connections between "pn532_1" and "ESP32"

Functionpn532_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

6. Connections between "cc1101_1" and "ESP32"

Functioncc1101_1ESP32
powerVCC3V3
groundGNDGND
spiMOSIGPIO 23
spiMISOGPIO 19
spiSCKGPIO 18
spiCSNGPIO 4

7. Connections between "microsd_1" and "ESP32"

Functionmicrosd_1ESP32
powerVCC3V3
groundGNDGND
spiMOSIGPIO 23
spiMISOGPIO 19
spiSCKGPIO 18
spiCSGPIO 13

8. Connections between "rdm6300_1" and "ESP32"

Functionrdm6300_1ESP32
powerVCCVIN
groundGNDGND
uartTXResistor P1EXT

9. Connections between "res_rfid_bottom_1" and "ESP32"

Functionres_rfid_bottom_1ESP32
groundP2GND
digitalP1GPIO 16

10. Connections between "ibutton_probe_1" and "ESP32"

Functionibutton_probe_1ESP32
dataDATAGPIO 17
groundGNDGND

11. Connections between "res_ibutton_pullup_1" and "ESP32"

Functionres_ibutton_pullup_1ESP32
powerP13V3
digitalP2DS1990A iButton Probe DATAEXT

12. Connections between "res_base_1" and "ESP32"

Functionres_base_1ESP32
digitalP1GPIO 25
digitalP2NPN Transistor (2N2222 / BC547) BASEEXT

13. Connections between "ir_driver_1" and "ESP32"

Functionir_driver_1ESP32
digitalCOLLECTOR940 nm Infrared LED CATHODEEXT
groundEMITTERGND

14. Connections between "res_ir_1" and "ESP32"

Functionres_ir_1ESP32
powerP13V3
digitalP2940 nm Infrared LED ANODEEXT

15. Connections between "battery_1" and "ESP32"

Functionbattery_1ESP32
power+VTP4056 Li-Ion/LiPo charger module with protection B+EXT
groundGNDTP4056 Li-Ion/LiPo charger module with protection B-EXT

16. Connections between "charger_1" and "ESP32"

Functioncharger_1ESP32
powerIN+USB charging connector positiveEXT
groundIN-USB charging connector groundEXT
powerOUT+Boost Converter VIN+EXT
groundOUT-Boost Converter VIN-EXT

17. Connections between "boost_1" and "ESP32"

Functionboost_1ESP32
powerVOUT+VIN
groundVOUT-GND

18. Connections between "res_rfid_top_1" and "ESP32"

Functionres_rfid_top_1ESP32
digitalP2Resistor P1EXT

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_PN532.h>
#include <IRremote.hpp>
#include <OneWire.h>
#include <ELECHOUSE_CC1101_SRC_DRV.h>


struct StoredIr {
  uint32_t magic;
  uint8_t protocol;
  uint16_t address;
  uint16_t command;
  uint32_t raw;
};


// Forward declarations
void setStatus(const char *line1, const char *line2);
void drawScreen();
void loadSavedIr();
void saveIr(const IRData &data);
void sendSavedIr();
void pollIr();
void pollNfc();
void pollRfid();
void pollIButton();
void pollRadio();
void handleControls();

constexpr uint8_t OLED_SDA_PIN = 21;
constexpr uint8_t OLED_SCL_PIN = 22;
constexpr uint8_t IR_RECEIVE_PIN = 27;
constexpr uint8_t IR_SEND_PIN = 25;
constexpr uint8_t JOYSTICK_X_PIN = 32;
constexpr uint8_t JOYSTICK_Y_PIN = 33;
constexpr uint8_t JOYSTICK_SW_PIN = 14;
constexpr uint8_t CLEAR_BUTTON_PIN = 26;
constexpr uint8_t RFID_RX_PIN = 16;
constexpr uint8_t IBUTTON_PIN = 17;
constexpr uint8_t SPI_SCK_PIN = 18;
constexpr uint8_t SPI_MISO_PIN = 19;
constexpr uint8_t SPI_MOSI_PIN = 23;
constexpr uint8_t CC1101_CS_PIN = 4;
constexpr uint8_t SD_CS_PIN = 13;
constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;
constexpr uint8_t OLED_RESET = 255;
constexpr uint32_t IR_FILE_MAGIC = 0x49525231UL;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_PN532 nfc(-1, -1, &Wire);
OneWire iButton(IBUTTON_PIN);
HardwareSerial rfidSerial(2);

const char *modeNames[] = {"IR remote", "NFC", "125k RFID", "iButton", "Sub-GHz RX"};
constexpr uint8_t MODE_COUNT = sizeof(modeNames) / sizeof(modeNames[0]);
uint8_t mode = 0;
char statusLine[42] = "Starting...";
char detailLine[42] = "";
bool sdReady = false;
bool nfcReady = false;
bool radioReady = false;
bool haveSavedIr = false;
uint32_t lastInputMs = 0;
uint32_t lastPollMs = 0;


StoredIr savedIr{};

void setStatus(const char *line1, const char *line2 = "") {
  strncpy(statusLine, line1, sizeof(statusLine) - 1);
  statusLine[sizeof(statusLine) - 1] = '\0';
  strncpy(detailLine, line2, sizeof(detailLine) - 1);
  detailLine[sizeof(detailLine) - 1] = '\0';
}

void drawScreen() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print(F("Pocket tool: "));
  display.println(modeNames[mode]);
  display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
  display.setCursor(0, 18);
  display.println(statusLine);
  display.setCursor(0, 32);
  display.println(detailLine);
  display.setCursor(0, 49);
  display.println(F("L/R mode  press action"));
  display.println(F("Button clears screen"));
  display.display();
}

void loadSavedIr() {
  if (!sdReady) return;
  File file = SD.open("/lastir.bin", FILE_READ);
  if (file && file.read(reinterpret_cast<uint8_t *>(&savedIr), sizeof(savedIr)) == sizeof(savedIr) && savedIr.magic == IR_FILE_MAGIC) {
    haveSavedIr = true;
  }
  if (file) file.close();
}

void saveIr(const IRData &data) {
  savedIr.magic = IR_FILE_MAGIC;
  savedIr.protocol = static_cast<uint8_t>(data.protocol);
  savedIr.address = data.address;
  savedIr.command = data.command;
  savedIr.raw = data.decodedRawData;
  haveSavedIr = true;
  if (sdReady) {
    SD.remove("/lastir.bin");
    File file = SD.open("/lastir.bin", FILE_WRITE);
    if (file) {
      file.write(reinterpret_cast<const uint8_t *>(&savedIr), sizeof(savedIr));
      file.close();
    }
  }
}

void sendSavedIr() {
  if (!haveSavedIr) {
    setStatus("No saved IR command", "Aim remote at receiver");
    drawScreen();
    return;
  }
  IRData data{};
  data.protocol = static_cast<decode_type_t>(savedIr.protocol);
  data.address = savedIr.address;
  data.command = savedIr.command;
  data.decodedRawData = savedIr.raw;
  IrSender.write(&data, NO_REPEATS);
  setStatus("Sent saved IR command", "Only use on your equipment");
  drawScreen();
}

void pollIr() {
  if (!IrReceiver.decode()) return;
  if (!(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)) {
    saveIr(IrReceiver.decodedIRData);
    snprintf(statusLine, sizeof(statusLine), "Saved protocol %u", static_cast<unsigned>(IrReceiver.decodedIRData.protocol));
    snprintf(detailLine, sizeof(detailLine), "Addr %04X Cmd %04X", IrReceiver.decodedIRData.address, IrReceiver.decodedIRData.command);
    drawScreen();
  }
  IrReceiver.resume();
}

void pollNfc() {
  if (!nfcReady || millis() - lastPollMs < 400) return;
  lastPollMs = millis();
  uint8_t uid[7];
  uint8_t uidLength = 0;
  if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 30)) {
    char uidText[25] = "UID ";
    for (uint8_t i = 0; i < uidLength && strlen(uidText) < 20; i++) {
      char hex[4];
      snprintf(hex, sizeof(hex), "%02X", uid[i]);
      strcat(uidText, hex);
    }
    setStatus("NFC tag read", uidText);
    drawScreen();
  }
}

void pollRfid() {
  static char tag[15] = "";
  static uint8_t index = 0;
  while (rfidSerial.available()) {
    char c = static_cast<char>(rfidSerial.read());
    if (c == '\n' || c == '\r') {
      if (index > 0) {
        tag[index] = '\0';
        setStatus("125 kHz tag read", tag);
        drawScreen();
        index = 0;
      }
    } else if (isxdigit(c) && index < sizeof(tag) - 1) {
      tag[index++] = c;
    }
  }
}

void pollIButton() {
  if (millis() - lastPollMs < 600) return;
  lastPollMs = millis();
  uint8_t address[8];
  iButton.reset_search();
  if (iButton.search(address)) {
    char serial[25] = "ROM ";
    for (uint8_t i = 0; i < 8; i++) {
      char hex[4];
      snprintf(hex, sizeof(hex), "%02X", address[i]);
      strcat(serial, hex);
    }
    setStatus("iButton read", serial);
    drawScreen();
  }
}

void pollRadio() {
  if (!radioReady || millis() - lastPollMs < 250) return;
  lastPollMs = millis();
  if (ELECHOUSE_cc1101.CheckRxFifo(0)) {
    byte packet[64];
    byte length = ELECHOUSE_cc1101.ReceiveData(packet);
    snprintf(statusLine, sizeof(statusLine), "RF packet received");
    snprintf(detailLine, sizeof(detailLine), "%u bytes at 433.92 MHz", static_cast<unsigned>(length));
    drawScreen();
  }
}

void handleControls() {
  if (millis() - lastInputMs < 250) return;
  int x = analogRead(JOYSTICK_X_PIN);
  if (x < 800 || x > 3300) {
    mode = x < 800 ? (mode + MODE_COUNT - 1) % MODE_COUNT : (mode + 1) % MODE_COUNT;
    lastInputMs = millis();
    setStatus("Select a function", "Bring your own tag/device");
    drawScreen();
  }
  if (digitalRead(CLEAR_BUTTON_PIN) == LOW) {
    lastInputMs = millis();
    setStatus("Ready", mode == 0 ? "Press stick to send saved IR" : "Waiting for a nearby tag");
    drawScreen();
  }
  if (digitalRead(JOYSTICK_SW_PIN) == LOW) {
    lastInputMs = millis();
    if (mode == 0) sendSavedIr();
  }
}

void setup() {
  pinMode(CLEAR_BUTTON_PIN, INPUT_PULLUP);
  pinMode(JOYSTICK_SW_PIN, INPUT_PULLUP);
  Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  SPI.begin(SPI_SCK_PIN, SPI_MISO_PIN, SPI_MOSI_PIN);
  sdReady = SD.begin(SD_CS_PIN);
  loadSavedIr();

  IrReceiver.begin(IR_RECEIVE_PIN, DISABLE_LED_FEEDBACK);
  IrSender.begin(IR_SEND_PIN, DISABLE_LED_FEEDBACK);
  rfidSerial.begin(9600, SERIAL_8N1, RFID_RX_PIN, -1);

  nfc.begin();
  uint32_t version = nfc.getFirmwareVersion();
  nfcReady = version != 0;
  if (nfcReady) nfc.SAMConfig();

  ELECHOUSE_cc1101.setSpiPin(SPI_SCK_PIN, SPI_MISO_PIN, SPI_MOSI_PIN, CC1101_CS_PIN);
  ELECHOUSE_cc1101.Init();
  radioReady = true;
  ELECHOUSE_cc1101.setMHZ(433.92);
  ELECHOUSE_cc1101.SetRx();
  setStatus("Ready", "L/R chooses a function");
  drawScreen();
}

void loop() {
  handleControls();
  if (mode == 0) pollIr();
  else if (mode == 1) pollNfc();
  else if (mode == 2) pollRfid();
  else if (mode == 3) pollIButton();
  else pollRadio();
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik