Datafield: Daylight Left

Hey all, a couple of weeks ago I released my first app, a datafield called 'Daylight Left', but I forgot to introduce it here.

So here's a link to the app page: https://apps.garmin.com/en-US/apps/0a593392-4ea8-4963-9b9f-2c464338e87b

I'd love any feedback for it.

The only thing I can really think of doing next for it is localizing it. I don't trust Google translate to do the right thing here, so if you have suggestions for how to write 'Daylight Left' in your native language, please leave them in the comments. Otherwise, I might just have to toss out a Mechanical Turk job for that.


Developing the datafield was a really interesting experience for me. I've done a little bit of Android development in the past, but mainly come from a Python and Ruby background. So, in case anyone's interested in the newbie experience, here's a summary of my experience developing the app.



Inspiration

The inspiration came from being out on a run or hike in the evening and wanting to make sure I was back to my car before dusk. Even though Garmin helpfully includes a Sunset datafield, I wanted something that counted-down, so that it had some 'urgency' as sunset approached.


Approach

To code this, having never made a Garmin app before, I looked around the API docs and their MonkeyC Intro PDF for a couple of hours trying to get a feel for whether this project was possible, and how hard it would be. Once I was satisfied that it was doable even for someone w/ no experience, I began researching algorithms for computing sunset given a user's lat/lng.

Googling around, I found this incredibly helpful document: http://williams.best.vwh.net/sunrise_sunset_algorithm.htm

Realizing it would probably be a fool's errand to try to implement this in MonkeyC directly since I know neither the algorithm nor the the language, I decided I would prototype it first in a language I'm more familiar with, Python. That led me to create: https://github.com/rconradharris/pysunset

I wasn't sure of the accuracy of the USNO sunset calculation, so I spent quite a bit of time porting NOAA's Javascript sunset calculator over to Python as well.

After comparing the results between the two, it turned out that the NOAA's *much* more complicated algorithm only differed by a few seconds from the USNO version. So, for my purposes, USNO was good enough and would be the version I'd port to MonkeyC.

Porting the Python code to MonkeyC turned out to be fairly straightforward, but there were a few hitches along the way.


Problem #1: What should it display after sunset?

One of the biggest problems I ran into was with respect to the boundary cases: what should the datafield display *after* sunset? Should it display time until sunrise? Should it display time *since* last sunset, like a negative countdown? If that's the case, then when should it start displaying the time for the *next* sunset?

After going around and around with this, I eventually decided that I would display time-until-sunset only during the day, e.g. the hours from sunrise to sunset. Every other time would be displayed as "00:00". I thought that would make the most sense to users.


Problem #2: Define "Daylight"

Another problem I ran into was figuring out exactly which definition for daylight I would use. As you know, there's still typically quite a bit of daylight left after a sunset, so a few different standard definitions of twilight exist (http://en.wikipedia.org/wiki/Twilight). Given my exact use case of getting back to the car, I thought that Civil twilight is probably the best match. And given that the algorithm I implemented already supported switching between these definitions by swapping out Zenith values, I considered using it.

However, I eventually decided to stick with sunset. For one, I thought users would intuitively understand that the easiest. Also, sunset is a conservative estimate. The amount of light available during Civil twilight will vary quite a bit based on geography (are you in a valley?) and weather (is it cloudy?). Sunset, though not ideal in all cases, seemed like the best-all-around choice.


Problem #3: Define Today

