Gold price alarm using ESP32-C3 0.42 OLED display – MicroPython

Why a Gold Price Alarm?

One day, I was looking at a gold price chart and noticed many fluctuations. I wanted to be notified of these changes so that I could buy gold at the bottom and sell it at the top. I needed a device like a gold price alarm, that checks the price at regular intervals and alerts me when the price goes above or below my target price.

Title in Other Languages

  • German: Goldpreis-Alarm ESP32-C3 0,42-Zoll-OLED-Display
  • French: Alarme de prix de l’or ESP32-C3 Écran OLED de 0,42 pouce
  • Spanish: Alarma de precio del oro ESP32-C3 Pantalla OLED de 0,42 pulgadas
  • Italian: Allarme prezzo dell’oro ESP32-C3 Display OLED da 0,42 pollici
  • Portuguese (Brazil): Alarme de Preço do Ouro ESP32-C3 Display OLED de 0,42 polegadas

Tip: Right-click anywhere on this page and choose “Translate to your language”.

Disclaimer

“This project is for educational purposes only and is not financial advice.”

Why did I choose the ESP32?

The ESP32 is cheap, has built-in Wi-Fi, and works well with MicroPython, so it is a perfect choice for this project. It comes in many models and variants, so you can choose the one that fits your needs.

Introduction

In this project, we will use an ESP32-C3 to get the live gold price from a web API, process the data, and decide whether to sound the alarm.

Parts list

  • ESP32-C3 board with a built-in 0.42-inch OLED display (to show the live price)
  • 2N2222A(plastic TO-92) NPN BJT transistor
  • 3.3 V active buzzer
  • Tactile switch to mute the alarm
  • LED (optional). I used the built-in LED on GPIO8.
  • 10 kΩ resistor for the transistor base
  • Breadboard
  • Wires
  • Flyback diode across the buzzer (optional). It is needed only with an electromagnetic buzzer. You can use a 1N4007, 1N4148, 1N5819, or BAT42.

Pin table

Component ESP32-C3 pin Notes
Buzzer (via 2N2222A) GPIO10 Drives the transistor base through a resistor
Mute switch GPIO3 Other side to GND, internal pull-up enabled (confirm this matches your code)
OLED SDA GPIO5 I²C, built into the board
OLED SCL GPIO6 I²C, built into the board
Built-in LED GPIO8 Active low on my board (confirm for yours)

Getting the gold price

I use a free price API over HTTPS. This is the most important design decision, because free APIs have request limits and can change their terms, so check the current terms before you use it. The price is fetched from https://api.gold-api.com/price/XAU as a JSON response.

Gold Price Alarm ESP32-C3 0.42 Inch OLED display work flow

How the Code Works (Step-by-Step)

Click here to Show MicroPython code

from machine import (Pin, I2C, deepsleep, reset_cause,
                     PWRON_RESET, HARD_RESET, WDT_RESET, DEEPSLEEP_RESET, SOFT_RESET)
from time import sleep_ms, ticks_ms, ticks_diff
from Secrets import SSID, PASSWORD
import esp32
import micropython
import ssd1306
import urequests
import network
import gc

micropython.alloc_emergency_exception_buf(100)   # readable errors inside IRQs

