ConnectIQ 2.3.1: method(:myMethod).invoke()= could not find symbol method (sometimes)

A Descriptive Title
method(:testMethod) gives an error in certain contexts. Used to work fine on ConnectIQ 2.2.6

The Environment:
- OS X and Windows
- Eclipse Neon.2 (4.6.2) and Mars (4.5.2)
- ConnectIQ 2.3.1

A detailed description of the issue
- A function testMethod() is created out of a class and without explicitly defining a module.
- It can be called from inside a class and from other functions at the same level
- It can also be called from inside a class using method(:testMethod).invoke()
- It can also be called from other functions at the same level if method(:testMethod) is passed as a parameter and then .invoke() is called on this parameter.
- But if you use method(:testMethod) in a function on that same level, you get a runtime error: "Could not find symbol method"

It worked fine on ConnectIQ 2.2.6, both on simulator and real devices. Is it a bug or not allowed anymore?


Steps to reproduce the issue

using Toybox.WatchUi as Ui;
using Toybox.System as Sys;

class TestFooView extends Ui.View {

function onUpdate(dc) {
// Call the parent onUpdate function to redraw the layout
View.onUpdate(dc);

Sys.println("Calling testMethod() from TestFooView class.");
testMethod();

Sys.println("Calling method(:testMethod).invoke() from TestFooView class.");
method(:testMethod).invoke();

Sys.println("Calling callTestMethodFromTheSameLevel() from TestFooView class.");
callTestMethodFromTheSameLevel();

Sys.println("Calling receiveMethodObjAndInvoke(method(:testMethod)) from TestFooView class.");
receiveMethodObjAndInvoke(method(:testMethod));

Sys.println("Calling createMethodObjAndInvoke() from TestFooView class.");
createMethodObjAndInvoke();
}

function onHide() {}
function initialize() {View.initialize();}
function onLayout(dc) {setLayout(Rez.Layouts.MainLayout(dc));}
function onShow() {}
}

function callTestMethodFromTheSameLevel(){
Sys.println(" callTestMethodFromTheSameLevel() calls testMethod()");
testMethod();
}

function receiveMethodObjAndInvoke(aMethod){
Sys.println(" receiveMethodObjAndInvoke(aMethod) calls aMethod.invoke()");
aMethod.invoke();
}

function createMethodObjAndInvoke(){
Sys.println(" createMethodObjAndInvoke() calls method(:testMethod).invoke()");
method(:testMethod).invoke(); // <------------------------------------------ HERE IS THE PROBLEM!
}

function testMethod(){
Sys.println("-> OK! testMethod() responded.");
}


Output CIQ 2.3.1:
Calling testMethod() from TestFooView class.
-> OK! testMethod() responded.
Calling method(:testMethod).invoke() from TestFooView class.
-> OK! testMethod() responded.
Calling callTestMethodFromTheSameLevel() from TestFooView class.
callTestMethodFromTheSameLevel() calls testMethod()
-> OK! testMethod() responded.
Calling receiveMethodObjAndInvoke(method(:testMethod)) from TestFooView class.
receiveMethodObjAndInvoke(aMethod) calls aMethod.invoke()
-> OK! testMethod() responded.
Calling createMethodObjAndInvoke() from TestFooView class.
createMethodObjAndInvoke() calls method(:testMethod).invoke()
Could not find symbol method.Failed invoking <symbol>Symbol Not Found Error
in createMethodObjAndInvoke (C:\Users\rrezende\workspace\TestFoo\source\TestFooView.mc:44)
in onUpdate (C:\Users\rrezende\workspace\TestFoo\source\TestFooView.mc:23)


