I wrote this post to gather my ideas on the development of a simple app and to help anyone else wanting to learn more about Monkey C and developing apps for Garmin devices. AI was not used in the writing of this blog post but was extensively used when coding the project.
Introduction
This summer I bikepacked across Germany with a friend, starting at the Dutch boarder and ending up in Poland.
It was a great trip that had us going through beautiful historic Hanse cities (shoutout to Wismar), great coastal scenery (shoutout to Rügen) and what I unaffectionately call the AfD heartlands (no shoutout here). Think of the AfD as a sort of German MAGA which has, troublingly, gained an increasingly larger portion of the vote in the past few years. No where is this more visible than in the former GDR, a large part of our trip; we found ourselves going through towns and localities with a 40%+ AfD voting record in the parliamentary elections earlier this year. If you look at the map of the election results the borders of the GDR are pretty clear to see.
While cycling an idea popped up: wouldn't it be nice to have a way to quickly check how the AfD had done in the last elections? We were taking out our phones, opening google maps to see the name of the town and searching on the internet for the results. All of this was a pain and stopped whatever flow we might be in, not to mention could be dangerous if we didn't stop. And we had to know. The good thing is that we, as many cyclists today, were equipped with bike GPS computers. These small devices allow people to follow pre-planned routes, track the route you've done, average speed etc. One of the more popular brands - Garmin - even allows for adding widgets or applications. It was then that the idea was born - have an app that tells you where you are and how many neo-nazi's were around.
The monkey business starts
As we started to think about how the app would work we didn't realize that the Garmin apps use a language called Monkey C. You've probably never heard of Monkey C and at the time of writing it doesn't even have a Wikipedia page; the closest you'll get is CodeMonkey. I am still pretty surprised that Garmin ultimately chose to make their own programming language, which, as far as I understand, is based on Java, C++ and Javascript. I am not fully convinced that it makes sense as it makes it harder to grow the ecosystem and makes it less attractive to develop apps for Garmin devices. And actually if you look at the Garmin app store ([Connect IQ}(https://apps.garmin.com/)) you'll see that most of the apps are either watch faces or apps with no ratings or downloads; to call this store dead is an understatement. But there do seem to be some justifications for Monkey C such as working with ultra limited RAM and having ultra low battery consumption. I'll stop questioning Garmin's programming language choice as the honest truth is I don't know enough to to know if it makes sense or not - Garmin made the choice and that's the hand I've been dealt if I want to make this silly app. I will say it is hard to take it seriously when the logo looks like it does and when there are .jungle files and you can make "Monkey Barrels".
Using Monkey C and vibe coding
So once I got back from the trip I booted up my computer, downloaded the SDK and quickly realized that vibe coding a language with pretty much no significant online footprint was going to be tricky. Also with pretty much zero Java knowledge, some obvious things seemed more difficult than they were. The documentation isn't always amazing but I would say that with a mix of vibe coding and documentation checkups you can get along. The hallucinations are pretty incredible and I think this also comes from the fact that Monkey C doesn't have a lot of things you would expect a programming language to have. For example there is no CSV or JSON handling and as far as I could find there wasn't even a .split()
equivalent to split a string and above all there are very little examples of code or blog posts about Monkey C.
Monkey C overview and getting started
Just a heads up in case it's not obvious, I'm no expert and I may get things wrong, this post is just my understanding of how things work.
It's pretty easy to get started with Monkey C, the SDK is easy to download and works super well with Visual Studio - to be fair to the Garmin team this was a really easy process and I was able to get started pretty quick which hopefully compensated for all the walls I would run into. When you first start a MonkeyC project using the SDK you're immediately given a series of choices that will then be translated to your manifest file. The manifest file is an XML file, which has a GUI provided by the Garmin SDK that makes it easy to edit. Here are the choices I made:
- App type: Widget
- Minimum supported API: 3.2.0 (seems like a standard)
- Products supported: Edge devices (the bike computer family)
- Permissions: You can edit this later, I found out that I had to tick
Positioning
to be able to access the GPS position.
The generated file in a new project will have a .mc
file extension and is divided into 5 sections. This is my understanding of how you should split your code in general:
Name | Purpose |
---|---|
function intialize() |
Initializes the app. I just enable the view |
function onLayout() |
Not really sure what this is for, seems like you can get some custom layouts loaded if you want. I didn't touch this |
function onShow() |
The code here will run when you bring the app to the front |
function onUpdate() |
How you update the screen |
function onHide() |
What the app does when you close it |
After some time fiddling around I finally understood how to make my first hello world by using dc.drawtext
within the onUpdate function.
Wanna make an app?
With everything in place now was it was time to start. In my mind the process of developing looked something like this:
1) Get the GPS location and test it with a GPX file
2) Get a polygon map of Germany with every municipality
3) Get the election results per municipality and cross reference that with the municipality list
4) Set a timer to check every now and then in which polygon I was, get the election results and display it on the screen
Seems pretty easy right? Well I was pretty close but I overestimated the memory capabilities of these devices and I expected that the polygon check would be easier to implement than it was. Don't forget, there is pretty much nothing written for Monkey C, this isn't like python where you can just look for a library, and while the vibe coding gets you far it's exactly in this kind of situation where it'll say something like "Ah I get it now, you want to see if your coordinates are inside the polygon.". Then you get offered some lines of code that looks like this:
import Toybox.PolygonCheck
function checkPolygon(latitude, longitude, polygonList){
var polygon;
var position = [latitude, longitude];
for( var i = 0; i < polygonList.length(); i += 1){
if (position.isInPolygon(polygonList[i]){
polygon = polygonList[i]
}
}
return polygon;
}
Needless to say none of PolygonCheck
doesn't exist and the function isInPolygon
is nothing more than a pipe dream.
But I am getting ahead of myself. Let's start at point 1.
Get the GPS location
As I mentioned earlier I had to alter the permission to allow the app to access the GPS. Then I started the tracking of the GPS location in the intialize()
part of the code. I went for LOCATION_CONTINUOUS
since that will allow me to check, well, continuously.
Position.enableLocationEvents(Position.LOCATION_CONTINUOUS, method(:onPosition));
The second variable I pass to the function is the method being called when the position is updated - so it'll fire whenever a new position is read.
My onPosition
function looks like this:
function onPosition(info as Position.Info) as Void{
if (info.position != null) {
myLocation = info.position.toDegrees();
} else {
System.println("GPS position is null");
}
}
Here I am basically assigning the coordinates to my global variable myLocation
so that it can be used elsewhere in the program asynchronously. I used quite a few global variables, not sure if it's good practice but it worked.
Get a polygon map of Germany
As I said earlier my first instinct was to go for polygon map and check whether my coordinates were inside each polygon. I found one here and it seemed pretty good, but there was one glaring problem. The file was massive, over 14.000KB once uncompressed. For an app, that according to Garmin should be less than 1MB, this wasn't looking good. I thought about compressing the file, shaving the polygons but this all seemed like a lot of work to probably end up with a file that was still too large. I could use API calls but I wanted a self contained app and wasn't interested in making a backend. In parallel I found that there was a way to check if you are in a polygon, using the ray casting algorithm, but it looked like it could take a lot of compute time for the roughly 11k municipalities.
So I decided to take another route. I figured that actually you just want to know which town you're closest to, so I found a dataset that had just that; the coordinates of every municipality along with other information that would later come in handy such as the population and the state or Bundes as they are called (big thanks to Ajay Patil). I figured at this point that I might run into more problems with this memory issue, but more on that later.
The new idea was to run through all the municipalities (or gemeinden in German) and see which one was closer. I thought about converting to a sphere coordinates and doing it "properly" but ultimately decided to leave it and just do a very simple and lazy check:
function distanceToCurrent(lat1, lon1, lat2, lon2) {
var dLat = lat1 - lat2;
var dLon = lon1 - lon2;
var distance = Math.sqrt(dLat*dLat + dLon*dLon);
return distance;
}
The reason this doesn't work perfectly is two fold:
- This assumes a flat world, but since the earth is round you might have a straight line closer to one town but another might be closer on the sphere.
- Latitude and Longitude aren't quite the same, as in, one degree of each isn't equal to the same distance.
Ultimately I predicted that the cities and towns were pretty close and that straight line math should be good enough. After doing some testing it looked like it was.
Degree | Distance/degree (roughly) |
---|---|
Latitude | 111Kms |
Longitude | 89Kms |
Regarding testing, I don't actually own a Garmin and I am not based in Germany. This makes it hard to test. The good thing is that the SDK has an emulator for all devices. The second is that the emulator allows you to feed a GPX file in and simulate a journey. Having just cycled through Germany, I had recorded enough data to be able to test extensively. All you have to do after building and running (press ctrl + shift + p and choose a device) is choose Simulation
-> Activity Data
and you'll get the following GUI pop-up. You can load in a GPX, click play and it will be like you're back on the bike. Well not really but you get the point.
Get the election results
This is the step I thought would be super easy. And to be fair it was, but the data I got wasn't as clean as I had hoped. The results for the german elections can be found here (big up the bundeswahlleiterin for providing the results). The issue with the data is that it wasn't per gemeinde, it was per wahlbezirk which translates roughly into an electoral district.
As you can see in the CSV file Husum, Stadt
has many entries. To do all of the matching and checking I switched to python which was a pleasant change of pace to vibe code with; the code worked (mostly) out of the box and there were much less hallucinations than with Monkey C. Also I actually understand python so it made it easier to follow. There are some interesting curiosities in the data cleaning that I did:
- Some gemeinde returned 0% when it seemed pretty improbable due to the size of the city (ex. Aachen). This is because, as far as I understand, AfD didn't run there for the Erststimmen but only for the Zweistimmen. I ended up only looking at the Erststimmen or first votes. The TL;DR is that in Germany you vote twice for the bundestag, once for a direct local candidate and then one for the general parliament. I'm sure it's more complicated than that but let's keep it at that.
- Berlin and Hamburg, likely due to their size, were counted as multiple different places. For these 2 I decided just to group everything together. At some point this data set and the one with the coordinates were going to have to be merged.
- Some gemeinden have the same name and in the processing further down the line could get mixed up. Mölln in Schleswig-Holstein and Mölln in Mecklenburg-Vorpommern have very different voting results. So double checking the Bundes would be good to see if we were looking at the same place or not. What if there were 2 places with the same name and same Bundes I hear you ask? I had already decided that checking the distance as if it was a flat surface was good enough and I was willing to take another shortcut.
So after some fiddling and cross checking and changing some names here and there (Delve
, the layman's name, won't match with Delve (einschl. Wallen)
the official name of the wahlbezirk) I was able to get data ready to be put into the app. I had [cityName, latitude, longitude, population, AfD%]
At this stage I was already looking to reduce the size of the data so I had already reduced the coordinates to 3 decimal points. To get that data into the app wasn't that convenient, there is no built in CSV or JSON parser, so I just had to set the data as a variable and load it in. The app was crashing at this point and the reason was simple: not enough RAM. I took some more steps to decrease file size:
- Reduced the number of gemeinden from 11k to roughly 6k. Not groundbreaking engineering I'll admit. I ranked the gemeinden by population and chose the largest ones.
- Then I changed the coordinate system. The Δlat and Δlong of Germany are both less than 10 which means representing it by 47.718 could be done with one less integer - 0.178. I also joined the coordinates into one number with the first 4 digits from one and the last 4 from the other. So something like (9.654, 47.718) became (96547718).
- I joined the cityName to population and AFD%. I was unsure if it was the number of variables being loaded or the actual data being loaded that was crashing it. I did get the file size down but I still haven't been able to get all the data working in one go. End result looks like this:
To add this data I made a module and imported it into the main file - the module is called Cities.mc. I would still like to have all the gemeinden so if anyone has an idea on how I can (easily) make the data available, I'd love to hear it.
Setting a timer
A timer starts on the onShow
and goes off every 5 seconds. It goes through all the gemeinde, checks the closest gemeinde and then updates the screen with that information. It stops when the app is hidden.
function onShow() as Void {
myTimer = new Timer.Timer();
myTimer.start(method(:onTick), 5000, true);
}
When the app is hidden, other than stopping the timer I also set large variables to null
to clean up the memory usage (does this make a difference? I don't know but I think so).
Wrapping everything up
So the app at this stage works pretty well and I asked my friend back in Germany to test it and got feedback that it was working. I polished it up a bit and this is what it looks like. In this example I just pulled the GPX file backwards and forwards to trigger new municipalities. I then upload it and hoped I would get approved by the Garmin gods; I wasn't 100% sure that my app was following the guidelines. It is now approved and you find the app here. The code is here
So what did I learn while making this project?
- Overall I am not a fan of Monkey C. I believe Garmin could do better to support developers, namely with more sample code and examples.
- Be very careful with what you're pasting. I copied and pasted so much stuff without checking it properly. I 100% believe that someone is inevitably going to run a bash script with a
sudo rm -rf /*
. I also think that Slopsquatting is inevitably going to become a problem. - It's fun to make things like this and with LLM vibe coding you can even do it in languages you are unfamiliar with. Although with vibe coding I wonder if projects like this will lose their value. It's much easier to make something so the "impress factor" goes down.
Top comments (0)