r/esp32

Image 1 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
Image 2 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
Image 3 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
Image 4 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
Image 5 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
Image 6 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
Image 7 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
Image 8 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
Image 9 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
Image 10 — I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]
▲ 10 r/esp32

I designed a magnetic voice-coil style actuator with a custom 98% efficient driver for ESP32s, and wrote an Arduino library to control it using just a single pin (10 Prototypes Later). [Project files included]

I am in my final year of a Math, Physics, and CS triple major, and I wanted to combine all three of my fields into a single, physical project built entirely from first principles.

I spent the last month and a half designing an open-source, highly efficient dual purpose actuator and voice-coil-style surface exciter (tactile transducer) from scratch. My goal was to build a system that can magnetically couple to household metal objects (like tin cans, pots, or even a toaster) to use them as acoustic resonators, keeping power waste close to zero. While being easily compatible and modular with microcontrollers like Arduino and ESP32.

I've also written libraries for the Arduino IDE, which can be used to easily drive the magnetic actuator, or to create a musical instrument.

***

💻 The Arduino Library & API :

Actuator Library

To make the hardware as accessible as possible, the custom library abstracts the timing and high-frequency switching under the hood. You can initialize the transducer and simply call strike() and retract(), or vibrateAtFrequency() and endMovment().

Library Link: ARAM Toggle Actuator Library

Repetitive Strike Example

//import ARAM Toggle Actuator Library
#include "ARAM_Toggle_Actuator.h";

//declare a toggle actuator
ARAM_TOGGLE_ACTUATOR a1;


void setup() {   
  //initialize a toggle actuator
  a1  = ARAM_TOGGLE_ACTUATOR(1);

  //declare as a standard actuator (as opposed to vibrator)
  a1.setActuatorMode(ARAM_TOGGLE_ACTUATOR_Actuator);

  //lets strike the actuator
  a1.strike();
  
  //wait 75 ms
  delay(75);

  //retract the actuator
  a1.retract();
  
  delay(75);

  a1.strike();

}

Playing Frequency Example

#include "ARAM_Toggle_Actuator.h";

int pinNum = 1;
int defualtFrequency = 800;
int playingFrequency = 1000;


ARAM_TOGGLE_ACTUATOR a1;
   

void setup() {

  a1  = ARAM_TOGGLE_ACTUATOR(pinNum,defualtFreq);
  a1.vibrateAtFrequency(playingFrequency);

}

Orchestra Library

In order to make an instrument out of multiple of these, I have provided another library called ARAM_Metal_Orchestra which allows adding up to 6 actuators simultaneously. The library is made to be directly compatible with MIDI commands, and there is example code to produce a Bluetooth Controlled MIDI Instrument out of household metals.

Library Link: ARAM Metal Orchestra

***

🛠️ Technical Specifications:

  • Single-Pin Control Hack: Standard H-bridges (like the L298N) require 2 to 4 microcontroller pins for direction and enable lines, which wastes GPIO pins. To solve this, I integrated custom toggle control logic so you can safely cycle polarities in both directions using just a single GPIO pin or a physical button. It is fully compatible with both 3.3V (ESP32/STM32) and 5V (Uno/Nano) logic levels, (Technically 1.8V logical as well, but reduced effiecnkey in that case).
  • The Driver Board: A custom-designed discrete MOSFET H-bridge that achieves 98% efficiency. This completely eliminates the severe thermal dissipation and voltage drop issues common with older, inefficient ICs like the L298N.
  • The Transducer / Actuator: A dual-purpose voice-coil system that acts as both a linear actuator and a tactile surface exciter. It features integrated neodymium magnetic feet to temporarily couple to flat or curved metal surfaces. Because the surface acts as the speaker cone, the material dictates the resonant harmonics and sound signature.

***

🎓 Educational Content in the Video (5 mins):

If you are interested in the physics and electrical engineering behind the build, the linked video is designed as an educational guide covering:

  • The "Potential Hill" Concept: A visual, animated breakdown of how permanent magnets behave like rolling balls on a magnetic potential slope when interacting magnetic fields.
  • Acoustic Resonance: How mechanical vibrations translate into audible sound waves depending on the resonant frequencies of different materials.
  • See the Transducer in Action: View a number of orchestra setups with household objects and hear the music they make.

It's the second in a series on electromagnetism, the previous one covers:

  • Electromagnetic Physics: A microscopic look at how moving electrons create circular magnetic fields, and how winding them into solenoids concentrates that field.
  • Driver Efficiency and Operation: Animation showing the qualitative difference in how much energy is wasted by various drivers, and their operating principles.

Video:

YT Video Link

***

📂 Open-Source Files

I’ve made all the project files free and open. You can find the BOM, STL files, and CAD project files linked in the YouTube video description above:

***

Any feedback on the discrete H-bridge, the single-pin logic layout, or the either of the two Arduino library's structure is highly appreciated! I'm hoping to one day do this full time when I'm out of school so feedback is very helpful. I'll be in the comments answering any questions about the build.

Ps. The actuator library has some unused code that will be removed, I ended up with a bunch of unnecessary checks in my state machine tracking down an issue that ended up not having anything to do with the state machine. I'll be cleaning the library over the next couple weeks.

u/Ok_Iron8063 — 3 hours ago
▲ 448 r/esp32+3 crossposts

I built a connected RGB lantern using ESP-NOW

For this battery-powered project I'm using MicroPython on a XIAO ESP32-S3 board, with a WS2812 LED strip. Multiple lamps can be synchronized together thanks to ESP-NOW.

Source code: https://codeberg.org/LAMPy/LAMPy

Hardware write-up: https://lampy.codeberg.page/

Let me know what you think!

u/lampyz — 21 hours ago
▲ 300 r/esp32

Someone made a wonderful little app for fluid animations on an esp32-s3 matrix

I had a spare esp32-s3 matrix, and wanted to see if I could make a fluid fidget toy out of it. I had been struggling with it for a few hours today and found a wonderful app on github to do this:

https://github.com/Vateva/Esp32FluidSimulation8x8

This person's version uses at a momentary switch (I don't have one, so I decided to modify to make it a series of taps until I buy one).

u/zmattmanz — 20 hours ago
▲ 9 r/esp32+2 crossposts

DeterministicESPAsyncWebServer

An HTTP/1.1 web server for ESP32 with a fully deterministic memory footprint, RFC 7230 compliant request parsing, and an OSI-layered architecture. Should be on the Arduino registry tonight.

I'm actively working on it; docs, second optimization pass, adding assertions to tests, etc. I'm going to be adding more hw crypto support because I ultimately want to ssh into this.

I built this from the ground up to be different from the existing library. Please share your thoughts, use the library, and report any bugs by opening an issue.

My next project is going to be like a web based terminal using this deterministic async webserver, fully featured and free under agpl, I want it to look like telnet or ssh.

Happy coding!

Github: github

API Documentation: docs

Git: git

