Firmware for a coin jar with lights and sound, running on an ESP32. An infrared light barrier detects every coin dropped in, the RGB LEDs flash gold and two small speakers play a short chime. The firmware updates itself from GitHub Releases.
The finished jar and the electronics in its top part.
The 3D printed jar is on Printables.
Tip
Control it from your phone. The jar works with the Android app esp-control-app: coin count, LED light and colors, volume and music streamed from the phone. Download the APK from its releases, see Android app below.
- ESP32 DevKit (
esp32dev) - 5x WS2811 5 mm RGB LED in a chain, 330 Ω resistor in the data line to the first LED
- IR light barrier: IR LED and a phototransistor read by the ADC (10 kΩ resistor on the phototransistor)
- 2x MAX98357 I2S amplifier with a small speaker each, one set to the left channel, one to the right
Wiring:
| Signal | GPIO |
|---|---|
| LED data (DIN of LED 1) | 33 |
| IR LED | 32 |
| Phototransistor (ADC) | 34 |
| I2S BCLK | 26 |
| I2S LRC (WS) | 25 |
| I2S DIN | 22 |
The LED pin and count are in include/config.h. The light barrier and I2S pins are defaults in include/CoinSensor.h and include/Sound.h and can be overridden with build flags.
The LEDs use the RGB color order (NEO_RGB in src/Led.cpp). If red and green are swapped, change it to NEO_GRB.
On startup the LEDs pulse orange while the device connects to WiFi and checks for updates. Then the light barrier is calibrated and the jar waits for coins.
| Event | LEDs | Sound |
|---|---|---|
| WiFi connection and update check | pulsing orange | – |
| Light barrier does not work | 3 red blinks | – |
| Coin dropped in | gold (color can be changed in the app) while the chime plays | chime from both speakers (0.6 s) |
| Permanent light switched on in the app | each LED in its own color, brightness adjustable; effects above return to it when they end | – |
| Pairing with the Android app | a sequence of 4 colors blinking repeatedly | – |
A coin is detected when the phototransistor value drops below 60 % of the idle value. The barrier is armed again once the value is back above 80 %. The idle value slowly follows changes of ambient light. The sensor is sampled about 2000 times per second.
Each coin is also logged to the serial monitor with the coin count, the measured value and the idle value. The count is saved to NVS (Preferences, namespace coinjar) after every coin and loaded at startup, so it is kept across reboots and firmware updates.
At startup the firmware measures the phototransistor with the IR LED off and on and prints both values to the serial monitor. The difference should ideally be above 1000. Below 200 the barrier is reported as not working (3 red blinks) and a hint is printed:
| Hint | Meaning |
|---|---|
| almost zero | the phototransistor, or the IR LED, is probably reversed |
| saturated | too much light, replace the 10 kΩ resistor with 4.7 kΩ |
| small difference | check the polarity of the IR LED and the phototransistor |
The chime is two mixed tones (1320 Hz and 1760 Hz) with a short fade-out, generated directly in the firmware. Volume is set in src/main.cpp with Sound::setVolume(), the range is 0 to 32767 and the default 2000 is quiet (1000 to 6000 is a sensible range). If the left and right speakers are swapped, set SWAP_LR in src/Sound.cpp to true.
The Android app can stream music to the jar: 16-bit mono PCM at 22050 Hz over WiFi. The jar buffers about 1 s of audio in a ring buffer and a separate FreeRTOS task plays it through I2S on both speakers, so the coin detection keeps running. The volume setting applies to the music as well. While the music plays, a dropped coin only flashes the LEDs; the chime is skipped. The stream stops when the app stops it, or by itself after 3 s without data.
Note
App: github.com/davidvancl/esp-control-app – the signed APK is in its latest release. The phone and the jar must be on the same WiFi network.
The jar is controlled over the local WiFi network using the esp-device-control library. The app finds it automatically.
Coin count, LED light and sound in the app.
| Part | What the app can do | Stored in the jar |
|---|---|---|
| Coins | show the total count live, correct it, reset the counter | yes, in NVS – survives reboots, power loss and OTA updates |
| Light | switch on a permanent light, set the color of each of the 5 LEDs, brightness, color of the coin flash | no, back to defaults after a reboot |
| Sound | chime volume, play the chime | no |
| Music | stream a song from the phone to the speakers, also in the background | – |
| Diagnostics | light barrier state, current and idle value | – |
Pairing: start pairing in the app; the LEDs blink a sequence of 4 colors (red, green, blue, yellow, purple) over and over, tap the same colors in the same order in the app.
The exposed values are listed in the library's DESIGN.md.
Copy include/secrets.example.h to include/secrets.h (it is in .gitignore) and fill it in. Up to 4 networks are supported, remove the ones you don't need:
#define SECRET_SSID "wifi-name"
#define SECRET_PASS "wifi-password"
#define SECRET_SSID_2 "second-wifi-name"
#define SECRET_PASS_2 "second-wifi-password"The networks visible in a scan are tried first, in this order. On the first upload over USB all networks are saved to EEPROM. Firmware built by GitHub Actions has no secrets.h and uses the saved networks. To change them, edit secrets.h and upload the firmware over USB again.
Requires PlatformIO (CLI or the VS Code extension).
pio run -t upload
pio device monitor
The firmware prints its version, the result of the update check and the light barrier calibration to the serial monitor.
src/main.cpp–setup()andloop(), reaction to a coin, values exposed to the Android appsrc/Led.cpp– WS2811 LEDs: startup pulsing, pairing color sequence, permanent light and brightnesssrc/CoinSensor.cpp– IR light barrier, calibration, coin detection and the stored coin countsrc/Sound.cpp– I2S output, the chime and the music stream from the appinclude/config.h– LED pin, count, brightness and pulsinginclude/WifiCredentials.h– usessecrets.hif it exists
Uses the esp-ota-updater library. On startup the device checks the latest release and updates itself if a newer version exists.
Releasing a new version:
- Increase
custom_versioninplatformio.ini. - Commit and push to
main. - GitHub Actions publish a release. The device updates after its next restart.
Do not push a locally changed version in platformio.ini unless you want to publish a release.