Output CIQ 2.2.6:
Calling testMethod() from TestFooView class.
-> OK! testMethod() responded.
Calling method(:testMethod).invoke() from TestFooView class.
-> OK! testMethod() responded.
Calling callTestMethodFromTheSameLevel() from TestFooView class.
callTestMethodFromTheSameLevel() calls testMethod()
-> OK! testMethod() responded.
Calling receiveMethodObjAndInvoke(method(:testMethod)) from TestFooView class.
receiveMethodObjAndInvoke(aMethod) calls aMethod.invoke()
-> OK! testMethod() responded.
Calling createMethodObjAndInvoke() from TestFooView class.
createMethodObjAndInvoke() calls method(:testMethod).invoke()
-> OK! testMethod() responded.
  • Thank you guys for studying the problem.

    I understand the point and the solutions. Still, I consider that it is necessary to discuss this issue further and address it properly in a next update (either making it more restrictive as Travis suggested or less restrictive as it was until 2.2.6).

    In my opinion it was quite practical (and cheap in terms of memory usage) when we could have some global functions which could be called from different classes (e.g. from a view and from a menu delegate), and then, from these functions, be able to call methods such as makeWebRequest() passing another global function as parameter with method(). As Travis pointed out, the solutions to achieve the same result require some extra overhead now, which I do not consider a progress when we are dealing with a low memory environment.

    But I'm just being a pragmatic about the trade-off between saving memory and getting useful error messages. I'll understand whichever decision Garmin takes, even if it is decided to keep it as it is in the current SDK.


    I would appreciate if we could know it as soon as possible, since it involves refactoring a considerable amount of code which used to work in my case. So my question is: will Garmin act on it and how?

    Another question: it seems that the apps created with CIQ 2.2.6 are not crashing due to this issue (luckily :-D). Is it possible that they will crash when new firmware updates come? This I would REALLY like to know on beforehand...

    Cheers!
  • Another question: it seems that the apps created with CIQ 2.2.6 are not crashing due to this issue (luckily :-D). Is it possible that they will crash when new firmware updates come? This I would REALLY like to know on beforehand...

    Cheers!


    With the exception of the Approach S60, all CIQ 2.x devices are already on 2.3.x. So in other words, the latest is already in place.
  • I am short of time and just tried Travis quick solution but I always get this error...


    I pasted your source into a simple sample program, set CUSTOM_FONT to false (because I didn't download your custom font) and did not see any problem in the simulator. I was able to reproduce the original problem posted by r.485 in the simulator, and my code above works around that problem. Do you only have problems on physical devices?
  • Still, I consider that it is necessary to discuss this issue further and address it properly in a next update (either making it more restrictive as Travis suggested or less restrictive as it was until 2.2.6).

    Agreed. It is unlikely that we'll be part of the discussion on the fix. By describing the problem in detail is helpful to others that may need to work around it in the meantime. It might also help the Garmin folks to figure out what is actually happening under the covers.

    I would appreciate if we could know it as soon as possible, since it involves refactoring a considerable amount of code which used to work in my case.

    It only requires refactoring if you want to make your code 'more correct'. If you just want to work around the problem, it should not be difficult update your code to explicitly qualify the object that you want to call the function on. This helper function should be sufficient...

    function explicitly_qualified_method(obj, symbol) {
    return new Toybox.Lang.Method(obj, symbol);
    }


    If you want to call a global function, the first parameter would be $. If you want a member function, it would be a reference to the object instance.

    Travis
  • I pasted your source into a simple sample program, set CUSTOM_FONT to false (because I didn't download your custom font) and did not see any problem in the simulator. I was able to reproduce the original problem posted by r.485 in the simulator, and my code above works around that problem. Do you only have problems on physical devices?


    Tested again with CUSTOM_FONT = false. I got a different error this time. Guess what, the errors are different if I build on eclipse and paste the prg file on the device or if I do build on Eclipse "build for device" and past on the device. The build for device may remove that information that I see here:

    ERROR: Symbol Not Found Error
    DETAILS: Could not find symbol Lang.
    STORE_ID: 00000000000000000000000000000000
    CALLSTACK:
    /home/cas/BodyFatControl/garmin_watchface/source/BodyFatControl_garmin_watchappView.mc (enableHRSensor:153)
    /home/cas/BodyFatControl/garmin_watchface/source/BodyFatControl_garmin_watchappView.mc (startSportMode:117)
    /home/cas/BodyFatControl/garmin_watchface/source/BodyFatControl_garmin_watchappView.mc (onKey:571)
    @PC = 0x30001c0d
    @PC = 0x30001e38

    The code on line 153: Sensor.enableSensorEvents(new Lang.method($, :onSensorHR));

    And now the log if build for device:

    ERROR: Symbol Not Found Error
    DETAILS: Failed invoking <symbol>
    STORE_ID: 00000000000000000000000000000000
    CALLSTACK:
    @PC = 0x100003bc
    @PC = 0x10000175
    @PC = 0x10001ed0
    @PC = 0x30001c0d
    @PC = 0x30001e38
  • With the "Build for Device Wizard", there's a checkbox at the bottom of the dialog: "Build Release Version of Project".

    I keep that box unchecked as I always want the debug symbols in a sideload. Sound like you have that box checked and the debug symbols aren't included in the .prg.

    Do you have a "using Toybox.Lang as Lang;" in you source file?
  • I use Sensor.enableSensorEvents() in many apps, but have never had a problem like you're seeing. What I do, is in initialize() for the view, do the setEnabledSensors() call followed by enableSensorEvents() - something like Sensor.enableSensorEvents(method(:sensorEvents));. Seems you don't enable the events until there's a key press. Not sure why that might cause a problem, but if you get the sensors going right away, you stand a better chance of external sensors being connected when you're ready to go. (a Tempe can take a minute to connect, and an external HRM will take longer than an OHRM)
  • There are really multiple solutions. The more correct solution (in my mind) is to get rid of the global functions onPhone and onSensorHR by making them methods of a class and then registering them with the system from that class. You should be able to move all of the methods to your app class like this...

    //
    // Note that I'm using C99 digraphs <% and %> to represent curly braces { and } below.
    //
    // This is only done so that I can post code to the forums. I cannot post code if I don't do this.
    // You will have to undo it if you actually use the code.

    class DFC extends App.AppBase {

    function initialize() <%
    AppBase.initialize();
    %>

    function onStart(state) <%
    Comm.registerForPhoneAppMessages(self.method(:onPhone));
    %>

    function onStop(state) <%
    Comm.registerForPhoneAppMessages(null);
    %>

    function getInitialView() <%
    return [ new DFC_garmin_watchappView(), new BaseInputDelegate()];
    %>

    //
    // the guts of the following functions should be moved from DFC_garmin_watchappView.mc to here
    //

    function onPhone(msg) <%
    %>

    function onSensorHR(info) <%
    %>

    function enableHRSensor() <%
    %>

    function disableHRSensor() <%
    %>
    }


    Tried that but didn't work, I get this error:

    ERROR: Invalid Value
    DETAILS: Failed invoking <symbol>
    STORE_ID: 00000000000000000000000000000000
    CALLSTACK:
    /home/cas/BodyFatControl/garmin_watchface/source/BodyFatControl_garmin_watchappView.mc (initialize:136)
    /home/cas/BodyFatControl/garmin_watchface/source/BodyFatControl_garmin_watchappApp.mc (getInitialView:23)

    Line 136: phoneMethod = method(:onPhone);

    Code is here: https://github.com/BodyFatControl/garmin_watchface/tree/testing/source

  • I use Sensor.enableSensorEvents() in many apps, but have never had a problem like you're seeing. What I do, is in initialize() for the view, do the setEnabledSensors() call followed by enableSensorEvents() - something like Sensor.enableSensorEvents(method(:sensorEvents));. Seems you don't enable the events until there's a key press. Not sure why that might cause a problem, but if you get the sensors going right away, you stand a better chance of external sensors being connected when you're ready to go. (a Tempe can take a minute to connect, and an external HRM will take longer than an OHRM)


    The thing is that my app is running 24h over 24h and so I just enable HR sensor when user want it enable (key press) and it disables automatically when HR < 90. Also, the sensor enables automatically when last minute of historic HR value > 90. The idea is to save battery.

    See here: https://apps.garmin.com/en-US/apps/ef509adc-a33a-4dec-9cb4-3a4b263ff471
  • /home/cas/BodyFatControl/garmin_watchface/source/BodyFatControl_garmin_watchappView.mc (initialize:136)
    /home/cas/BodyFatControl/garmin_watchface/source/BodyFatControl_garmin_watchappApp.mc (getInitialView:23)


    The call stack indicates you didn't remove the onPhone() stuff from your view class.