Newbie question about getting app to work

This is my first attempt to write a datafield for my Edge Explore.  I'm also an absolute novice at programming as the last time I have done any was for my engineering degree >30 years ago.  Having said that I thought I would give it a try and having glued together a collection of functions which I found in various examples and some maths functions I have an app which runs in the Simulator and does pretty well what I want.  The problem is that on the device itself I get nothing, just a blank field.  I'm left wondering what it is that I may have missed which means that the app doesn't work on the device itself.

The app is a virtual pacer for audax events and is intended to calculate the amount of time in hand - in real time - over minimum pace.  The inputs are:

Pace speed, and latest time at control point (hours and minutes) from Settings accessed through Garmin Connect.  These come up as expected in GC and claim to save settings when entered.

Time of day and Distance to Destination are taken from the device itself.

As I said above, when I run the app in the simulator everything responds in the way it should to inputs from the App Settings Editor and changing the Distance to Destination in the simulator.  The maths also seems to be right and giving me the output I expect - it's just not coming up on the device (and making the device alerts go off almost continuously).

I'm sure that there's something vital which I've either missed or misapplied, I would appreciate any pointers (please excuse the inept programming):

using Toybox.WatchUi;
using Toybox.Time;
using Toybox.System as Sys;
using Toybox.Activity;
using Toybox.Application;

class AudaxTimesView extends WatchUi.SimpleDataField {

// Set the label of the data field here.
function initialize() {
SimpleDataField.initialize();
label = "Time in Hand";
}

// The given info object contains all the current workout
// information. Calculate a value and return it in this method.
// Note that compute() and onUpdate() are asynchronous, and there is no
// guarantee that compute() will be called before onUpdate().
function compute(info) {
// See Activity.Info in the documentation for available information.

var LastTimeMinute = Application.getApp().getProperty("LastTimeMinute");
var LastTimeHour = Application.getApp().getProperty("LastTimeHour");
var PacerSpeed = Application.getApp().getProperty("PacerSpeed");

var TimeNow = Sys.getClockTime();

var TimeNowHr = TimeNow.hour;
var TimeNowMin = TimeNow.min;
var TimeNowSec = TimeNow.sec;

var activityInfo = Toybox.Activity.getActivityInfo();
var DTRSelf = activityInfo.distanceToDestination;

var TimeNum = (TimeNowHr * 3600) + (TimeNowMin * 60) + TimeNowSec;
var ControlAge = (LastTimeHour*3600) + (LastTimeMinute*60);
if ((ControlAge - TimeNum) < (-43200)) {
ControlAge = ControlAge + 86400;
}
var DTRPacer = ((ControlAge - TimeNum)*PacerSpeed)/3.6; // calculates how far the pacer is from the end (metres)
var PacerTrail = DTRPacer - DTRSelf; // calculates distance betwwen self and pacer
var TimeInHand = (PacerTrail / (PacerSpeed/3.6)) ; // calculates how long it takes for pacer to catch

var InHandHour = TimeInHand/3600;
var InHandHourInt = InHandHour.toNumber();
var InHandMin = (TimeInHand - (InHandHourInt*3600))/60;
var InHandMinInt = InHandMin.toNumber();

return new Time.Duration(TimeInHand);

}

  • How are you testing on the device? Did you sideload the app or did you upload a beta version to the store?

    If you sideloaded the app then you have no (easy) way of setting App Settings on the watch. For example, when you test on the watch, what do you expect the value of "PacerSpeed" to be? Do you have a default value for PacerSpeed which isn't null or 0? If PacerSpeed is null or 0, the division will fail and the app will crash.

  • Other than that, I wouldn't call getProperty() in compute, as that runs every second and reading app properties from storage is pretty slow. I would also have some guard code which returns some default value (such as "--") in the case that PacerSpeed is 0. (This may not be necessary if you are absolutely sure that that PacerSpeed is not 0.)

  • Thanks for the response.

    I'm using beta versions and the Store.  I understand that it's not easy to update settings via GC when using a sideloaded app, and that's one more process to get to grips with which I don't need.  I didn't put any error traps in, apart from a minimum value for PacerSpeed in settings.xml as I can trust myself not to have any zero denominators while debugging, but the checks will be there when it's finished.

    To help with the debug I have commented out much of the code (I know the maths is doing what I expect it to in the simulator).  With the getProperty() call in the compute routine this means I get the updates into the simulator from the App Settings panel every second and I think this is what's causing the problem on the Explore.  Following advice on another thread I put the getProperty() call into a onSettingsChanged() function and moved it from the View into the App.  The compute(info) function now only had one line of active code which takes the value of LastTimeHour (initialised to 12) and multiplies it by 3600.  This is now what happens:

