Creating a FIT-File in an app

Hi,

I am doing my first steps in coding an app. My first app is an easy table tennis app. I have a lot of statistical information to write to the FIT file but I want to do the basics first: writing the heart rate. Therefore I have done this:

using Toybox.ActivityRecording as Rec;
using Toybox.Sensor as Snsr;

class TableTennisView extends Ui.View {

var session;
var heartRateFitField;

function initialize() {
View.initialize();
Snsr.setEnabledSensors( [Snsr.SENSOR_HEARTRATE] );
Snsr.enableSensorEvents( method(:onSnsr) );


}

function onSnsr(sensor_info)
{
var HR = sensor_info.heartRate;
if (session != null && session.isRecording() && HR != null) {
heartRateFitField.setData(HR);
}
}

function onUpdate(dc) {
if ( ( session == null ) || ( session.isRecording() == false ) ) {
var sessionOptions = {:sport => Rec.SPORT_TENNIS,:subSport => Rec.SUB_SPORT_MATCH, :name => "Tischtennis"};
session = Rec.createSession(sessionOptions);
Sys.println(session);
heartRateFitField = session.createField("Heart Rate", 0, Rec.Session.DATA_TYPE_UINT8, {:mesgType => Rec.Session.MESG_TYPE_RECORD, :units => "bpm"});
session.start();
}
}
}


Nevertheless, I get this failure during runtime for the createField-line:

Failed invoking <symbol>
UnexpectedTypeException: Expected Number/Float/Boolean/Long/Double, given Class definition


