Building a Three-Stock Ticker with ESP32 and M5Stack Core2

Building a Three-Stock Ticker with ESP32 and M5Stack Core2

Engineering a responsive, secure stock ticker with an ESP32, M5Stack Core2, PlatformIO, FreeRTOS, and Alpha Vantage

A small stock ticker is a useful embedded-systems exercise because it crosses several boundaries at once. The device must join Wi-Fi, establish a trusted TLS connection, parse remote JSON, transform time-series data, draw a stable user interface, react to input, and recover when any external dependency fails.

I built this project around the M5Stack Core2 and Alpha Vantage. Three labels at the bottom of the display map to three configurable symbols. Pressing a label's corresponding touch button fetches that stock, calculates its five-trading-day change, and redraws the graph. A top-center Wi-Fi glyph makes connection state visible without consuming the main data area.

The complete source is available in the M5Stack Core2 Stock Ticker repository.

What an ESP32 is

ESP32 is a family of systems on a chip from Espressif. A system on a chip, or SoC, combines processor cores, memory controllers, radios, and hardware peripherals on one package. Unlike a desktop processor, an ESP32 is designed to control hardware directly while operating within tight power, memory, and storage budgets.

The original ESP32 used by Core2 provides two Xtensa 32-bit LX6 processor cores running at up to 240 MHz. It also provides 2.4 GHz Wi-Fi, Bluetooth capability, GPIO, analog conversion, serial buses, timers, and hardware support useful for cryptography. This combination makes it a practical bridge between physical inputs and cloud services.

An ESP32 is not a small Linux computer. Firmware normally runs directly on the chip through an embedded framework such as Arduino or ESP-IDF. There is no virtual memory safety net, memory is finite, and blocking one task can make the whole product feel frozen. Those constraints shape architecture more than raw clock speed does.

Why M5Stack Core2

M5Stack Core2 packages an ESP32-D0WDQ6-V3 into a compact, finished device rather than a bare development board. According to the official Core2 documentation, its relevant hardware includes:

  • Dual-core Xtensa LX6 processor at up to 240 MHz
  • 520 KB SRAM, 16 MB flash, and 8 MB quad PSRAM
  • 2-inch, 320 x 240 IPS capacitive touch display
  • Three programmable touch zones presented as virtual front buttons
  • 500 mAh lithium battery with AXP192 power management
  • USB Type-C programming and power
  • BM8563 real-time clock and MPU6886 six-axis IMU
  • Speaker, microphone, vibration motor, microSD slot, and expansion connectors

The integrated display and enclosure mattered most for this project. A stock ticker needs to sit on a desk and communicate at a glance. Core2 offers enough pixels for a price, percentage change, graph, dates, status, and three controls, while remaining small enough to behave like an appliance.

The three front controls are touch regions rather than mechanical switches. M5Unified exposes them as M5.BtnA, M5.BtnB, and M5.BtnC, which gives the application a simple event model after M5.update() refreshes input state in each loop iteration.

Product behavior

The ticker starts with the left symbol selected. It connects to Wi-Fi, obtains network time, and requests market data. Each bottom label occupies the same horizontal region as its touch target:

  • Button A selects symbol_a
  • Button B selects symbol_b
  • Button C selects symbol_c

A selected symbol is highlighted. Green, red, and gray graphs represent rising, falling, and flat periods. The header shows current symbol, Wi-Fi state, and request state. When an update fails, valid existing data remains visible and is marked stale rather than being replaced by an empty error screen.

Configuration stays outside source code:

[wifi]
ssid = your-wifi-name
password = your-wifi-password

[market]
api_key = your-alpha-vantage-api-key
symbol_a = MSFT
symbol_b = AAPL
symbol_c = GOOGL
refresh_minutes = 30

A PlatformIO pre-build script validates these settings, converts symbols to uppercase, rejects unsafe values, and generates a C++ header under ignored build output. The real settings.ini is excluded from Git. Only settings.ini.default is published.

Build-time configuration prevents accidental source-control disclosure, but it does not make secrets unrecoverable. Wi-Fi and API credentials compiled into firmware may be extracted by someone with physical access to flash.

Architecture

The main loop remains an orchestrator. Networking, parsing, time-series logic, Wi-Fi state, and drawing live in separate components.

flowchart LR
    Buttons[Core2 touch buttons] --> Main[Responsive main loop]
    Wifi[Wi-Fi state machine] --> Main
    Main --> Requests[FreeRTOS request queue]
    Requests --> Worker[Market-data task]
    Worker --> HTTPS[Certificate-validated HTTPS]
    HTTPS --> Alpha[Alpha Vantage]
    Alpha --> Parser[JSON parser]
    Parser --> Series[Five-day series]
    Series --> Results[FreeRTOS result queue]
    Results --> Main
    Main --> Renderer[320 x 240 canvas renderer]

Responsive control loop

loop() calls M5.update(), processes button clicks, advances the Wi-Fi state machine, receives completed market requests, and decides whether another update is due. It ends with a five-millisecond delay. No HTTP request runs in this path.

