DEV Community

Sam Hartley
Sam Hartley

Posted on

I Built a Garmin Watch Face with Live Stock Charts in Monkey C — Here’s What I Learned

I wanted stock prices on my wrist. Not a notification — an actual chart. So I built a custom Garmin watch face with live sparkline charts for 5 configurable assets.

Here’s what I learned building StockFaceTC for the Garmin Venu 2 Plus.

The Challenge

Garmin watch faces are tiny — 124KB memory, 416×416 AMOLED display, and a language (Monkey C) that most devs have never heard of. No npm. No frameworks. No Stack Overflow answers.

Oh, and the “simulator” can’t make web requests. You test API calls on real hardware or not at all.

What I Built

A watch face showing:

  • Time, date, and health stats (heart rate, steps, floors)
  • 5 polar sparkline charts in an outer ring — each showing 24 hours of price data
  • Auto-updates every 15 minutes via Twelve Data API
  • 6-hour weather forecast with custom-drawn icons
  • Fully configurable tickers via the Garmin Connect app

Default tickers: AAPL, TSLA, Gold, BTC/USD, ETH/USD

The Font Problem (and How I Solved It)

Garmin’s VectorFont API isn’t available on all devices. The Venu 2 Plus? Nope.

So I built my own: PrimitiveFont — a complete vector font library using only drawLine() calls. Every character is defined on a 5×7 grid and rendered procedurally.

// “A” on a 5×7 grid
CHAR_A = [0,6, 0,2,  0,2, 2,0,  2,0, 4,2,  4,2, 4,6,  0,4, 4,4]
Enter fullscreen mode Exit fullscreen mode

Features:

  • Full alphabet (A-Z, a-z, 0-9, specials)
  • Proportional widths (Arial-like metrics — ‘i’ is narrow, ‘M’ is wide)
  • Bold, italic, underline
  • Arc text — characters curved along a circular path (for the ring labels!)
  • Any size via simple scaling: sizePx / 7.0

The entire library is ~21KB — well within the 124KB budget.

Architecture

Garmin Background Service (every 15 min)
  → Twelve Data API (1h candles × 24 = full day)
  → Phone acts as transparent HTTP proxy
  → JSON parsed in background thread (64KB limit!)
  → Prices + display names stored in model
  → View renders sparklines + arc labels on update
Enter fullscreen mode Exit fullscreen mode

Key Constraint: 64KB Background Memory

The background service that fetches data runs in a separate 64KB sandbox. You can’t access the main app’s memory. The only way to pass data: Background.exit(dictionary), which the main app receives in onBackgroundData().

This means: parse JSON, extract only what you need, pass a minimal dictionary. No room for raw API responses.

Dynamic Labels from API

One early mistake: I hardcoded display names like “XAU/USD” → “GOLD”. That doesn’t scale.

Better approach: The Twelve Data API returns metadata with every response:

“meta”: {
    “symbol”: “XAU/USD”,
    “currency_base”: “Gold Spot”,
    “type”: “Precious Metal”
}
Enter fullscreen mode Exit fullscreen mode

Now labels come from the API itself:

  • Stocks: meta.symbol → “AAPL”
  • Crypto: meta.currency_base → “Bitcoin” → “BITCOIN”
  • Commodities: meta.currency_base → “Gold Spot” → “GOLD”

Zero hardcoded aliases. Add any ticker, get the right label automatically.

Simulator Gotchas

If you’re building Connect IQ apps, save yourself some pain:

  1. Properties are cached forever — changing properties.xml defaults does nothing after first run. Use “Reset All App Data” first.
  2. makeWebRequest() doesn’t work in the simulator — you need a real phone connected via Bluetooth.
  3. getSettingsView() must be implemented (even returning null) or the settings menu stays greyed out.
  4. No VectorFont on many devices — if you need custom text, roll your own like PrimitiveFont.

API Budget

Twelve Data free tier: 800 API calls/day.

My budget: 5 symbols × 96 fetches/day (every 15 min) = 480 calls. Well within limits.

The Code

Source is on GitLab. Feel free to fork it, break it, or build something cooler.


What’s the weirdest hardware you’ve built for? Drop it in the comments.

Top comments (0)