# ---------------- Configuration ----------------
PRICE_URL = 'https://api.gold-api.com/price/XAU'
HIGH_LIMIT = 4130.0         # alarm when price >  this value
LOW_LIMIT = 4000.0          # alarm when price < this value FETCH_INTERVAL = 300000 # ms in deep sleep between price requests ALARM_DURATION_MS = 10000 # how long the alarm beeps WIFI_TIMEOUT_MS = 15000 SHORT_MAX_MS = 500 # shorter than this = short press LONG_MIN_MS = 3000 # this or longer = long press IGNORE_AFTER_MS = 300 # ignore bounce events after a press finished # ---------------- Startup info ---------------- RESET_CAUSES = { PWRON_RESET: "Power on / brownout", HARD_RESET: "Hard reset / crash", WDT_RESET: "Watchdog", DEEPSLEEP_RESET: "Wake from deep sleep", SOFT_RESET: "Soft reset", } print("Reset cause:", RESET_CAUSES.get(reset_cause(), reset_cause())) gc.collect() print("Free RAM:", gc.mem_free()) # ---------------- Hardware ---------------- i2c = I2C(0, scl=Pin(6), sda=Pin(5)) oled = ssd1306.SSD1306_I2C(72, 40, i2c) # 0.42" OLED: 9 chars x 5 lines alarm_pin = Pin(10, Pin.OUT, value=0) # buzzer or LED Button = Pin(3, Pin.IN, Pin.PULL_UP) # pressed = 0 # Deep sleep wake-up works on a level, so wake when the pin is LOW esp32.wake_on_gpio((Button,), esp32.WAKEUP_ALL_LOW) # ---------------- State ---------------- alarm_muted = False # toggled by a short press alarm_stop = False # set by a long press: end the alarm now busy = False # a button press is being handled last_end = 0 # time when the last press finished # ---------------- Helpers ---------------- def oled_print(text): """Print text on the OLED, wrapping every 9 characters (max 5 lines).""" oled.fill(0) for line in range(5): chunk = text[line * 9:(line + 1) * 9] if not chunk: break oled.text(chunk, 0, line * 8) oled.show() def go_to_sleep(): """Turn off the outputs and enter deep sleep. Never returns.""" alarm_pin.off() # Uncomment the line below to turn off the display during deep sleep. # This saves a little more battery power. # oled.poweroff() deepsleep(FETCH_INTERVAL) # ---------------- Button handling ---------------- def on_short_press(): global alarm_muted alarm_muted = not alarm_muted print("Short press: alarm", "muted" if alarm_muted else "active") def on_long_press(): global alarm_stop alarm_stop = True print("Long press: alarm ended") def BTN_PRESS_FUNC(pin): """Runs outside the interrupt (scheduled), so sleep and print are safe.""" global busy, last_end alarm_pin.off() # no constant tone while the button is held start = ticks_ms() long_detected = False while pin.value() == 0: # measure press time if ticks_diff(ticks_ms(), start) >= LONG_MIN_MS:
            long_detected = True
            break
        sleep_ms(10)

    if long_detected:
        on_long_press()
        while pin.value() == 0:                   # wait for release
            sleep_ms(20)
    else:
        duration = ticks_diff(ticks_ms(), start)
        if duration < SHORT_MAX_MS:
            on_short_press()
        else:
            print("Ignored:", duration, "ms")     # between 500 ms and 3 s

    sleep_ms(50)                                  # let the release bounce finish
    last_end = ticks_ms()
    busy = False


def BTN_PRESS_HANDLER(pin):
    """IRQ handler: must be very short. It only schedules the real work."""
    global busy
    if busy or ticks_diff(ticks_ms(), last_end) < IGNORE_AFTER_MS:
        return                                    # bounce or stale event
    busy = True
    try:
        micropython.schedule(BTN_PRESS_FUNC, pin)
    except RuntimeError:                          # schedule queue is full
        busy = False


Button.irq(handler=BTN_PRESS_HANDLER, trigger=Pin.IRQ_FALLING)


# ---------------- Alarm ----------------
def run_alarm():
    """Beep for ALARM_DURATION_MS. Short press = mute/unmute, long press = end."""
    start = ticks_ms()
    while ticks_diff(ticks_ms(), start) < ALARM_DURATION_MS and not alarm_stop:
        if alarm_muted:
            alarm_pin.off()
        else:
            alarm_pin.value(not alarm_pin.value())   # toggle = beep
        sleep_ms(200)
    alarm_pin.off()


# ---------------- Wi-Fi and price ----------------
def connect_wifi(ssid, password, timeout_ms=WIFI_TIMEOUT_MS):
    """Connect to Wi-Fi. Return True on success, False on timeout."""
    wlan = network.WLAN(network.WLAN.IF_STA)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(ssid, password)
        oled_print("Connecting wifi!!!")
        print("Connecting wifi!!!")

    start = ticks_ms()
    while ticks_diff(ticks_ms(), start) < timeout_ms: if wlan.isconnected(): print("Wi-Fi connected:", wlan.ifconfig()) oled_print("Online!") return True sleep_ms(500) print("Wi-Fi not found!") return False def find_key(obj, target_key): """Recursively search for a key in a nested dict or list.""" if isinstance(obj, dict): if target_key in obj: return obj[target_key] items = obj.values() elif isinstance(obj, list): items = obj else: return None for item in items: result = find_key(item, target_key) if result is not None: return result return None def get_gold_price(url=PRICE_URL, target_key='price'): """Return the gold price as float, or None on any error.""" response = None try: gc.collect() # free RAM before the TLS connection response = urequests.get(url) if response.status_code != 200: print("HTTP error:", response.status_code) return None price = find_key(response.json(), target_key) if price is None: print("Key not found:", target_key) return None price = float(price) print("Gold Price (XAU/USD):", price) return price except Exception as e: print("Error fetching price:", e) return None finally: if response is not None: response.close() # ---------------- Main program ---------------- # Deep sleep ends with a reset, so this code runs once per wake-up. try: if connect_wifi(SSID, PASSWORD): price = get_gold_price() if price is not None: alarm_on = price > HIGH_LIMIT or price < LOW_LIMIT
            oled_print('GoldPrice{:9.2f}H{:8.2f}L{:8.2f}Alarm:{}'.format(
                price, HIGH_LIMIT, LOW_LIMIT, 'ON' if alarm_on else 'OFF'))
            if alarm_on:
                run_alarm()
                if alarm_stop:
                    oled_print("Alarm off")      # visitor ended the alarm
                    sleep_ms(2000)
            else:
                sleep_ms(5000)                   # keep the values visible for 5 s
        else:
            oled_print("Price error")
            sleep_ms(3000)
    else:
        oled_print("No Wi-Fi!")
        sleep_ms(3000)
    go_to_sleep()