Wi-Fi connection is also non-blocking. A connection attempt receives 15 seconds to complete. Failures enter exponential backoff, beginning at five seconds and capping at five minutes. This keeps touch handling and status rendering alive while the network is unavailable.

Background market task

HTTPS work runs in a FreeRTOS task pinned to the other ESP32 core. A one element request queue carries the selected symbol index to that task. A one element result queue returns parsed data.

Each response carries its symbol index. If a user selects another stock before the current request completes, the main loop recognizes the stale response, discards it, and immediately starts a fetch for the current selection. Tagging asynchronous work is a small design choice that prevents a subtle race: a late response must never repaint the screen with the wrong stock.

Secure and bounded networking

WiFiClientSecure validates the server with a trusted GTS root certificate. The code does not disable TLS verification. HTTP connection and stream reads use
12-second timeouts, while response bodies are capped at 64 KiB.

The response buffer prefers PSRAM and falls back to normal heap memory. JSON
parsing accepts only finite, positive close prices and recognized timestamp
lengths. The series has fixed capacity, so malformed or unexpectedly large
responses cannot grow application memory without limit.

Market-data fallback

Alpha Vantage intraday data requires premium entitlement. The client first asks for regular-market, 60-minute data. If Alpha Vantage returns its premium endpoint message, the client remembers that result and switches to daily data for later requests.

Daily fallback changes more than parsing. Free Alpha Vantage keys have a daily request budget, so fallback refreshes are clamped to at least 65 minutes. Button presses still consume calls, which the README states explicitly.

Stable rendering

The display uses a full-screen M5Canvas sprite. Each frame is drawn off-screen and pushed once, reducing visible flicker. Fixed regions reserve space for:

  • Symbol, Wi-Fi glyph, and request status
  • Price and percentage change
  • Graph and date endpoints
  • Three button labels

The graph scales values into its bounding rectangle with five percent vertical padding. Flat series receive synthetic padding so division by zero cannot occur and the line remains visible.

Hard lessons from real hardware

The most useful lessons arrived after the firmware compiled.

TLS uses serious stack space

The first network worker had a 12 KiB task stack. It looked generous until mbedTLS began certificate verification. Core2 then rebooted during every update with a FreeRTOS stack-canary panic in the market-data task.

Backtrace decoding placed the failure inside X.509 verification. The fix had two parts:

  • Increase the worker stack to 32 KiB
  • Move large request and result objects from automatic task storage to static
    storage

After flashing the change, uxTaskGetStackHighWaterMark() reported 19,488 bytes of minimum free stack. Measuring the margin on real hardware was more valuable than estimating it from source.

API plans are part of system design

The premium intraday endpoint worked as an interface but not with a free API key. Treating that response as a generic failure would leave the product unusable. Instead, the client interprets the provider's entitlement response and chooses a supported data source with a quota-safe schedule.

External-service pricing and quotas are runtime constraints. They belong in architecture, tests, UI status, and documentation, not in deployment notes alone.

Keep large payloads away from scarce internal memory

A 64 KiB response is modest on a workstation and significant on a microcontroller. Core2's 8 MB PSRAM makes bounded JSON retrieval practical, but code still needs a fallback and explicit allocation checks. PSRAM is capacity, not permission to ignore ownership and limits.

Preserve useful state during failure

Networks fail. DNS fails. APIs throttle. A display that erases a valid graph on every transient error becomes less useful than one that marks existing data as stale. Embedded resilience often means keeping the last trustworthy state while making freshness visible.

Build success is not device success

Compilation validated types and APIs. Host-side test compilation validated pure series and parser logic. Neither proved touch alignment, text fit, TLS stack margin, serial stability, or whether the graph was readable at arm's length.

The final validation loop included firmware upload, serial monitoring, physical button presses, and visual inspection. Hardware was not a release target added at the end. It was part of the test environment.

Toolchain and validation

The project uses PlatformIO with the Arduino framework, GNU++17, M5Unified, and ArduinoJson. The target is explicitly pinned to m5stack-core2 with a 16 MB partition layout and PSRAM enabled.

Typical commands are:

pio run -e m5stack-core2
pio test -e m5stack-core2 --without-uploading --without-testing
pio check -e m5stack-core2 --skip-packages
pio run -e m5stack-core2 --target upload
pio device monitor --baud 115200

Pure logic tests cover chronological ordering, five-trading-day trimming, flat ranges, percentage trends, malformed JSON, API errors, and daily timestamp formats. Device checks cover connectivity, touch mapping, screen layout, API behavior, and recovery.

What I would build next

The current firmware intentionally keeps one series in memory. Useful next steps would include per-symbol caching, a battery indicator, market-open awareness, and touch gestures for changing time windows. Persistent caching on microSD could retain charts across restarts, while the RTC could support clearer local timestamps.

Those additions should preserve the central rule learned here: keep the user interface responsive and make every asynchronous result prove that it still belongs to the state on screen.

Source and references

→ Disclaimer