```txt
Features
Zero heap allocation *ever*. The event queue, connection pool, HTTP pool, WebSocket pool, and SSE pool are all statically sized in BSS; the entire memory footprint is fixed at link time
RFC 7230 compliant request parser validates method, path, header field-names, and field-values byte-by-byte before storing anything
WebSocket support (RFC 6455) SHA-1 handshake via mbedTLS hardware accelerator, frame parser, ping/pong/close handled automatically
Server-Sent Events persistent connections, per-connection and broadcast push
HTTP Basic Authentication per-route credential checking via mbedTLS base64
Static file serving chunked reads from any Arduino FS (LittleFS, SPIFFS, SD)
Multipart form-data parser in-place, no allocation, up to MAX_MULTIPART_PARTS parts
Compile-time feature flags strip unused subsystems entirely; a REST-only build includes none of the above
Compile-time configuration every buffer, pool, and timeout is a #define; illegal combinations produce #error messages
Diagnostic JSON endpoint optional DETWS_ENABLE_DIAG build-config dump, disabled by default for security
Backpressure-aware TCP shrinks the receive window instead of dropping data when the ring buffer fills
CORS preflight short-circuit OPTIONS answered with 204 automatically when CORS is enabled
restart() hard-resets all connections and reinitializes on the same port without touching the WiFi/TCP stack
321 tests across nine Unity test suites, runnable on native x86/x64 (no hardware required)
```
u/dstroy0 — 11 hours ago
▲ 30 r/esp32

weather display - help from Claude

i got claude to help me make a weather display! at first there was no sun and no time, and eventually got it to add a little more features. and learned how to edit the smiley face in PlatformIO. this was a fun little project experiment, i can’t wait to try more. claude was great help

u/mack-y0 — 18 hours ago
▲ 360 r/esp32

2G was shut down in France and left my alarm panel deaf and mute — so I built my own IP gateway (ESP32)

I'd been wanting to do an ESP32 project for a long time, but I was waiting for the right use case. I finally got it.

I own an alarm panel whose GSM module sent alerts and let me control it by SMS — handy and reliable for years.

With the 2G shutdown in France (June 2026), the panel went deaf and mute: no more alerts, no more remote control. And no possible upgrade, since the manufacturer had gone bankrupt in the meantime. The only "official" option was to switch brands and replace the whole installation : panel, detectors, sirens etc… even though everything still works perfectly.

So I built my own IP gateway.

The panel lends itself well to this

It has a terminal block on the back that makes it easy to interface with.

A programmable output (PGM) delivers 12 V / 100 mA and can be configured as:

  • alarm output
  • armed output
  • power fault
  • detector communication fault
  • manual trigger / control

And several wired detection zones are configurable:

  • perimeter with delay
  • perimeter
  • interior
  • panic
  • 24h / permanent
  • fire
  • key zone

Stack

Hardware

  • DFRobot FireBeetle 2 DFR1140 — chosen for the built-in charger and the external antenna connector
  • 3.7 V 1000 mAh LiPo battery
  • External Wi-Fi antenna
  • Custom PCB

Backend

  • Cloudflare Worker
  • Custom PWA

What the ESP32 does

The board interfaces with the panel through PC817 optocouplers.

Reading — it reads the actual state of the panel, so it stays in sync even when I use the remotes or the keypad:

  • Alarm directly from the built-in piezo siren signal (~1 kHz oscillation)
  • Armed / disarmed via the PGM output (12 V) — since the alarm is detected from the siren, the PGM stays free to report this state
  • Mains / battery — the ESP32 measures its own supply voltage

Commands:

  • Arm / disarm via zone Z39 configured as a key zone
  • Trigger the siren via Z40 in permanent-detection mode — a feature that didn't even exist in the original app!

I made my own PCB to put it all together cleanly.

Backend / app

No server: everything runs on a Cloudflare Worker (free tier), acting as a mailbox. The ESP32 drops off its state and picks up pending commands; the app reads the state and writes my commands. The Worker keeps the current state and an event history .

The key point is that the ESP32 only makes outbound requests: it calls the Worker every few seconds, but never accepts an incoming connection. That's what makes the whole thing simple:

  • nothing to configure on the network side — no open ports, no port forwarding, no dynamic DNS.
  • nothing is exposed on the alarm side — the only thing visible from the internet is the Worker. The panel itself isn't reachable.

I chose this polling approach over a persistent connection (WebSocket/MQTT): a few seconds of latency on commands isn't noticeable in practice, and more importantly the intrusion alert itself is instant — it doesn't depend on polling, the ESP32 pushes the notification as soon as it detects the siren.

The app is a PWA on my own domain. It shows real-time state, command buttons, history and settings — and it's served by the Worker itself, with no dedicated server. I use native web push notifications (intrusion, power loss, gateway unreachable).

On the security side :

  • the app is protected by a 256-bit token + Cloudflare Access;
  • the ESP32 has its own secret, separate from the app's;
  • electrical fail-safe: an ESP32 failure can never disarm the panel. When the ESP loses power, the key-zone loop can only open (or stay open) — and an open loop arms the system. So a gateway failure leaves the house at least as protected as before, never less;
  • watchdog: automatic reboot if the firmware hangs, with resync to the panel's actual state on restart;
  • heartbeat: the Worker knows when the ESP32 last checked in. If it goes quiet for too long, I get a "gateway unreachable" notification.
  • the ESP32 and the panel each have their own backup battery, and the connection runs through a 4G router on a UPS.

This is my first ESP32 project and I'm pretty happy with the result. It's up and running, and it seems reliable. What do you think? Any criticism or improvement ideas welcome.

If you're interested in the schematic or the code, I'm happy to share.

u/Dense-Log-168 — 1 day ago
▲ 4 r/esp32