I have tried really a lot of things to get that running, but it will not work. Can someone point me in the right direction?
  • Hi again,

    apart from my problem, that I cannot view the informations in the FIT file due to compatibility issues between the iq file and monkeygrap, I have some other issues:

    • The app created a FIT file with all that default information, Jim mentioned already. Saying this the fit file contains also speed information although I have not activated the GPS sensor. Is there a way to prevent this?
    • The session's name I gave in the createSession(options) is not displayed in GC, but the name was "unbekannt" (german word for unknown). I thought that this name is passed through to GC.
  • The app created a FIT file with all that default information, Jim mentioned already. Saying this the fit file contains also speed information although I have not activated the GPS sensor. Is there a way to prevent this?

    You'll likely get a "speed" based on steps and stride length, even without GPS. No way to block that. It might vary based on the sport you are using though.
    The session's name I gave in the createSession(options) is not displayed in GC, but the name was "unbekannt" (german word for unknown). I thought that this name is passed through to GC.


    The name isn't used. In Garmin Connect it will be "unknown" without GPS. With GPS it will be something like "mytown - Running". (there is a setting in GC so it will always be "unknown") With GPS, GC uses your start location to determine "mytown", and the second part is the sport, so I'll see things like "mytown - Hiking" and "nexttownover - Walking" with my apps.
  • Hi again,

    The app is now live on Garmin connect store but the data I contributed to the Fit-file is not visible on GC. The fit file itself with it's default information seems to work therefore I assume that the createField-code is also executed since the code is directly below the session definition:

    var sessionOptions = {:sport => Rec.SPORT_TENNIS,:subSport => Rec.SUB_SPORT_MATCH, :name => "Tischtennis"};
    session = Rec.createSession(sessionOptions);
    gameScoreField = session.createField("Game Score", 0, Fit.DATA_TYPE_FLOAT, {:mesgType => Fit.MESG_TYPE_LAP, :units => "P1.P2"});
    curMatchScoreField = session.createField("Match Score", 1, Fit.DATA_TYPE_FLOAT, {:mesgType => Fit.MESG_TYPE_LAP, :units => "P1.P2"});
    matchScoreField = session.createField("Match Score", 2, Fit.DATA_TYPE_FLOAT, {:mesgType => Fit.MESG_TYPE_SESSION, :units => "P1.P2"});
    pl1NameField = session.createField("Player 1", 3, Fit.DATA_TYPE_STRING, {:mesgType => Fit.MESG_TYPE_SESSION, :count => 10});
    pl2NameField = session.createField("Player 2", 4, Fit.DATA_TYPE_STRING, {:mesgType => Fit.MESG_TYPE_SESSION, :count => 10});
    totalnumOfPlayedPointsField = session.createField("Played Points", 5, Fit.DATA_TYPE_UINT32, {:mesgType => Fit.MESG_TYPE_SESSION} );
    totalNumOfSetpointsField = session.createField("Total Setpoints", 6, Fit.DATA_TYPE_UINT32, {:mesgType => Fit.MESG_TYPE_SESSION} );
    session.start();

    pl1NameField.setData(pl1Name);
    pl2NameField.setData(pl2Name);



    What is really confusing to me is that I have to reference the FitContributor (=Fit in my code) although I don't use it. I don't know whether this affects the problem I have. The problem is, that I can't find any of the values above in my Fit file displayed on GC web (note that the most setData() methods of the fields are called anywhere else in the code and are not depicted here). Any ideas what's going wrong?
  • What is really confusing to me is that I have to reference the FitContributor (=Fit in my code) although I don't use it.


    Really? Almost all of the code you posted refers to things in the Toybox.FitContributor module...

    gameScoreField = session.createField("Game Score", 0, Fit.DATA_TYPE_FLOAT, {:mesgType => Fit.MESG_TYPE_LAP, :units => "P1.P2"});
    curMatchScoreField = session.createField("Match Score", 1, Fit.DATA_TYPE_FLOAT, {:mesgType => Fit.MESG_TYPE_LAP, :units => "P1.P2"});
    matchScoreField = session.createField("Match Score", 2, Fit.DATA_TYPE_FLOAT, {:mesgType => Fit.MESG_TYPE_SESSION, :units => "P1.P2"});
    pl1NameField = session.createField("Player 1", 3, Fit.DATA_TYPE_STRING, {:mesgType => Fit.MESG_TYPE_SESSION, :count => 10});
    pl2NameField = session.createField("Player 2", 4, Fit.DATA_TYPE_STRING, {:mesgType => Fit.MESG_TYPE_SESSION, :count => 10});
    totalnumOfPlayedPointsField = session.createField("Played Points", 5, Fit.DATA_TYPE_UINT32, {:mesgType => Fit.MESG_TYPE_SESSION} );
    totalNumOfSetpointsField = session.createField("Total Setpoints", 6, Fit.DATA_TYPE_UINT32, {:mesgType => Fit.MESG_TYPE_SESSION} );


    Can you show us the fit_contributsions xml defining the fit fields (as described in the programmer's guide here)?
  • Hi Travis,

    in my understanding there are two classes (once the Session class which is part of the ActivityRecording class and secondly the watchUI class). Both classes offer a createField() method which (so was my understanding) are different ways to manage custom Fit fields. Till writing these post's lines I was the opinion that the createField() method was defined in the FitContributor class instead of the watchUI class. But it is only the return value (type Field) which is defined in the FitContributor class.

    This is the reason why I messed up things and thought that the two createField() methods are doing things totally different. But they are not. I currently have no fitContribution declaration at all in the resources XML file but I understand now that (and why) they are mandatorily needed.

    I will correct things as soon as possible and give it a try. I will update the thread then and share my experiences. Thanks for now.
  • OK, including the fields in the resources file with a fitContributor-XML tag did help. Thanks for that.

    Just another question concerning saving the Fit-file. I do the following:
    if (session != null && session.isRecording()) {
    session.stop();
    session.save();
    session = null;
    }
    Sys.exit();


    By doing this, the simulator quits properly. Nevertheless, if this is executed on the real watch it looks like the app has hang up itself. I can going back by pressing the back button and the fit file is stored and transferred to GC properly. Nevertheless, the hung up screen looks not very professional. Is there something I can do to make things more "professional"? I mean the user does not know at once that the back button still works.
  • When you make the save() call, you should be able to display a progress bar that goes away after some predetermined amount of time has elapsed. You _may_ (I've never tried) be able to tell if the save completed by looking at the result of the save() call. If that works, then you should be able to display the progress bar until the save is complete and no longer (if not, it seems like there should be an enhancement request to be able to do this).

    Anyways, here is some code...

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

    function onBack() {
    return true;
    }
    }

    class TimedProgressView extends Ui.ProgressBar
    {
    hidden var _M_period;
    hidden var _M_cycles;
    hidden var _M_count;
    hidden var _M_callback;

    function initialize(displayString, period, cycles, callback) {
    _M_period = period;
    _M_cycles = cycles;
    _M_count = 1;
    _M_callback = callback;

    ProgressBar.initialize(displayString, 100 * _M_count / _M_cycles);
    }

    hidden var _M_timer;

    function onTimer() {

    if (_M_cycles < _M_count) {
    _M_timer.stop();
    _M_timer = null;

    Ui.popView(Ui.SLIDE_DOWN);

    // invoke the callback so the app can do whatever it needs to
    // but our part of the app doesn't need to know.
    _M_callback.invoke();
    _M_callback = null;
    }
    else {
    _M_count += 1;

    setProgress(100 * _M_count / _M_cycles);
    }
    }

    function pushView() {
    Ui.pushView(self, new TimedProgressDelegate(), Ui.SLIDE_UP);

    _M_timer = new Timer.Timer();
    _M_timer.start(self.method(:onTimer), _M_period, true);
    }
    }


    if (session != null && session.isRecording()) {
    session.stop();
    session.save();

    // display a progress bar that updates once every 500ms for 5 seconds
    var view = new TimedProgressView("Saving...", 500, 10, self.method(:onSaveComplete));
    view.pushView();

    session = null;
    }
  • What watch/FW are you seeing this on?

    In the past (earlier versions of FW), I would put in some (sometimes two) popView calls if things got "stuck" in addition to (or in stead of) the Sys.exit().

    Ui.popView(Ui.SLIDE_IMMEDIATE);
  • This forum is really great. Your posts, especially from Travis and Jim, are so helpful. Thanks for all your help for us "beginners".

    Travis, I will try this to incorporate and will report results here.

    @Jim, GC shows: "Software: 5.40.0.0" (for my 235)
  • I'd still try using the popviews before the Sys.exit(). That should back you all the way out of the app

    so
    Ui.popView(Ui.SLIDE_IMMEDIATE);
    Ui.popView(Ui.SLIDE_IMMEDIATE);
    Sys.exit();


    What happens is when you pop the main view, the app exits... I found an app where I still do that and don't even do the Sys.exit() at all.