UnexpectedTypeException in ActivityMonitor.getHeartRateHistory()

The following code throws "UnexpectedTypeException: Expected an object of type Time.Duration".
I checked the type of the duration variable and it is in fact Time.Duration.
If duration is null or a number, the function works as expected.
I have tested it only in the simulator on FR645 device.

using Toybox.ActivityMonitor;
using Toybox.Time;

var duration = new Time.Duration(60); // seconds
ActivityMonitor.getHeartRateHistory(duration, false);
  • I'm not sure what you want to do here. It would be the oldest to newest 60 seconds of samples. What is it you want to get? Samples saved in history are on the order of once every 90 seconds (last times I checked - you can see the rate by looking at the difference between two "whens" in samples)

    if instead of 60, if you use 600, does that work? The duration could be too short.
  • Out of curiosity, I tried the posted code, and yep, it crashes.

    TL;DR: I think it has something to do with the amount of time the simulator has been open. The longer it's open, the larger your duration has to be before it crashes (which is counter-intuitive to say the least). Or maybe there's some other random element at play.

    As per jim's suggestion, I tried different durations
    var d1 = new Time.Duration(1200);
    System.println("d1 = " + d1);
    System.println((d1 instanceof Toybox.Time.Duration).toString()); // obviously superfluous, but whatevs
    System.println("d1.value() = " + d1.value());


    var d2 = new Time.Duration(600);
    System.println("d2 = " + d2);
    System.println((d2 instanceof Toybox.Time.Duration).toString());
    System.println("d2.value() = " + d2.value());

    var d3 = new Time.Duration(60);
    System.println("d3 = " + d3);
    System.println((d3 instanceof Toybox.Time.Duration).toString());
    System.println("d3.value() = " + d3.value());

    ActivityMonitor.getHeartRateHistory(d1, false);
    System.println("After getHeartRateHistory d1");

    ActivityMonitor.getHeartRateHistory(d2, false);
    System.println("After getHeartRateHistory d2");

    ActivityMonitor.getHeartRateHistory(d3, false);
    System.println("After getHeartRateHistory d3");


    Running the above code on the 935 simulator produces output similar to the following (newlines added for clarity):
    d1 = Obj: 151
    true
    d1.value() = 1200

    d2 = Obj: 152
    true
    d2.value() = 600

    d3 = Obj: 153
    true
    d3.value() = 60

    ^ This part never changes

    After getHeartRateHistory d1
    (null)
    Unhandled Exception
    UnexpectedTypeException: Expected an object of type Time.Duration
    Native Function
    ...
    (at.getHeartRateHistory(d2, false);)

    ^ This part changes, seemingly based on the length of time the simulator has been open


    I noticed that the problem happens seemingly at random.
    I've seen:
    - All 3 statements succeed (1200, 600, 60) -- immediately after sim is opened
    - The first two succeed, and 60 fails -- after sim is open for a longer time
    - The first succeeds, and 600 fails -- after even longer
    - The first fails (1200) -- after much longer

    I think it has something to do with the amount of time the simulator has been open. The longer it's open, the larger your duration has to be, apparently. If you close and open the simulator, it seems to reset the problem and the code works again for a little while.

    My two cents:
    - People have been complaining about this since 2016:
    https://forums.garmin.com/forum/deve...-does-not-work
    https://forums.garmin.com/forum/deve...n-a-watch-face
    https://forums.garmin.com/forum/deve...story-iterator
    https://github.com/HookyQR/TidyWatch/issues/2
    "getHeartRateHistory doesn't accept a duration like it should"


    - This isn't reasonable behaviour in response to a duration that's "too short", if that's truly the problem
    1) How does the dev know beforehand what the appropriate duration is, especially going forward with new devices?
    2) The error is neither an unexpected type nor should it throw an exception. It would be an input validation error which should be handled gracefully (e.g. by returning null)

    - Even if it was reasonable behaviour, it's obviously not happening consistently

    Seems like a legit bug to me. Sorta wish it had been taken seriously in 2016 or 2017, but I get it....
  • Like FlowState said, a short duration shouldn't cause this, but based on what's trying to be done, there could be a workaround, as waiting for a fix could be a while.
    A duration of 60sec when new data is only added every 90 could cause problems in itself based on what the data is used for.
  • It's not just a "short" duration, at least in the simulator. If you wait long enough, then a duration of 600 will also cause a crash.

    The workaround I've seen is that people use a sample count instead of a duration -- they estimate the number of samples to retrieve, based on the duration they want and the assumed history sample rate.

    If it's 90 seconds like jim said, then I guess you would divide your duration by 90.

    In the TidyWatch project I linked, the dev apparently assumed a sample rate of about 80 seconds per sample, and used 180 samples to retrieve 4 hours worth of data.

    getHeartRateHistory doesn't accept a duration like it should. Working around with an 'approximate' 4 hours (180 samples for the ~80 samples per second).

    (He meant 80 seconds per sample...)
    https://github.com/HookyQR/TidyWatch...ce/TidyData.mc
    return ActMon.getHeartRateHistory(180, true); // 81 seconds between updates ... maybe?


    I can imagine how a workaround like this could break on future devices/CIQ versions....
  • Yes, in the sim, the "when" for all samples in the ActMon or SH getXYZHistory are from the time the sim started, and the data doesn't change, even with simulated data. Another reason using duration is tricky, and a reason I don't use it. :)

    If I want the last sample, I just use the number 1.

    The interval isn't fixed and can vary by device, with kind of a constant of "4hrs of data for the HR widget", so things like screen and graph width come into play. This may have changed in newer FW, but that was the case last time I checked the number of sample returned.

    If the goal is to get HR for "real time HR" on a watchface, you use Activity.Info.currentHeartRate for that, and possibly, getHeartRateHistory as a fallback (This is what I was thinking with a duration of 60 seconds, but could be wrong..)
  • The interval isn't fixed and can vary by device, with kind of a constant of "4hrs of data for the HR widget", so things like screen and graph width come into play.


    All the more reason this bug is significant. Appreciate the tips, but it would also be nice if this stuff worked as documented/designed.

    I could be wrong, but I think the only reason duration is tricky here is because it doesn't work.... The concept of "I want HR history for the last X seconds" seems quite straightforward to me. If the watch has been up for < X seconds, then give me fewer samples. In no way does it make sense that a duration of 600 seconds should cause a crash (and not even consistently).

    BTW, I left the sim open for a long time (over 30 minutes), and now a duration value of 1200 causes the crash, too....

    Maybe the bug is something as simple as:
    if (duration < upTime)
    throw exception;


    Perhaps the intended code was
    if (duration > upTime)
    throw exception;


    but I still think that's wrong, unless it was fully documented and the type of the exception was to reflect the actual problem (DurationTooLongException or something).

    I'm guessing what's actually happening is that in getHeartRateHistory(), there's some kind of (incorrect) math being done between two Durations or Moments, which fails when the duration parameter is less than the uptime (*), resulting a null result (hence the "(null)" in the trace), leading to:
    "UnexpectedTypeException: Expected an object of type Time.Duration"

    The key is obviously that the unexpected type exception is happening in the native library function, not the app code.

    (*) Probably the opposite of the intended logic
  • It shouldn't crash the app, but probably return null for the iterator or an iterator with no samples if there's nothing for the duration.

    The main reason I don't use a duration is I know I either want 1, all that are available, or as many as I can fit on a graph, so it's always a number.
  • - Yes, I agree it should return null instead of throwing an exception, if data is legitimately unavailable, if only because CIQ exceptions are expensive to handle. In a more resource-rich environment, I can understand coding with all kinds of exceptions to handle every kind of problem, including non-fatal errors like "invalid input" and "I can't do that right now". I'm sure there's different schools of thought as to whether "no data available" should return null, the empty set, or throw an exception, but I'm sure there are different situations where any one of those answers is valid
    - The problem here is obviously not that there's nothing for the duration, since values of 60, 600, and 1200 seconds are successively rejected, as the simulator stays open for longer periods of time. Apparently, the longer I wait, the larger the range of numbers that will crash, which is the opposite of what you'd expect if it was a case of "not enough data available to fulfill your request".

    If I wait long enough, I bet I could make it crash on any number I want (up to the 7-day Duration limit)
  • It's very simple. If the code doesn't work as documented, then there is a bug. It has to be fixed or the documentation has to be changed. Otherwise, people like me will keep wasting time trying to figure out what they are doing wrong.

    I think it would be better to fix it, because having the ability to get heart rate data for the last X seconds / minutes / hours is valuable.

    If I call the getHeartRateHistory with 60 duration and there are no samples in that time frame, then I would expect an "empty" iterator, rather than an exception.
  • If I call the getHeartRateHistory with 60 duration and there are no samples in that time frame, then I would expect an "empty" iterator, rather than an exception.


    I would agree 100%, except heart rate iterators have getMin() and getMax() functions. So if no samples are available, there's two choices for getHeartRateHistory():
    - Return null (which is one null check for the developer)
    - Return an "empty iterator" which returns what for getMin() and getMax()? Null? Now there's 3 null checks to be made, including the check for next()

    Because of those two functions that return additional data, I would personally prefer to receive "null" when no samples are available, because it reduces the number of potential checks in the worst case. Yes, I realize that you could write the code in a certain order to avoid checking 3 things, but it just makes things simpler imo, even from a documentation pov.

    https://developer.garmin.com/downloa...eIterator.html

    Anyway, I can't emphasize enough that this bug is clearly is not about not having any/enough samples available for a given duration, although I do agree that there the return value/behaviour for the actual "no samples" situation needs to be sane (*), robust, and well-documented. The documentation actually says nothing about what will happen if the requested period is too small or too big.

    (*) In CIQ, this probably means that it shouldn't throw an exception, since there's not a lot of room for extraneous code

    I think it would be better to fix it, because having the ability to get heart rate data for the last X seconds / minutes / hours is valuable.


    Yep. There's obviously a reason it's right there in the API, too bad it doesn't seem to work. The existence of various workarounds, questions, and discussions proves that it's not some useless feature that nobody wants.

    Otherwise, people like me will keep wasting time trying to figure out what they are doing wrong.


    This is one of a few bugs, limitations or unexpected behaviours that people have been "rediscovering" over and over again, sometimes for years.