Last spring I got annoyed enough at constantly pulling out my phone to check stock prices that I decided to just... put them on my watch.
Not as notifications. Not as a widget. A proper watch face with live sparkline charts for 5 configurable assets — crypto, stocks, commodities — updating every 15 minutes automatically.
Here's what building StockFaceTC for the Garmin Venu 2 Plus actually looked like.
The Language Nobody Talks About: Monkey C
First thing you'll notice: there's basically no community around Garmin development compared to iOS or Android. Stack Overflow has maybe 200 questions. The official forums are a ghost town. The documentation has dead links.
The language itself — Monkey C — is a mix of Java and JavaScript with Garmin-specific APIs bolted on. It's fine. The runtime constraints are the real challenge:
- 124KB total memory for the watch face
- 64KB separate sandbox for background services
- No npm, no frameworks, no familiar tooling
The simulator is decent but has one critical limitation: it can't make real web requests. You test your API calls on real hardware connected to a phone via Bluetooth, or you don't test them at all.
The Font Problem That Cost Me a Week
I wanted custom text for the sparkline labels. Garmin has a VectorFont API for this.
The Venu 2 Plus doesn't support it.
Neither do most Garmin devices, it turns out.
So I built PrimitiveFont — a complete vector font rendered entirely with drawLine() calls. Every character is defined on a 5×7 grid:
// "A" on a 5×7 grid
const CHAR_A = [0,6, 0,2, 0,2, 2,0, 2,0, 4,2, 4,2, 4,6, 0,4, 4,4];
Multiply by scale factor (sizePx / 7.0) and you have arbitrary text at any size.
I went further and added proportional widths (so "i" is narrow, "M" is wide), bold, italic, and — the fun one — arc text. Characters curved along a circular path for the ring labels around the chart.
The whole library is ~21KB. Fits comfortably.
The 64KB Background Sandwich
Here's the architecture problem: Garmin watch faces have a background service that runs separately from the main app, in its own 64KB memory sandbox. It can't share memory with the main face directly.
The only data transfer mechanism: Background.exit(dictionary) — pass a dictionary out, receive it in onBackgroundData() in the main app.
This means you can't just parse a full API response and hand it over. You have to:
- Fetch the data
- Extract only what you need (closing prices for 24 candles × 5 symbols)
- Pack it into the tightest dictionary you can
- Exit
My fetch loop handles 5 symbols sequentially, parses the JSON inline, and builds a compact model before exiting. The background service uses maybe 40KB peak. Close enough.
Getting Labels Right (Not by Hardcoding Them)
Early version I hardcoded display names: "XAU/USD" → "GOLD". Obviously terrible.
The Twelve Data API returns metadata with every response:
{
"meta": {
"symbol": "BTC/USD",
"currency_base": "Bitcoin",
"type": "Digital Currency"
}
}
So now:
-
Stocks →
meta.symbol→ "AAPL" -
Crypto →
meta.currency_base→ "Bitcoin" → "BITCOIN" (truncated to fit) -
Commodities →
meta.currency_base→ "Gold Spot" → "GOLD"
Zero hardcoded aliases. Add any valid ticker, get the right label. Works for everything in their catalog.
Budget Arithmetic
Twelve Data's free tier gives you 800 API calls/day.
My setup: 5 symbols × 4 fetches/hour × 24 hours = 480 calls/day.
Fits with 320 to spare. No paid tier needed.
Gotchas I Wish Someone Had Documented
Properties are cached forever. Changing defaults in properties.xml after the first install does nothing. You have to "Reset All App Data" in the simulator menu. Spent an embarrassing amount of time on this.
makeWebRequest() is stubbed in the simulator. It runs without errors, triggers the callback with null data, and you wonder why your code is broken. It's not — you just need a real device.
getSettingsView() must be implemented. Even if you return null. Otherwise the settings option in Connect app stays greyed out and you'll waste 30 minutes thinking it's a permissions issue.
The simulator's memory limits aren't accurate. It'll run code that crashes on device. Always test on hardware before you call it done.
The End Result
The watch face has been on my wrist every day for about 4 months now. The sparklines are genuinely useful — you can see at a glance whether BTC had a rough night or AAPL is doing something interesting.
The hardest part wasn't the code. It was the documentation gaps and the "you'll just have to test on device" reality of Connect IQ development.
But it runs. Updates every 15 minutes. Uses a free API tier. And cost exactly $0 beyond the hardware I already owned.
If you're thinking about building a Garmin app: the platform is rough around the edges, but it's real code running on real hardware on your wrist. That's genuinely satisfying in a way that web apps aren't.
The source is on GitLab if you want to poke around. Happy to answer questions in the comments — there aren't many of us in the Garmin dev community, might as well help each other.
Top comments (0)