Blinking LED project not working :(

I just got an ESP32 yesterday. I first tested the onboard LED with the basic blink example, and it worked perfectly, Then I tried doing the same with an external LED, but I can't get it to work

What I've tried

  • Used two different 100 Ω resistors (also tried 150 Ω)
  • Tried multiple GPIO pins
  • Tried another LED
  • Changing the code
  • trying to make always working to check if it works and still not lighting up
  • The LED polarity is correct:
    • Anode → resistor → GPIO
    • Cathode → GND
  • The LED works fine with a small battery

this is the final code and connection in the pictures
I am really annoyed that I spent lots of time trying to fix it and I'm honestly out of ideas. Is there something obvious I'm missing?

and thx in advance.

Edit the LED never lights up

Solution: the pinout order was different so always try to know what kind of ESP32 you're using

u/Alarming_Bar6382 — 1 day ago
▲ 86 r/esp32

Testing My DIY ESP32-S3 Walkie-Talkie Prototype

In this video, I’m testing a walkie-talkie prototype based on the ESP32-S3. Using the ESP-NOW protocol, I achieved instant and stable communication. While the audio quality isn’t perfect every time, it demonstrates the core functionality and potential of the device. I’d love to hear your thoughts in the comments

u/twintowers-killer — 1 day ago
▲ 125 r/esp32

A much needed update to my custom ESP32 air quality monitor (Custom PCB, e-Paper display, SEN66)

Last year I posted my first attempt at a custom ESP32 air quality monitor. It was a fun project, but the hardware and firmware definitely still needed some work.

I spent the last few months rebuilding it from the ground up to be a much more polished device. I completely redesigned the PCB. It keeps the ESP32-C3, but instead of the original multi-sensor layout I went with a single Sensirion SEN66 module. It significantly simplifies things while keeping all the CO2, particulates, VOC Index, temp, and humidity measurements. It also adds NOx Index which is nice. I kept the same e-paper display (no backlight and I really like the clean aesthetics of it) with an updated layout.

On the software side, I moved everything over to run natively on ESPHome. And because of that, it easily integrates right into Home Assistant.

I also made a simple local web interface that runs on the esp32 for viewing the live data on your local network. On top of that I got OTA updates working so whenever a new build is available on GitHub, I can just do a one click firmware update from the web UI or Home Assistant.

Overall I'm super happy with how it turned out, but let me know what you guys think!

All the documentation and design files are available at my GitHub repo (PCB files, firmware, and enclosure 3D models): https://github.com/EnriqueNeyra/aether

I also put together a new video going over the device as a whole if you want to see more: https://youtu.be/wnY3weUh9ps

u/EnriqueN01 — 1 day ago
▲ 18 r/esp32+2 crossposts

Dual-Zone Peltier Cooler Controller (ESPHome / ESP32) - Architecture and Logic Review

I am building a dual-zone Peltier wine fridge cooling controller using ESPHome on an ESP32. The system regulates two separate thermoelectric cooling (TEC) modules powered by a 13.0V to 14.4V Solar/LiFePO4 battery bank. A Shelly Gen4 unit handles the high-level power control to the main busbars, and the ESP32 receives dedicated power via a USB-C PD board. I am unfortunately at least 2x the cost to replace the darn thing, but I wanted to run the fridge natively on available DC instead of having the inverter run 24x7 to drive dedicated AC 220v controller boards - and one of the original controller boards was fried and driving the zone down to 2°C continously. The fridge was a gift from a good friend who suddenly passed away last January, so I'm defo keeping it...

The primary goal is independent or synchronized thermal management for two zones, using an OLED display and a physical rotary encoder for local management, paired with an MQTT integration into Home Assistant.

The core hardware configuration, current software design, and roadblocks are detailed below. AI disclosure - I have used Gemini to support coding based on my design and components available.

The Hardware Stack

  • Overall power 13.0-14.4v from LiFEPO4 system
  • Shelly Gen4 handling main power control to busbars
  • ESP32 Power supplied thru USBC PD board
  • ESP32 USBC 38-pin
  • 2x Monster Moto controllers, one for each TEC unit, actively cooled
  • 4x NTC thermistors on inside/outside of TEC plates, 10k pullups
  • 3x Dallas DS18B20 3-wire thermal sensor

Control and Actuation: The brains of the operation is a 38-pin ESP32 development board. Actuation is handled by two Monster Moto controllers (one per TEC module) that are actively cooled.

Power Delivery: The system is strictly configured for cooling only. Polarity switching is not planned. Because TEC modules do not tolerate raw high-frequency PWM well, a downstream low-pass filter consisting of a smoothing coil and a 6800uF capacitor is placed after the Moto controller outputs. The LEDC base frequency is configured to 10kHz.

Sensors: Thermal monitoring relies on two distinct sensor types. Four 10k NTC thermistors with 10k pullup resistors track the internal cold plates and the external warm-side heatsinks for both zones. Three Dallas DS18B20 1-wire sensors handle auxiliary monitoring: two sit directly under the Monster Moto MOSFET chips to monitor driver temperatures, and one monitors ambient air temperature under the chassis.

Logic, Safety Watchdogs, and UI State Machine

The firmware uses the ESPHome native PID climate platform to adjust the LEDC output level, capped at a maximum duty cycle of 80%. To filter out voltage noise on the analog lines, all NTC sensors use a median filter window.

Because a thermal runaway or a frozen controller can quickly destroy the components, the code enforces strict safety limits. The system drops into a hard `ERROR` state and cuts PWM duty cycles to 0% if the warm heatsinks exceed 60°C, the Moto drivers exceed 60°C, or an NTC sensor persistently (3 strikes concept) drops offline. There is also a runaway watchdog: if a zone calls for more than 70% power for 5 minutes straight without achieving at least a 2°C temperature drop, the system trips.

The physical interface uses an SH1106 1.3-inch OLED display driven by an internal state machine (`ui_state`). It manages the transitions between:

  1. NORMAL: Displays alternating zone metrics or synchronized statistics.
  2. SET_MODE: Toggles between independent zone control and `SYNC` mode.
  3. SET_TEMP_Z1 / SET_TEMP_Z2 / SET_TEMP_SYNC: Dedicated target temperature adjustment windows using a rotary encoder.

Problems Overcome and Dead Ends

Building this out highlighted several edge cases that required architectural revisions:

Persistent Boot Loops: Storing the system state in persistent global memory introduced a dangerous edge case. If the system crashed due to a critical hardware fault (like a disconnected NTC sensor), it would reboot, attempt to initialize the PID loop in the last known active state, immediately fault out, and restart again. This was resolved by intercepting the boot sequence using a custom `on_boot` lambda. If an `ERROR` flag is detected in flash memory, the standard initialization bypasses the operational loops entirely, keeping the PWM lines firmly locked at zero until a manual reset is triggered.

Circuit Protection Deficiencies: Early iterations exposed the vulnerability of the switching components to voltage spikes generated by inductive feedback. Inductive loads on the driver outputs required proper flyback protection. Implementing dedicated diode protection across the output paths resolved the risk of punching holes through the switching hardware.

Level Shifter Assumptions: Initial designs factored in hardware level shifters for the LEDC outputs to the driver boards. Bench testing proved that the ESP32 pins (GPIO4 and GPIO13) can trigger the logic gates cleanly without the added latency and complexity of unnecessary shifting components.

The Error State Trap: Once the watchdog tripped into an `ERROR` mode, the system locked down. Originally, clearing this required an MQTT command or a full power cycle. This was fixed by mapping a multi-purpose physical hardware escape route via the `Run Mode` button logic. If the system is bricked in an error state, a physical press acts as a diagnostic reset, clearing the global error code back to 0 and restoring `STANDBY` mode.

Current Status and Areas for Input

The MQTT reporting and remote target configurations work correctly for individual zones. The local display properly switches between languages and states based on user interaction arrays.

I am looking for feedback on a few areas:

  1. PID Tuning: The current tuning constants are `kp: 0.02`, `ki: 0.0005`, and `kd: 0.03`. Given the high thermal mass of the cooler chassis combined with the filtering lag of the low-pass LC network, what are the best practices for tuning ESPHome's PID component to minimize overshoot without causing cyclic hunting?
  2. State Machine Optimization: Managing five distinct UI states and dual-loop sync logic purely inside ESPHome lambdas is getting dense. Are there cleaner ways to structure conditional menus and button-hold actions without writing excessive inline C++ within the YAML file?
  3. Failsafe Operation: What other failsafe concepts can I add to ensure that the system can operate unattended for days, reporting back regularly to my Home Assistant system on health and overall operation. I am most concerned about thermal runaway and/or a fried component staying in the "on" state - hence using the multiple kill switches and Shelly Gen4 as a last resort.

Looking forward to hearing your thoughts or seeing similar control loops you have built. I can share the HA ESPHome YAML separately or DM if you are interested. Edit - formatting.

u/800ASKDANE — 21 hours ago
▲ 46 r/esp32

Arduino Nano ESP32 @ 720x720, up to 40 FPS (so far)

Wanna share something I think is pretty cool.

Recently I worked with a Raspberry Pi Zero 2 W and played some AAA games over the internet using my custom encoder/decoder pipeline. There is also some gameplay on YouTube if you are curious, with voiced explanation. The system streams from Windows or Linux with low latency to the device.

I was curious if I could lower the microcontroller requirements even more, slower wifi and less cpu, and do the same with an Arduino Nano ESP32. It works pretty well. This little ESP32 part fits the setup surprisingly nicely. I am basically close to the ceiling, afaik, of what it can do here: receiving the stream over wifi, calculating CRC, handling some control logic, and forwarding the encoded data to my decoder via SPI.

The ESP32 is not doing the heavy video decoding itself. The actual decoder is my small square board in the setup. It is optimized for this use case, decodes the stream, and then drives the parallel RGB display directly.

The codec is custom too. It is not just sending full frames all the time. It uses delta-frame logic, so if only something small changes, like a cursor, only a small part of the frame has to be sent. That is one of the reasons why this can work on such a small part.

The whole thing on the breadboard runs with very low power consumption, below 1.5 W during active streaming and full brightness. In this video I stream over the local network. I am pretty sure I can also get useful results over the internet, like with the Raspberry Pi, if I route correctly to my home PC.

The Arduino provides everything I need here: a clean enough clock for my decoder, I2C for the backlight driver and touch input, and SPI for data transmission. Touch input is converted and sent to the server as mouse input.

My home PC is just an older 2017-ish gaming PC with an Nvidia GTX 1050 Ti. The encoder is written with DirectX and OpenGL, which means it can run on many GPUs or iGPUs that support basic hardware acceleration. In turn, I can stream nearly every graphics program or game that also runs on my PC.

I have seen many cool display projects in this sub, so I am curious if anyone knows something comparable, especially in terms of resolution, frame rate, latency, or power consumption. If you have seen or built something similar, please let me know.

I am looking forward to building a small device around this, probably with a simple keyboard. I am not sure yet if I should aim more for gaming inputs, a QWERTY keyboard, or both.

Feedback is welcome. I would also be happy to answer questions if you have any.

u/gitzian — 23 hours ago
▲ 6 r/esp32

Where do you buy your ESP32s?

Hey everyone,

I’m a 15 year old student just getting started with electronics, and I’ve been looking to buy some ESP32s for my first real project: an ISS tracking antenna. I’m finding them at around 6€ each on AliExpress, which feels a bit expensive.

I recently found ESP32 S3 on Alibaba for around 2€/unit with a minimum order of 5 pieces. The seller has great reviews but the product itself has no reviews yet.

A few questions:

  • Where do you usually buy your ESP32s at the best prices?
  • Is Alibaba reliable for this type of component?
  • Any other platforms I should check?

Thanks!

reddit.com
u/Friendly_Listen9468 — 1 day ago
▲ 83 r/esp32

This is an ESP32 outputting via AV... in a NES controller.

If anyone saw my ESP32-S3 SNES controller HDMI thing from yesterday. I had also been working on a sister project, not actually sure which one I thought of first but the other came shortly after and I have been working on and off for a few months now.

This one is a ESP32 that outputs composite. Its based on some work done by much cleverer people than me who worked out you can actually output AV signals using only an ESP32, so I asked AI if it could use that sample code to write a TVOut driver for RetroGo, since that already has quite a few emulators and has a lovely interface.

It was not a quick process, lots of no sync, half syncs, wrong colours, panics, crashes etc but once it got something semi stable, it actually moved quite quickly, however it turns out that processing an AV signal is quite expensive (both CPU and memory) compared to outputting a SPI signal to a LCD so performance is hampered, but luckily the original Retro-Go emulators are so well optimised they still play full speed, even Doom!

This one is a very simple device, just the bare basics of a ESP32, SD slot, voltage reg, AV port and USB port plus some caps/resistors.

Not sure how popular a AV device will be in 2026 but I made it for fun so I will be sharing all the files, gerbers etc over the next week or so

u/DynaMight1124 — 1 day ago
▲ 29 r/esp32

Why is my esp32 C3 super mini overheating ?

I am using two super mini's for a rc plane project, where one is the transmitter and receiver, i am using my phone's charging cable for uploading the code and it's working, but

After i uploaded the code on the transmitter and powered the transmitter with my laptop (through which i uploaded the code) and the receiver with 3.7 V li-po battery, after a few seconds the transmitter was noticeably hot while the receiver was just a bit warm, and i also tried to reverse the connection, connecting the transmitter to the li-po battery and the receiver to the laptop and this time the receiver's temprature started to increase within a few seconds of connecting it while the transmitter was just lukewarm.

I am not much experienced with using esp's, so i don't know much about them, but i don't think it should be becoming too hot to touch in a matter of seconds.

If anyone knows what the problem is and how I can fix it, pls help 🙏

u/Dani_bigfan_567894 — 1 day ago
▲ 3 r/esp32+1 crossposts

esp32-s3-zero: WiFi without antenna

I recently started using an esp32-s3-zero but had trouble with WiFi signal. So I tried the antenna mod (https://peterneufeld.wordpress.com/2025/05/11/esp32-s3-zero-antenna-modification/) and it was initially giving better signal.

However while manipulating the board, I accidentally forced on the antenna and tore out one of the two pads of the board connection to the antenna. The pad is still soldered to the antenna but no longer attached to the board.

My signal is now very weak as there is no more electrical connection to the antenna.

Any idea what I could do as without WiFi the board is of no much use to me?

https://preview.redd.it/py5c9g3oigbh1.jpg?width=3072&format=pjpg&auto=webp&s=7a8279e6f07a6ef46d7370e8ef98dbf0da1baa96

reddit.com
u/X-dark — 23 hours ago
▲ 16 r/esp32+2 crossposts

Turned a LILYGO T-Display into a little Claude usage meter,

A few days ago I ran into alessandro-001's ESP32-claude-usage-display and really liked the idea: a tiny always-on screen that shows how much of your Claude limits you've burned through. I forked it, then ended up rewriting a big chunk, so figured I'd share where it landed.

I'm running it on a LILYGO T-Display S3 (the small 1.9" color one).

The part of the original that bugged me was the auth. It grabs your usage by pasting your claude.ai browser session cookie straight onto the device. That cookie lives in the ESP32's flash and expires/rotates, so you're re-pasting it every so often, and I wasn't thrilled about a live session token sitting on a microcontroller on my wifi.

So I moved the whole credential side off the device. There's now a small Python proxy that runs as a Home Assistant add-on. It talks to Anthropic's OAuth usage endpoint (same data Claude Code's /usage shows), does the token refresh itself, caches the result, and hands the ESP32 a stripped-down JSON with just the percentages and reset times. The Claude token never leaves my HA box; the display only ever talks to the proxy. When I'm not home I expose the proxy through Tailscale Funnel, so it's a single URL that works everywhere with a proper cert.

Couple of other things I added:

  • Multiple wifi networks (up to 4). I wanted to actually carry it to the office, so it connects to whichever saved network is in range and switches on its own when I move.
  • Redid the UI. Dropped the old number-plus-bar for a ring gauge with the percentage in the middle (color-coded by how close I am to the cap), a countdown to the next 5h reset, and the 7-day reset as an actual local date/time.

Repo and a v1.0 release with a prebuilt binary: https://github.com/AussieCH/ESP32-claude-usage-display

Fair warning that's also in the readme: polling that usage endpoint from a third-party thing is a bit of a ToS grey area, so treat it as a "your own account, at your own risk" project.

Happy to answer anything. Also fully prepared to be told I overengineered the wifi part.

u/Squeeech — 1 day ago
▲ 11 r/esp32

Can't get 60 FPS using ESP32-P4 5" CrowPanel

The video was recorded from an iPhone at 240 FPS, to show the 60 FPS results of the app.

Every frame the "Hello: <N>" message shows how many times the widget-update callback has been called before 1 second has elapsed. This callback is not called 60 times per second, but instead slightly over 50 FPS.

Goal is a solid 60 FPS with no fluctuation. This is a simple real-time system, and seems like there should be no hidden tasks perturbing frame rate.

I'm only updating a tiny portion of the frame buffer, and LVGL's .full_refresh is set to false (see source code). DMA and double buffer are active.

Different Refresh Rates (see details in video of refresh rates):

  • Parallel RGB LCD interface supports up to 40 MHz, but only setting to 25000000, with what seems like appropriate porch values
  • LVGL Task (OS task) runs at 16 ms
  • LVGL Display Callback (that updates the hello label) also runs at 16 ms.

No obvious bottle necks:

  • Everything must be done within the LCD refresh rate of 16.7 msecs.
  • LVGL task and display rates are both set to 16 msecs, so their processing is slightly faster to make sure always ready for the LCD.
  • CPU jumps around between 3% and 50%, so it's mostly idle.
  • LVGL task shows most of the time is in the transfer to the frame buffer and not processing, ranging from 3 to 7 msecs. So much faster than the 16 ms limit.

The system is mostly idle, with no other tasks running to perturb timing, yet the FPS is fluctuating as if something is getting in the way.

The device I'm testing on is the CrowPanel (CrowPanel Advanced 5inch |ESP32-P4 HMI AI Display 800x480 IPS Touch Screen):

https://www.elecrow.com/crowpanel-advanced-5inch-esp32-p4-hmi-ai-display-800x480-ips-touch-screen-with-wifi-6.html

It's supposedly capable of buttery smooth 60 FPS if the app is properly written.

I'm using ESP-IDF 6.0.2

Dependencies from main/idf_component.yml:

dependencies:
  espressif/esp_lvgl_port: "^2.8.0"
  lvgl/lvgl: "^9.5.0"

Does anyone see anything I'm doing wrong that stopping this app from getting a solid 60 FPS with no fluctuation?

Here's the source code (4 files):

sdkconfig.defaults

# Needed is using v1.x ESP32-P4 chip version
CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y

CONFIG_IDF_EXPERIMENTAL_FEATURES=y

CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y

CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_HEX=y
CONFIG_SPIRAM_SPEED_200M=y

# Extra Fonts
CONFIG_LV_FONT_MONTSERRAT_24=y

# LVGL Performance monitoring (will display results on screen, will show render and flush times)
CONFIG_LV_USE_SYSMON=y
CONFIG_LV_USE_PERF_MONITOR=y
CONFIG_LV_PERF_MONITOR_ALIGN_BOTTOM_RIGHT=y

CONFIG_LV_USE_PPA=y
CONFIG_LV_DRAW_BUF_ALIGN=64

lcd_panel_rgb_16bit.h

#pragma once

#include "esp_err.h"

esp_err_t lcd_display_init();
uint32_t lcd_pixel_update_freq();
float lcd_pixel_update_fps();
uint32_t lvgl_refresh_period_ms();

lcd_panel_rgb_16bit.c

#include "lcd_panel_rgb_16bit.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_lcd_panel_rgb.h"
#include "esp_lvgl_port.h"
#include "math.h"

#define LCD_PANEL_TAG "LCD_PANEL"
#define LCD_PANEL_INFO(fmt, ...) ESP_LOGI(LCD_PANEL_TAG, fmt, ##__VA_ARGS__)
#define LCD_PANEL_DEBUG(fmt, ...) ESP_LOGD(LCD_PANEL_TAG, fmt, ##__VA_ARGS__)
#define LCD_PANEL_ERROR(fmt, ...) ESP_LOGE(LCD_PANEL_TAG, fmt, ##__VA_ARGS__)

// Screen info
#define RGB_LCD_H_RES  800
#define RGB_LCD_V_RES  480
#define BITS_PER_PIXEL 16

// Control pins
#define RGB_PIN_NUM_DISP_EN        -1
#define RGB_PIN_NUM_HSYNC          40
#define RGB_PIN_NUM_VSYNC          41
#define RGB_PIN_NUM_DE             2
#define RGB_PIN_NUM_PCLK           3

// 16 data pins
#define RGB_PIN_NUM_DATA0          8
#define RGB_PIN_NUM_DATA1          7
#define RGB_PIN_NUM_DATA2          6
#define RGB_PIN_NUM_DATA3          5
#define RGB_PIN_NUM_DATA4          4
#define RGB_PIN_NUM_DATA5          14
#define RGB_PIN_NUM_DATA6          13
#define RGB_PIN_NUM_DATA7          12
#define RGB_PIN_NUM_DATA8          11
#define RGB_PIN_NUM_DATA9          10
#define RGB_PIN_NUM_DATA10         9
#define RGB_PIN_NUM_DATA11         19
#define RGB_PIN_NUM_DATA12         18
#define RGB_PIN_NUM_DATA13         17
#define RGB_PIN_NUM_DATA14         16
#define RGB_PIN_NUM_DATA15         15

// ----------------------------------------------------------------------------------
// LCD Panel refresh rate
// ----------------------------------------------------------------------------------
typedef struct {
  uint32_t pclk_hz;  // Pixel clock frequency
  uint32_t hsync_pulse_width;  // HSYNC pulse width (in PCLK cycles)
  uint32_t hsync_back_porch;   // Horizontal back porch (PCLK cycles between HSYNC and active data)
  uint32_t hsync_front_porch;  // Horizontal front porch (PCLK cycles between active data and next HSYNC)
  uint32_t vsync_pulse_width;  // VSYNC pulse width (in lines)
  uint32_t vsync_back_porch;   // Vertical back porch (blank lines between VSYNC and frame start)
  uint32_t vsync_front_porch;  // Vertical front porch (blank lines between frame end and next VSYNC)
} PanelFreqInfo;

/*+*/
// Goal is a solid 60 FPS with no fluctuation, when only updating a tiny portion of the frame buffer.
// Diffent Refresh Rates:
//   - Parallel RGB LCD interface supports up to 40 MHz, but exceeding ~25000000 will start overclocking ESP32P4 internals
//     Since it has no GRAM, it's needs at steady stream coming in from PSRAM if double buffering is active
//   - LVGL Task (OS task), not sychronized to the LCD VSYNC
//   - LVGL Display Callback. No synchronize to the LVGL Task, but LVGL task needs to be away to check if time for the callback. The callback synchronizes to the LCD VSYNC.
// No bottle necks
//   - Everthing must be done within the LDC refresh rate of 16.7 msecs.
//   - LVGL task and display rates are both set to 16 msecs.
//   - CPU jumps around between 3% and 50%
//   - LVGL task shows most of the time is in the transfer to the frame buffer and not processing, ranging from 3 to 7 msecs.
// Result
//   - The system is mostly idle, with no other tasks running to purtub timing, yet the FPS is flutuating as if something is getting in the way.


// Partial frame_buffer data transfer is only about
// Doesn't make sense that a solid 60 FPS is not achieve since there should be no other tasks running


// FPS = pclk_hz / (RGB_LCD_H_RES + hsync_pulse_width + hsync_back_porch + hsync_front_porch) * (RGB_LCD_V_RES + vsync_pulse_width + vsync_back_porch + vsync_front_porch)
//                                                  pclk_hz  HPW  HBP  HFP  VPW  VBP  VFP
//static const PanelFreqInfo L_panel_freq_info = { 16600000,   4,  88, 164,   4,  32,   9};       //  29.9 FPS
//static const PanelFreqInfo L_panel_freq_info = { 18000000,   4,   8,   8,   4,  16,  16};       //  42.5 FPS (25.5 ms/frame)
//static const PanelFreqInfo L_panel_freq_info = { 25000000,   4,  40,  40,   4,  29,  13};       //  53.8 FPS (18.6 ms/frame)
//static const PanelFreqInfo L_panel_freq_info = { 25000000,   4,  12,  20,   4,  12,  14};       //  58.6 FPS (17.1 ms/frame)
  static const PanelFreqInfo L_panel_freq_info = { 25000000,   4,  16,  20,   2,  6,    8};       //  60.0 FPS (16.7 ms/frame)
//static const PanelFreqInfo L_panel_freq_info = { 28000000,   4,  24,  32,   4,  8,    8};       //  65.1 FPS (15.4 ms/frame)
// NOTE: Anything greater overclocks the CrowPanel ESP32-P4 5" internals and is likely to cause screen glitching

uint32_t lcd_pixel_update_freq() {
  return L_panel_freq_info.pclk_hz;
}

float lcd_pixel_update_fps() {
  float h = RGB_LCD_H_RES + L_panel_freq_info.hsync_pulse_width + L_panel_freq_info.hsync_back_porch + L_panel_freq_info.hsync_front_porch;
  float v = RGB_LCD_V_RES + L_panel_freq_info.vsync_pulse_width + L_panel_freq_info.vsync_back_porch + L_panel_freq_info.vsync_front_porch;
  float fps = L_panel_freq_info.pclk_hz / h / v;
  return fps;
}

static esp_err_t lcd_panel_init(esp_lcd_panel_handle_t *lcd_panel_handle_out) {
  esp_err_t err = ESP_OK;

  esp_lcd_rgb_panel_config_t panel_config = {
    .data_width = BITS_PER_PIXEL,
    .dma_burst_size = 64,                           // DMA burst size alignment for PSRAM buffer
    .num_fbs = 2,                                   // Number of frame buffers
    .flags.fb_in_psram = true,                      // Allocate frame buffer in PSRAM
    //.bounce_buffer_size_px = 20 * RGB_LCD_H_RES,  // Set bounce buffer size if enabled
    .clk_src = LCD_CLK_SRC_DEFAULT,                 // Clock source for RGB LCD peripheral
    .timings = {
      .h_res = RGB_LCD_H_RES,
      .v_res = RGB_LCD_V_RES,
      .pclk_hz = L_panel_freq_info.pclk_hz,
      .hsync_pulse_width = L_panel_freq_info.hsync_pulse_width,
      .hsync_back_porch = L_panel_freq_info.hsync_back_porch,
      .hsync_front_porch = L_panel_freq_info.hsync_front_porch,
      .vsync_pulse_width = L_panel_freq_info.vsync_pulse_width,
      .vsync_back_porch = L_panel_freq_info.vsync_back_porch,
      .vsync_front_porch = L_panel_freq_info.vsync_front_porch,
      .flags = {
.hsync_idle_low = false,            // HSYNC signal is low in IDLE state
.vsync_idle_low = false,            // VSYNC signal is low in IDLE state
.de_idle_high = false,              // DE signal is high in IDLE state
.pclk_active_neg = true,            // RGB data is sampled on falling edge of PCLK
.pclk_idle_high = true,             // PCLK remains high during idle periods
      },
    },

    // Pins
    .disp_gpio_num  = RGB_PIN_NUM_DISP_EN,          // Display enable control pin, -1 if unused
    .pclk_gpio_num  = RGB_PIN_NUM_PCLK,
    .vsync_gpio_num = RGB_PIN_NUM_VSYNC,
    .hsync_gpio_num = RGB_PIN_NUM_HSYNC,
    .de_gpio_num    = RGB_PIN_NUM_DE,
    .data_gpio_nums = {
      RGB_PIN_NUM_DATA0,
      RGB_PIN_NUM_DATA1,
      RGB_PIN_NUM_DATA2,
      RGB_PIN_NUM_DATA3,
      RGB_PIN_NUM_DATA4,
      RGB_PIN_NUM_DATA5,
      RGB_PIN_NUM_DATA6,
      RGB_PIN_NUM_DATA7,
      RGB_PIN_NUM_DATA8,
      RGB_PIN_NUM_DATA9,
      RGB_PIN_NUM_DATA10,
      RGB_PIN_NUM_DATA11,
      RGB_PIN_NUM_DATA12,
      RGB_PIN_NUM_DATA13,
      RGB_PIN_NUM_DATA14,
      RGB_PIN_NUM_DATA15,
    },                                          
  };

  // Create the LCD panel instance
  esp_lcd_panel_handle_t lcd_panel_handle;
  err = esp_lcd_new_rgb_panel(&amp;panel_config, &amp;lcd_panel_handle);
  if (err != ESP_OK) return err;

  // Reset and initialize the display
  ESP_ERROR_CHECK(esp_lcd_panel_reset(lcd_panel_handle));
  ESP_ERROR_CHECK(esp_lcd_panel_init(lcd_panel_handle));

  *lcd_panel_handle_out = lcd_panel_handle;
  return err;
}

uint32_t lvgl_refresh_period_ms() {
  // This is meant to be slightly faster than the LCD display rate, so the LCD always has something new to display.
  uint32_t refresh_period = (uint32_t)(1000.0 / lcd_pixel_update_fps()); // Truncates, so is likely to be slighly faster than LCD display refresh rate
  return refresh_period;
}

static esp_err_t lvgl_init(esp_lcd_panel_handle_t _lcd_panel_handle) {
  esp_err_t err = ESP_OK;

  // Create LVGL event handler task
  uint32_t refresh_period_ms = lvgl_refresh_period_ms();
  const lvgl_port_cfg_t lvgl_cfg = {
    .task_priority = 4,
    .task_stack = 8192*2,
    .task_affinity = 1,                   // No CPU core affinity, or lock to core 1 (significantly improves CPU usage). Main task uses core 0.
    .task_max_sleep_ms = 7,               // This is the minimum time the LVGL task if forced to sleep, but does not cause the task to wake up early. Keep this less than timer_period_ms
    .timer_period_ms = refresh_period_ms, // Tells FreeRTOS how often to call lv_timer_handler(). 16 ms is 62.5 FPS. This should take precedence over CONFIG_LV_DEF_REFR_PERIOD.
  };
  err = lvgl_port_init(&amp;lvgl_cfg);
  if (err != ESP_OK) {
    LCD_PANEL_ERROR("LVGL port initialization failed");        // Log error if LVGL init fails
  }

  const lvgl_port_display_cfg_t disp_cfg = {
    // .io_handle = mipi_dbi_io,
    .panel_handle = _lcd_panel_handle,
    .control_handle = _lcd_panel_handle,
    .buffer_size = (RGB_LCD_H_RES * RGB_LCD_V_RES),          // LVGL buffer size
    .double_buffer = true,                                   // True is double buffering, false is single buffer
    .hres = RGB_LCD_H_RES,                                   // Horizontal resolution
    .vres = RGB_LCD_V_RES,                                   // Vertical resolution
    .monochrome = false,                                     // Color display
    .color_format = LV_COLOR_FORMAT_RGB565,                  // Color format for LVGL 9
    .rotation = {
      .swap_xy = false,                                      // No XY swap
      .mirror_x = false,                                     // No X mirroring
      .mirror_y = false,                                     // No Y mirroring
    },
    .flags = {
      .buff_dma = true,                                      // Disable DMA buffer
      .buff_spiram = true,                                   // Allocate buffer in PSRAM
      .sw_rotate = false,                                    // Disable software rotation
      .swap_bytes = false,                                   // Swap byte order for RGB565
      .full_refresh = false,                                 // Disable full screen refresh
      .direct_mode = true,                                   // Enable direct rendering mode
    },
  };

  // Sync with the VSYNC signal to avoid tearing. Also stop race conditions that will cause reboot.
  const lvgl_port_display_rgb_cfg_t lvgl_rgb_cfg = {
    .flags = {
      .avoid_tearing = true,
    },
  };

  // Inform LVGL of the display
  lv_display_t *lvgl_display = lvgl_port_add_disp_rgb(&amp;disp_cfg, &amp;lvgl_rgb_cfg);
  if (lvgl_display == NULL) {
    err = ESP_FAIL;
    LCD_PANEL_ERROR("LVGL rgb port add fail");
  }

  return err;
}

esp_err_t lcd_display_init() {
  esp_err_t err = ESP_OK;

  esp_lcd_panel_handle_t lcd_panel_handle = NULL;
  err = lcd_panel_init(&amp;lcd_panel_handle);
  if (err != ESP_OK) {
    LCD_PANEL_ERROR("lcd_panel_init() failed");
    return err;
  }

  err = lvgl_init(lcd_panel_handle);
  if (err != ESP_OK) {
    LCD_PANEL_ERROR("lvgl_init() failed");
    return err;
  }
  return err;
}

main.c

#include "lcd_panel_rgb_16bit.h"
#include &lt;freertos/FreeRTOS.h&gt;
#include &lt;freertos/task.h&gt;
#include &lt;lvgl.h&gt;
#include &lt;esp_lvgl_port.h&gt;
#include &lt;esp_private/esp_clk.h&gt;
#include &lt;esp_log.h&gt;
#include &lt;esp_timer.h&gt;
#include &lt;math.h&gt;
#include &lt;stdio.h&gt;

#define ANIMATION_DURATION_SECS 1.0 // For the text label that moves around in a circle

static int32_t L_prev_progress_100X = 0;
static int32_t L_prev_frame = 1;
static int64_t L_anim_start_time_usecs = 0;

#define MAIN_TAG "MAIN"
#define MAIN_INFO(fmt, ...) ESP_LOGI(MAIN_TAG, fmt, ##__VA_ARGS__)
#define MAIN_DEBUG(fmt, ...) ESP_LOGD(MAIN_TAG, fmt, ##__VA_ARGS__)
#define MAIN_ERROR(fmt, ...) ESP_LOGE(MAIN_TAG, fmt, ##__VA_ARGS__)

static void titleLocationCallback(void *_widget, int32_t _progress_100X) {
  float progress = _progress_100X / 100.0; // 0 .. 1
  int32_t dx = 50 * cos(2.0 * M_PI * progress);
  int32_t dy = 50 * sin(2.0 * M_PI * progress);
  lv_obj_set_pos(_widget, dx, dy); // Relative to parent alignment which is currently centered

  // Show frame number in text
  if (_progress_100X &lt; L_prev_progress_100X) {
    L_prev_frame = 1;
  } else {
    L_prev_frame++;
  }
  L_prev_progress_100X = _progress_100X;
  char text[20];
  snprintf(text, 20, "Hello: %ld", L_prev_frame);
  lv_label_set_text(_widget, text);
}

static void renderReadyCallback(lv_event_t *_event) {
  // This runs on the LVGL task thread right after flushing finishes
  lv_event_code_t code = lv_event_get_code(_event);
  if (code == LV_EVENT_REFR_START) {  // Sent before a refreshing cycle starts. Ideal for preparing/updating widget states right before rendering.
    lv_obj_t *title_label = (lv_obj_t *)lv_event_get_user_data(_event);
    // Get elapsed seconds
    int64_t curr_time_usecs = esp_timer_get_time();
    int64_t elapsed_usecs = curr_time_usecs - L_anim_start_time_usecs;
    double remainder_secs = fmod(elapsed_usecs / 1000000.0, ANIMATION_DURATION_SECS);
    double progress = remainder_secs / ANIMATION_DURATION_SECS;
    int32_t progress_100X = (int32_t)(progress * 100.0);
    // Update title
    titleLocationCallback(title_label, progress_100X);
  }
  // LV_EVENT_REFR_READY: Sent after the refresh cycle completes (after rendering + flush callback). Could be used to verify render and idle time.
}

static void createUI(void) {
  // IMPORTANT: If the lock stays active too long, the screen will shift
  char mcu_freq_text[100];
  snprintf(mcu_freq_text, 100, "MCU: %0.2f MHz", esp_clk_cpu_freq()/1000000.0);
  char lcd_display_info_text[100];
  snprintf(lcd_display_info_text, 100, "LCD: %0.2f MHz,  %0.2f ms,  %0.2f FPS", lcd_pixel_update_freq()/1000000.0, 1000.0/lcd_pixel_update_fps(), lcd_pixel_update_fps());
  char lvgl_task_info_text[100];
  uint32_t task_refresh_period_ms = lvgl_refresh_period_ms();
  snprintf(lvgl_task_info_text, 100, "LVGL Task: %lu ms,  %0.2f FPS", task_refresh_period_ms, 1000.0/task_refresh_period_ms);
  uint32_t display_refresh_period_ms = task_refresh_period_ms;
  char lvgl_display_info_text[100];
  snprintf(lvgl_display_info_text, 100, "LVGL Display: %lu ms,  %0.2f FPS", display_refresh_period_ms, 1000.0/display_refresh_period_ms);

  // Lock the LVGL mutex before creating
  lvgl_port_lock(portMAX_DELAY); // Wait max amount of time

  // Create main screen
  lv_obj_t *scr = lv_scr_act();
  lv_obj_set_style_bg_color(scr, lv_color_hex(0xFFFFFF), LV_PART_MAIN);  // Set white background

  // Create title label
  lv_obj_t *title_label = lv_label_create(scr);
  lv_label_set_text(title_label, "Hello");
  lv_obj_align(title_label, LV_ALIGN_CENTER, 0, 50);
  lv_obj_set_style_text_font(title_label, &amp;lv_font_montserrat_24, 0);

  // Animate the title
  {
    // Let the LVGL task catch up
    lvgl_port_unlock();
    vTaskDelay(pdMS_TO_TICKS(300));

    // IMPORTANT: This synchronizes with the LVGL display callback rate. Using an lv_anim would use a different timer and diverge from the callback frame rate.
    L_anim_start_time_usecs = esp_timer_get_time();
    void *user_data = title_label;
    (void)lv_display_add_event_cb(lv_display_get_default(), renderReadyCallback, LV_EVENT_REFR_START, user_data);

    lvgl_port_lock(portMAX_DELAY); // Wait max amount of time
  }

  // Let the LVGL task catch up
  lvgl_port_unlock();
  vTaskDelay(pdMS_TO_TICKS(300));
  lvgl_port_lock(portMAX_DELAY); // Wait max amount of time

  // Display some helpful stats
  {
    lv_obj_t *mcu_freq_label = lv_label_create(scr);
    lv_label_set_text(mcu_freq_label, mcu_freq_text);
    lv_obj_align(mcu_freq_label, LV_ALIGN_BOTTOM_MID, 0, -100);
    lv_obj_set_style_text_font(mcu_freq_label, &amp;lv_font_montserrat_24, 0);
  }
  {
    lv_obj_t *lcd_display_info_label = lv_label_create(scr);
    lv_label_set_text(lcd_display_info_label, lcd_display_info_text);
    lv_obj_align(lcd_display_info_label, LV_ALIGN_BOTTOM_MID, 0, -70);
    lv_obj_set_style_text_font(lcd_display_info_label, &amp;lv_font_montserrat_24, 0);
  }
  {
    lv_obj_t *lvgl_info_label = lv_label_create(scr);
    lv_label_set_text(lvgl_info_label, lvgl_task_info_text);
    lv_obj_align(lvgl_info_label, LV_ALIGN_BOTTOM_MID, 0, -40);
    lv_obj_set_style_text_font(lvgl_info_label, &amp;lv_font_montserrat_24, 0);
  }
  {
    lv_obj_t *lvgl_info_label = lv_label_create(scr);
    lv_label_set_text(lvgl_info_label, lvgl_display_info_text);
    lv_obj_align(lvgl_info_label, LV_ALIGN_BOTTOM_MID, 0, -10);
    lv_obj_set_style_text_font(lvgl_info_label, &amp;lv_font_montserrat_24, 0);
  }

  // Let the LVGL task catch up
  lvgl_port_unlock();
  vTaskDelay(pdMS_TO_TICKS(100));
  lvgl_port_lock(portMAX_DELAY); // Wait max amount of time

  // IMPORTANT: This does not wake up the LVGL task; the wakeup time is defined by lvgl_port_init().
  //            When the task does wakeup, it will check to see if this timer is ready for the callback.
  //            The LVGL display_refresh_period_ms should &lt;= the LVGL task_refresh_period_ms refresh time to make sure the display callback is always ready for an update
  lv_timer_set_period(lv_display_get_refr_timer(lv_display_get_default()), display_refresh_period_ms);

  lvgl_port_unlock();
}

void app_main(void) {
  // Init
  MAIN_INFO("Starting application...");

  // Initialize LCD hardware and LVGL
  esp_err_t err = lcd_display_init();
  if (err != ESP_OK) {
    while (1) {
      MAIN_ERROR("[%s] init failed: %s", "Hello FPS App", esp_err_to_name(err));
      vTaskDelay(pdMS_TO_TICKS(1000));
    }
  }
  MAIN_INFO("LCD display initialized");

  // Create UI
  createUI();
  MAIN_INFO("UI created");

  // Allow LVGL background event handler to process events
  MAIN_INFO("Main task going to sleep");
  while (1) {
    // Sleep and do nothing
    vTaskDelay(pdMS_TO_TICKS(10000));
  }
}
u/DitUser23 — 1 day ago
▲ 32 r/esp32

I Built most meaningful Esp32 project even until this for me

Hey everyone!

I wanted to share a project that recently became incredibly meaningful to me. I’ve been building my own DIY Tamagotchi using an ESP32-C3 and a 1.69-inch ST7789 display.

While I was right in the middle of working on the firmware, my orange cat, Tarçın, managed to run away from home. I was absolutely devastated because he’s truly the happiest part of my life. Thankfully, he showed back up at the door a day later, completely exhausted but completely safe.

That scare completely changed how I looked at this build. To celebrate him being home, I used Gemini AI to help me sketch out pixel art frames of his likeness and coded him directly into the hardware as the main character! Now, he lives on my desk as a virtual pet companion. You can feed him, play mini-games, clean up the room, and put him to sleep—just like a classic Tamagotchi.

🛠️ The Hardware Setup

  • MCU: ESP32-C3 (Handmade compact dev board layout)
  • Display: 1.69-inch ST7789 screen (240x280 resolution)
  • Inputs: 3x Tactile buttons using internal INPUT_PULLUP (no external resistors needed!)
  • Audio: A small buzzer for game alerts

💻 Make It Your Own (Completely Open Source!)

The software is written in the Arduino IDE (v2.3.10). I designed it with a highly modular layout split into specific files like character, input, display, and sprites so anyone can modify it. It’s fully open-source under the GPL-3 license.

If you want to immortalize your own pet into a handheld virtual pet, I made sure the sprite logic is straightforward to swap out. If you keep your custom animations at a 90x90 resolution, you can just convert your images to a hex matrix and paste them straight into the code!

🎥 Watch the Step-by-Step Guide & Full Demo:

https://www.youtube.com/watch?v=zm_Z1XUjkTI

💻 Grab the Schematic & Source Code on GitHub:

https://github.com/derdacavga/Esp32-Tamagotchi

(Quick tip for anyone trying to build this: If you run into flash memory limits adding tons of custom pet animations, try swapping the screen for a lower-resolution ST7735 display! It frees up massive memory for extra frame assets.)

Let me know what you think, or if you have any ideas for mini-games I should add to the next software update!

youtu.be
u/ikilim — 1 day ago
▲ 291 r/esp32

ESP32-S3 offline hiking GPS (GPX route executor + RLCD + low-power design)

I’ve open-sourced a small offline hiking GPS project built on ESP32-S3:

👉 https://github.com/chengshiwu619/hiking-gps

This is not a fitness tracker or smartwatch clone — it’s a GPX route execution device for hiking.

🎯 What it does

You load a GPX route before hiking, then the device helps you execute it in real terrain.

It focuses on answering:

  • Am I still on the route?
  • Am I deviating / how far off-route?
  • How far to finish / how much ascent remains?
  • What terrain comes next (slope / climb pressure)?

No accounts, no cloud, no online maps.

🧠 Design philosophy

  • Offline-first (fully usable without phone signal)
  • Low-power reflective LCD (ST7305 1-bit display)
  • Minimal UI, optimized for “glanceable hiking info”
  • Route execution > navigation planning

🔧 Hardware

  • ESP32-S3 (16MB Flash / 8MB PSRAM)
  • Waveshare 4.2” RLCD (ST7305, monochrome)
  • ATGM336H GPS module
  • BMP280 barometer (altitude assist)
  • EC11 rotary encoder
  • SD card (GPX + preprocessed map tiles)

🧩 Software stack

  • ESP-IDF + LVGL
  • Custom GPX parser + route profiler
  • Offline 1-bit map tiles (pre-converted)
  • Real GPS tracking with fallback simulation
  • Route matching + deviation detection

⚙️ Current status

  • Real GPS fix integrated and stable
  • GPX route execution pipeline working
  • Live metrics: distance / ascent / deviation
  • RLCD display pipeline running on real hardware
  • EC11 input + UI navigation working

📌 Project focus (important)

This is intentionally not:

  • not Garmin / Suunto competitor
  • not smartwatch OS
  • not online map system
  • not fitness/social platform

It’s a single-purpose route execution tool for hiking

If anyone is interested in:

  • ESP32-S3 real-time embedded UI systems
  • offline navigation / mapping
  • low-power LVGL applications
  • GPS + GPX processing pipelines

I’m happy to go deeper into architecture or hardware design decisions.

u/LankyEvening6102 — 2 days ago