One particularly annoying problem I ran into was with the `Time.today()` function. The docs say that it will give you the number of seconds since midnight based on your current GPS location. I was, however, getting some very strange results as I was testing my app during the evening (I'm in Austin, TX which is CDT).

I eventually traced the cause back to the `Time.today()` function rolling over at UTC's midnight, not my local midnight.

This led me to create a new module called `LocalTime` which handles local time correctly. `LocalTime.today()` and `LocalTime.now()` work just like their `Time` counterparts but work are/actually based on your GPS location. Once I switched to using these two functions, everything worked correctly.


Problem #4: Tests?

The final problem I had was, how do I test the app? At first I was using the simulator and just testing everything out manually. This, as I'm sure you all know, is actually a fairly laborious activity, and wouldn't help prevent regressions.

After running into that `Time.today()` issue, I decided I would try to unit-test my code. I didn't see any built-in unit-test framework, so I decided to roll my own minimal version.

I looked at Python's `unittest` and C#'s NUnit for inspiration and created a very tiny framework based on the two.

All in all the test-framework--I'm calling it MUnit--works, but there are a couple of weird things I hit along the way.

First, inheritance seems to not work quite right. When I tried to have tests inherit from a class defined in a separate module it wouldn't work (I have no idea why it works for Toybox classes). I eventually solved it by making `TestCase`, from which all tests inherit, a 'naked' class definition, e.g. not within a module scope.

Another issue was how to discover the tests. There really isn't any introspection capability in MonkeyC despite it being so dynamic in some ways. I eventually decided to have a 'test-registry` dictionary that each `TestCase` would override. The key would be a symbol representing the function name, and we would use that to dynamically invoke the test. Since there isn't an analogous function to Ruby's `Symbol.to_s` method which would give us a string representation, I had to use a separate, duplicate string as the value of the dictionary to specify the function's human-readable name. Ugly, but worked.

The final issue was the lack of exception handling. Most xUnit frameworks rely on exception handling to signal a test failure, usually by way of an `AssertionError`. I tried this at first, but quickly realized that exception handling in MonkeyC isn't even close to working yet.

So, I abandoned that and moved on to setting a global `failure` flag. Rather than raise an exception, each assertion would set this global flag if the test failed and register the name of the failing test, then the `TestRunner` class would report a result summary at the end.

Not ideal, but, again, it worked.


Release

Once I built the app, and unit-tested it, my final task was releasing it. My first step here was to functionally test it for a day or two. I just went for a couple of walks every day, tried the datafield out, and made sure the numbers looked sane. I also checked it in the evening to make sure the UTC midnight-rollover bug was still fixed.

Once I was sure the app was working, my next task was to build the production binary.

I wanted the binary to be lean-and-mean, so I went through and commented out any logging I'd done, as well as the entirety of the testing code. (Sidenote: I really wish MonkeyC had a pre-processor. Being able to do `#ifdef DEBUG` would be much easier than manually commenting out individual lines).

The next step after building the binary was building the screenshots. At first I didn't realize that the simulator had screenshoting built-in, so I was using the Mac screenshot function. Once I realized that it was builtin though, I went through the process of simulating and screenshotting the datafield for each watch-type. (The Epix looks most bad-ass, and my watch, the Vivoactive looks the lamest, but that's just my opinion :)

The next to last step was finding a snazzy graphic to use for the app-page. I didn't want to use a screenshot because I thought that would look too 'vanilla', so I searched around and found a nice looking piece of free-clipart that showed dusk.

The final step was just uploading it to Garmin which went fairly smoothly. After 3 days, I received approval, and began seeing downloads almost immediately.


Conclusion

All in all, I found the whole experience really enjoyable. There were definite frustrations, with the language, the SDK, and the simulator, but, so far at least, no obstacle was insurmountable. MonkeyC is actually pretty fun when you get used to it! I'm really looking forward to it maturing and when I can finally remove some my nasty-workarounds :)

I'm already mid-way through development of my next app which I hope should make the watch a bit more usable even when not near your phone. Stay tuned...
  • Great write up! Interesting read :-)
  • Thank you for great field, it served me well in my recent travel, where I didn't know local environment well.

    I suggest you to reconsider the way fields operates after the sunset, and that's why:
    Let's say I'm running, sun goes down and field shows 00:00. It is still enough light and I'd like to check, for how long the twilight will be there. I had to check timer and remember the time, really tough task when running ))

    If possible, let field to go into the minus "-00:00" and also preferably change the view, like make digits much smaller and invert the background (white on black).

    Logic could be like this:
    if it is a daytime, show time to sunset
    if it is a nighttime, change background and show time after last sunset
  • SERGEYVDNKH,

    Thanks for the feedback here and glad you're finding it useful!

    I like the suggestion of the counter going negative; an unreleased version actually did that. I ditched it to keep things simple, but with your feedback, I'm thinking about adding it back. Stay tuned for an update.
  • Very useful information! Cycling backroads in the country gets a bit intense after sunset. There's no street lamps. And, the occasional dog chase leaves you wondering how close, and how many dogs there are! With Daylight Left, I can compare my ride time to the daylight left. When they're equal, it's time to head home.
    Thanks!
  • , would you be willing to share the code you wrote for calculating the sun position - specifically for getting civil twilight? 

    I've spent the last few days on-and-off trying to implement it in Monkey C, and it's been tough finding a straightforward formula. The link you provided is no longer active, and the definition I've been trying to use isn't outputting reasonable values (I think because sometimes the author uses radian and sometimes degrees, without specifying clearly enough).  


    I'm trying to add small arcs around my analog clock face to show the span between sunset & civil twilight.  (You can see the "redline" below which maps to sleep time.  The plan is to add a small arc like this around sunset, starting at sunset and ending at civil twilight.