I Built a Garmin Watch Face with Live Stock Charts in Monkey C - Here’s What I Learned
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]
Features:
- Full alphabet (A-Z, a-z, 0-9, specials)
- Proportional widths (Arial-like metrics -
iis narrow,Mis 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
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"
}
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:
- Properties are cached forever - changing
properties.xmldefaults does nothing after first run. Use “Reset All App Data” first. makeWebRequest()doesn’t work in the simulator - you need a real phone connected via Bluetooth.getSettingsView()must be implemented (even returningnull) or the settings menu stays greyed out.- No
VectorFonton 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.
Comments
No comments yet. Start the discussion.