makeWebRequest call crashes Garmin Connect Mobile

I wrote an app that is using makeWebRequest to load JSON data. When I run in the emulator everything works well and I get my results.

When I run on my watch (vivoactive HR) using the latest version (3.11) of iOS GCM on the latest version of iOS 10 I get back a -2 error. If I have GCM running in the foreground on my phone I can see it crash as soon as the request is made.

How do I figure out why GCM is crashing? Is there anything that I can do differently in my app? I know that there were bugs with GCM on iOS 10 requesting web data, but the most recent build seems to have fixed that (at least for the build in weather widget).

Here is an example of the data that it is getting back:
http://tidesandcurrents.noaa.gov/api/datagetter?begin_date=20161006%2000%3A00&end_date=20161006%2006%3A00&station=8454000&product=predictions&datum=mllw&units=english&time_zone=lst&application=web_services&format=json


and the code:
function requestNextTideData()
{
var begin_date = startTimes[iTimes];
var end_date = endTimes[iTimes];

System.println("makeRequest()");
var url = "tidesandcurrents.noaa.gov/.../datagetter";
var station = stations[iStation]["id"];
System.println("begin_data = " + begin_date + ", end_date = " + end_date + ", station = " + station);
Comm.makeWebRequest(
url,
{
"begin_date" => begin_date,
"end_date" => end_date,
"station" => stations[iStation]["id"],
"product" => "predictions",
"datum" => "mllw",
"units" => "english",
"time_zone" => "lst_ldt",
"application" => "web_services",
"format" => "json"
},
{
:method => Comm.HTTP_REQUEST_METHOD_GET,
:responseType => Comm.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReceive));
System.println("makeRequest() done");
}
  • Assuming you're doing conversions on the device, I think you'd see some speedup if you did some server side processing on the data. You would need to write a web service of your own that accesses the data from the original source, converts the date-time values into timestamps and the measurements into actual floating-point values before forwarding the result back to the client. That would eliminate a lot of the storage cost on the device side when initially processing the response, and possibly reduce the conversion costs.

    Additionally, it might help if you add a simple queuing mechanism for your requests. It might reduce parallelism (because the mobile phone can serve multiple requests simultaneously), but you'd get ordered responses and you could easily add support for user data being passed back with your response. Something like this...

    using Toybox.Communications as Comm;
    using Toybox.System as Sys;
    using Toybox.Lang as Lang;
    using Toybox.Time as Time;
    using Toybox.Time.Gregorian as Gregorian;

    class WebRequestManager
    {
    // internal implementation detail
    class Request {
    var url;
    var params;
    var options;
    var callback;
    var user_data;
    var next;

    function initialize(url, params, options, callback, user_data) {
    self.url = url;
    self.params = params;
    self.options = options;
    self.callback = callback;
    self.user_data = user_data;
    }

    function makeWebRequest(callback) {
    Comm.makeWebRequest(self.url, self.params, self.options, callback);
    }

    function onWebResponse(code, data) {
    callback.invoke(code, data, self.user_data);
    }
    }


    function initialize() {
    }

    hidden var queue_head;
    hidden var queue_tail;

    function makeWebRequest(url, params, options, callback, user_data) {
    var request = new Request(url, params, options, callback, user_data);

    // no queued requests
    if (queue_head == null) {
    // put our request at the front of the queue
    queue_head = request;
    queue_tail = request;

    // and submit the request
    request.makeWebRequest(self.method(:onWebResponse));
    }

    // put the new request at the end of the queue. it will execute when
    // all of the previously queued requests complete
    else {
    queue_tail.next = request;
    queue_tail = request;
    }
    }

    function cancelAll() {
    Comm.cancelAll();

    // clear out our queue of requests
    queue_head = null;
    queue_tail = null;
    }

    function onWebResponse(code, data) {
    // get handle to the top request
    var request = queue_head;

    // remove the top request
    queue_head = request.next;
    request.next = null;

    if (queue_head == null) {
    queue_tail = null;
    }

    // invoke the callback contained in the response
    request.onWebResponse(code, data);

    // submit another response if there are more
    if (queue_head != null) {
    queue_head.makeWebRequest(self.method(:onWebResponse));
    }
    }
    }


    Then you should be able to queue up your requests like this...

    class MyBehaviorDelegate extends Ui.BehaviorDelegate
    {
    function initialize(manager) {
    BehaviorDelegate.initialize();
    _manager = manager;
    }

    // helper function formats a time for the web service request given a
    // broken-down time Time.Info
    hidden function format_time(info) {
    return Lang.format("$1$$2$$3$ $4$:$5$", [
    info.year.format("%04d"),
    info.month.format("%02d"),
    info.day.format("%02d"),
    info.hour.format("%02d"),
    info.min.format("%02d")
    ]);
    }

    function onSelect() {

    var options = {
    :method => Comm.HTTP_REQUEST_METHOD_GET,
    :responseType => Comm.HTTP_RESPONSE_CONTENT_TYPE_JSON,
    :headers => {
    "Content-Type" => Comm.REQUEST_CONTENT_TYPE_JSON
    }
    };

    // get samples for every 6 minutes
    var off = new Time.Duration(360);

    // starting now, and going for 60 minutes
    var beg = Time.now();
    var end = beg.add(new Time.Duration(3600));

    var url = "tidesandcurrents.noaa.gov/.../datagetter";

    for (var user_data = 0; beg.lessThan(end); beg = beg.add(off)) {

    var beg_date = Gregorian.info(beg, Time.FORMAT_SHORT);
    var end_date = Gregorian.info(end, Time.FORMAT_SHORT);

    var params = {
    "begin_date" => format_time(beg_date),
    "end_date" => format_time(end_date),
    "station" => "8454000",
    "product" => "predictions",
    "datum" => "mllw",
    "units" => "english",
    "time_zone" => "lst",
    "application" => "web_services",
    "format" => "json"
    };

    manager.makeWebRequest(url, options, params, self.method(:onWebResponse), user_data);

    user_data += 1;
    }

    return true;
    }

    function onWebResponse(code, data, user_data) {
    Sys.println(code);
    Sys.println(user_data);
    Sys.println(data);
    }
    }
  • Could you do something like giving each request have it's own handler? onReceive1(), onReceive2(), onReceive3() for example, and then in each of those you call a common function and pass it a parameter as to which one you got?


    It would be more generic to use a delegate. I'm not sure if alexphredorg considers this to be a hack or not, but it is a fairly elegant solution and fits right in with normal asynchronous programming patterns.

    // similar to the Request wrapper above...
    class WebResponseDelegate
    {
    hidden var _callback;
    hidden var _params;

    function initialize(params) {
    self._params = params;
    }

    function onWebResponse(code, data) {
    _callback.invoke(code, data, self._params);
    }

    function makeWebRequest(url, params, options, callback) {
    _callback = callback;
    Comm.makeWebRequest(url, params, options, self.method(:onWebResponse));
    }
    }


    Then you use it where you want to attach data to a request...

    class MyBehaviorDelegate extends Ui.BehaviorDelegate
    {
    function initialize() {
    BehaviorDelegate.initialize();
    }

    function onSelect() {

    //whatever you want
    var url = "http://...";

    var options = {
    // whatever you want...
    };

    var params {
    // whatever you want...
    };

    // create a delegate to call our function with the additional parameter
    var delegate = new WebResponseDelegate(1);

    // ask the delegate to make the request
    delegate.makeWebRequest(url, options, params, self.method(:onWebResponse));

    return true;
    }

    function onWebResponse(code, data, param) {
    Sys.println(code);
    Sys.println(param);
    Sys.println(data);
    }
    }


    That said, I like the idea of having something between the Comm.makeWebRequest() method so that I can do other things. For instance, you could easily implement a system that allowed you to have up to three simultaneous requests, and to queue additional requests beyond the three. You could implement request cancellation for individual requests that haven't been submitted yet, ...