Timer in Data field

I am trying to make a timer data field for official races that tracks your chip time, this means that it will start the timer not when the activity starts but instead when a push notification is sent to the watch (notification of passing the starting line) and stop it when another notification is sent (notification of passing the finish line). The sending information has already been handled but I am having difficulty displaying a correctly incrementing timer. The main issue is that the timer class cannot be used in a datafield. I tried to make it with a display that updated every second but it wasn't smooth and incremented slow, causing it to fall behind where it should be. Another roadblock is that there is a chance that the starting time will have an issue meaning that a runner's timer is incorrect and I need to have the functionality to send their correct time so that the timer can be set to that and carry on as normal 

  • Instead of starting a timer, compute the elapsed time since receiving the start notification. This way you won't fall behind. If the notification contains the actual time of day of the start, even better.

  • EDIT: ars.fabula said it a lot more concisely than me haha. As they pointed out, there’s also the issue of accounting for any possible gap between sending and receiving the start notification. But if time of day is used to account for this gap, then ofc you have time assume that the time of day is correct on both the sending and receiving device

    Instead of constantly incrementing a timer variable (whether it’s in onUpdate() or compute()), I would use a built-in device clock as a reference, and use a couple of bookkeeping values to track the state of your timer with respect to the reference.

    For display (or other purposes), the value of your timer will be calculated from the reference clock and your bookkeeping values, but it won’t be part of the state of the app. (This avoids any kind of issues with trying to constantly update the “current timer value” and being off due to various assumptions about how often onUpdate() or compute() is called. IOW, this approach relies on the reference clock to reliably increment itself so you don’t have to increment anything.)

    Even if I were writing a device app (which supports the Timer class), this is the approach I would use. (I have used this approach to make a stopwatch device app)

    Here’s the implementation:

    - use System.getTimer() as a reference clock (despite the name, this is the system uptime in milliseconds) [*]

    [*] Note that as a 32-bit signed integer (Number type), the result of System.getTimer() will wrap around ~25 days after reboot, and every ~50 days thereafter. If you’d rather not worry about this edge case in your app, you can use Activity.Info.elapsedTime (also a Number and in milliseconds), which starts at 0 when the activity starts, and therefore has no realistic wraparound issues (I’m sure it isn’t realistic for an activity to even approach 25 days on a Garmin.) However, elapsedTime doesn’t start incrementing until the activity starts, so it’s not suitable if it’s possible for your timer to start before the activity does.

    Depending on how you do your math, the wraparound case may not even matter

    - record the value of System.getTimer() when your timer starts: let’s call this startTime (Number)

    - to allow for on-the-fly corrections as you’ve described, keep a separate value called offsetTime (Number) which will be used as a correction term. When your app receives a timer correction, set startTime to the current value of System.getTimer(), and set offsetTime to the value of the corrected timer value

    - at any given point in time, the current value of the timer is calculated as: System.getTimer() - startTime + offsetTime

    - pausing/unpausing the timer works similar to a correction, except you also need to track a timerState value (with at least two values: paused and running.).

    Pause: set offsetTime to the current value of the timer (as in the previous point), set startTime to null, set timerState to paused. When the timer is paused, the current value of the timer is simply offsetTime.

    Unpause: set startTime to the current value of System.getTimer(), set timerState to running

  • Thank you, I was using time of day and this elapsed time system but was instead sending the elapsed time calculation from the companion app instead of using the watch to do that. If I remember correctly the reason I did this was because I was having trouble using epoch time with the Garmin and was also worried that doing the computation on the watch would delay the timer, do you think that neither of these will be concerns when using the System.getTimer()? There is a way for the timer to start before the activity so do you have ideas for how to handle the roll over edge cases?

  • It will always start before the activity, as it starts when you boot the device.

    To handle the roll over you just need to check if the current value is less than the last value. When this happens you can adjust the startTime to make the calculations work again. However you might get into trouble when your own activity reaches 25 days ;)

  • Great thank you, I'll work on that and come back if I have issues

  • I think for this use case a watch app makes a lot more sense.

  • On one hand yes, because it does the auto start and stop, but on the other hand, the one thing we all have in common is that we already have the Run activity set up with all our favorite data fields and layouts set up for a running race, which you won't be able to use in an app, which will make an app much less attractive to everyone (even to the developer I guess, but certainly most of other potential users)

    I wonder what's the technical background for this!? Is there some ANT or BT device sending a signal when it records that someone passed over the chip detector? Does it also send the chip id, so in the start you know when it records you and not the 10 other people in the same line?

    Also I'm curious what is the motivation? We are all used to press the start and the stop button as we pass the detector. What's the value that makes such a hassle worth? The difference will be less than a second

  • Unfortunately we have already decided on a datafield so as long as it works I will be using that even if it is easier with a watch app. I have it mostly working, the only issue that I found was that the start time uses epoch time of day whereas the garmin uses its own clock, for most instances this is fine because I can store the original start time in my companion app then send whatever the difference between that and the new starttime is to the garmin to then change the timer off of. However, I can't use this system for the first time the data is sent. The best I can do is use the companion app's current epoch time to get elapsed time then guess how long it will take to send that information to the watch. It is honestly not a big issue and is probably fine if I can't solve it because it is only off by a few seconds and I can probably just play around with adding that time to make it more accurate but I'm worried that it could somehow cause issues later so it would be nice to fix if there is an easy solution. I also haven't implemented the roll over but it would probably look something like this: check if the time has gone down/set to 0, store what the value was before rolling over, then use that stored value plus current time instead of current time for the rest?

    Yes, it uses the bt chip detection with chip ids, this timer is part of a bigger datafield that will give information like what place you are in, mile pace, and how far behind of the person in front of you you are (probably options of about 15 pieces of information), so having it on the same view without having to start another timer makes it easier for the runner, also not having to remember to start their own timer. It is also the "official" time, so I think runners may prefer that to their own timer. I've already gotten most of the other pieces of information that will be displayed but I was having trouble with the stopwatch which we felt is one of the more important ones, thank you for the help!   

  • By Epoch Time, you mean what is commonly called Unix Time?  In MonkeyC, Time.now().value() gives you that, and it's easy to display as local time.  I do it all the time for things I get from a website

  • IMO the time of day clock / unix time is fine (and in some cases, necessary) for synchronization (again, this assumes that both the sender and receiver have the correct time set.) As jim_m_58 mentioned, obviously Garmin devices also have a time of day / wall clock, since they have to be able to display the current date and time to the user.

    However, for the purposes of keeping track of your timer, I would still use something like the system uptime, because the time of day clock has a couple of disadvantages:

    - if it’s modified (other than the expected regular increments) during an activity, then this will obviously mess up your timer. Consider the case of when the time of day is off by a few seconds (or a lot more than that), but the watch gets a GPS lock and corrects the time of day clock. Ofc, in this case, you will also have an issue synchronizing your timer with the sender

    - the system uptime has millisecond resolution whereas the time of day clock only has second resolution. the user may not care about fractions of seconds during the activity, but they may care after the fact

    IOW, even if I had to use the time of day / unix time for the intiial sync, I would still use the system uptime as the reference clock (for timer bookkeeping purposes).

    If you must use the time of day clock — especially for synchronization — I would recommend using Timer.getCurrentTime({:currentTimeType => Time.CURRENT_TIME_RTC}). This throws an exception “if the real-time clock value is not valid, i.e. synced with trusted source such as GPS.”

    If it fails, you can decide on a fallback solution, such as:

    - prompting the user to get a time sync with GPS before allowing the timer to run. (This is probably not the best solution)

    - instead of using the timestamp from the sync message to synchronize the timer, instead assume that the timer started at the instant the message was received (or shortly before that). (*)

    (*) In any case, you may wish to perform a sanity check on the value of the sync timestamp. If the sync message is received with a timestamp that’s too old, or a timestamp from the future, then it’s obvious that the timestamp can’t be used