    Switch device on - datafield appears with label "Time in Hand" and a value of 43200.  So far so good.

    In GC, go to app settings and reset value of Hour, on hitting Done button the data field (label and number) disappear leaving a completely blank field which can only be restored by turning the device off and on.

    It seems like GC is sending something to the device which it can't read or can't use because it's in the wrong format, although I would have thought that would show up in the simulator??  

    I've copied the code from the View.mc App.mc and settings.xml below:

    AudaxTimesView.mc       code  - including lines commented out:

    using Toybox.WatchUi;
    using Toybox.Time;
    using Toybox.System as Sys;
    using Toybox.Activity;
    using Toybox.Application;

    class AudaxTimesView extends WatchUi.SimpleDataField {

    // Set the label of the data field here.
    function initialize() {
    SimpleDataField.initialize();
    label = "Time in Hand";
    }
    var LastTimeMinute = 0;
    var LastTimeHour = 12;
    var PacerSpeed = 15;


    // The given info object contains all the current workout
    // information. Calculate a value and return it in this method.
    // Note that compute() and onUpdate() are asynchronous, and there is no
    // guarantee that compute() will be called before onUpdate().
    function compute(info) {
    // See Activity.Info in the documentation for available information.


    //var TimeNow = Sys.getClockTime();

    //var TimeNowHr = TimeNow.hour;
    // var TimeNowMin = TimeNow.min;
    // var TimeNowSec = TimeNow.sec;

    //var activityInfo = Toybox.Activity.getActivityInfo();
    //var DTRSelf = activityInfo.distanceToDestination;

    //var TimeNum = (TimeNowHr * 3600) + (TimeNowMin * 60) + TimeNowSec;


    var ControlAge = (LastTimeHour*3600) + (LastTimeMinute*60);


    // if ((ControlAge - TimeNum) < (-43200)) {
    // ControlAge = ControlAge + 86400;
    //}
    //var DTRPacer = ((ControlAge - TimeNum)*PacerSpeed)/3.6; // calculates how far the pacer is from the end (metres)
    // var PacerTrail = DTRPacer - DTRSelf; // calculates distance betwwen self and pacer
    //var TimeInHand = (PacerTrail / (PacerSpeed/3.6)) ; // calculates how long it takes for pacer to catch

    //var InHandHour = TimeInHand/3600;
    // var InHandHourInt = InHandHour.toNumber();
    //var InHandMin = (TimeInHand - (InHandHourInt*3600))/60;
    //var InHandMinInt = InHandMin.toNumber();

    //return new Time.Duration(TimeInHand);
    return ControlAge;

    }

    AudaxTimesApp.mc     code

    using Toybox.Application;

    class AudaxTimesApp extends Application.AppBase {

    function initialize() {
    AppBase.initialize();

    }
    function onSettingsChanged() {
    LastTimeMinute = Application.getApp().getProperty("LastTimeMinute");
    LastTimeHour = Application.getApp().getProperty("LastTimeHour");
    PacerSpeed = Application.getApp().getProperty("PacerSpeed");
    }

    // onStart() is called on application start up
    function onStart(state) {
    }

    // onStop() is called when your application is exiting
    function onStop(state) {
    }

    // Return the initial view of your application here
    function getInitialView() {
    return [ new AudaxTimesView() ];
    }

    }

    settings.xml

    <resources>
    <properties>
    <property id="PacerSpeed" type="number">0</property>
    <property id="LastTimeHour" type="number">0</property>
    <property id="LastTimeMinute" type="number">0</property>
    </properties>

    <settings>

    <setting propertyKey="@Properties.PacerSpeed" title="@Strings.PacerSpeed">
    <settingConfig type="numeric" min ="5" />
    </setting>

    <setting propertyKey="@Properties.LastTimeHour" title="@Strings.LastTimeHour">
    <settingConfig type="numeric" min = "0" max = "23"/>
    </setting>

    <setting propertyKey="@Properties.LastTimeMinute" title="@Strings.LastTimeMinute">
    <settingConfig type="numeric" min = "0" max = "59" />
    </setting>

    </settings>
    </resources>

  • But your default PacerSpeed is 0. If the user never sets a speed then your code will automatically crash, as it's not valid to divide by zero.

    <property id="PacerSpeed" type="number">0</property>