except KeyboardInterrupt:
    print("KeyboardInterrupt occurred!")
  

1. Hardware Initialization

At boot, the script identifies why the ESP32 woke up (e.g., initial power-on or waking up from a deep sleep). It then sets up the hardware:

  • I²C Display: Initializes the 0.42″ OLED screen (72×40 pixels) using pins GPIO 6 (SCL) and GPIO 5 (SDA).
  • Buzzer/LED: Configures GPIO 10 as an output to control the alarm tone.
  • Wake-Up Button: Sets up GPIO 3 with a pull-up resistor. Crucially, it instructs the ESP32 to monitor this pin so that pressing the button can instantly wake the chip from its deep sleep.

2. Battery-Saving Deep Sleep Loop

Deep sleep terminates the script and powers down most of the ESP32 internal components. When the FETCH_INTERVAL timer (set to 5 minutes) expires, or when the button is pressed, the chip resets and executes the script from the top down. This cycle dramatically extends battery life for portable setups.

3. Wi-Fi and API Network Requests

Once awake, the ESP32 connects to your local Wi-Fi network using credentials stored in a separate Secrets.py file. After securing a connection, it makes an HTTP GET request to https://api.gold-api.com/price/XAU . It parses the incoming JSON data to isolate the live spot price of Gold (XAU) in USD.

4. Smart Display Logic (oled_print)

Because a 0.42″ display is incredibly small, the oled_print function splits strings into chunks of 9 characters and wraps them cleanly across 5 lines. It displays the active gold price, your high/low target limits, and whether the alarm is currently triggered.

5. Non-Blocking Button Handling (Short vs. Long Press)

To prevent the user interface from freezing during an alert, button presses are handled using Hardware Interrupts (IRQs) and MicroPython’s asynchronous scheduler (micropython.schedule).

  • Short Press (< 0.5s): Mutes or unmutes the active buzzer tone.
  • Long Press (> 3.0s): Forces the active alarm sequence to stop immediately.
  • Debouncing: Built-in timers ignore rapid electrical noise (bounce) from the mechanical switch.

* For duration between 0.5s and 3s the button press will be ignored.

6. The Main Execution Logic

Every time the ESP32 runs its main cycle, it follows this automated decision tree:

  • Connect to Wi-Fi → Fetch the gold price.
  • Evaluate Limits: Checks if the gold price is above HIGH_LIMIT (e.g., $4130.0) or below LOW_LIMIT (e.g., $4000.0).
  • Trigger Alarm: If a limit is breached, the run_alarm() function toggles the buzzer pin to create a pulsing beep for 10 seconds.
  • Sleep: If the price is normal, it holds the data on the screen for 5 seconds so you can read it, then shuts down into deep sleep for 5 minutes. If a network or API error occurs, it displays a quick error message before sleeping to retry later.

FAQ

1. Why is a transistor needed?

  • For Electromagnetic buzzers: They draw 30–100 mA, which exceeds the recommended 20 mA limit of an ESP32 pin and can damage it . They also produce dangerous high-voltage spikes when turned off. A transistor acts as a rugged buffer to protect the microcontroller.
  • For Piezoelectric buzzers: They require higher voltages (5-12 V) to achieve full volume. Because the ESP32 only outputs 3.3 V, a transistor is needed to let the low-voltage ESP32 safely switch a louder, higher-voltage power source.

2. Why is a flyback diode needed with an electromagnetic buzzer?

An electromagnetic buzzer contains an inductive coil that creates a destructive high-voltage spike (back EMF) whenever it is turned off.

A flyback diode is needed to safely redirect and dissipate this voltage spike, preventing it from destroying your driving transistor or microcontroller.

3. What is the I²C address of the OLED display?

The I²C address is not explicitly written in the code snippet because it is relying on the library’s default address value.

In MicroPython’s standard ssd1306 library, the addr parameter is optional and defaults automatically to 0x3C. My OLED display uses an SSD1306-compatible driver.

Because we didn’t pass a 4th argument, the library processes it using its default definition: SSD1306_I2C(width, height, i2c, addr=0x3C).

Troubleshooting

If you tried this project and there was any problem, do not hesitate to ask in the comments. We will be happy to help you solve it.

Possible improvements

In the future, I may add a web server to set the gold price criteria.

Download section

“This project is for educational purposes only and is not financial advice.”

Click on the button below to get MicproPython and fritzing files

Password of the file: microDIYpro.com

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top