    If you want the default to be 5 (for example), you have to say so:

    <property id="PacerSpeed" type="number">5</property>

    If changing settings is apparently breaking things, IIRC there used to be a known bug / quirk with Garmin Connect settings where if you type in an invalid setting (e.g. out of range), it resets all your settings to default. And in this case you have a default setting which literally causes your app to crash.

    I can't explain why it's crashing now (as opposed to with the original code), since your new code doesn't have any division. It's also possible that the for the bug I mentioned (*), all properties are either deleted or set to null, which means  you would have to put guard code in your app to check for a null value from getProperty().

    Others have complained about a similar issue:

    forums.garmin.com/.../onsettingschanged-and-null-values

  • I've spent a bit of time searching the forums and examples to try to see where the problem is but I can't see it.  I've set up non-zero defaults in settings.xml as below:

    <resources>
    <properties>
    <property id="PacerSpeed" type="float">3</property>
    <property id="CloseHour" type="number">12</property>
    <property id="CloseMinute" type="number">15</property>
    </properties>

    <settings>

    <setting propertyKey="@Properties.PacerSpeed" title="@Strings.PacerSpeed">
    <settingConfig type="numeric" min ="3" max ="30" />
    </setting>

    <setting propertyKey="@Properties.CloseHour" title="@Strings.CloseHour">
    <settingConfig type="numeric" min = "0" max = "23"/>
    </setting>

    <setting propertyKey="@Properties.CloseMinute" title="@Strings.CloseMinute">
    <settingConfig type="numeric" min = "0" max = "59" />
    </setting>

    </settings>
    </resources>

    The app runs fine with the defaults (so long as there is a valid value coming from activityInfo.distanceToDestination) and the number appearing on screen looks to be OK and in agreement with the simulator output so I'm pretty sure the maths is right, but still crashes as soon as the settings in GCM are changed.  I guess that I could put in some sort of error trap to return admissible values if the numbers coming out of GCM were null, but then the app wouldn't calculate properly as the input values would be wrong.

    I'm wondering if I should bite the bullet and delve into setting up an on-device menu to cut out GCM altogether.  It's more to learn (not a bad thing in itself) and from a user perspective I like the idea of keeping it all on one device - especially for long events. 

  • Well, IMO the first thing to do is determine what is actually happening, before deciding how to handle the problem.

    Also, on-device settings for data fields are only available for CIQ 3.2 and up. Took a quick look at my Garmin devices folder and it seems Edge Explore is on CIQ 3.1.

    It seems to me if your app is crashing, there are several ways to determine what the problem is:

    - Look at the ERA report for your app (assuming it's sent for beta apps)

    - Look at CIQ_LOG.yml on your device

    - Insert test code around the area that you think is causing the crash. In this case I would print out the type and value of each property that you read, when you read it

    e.g. See this pastebin link: https://pastebin.com/QiaVb0Rt. Sorry, the forum won't let me post this code either inline or in a code block. Some things never change.

    In order to see the output of System.println on an app running on a real device, create an empty file with the same name as your app's PRG in GARMIN/APPS/LOGS/, except replace the ".PRG" extension with ".TXT"

    Also, it seems that the code you posted is missing the part is initialize() where you initially read settings from properties. onSettingsChanged() is only going to be called when settings are changed after the data field starts up, which could be never. i.e. when you first launch an activity, the data field will start up and not read config from properties, but it'll just use your hardcoded defaults.

    It's already mentioned in the new developer FAQ that type you get back from settings may not be the type you expect. e.g. You may have defined a property as a number, but get a string back instead. (It's definitely a bug, but nothing to do but work around it).

    forums.garmin.com/.../new-developer-faq

    My app uses app settings, and it sometimes crashes after changing the settings, but the rest of the time it works fine. What can cause this?

    In the past, there have been issues where the client used to manage app settings, like the Connect IQ Store app, Garmin Express, or Garmin Connect Mobile, has returned a setting value with an unexpected type, so it's good practice to check a setting value to ensure it's the expected type. For example, this code will check for a null value and convert the value to a Number as long as it is not null. Otherwise, a default value is returned...

  • Thanks for getting back so quickly with you suggestions.  The initial start-up of the datafield is as you describe, that it runs OK with the values which are defined right at the start of the View (actually before the initialize() line), but in reality in order for the datafield to be of use the settings need to be updated so that they are appropriate to the ride, i.e. last finish time and minimum pace.  This will need to be updated several times during the course of the ride at each check point.

    I'll try the System.println on the device, thanks for explaining that.