Connect IQ SDK

Positioning and Sensors

Monkey C provides access to the wearable’s available sensors, which may include the GPS, altimeter, thermometer, and supported ANT sensors.

Location

A Location is an abstraction of a coordinate. It exposes the ability to retrieve the coordinates in radians or decimal degrees and then provides a method to convert to coordinate formats supported by the Garmin system. The Position module also exposes string parsing interface to convert from various coordinate formats to a Location object.

// The GEO enum is used to retrieve coordinates in various String representations.
enum
{
    GEO_DEG,    // Degree Format, ddd.dddddd: 38.278652
    GEO_DM,     // Degree/Minute Format, dddmm.mmm: 38 27.865'
    GEO_DMS,    // Degree/Minute/Seconds Format, dddmmss: 38 27' 8"
    GEO_MGRS    // Military Grid Reference System (MGRS): 4QFJ12345678
}

// The Location object represents a position. It provides accessor
// methods for retrieving the coordinates in various formats.
class Location
{
    // Constructor: create a coordinate based off an options hash table
    // @param [Dictionary] options Hash table of options
    // @option options [Number] :latitude The latitude
    // @option options [Number] :longitude The longitude
    // @option options [Symbol] :format The format of lat/long (possible
    //        values are :degrees, :radians, or :semicircles)
    function initialize( options );

    // Use toDegrees() to retrieve the coordinate back as an Array of degree values.
    // @return [Array] An Array of the latitude and the longitude in degree format
    function toDegrees();

    // Use toRadians() to retrieve the coordinate back as an Array of radian values.
    // @return [Array] An Array of the latitude and the longitude in radian format
    function toRadians();

    // Use toGeoString() to get a String representation of the coordinate.
    // @param format Coordinate format to which coordinate should be
    //      converted (GEO constant)
    // @return [String] Formatted coordinate String
    function toGeoString( format );
}

Location Events

To enable the GPS call the enableLocationEvents() method. The parameters are outlined below:

enum
{
    LOCATION_ONE_SHOT,      // One-time retrieval of Location
    LOCATION_CONTINUOUS,    // Register for Location updates
    LOCATION_DISABLE        // Unregister for Location updates
}

// Request a location event with enableLocationEvents().
// @param type LOCATION_ONE_SHOT for a single location request,
//       LOCATION_CONTINUOUS to enable location tracking, and
//       LOCATION_DISABLE to turn off location tracking
// @param [Method] listener Method object to call with location updates
function enableLocationEvents( type, listener );

To register a position listener, use the method() call to create a Method callback:

function onPosition( info ) {
    Sys.println( "Position " + info.position.toGeoString( Position.GEO_DM ) );
}

function initializeListener() {
    Position.enableLocationEvents( Position.LOCATION_CONTINUOUS, method( :onPosition ) );
}

All of the location information will be sent in an Info object:

// The Location.Info class contains all information necessary for the Location.
// It can be passed on the update or it can be retrieved on demand.
class Info
{
    var position;   // Lat/lon
    var speed;      // Speed in meters per second
    var altitude;   // Altitude in meters, mean sea level
    var accuracy;   // Accuracy - good, usable, poor, not available
    var heading;    // Heading in radians
    var when;       // Moment Object: GPS time stamp of fix
}

// Use getInfo() to retrieve the current Location.Info
// @return [Location.Info] The Info object containing the current information
function getInfo();

Sensors

The Sensor module allows the app to enable and receive information from Garmin ANT+ sensors. To receive information, a listener method must be assigned and the sensors enabled:

function initialize() {
    Sensor.setEnabledSensors( [Sensor.SENSOR_HEARTRATE] );
    Sensor.enableSensorEvents( method( :onSensor ) );
}

function onSensor(sensorInfo) {
    System.println( "Heart Rate: " + sensorInfo.heartRate );
}

Sensor information is packaged in the Sensor.Info object:

// The Sensor.Info class contains all information necessary for the Sensor.
// It can be passed on the update or it can be retrieved on demand.
class Info
{
    var speed;          // Speed in meters per second
    var cadence;        // Cadence in revolutions per minute
    var heartRate;      // HR in beats per minute
    var temperature;    // Temperature in degrees Centigrade
    var altitude;       // Altitude in meters
    var pressure;       // Pressure in Pa
    var heading;        // Heading in Radians
}

The simulator can simulate sensor data via the Simulation menu by selecting Fit Data > Simulate Data. This generates valid but random values that can be read in via the sensor interface. For more acuate simulation, the simulator can play back a FIT file and feed the input into the Sensor module. To do this, select Simulation > Fit Data > Playback File and choose a FIT file from the dialog.

Accelerometer Data (CIQ 2.3)

The Sensor module now contains the ability to fetch samples of accelerometer data for a defined period of time with a certain frequency. This feature enables developers to implement their own custom motion detection algorithms within a Connect IQ application.

Accelerometer data can be obtained by registering a sensor data listener, which provides sampling parameters as well as a user-defined callback method that will be executed when new data is available. Sensor data requests are managed via the following new Sensor module methods and classes:


class SensorData
{
    // An AccelerometerData object.
    var accelerometerData;
}

class AccelerometerData
{
    // Array of sample x coordinates as floating point values.
    var x;

    // Array of sample y coordinates as floating point values.
    var y;

    // Array of sample z coordinates as floating point values.
    var z;

    // Array of sample pitch values. Can be null if pitch was not requested.
    var pitch;

    // Array of sample roll values. Can be null if roll was not requested.
    var roll;

    // Array of sample vector power values. Can be null if power was not requested.
    var power;
}

// Register a callback to fetch data from various sensors (currently only accelerometer data is supported)
function registerSensorDataListener(listener, options);

// Deregister a previously registered data request
function unregisterSensorDataListener();

// Get the maximum sample rate supported by the system
function getMaxSampleRate();

Calling registerSensorDataRequest() will enable your application to receive accelerometer data via the callback that you provide. The type and amount of data provided is configured via the options dictionary parameter, which supports the following fields:

Option Description
:period Period of time to request samples in ms. Maximum is 4000 (4 seconds)
:sampleRate Samples per second to request in Hz. Use getMaxSampleRate() to determine what the system can support.
:enableAccelerometer Set to true to fetch data from the accelerometer
:includePower Valid when :enableAccelerometer set to true. Requests that the power array be computed. Default false
:includePitch Valid when :enableAccelerometer set to true. Requests that pitch array be computed. Default false
:includeRoll Valid when :enableAccelerometer set to true. Requests that roll array be computed. Default false

Additionally, you must provide a callback method that accepts a single parameter, which is invoked by the system when new data is available. This method is passed a SensorData object populated with the requested sensor data. Note that only a single sensor data request can be active at any given time, so if you wish to submit a request while an existing one is active, you must first call deregisterSensorDataRequest(). After registerSensorDataRequest() is called, the callback that you provided will be invoked whenever a new set of samples are available. For example, if you request four seconds worth of data at 25Hz, the callback will be invoked every 100 samples taken.

A simplified usage example can be found below (for a more detailed sample see the PitchCounter sample app distributed with the SDK).


using Toybox.Sensor;

class MyAccelHistoryClass
{
    hidden var mSamplesX = null;
    hidden var mSamplesY = null;
    hidden var mSamplesZ = null;

    // Initializes the view and registers for accelerometer data
    function enableAccel() {
        var maxSampleRate = Sensor.getMaxSampleRate();

         // initialize accelerometer to request the maximum amount of data possible
        var options = {:period => 4000, :sampleRate => maxSampleRate, :enableAccelerometer => true};
        try {
            Sensor.registerSensorDataListener(method(:accelHistoryCallback), options);
        }
        catch(e) {
            System.println(e.getErrorMessage());
        }
    }

    // Prints acclerometer data that is recevied from the system
    function accelHistoryCallback(sensorData) {
        mSamplesX = sensorData.accelerometerData.x;
        mSamplesY = sensorData.accelerometerData.y;
        mSamplesZ = sensorData.accelerometerData.z;

        Toybox.System.println("Raw samples, X axis: " + mSamplesX);
        Toybox.System.println("Raw samples, Y axis: " + mSamplesY);
        Toybox.System.println("Raw samples, Z axis: " + mSamplesZ);
    }

    function disableAccel() {
        Sensor.unregisterSensorDataListener();
    }
}

Filtering Accelerometer Data

When processing accelerometer data, you may wish to apply filters to the raw sensor input that your application receives. IIR and FIR filter objects are provided to aid the developer in sample filtering. The following new objects have been added to the Math module:


module Math
{
    class FirFilter extends Filter
    {
        // Initialize the FIR filter
        function initialize(dictionary);

        // Apply the FIR filter to an array of samples
        function apply(data);
    }

    class IirFilter extends Filter
    {
        // Initialize the IIR filter
        function initialize(dictionary);

        // Apply the IIR filter to an array of samples
        function apply(data);
    }
}

Each filter class requires coefficients to be provided, which is done via the dictionary parameter. The options supported by each filter class are as follows:

FIR Filter

Option Description
:coefficients An array of float values that specify the filter coefficients. This can also be a resource ID referring to an embedded JSON resource that defines the array of values.
:gain A float value that specifies a multiplier to be applied to the coefficients.

IIR Filter

Option Description
::coefficientList1 An array of float values that specify the filter coefficients. This can also be a resource ID referring to an embedded JSON resource that defines the array of values.
:coefficientList2 An array of float values that specify the filter coefficients. This can also be a resource ID referring to an embedded JSON resource that defines the array of values.
:gain A float value that specifies a multiplier to be applied to the coefficients.

Once the filter object has been constructed, the apply function can be called, passing in an array of samples, and an array containing the filtered results will be returned.

A simplified usage example can be found below (for a more detailed sample see the PitchCounter sample app distributed with the SDK).

using Toybox.Sensor;

class MyAccelHistoryClass
{
    hidden var mFilter = null;
    hidden var mSamplesX = null;
    hidden var mSamplesY = null;
    hidden var mSamplesZ = null;

    function initialize() {
        mFilter = new Sensor.FirFilter({:coefficients=>[0.22,0.12,0.55], :gain=>1.2);
    }
    // Initializes the view and registers for accelerometer data
    function enableAccel() {
        var maxSampleRate = Sensor.getMaxSampleRate();

         // initialize accelerometer to request the maximum amount of data possible
        var options = {:period => 4000, :sampleRate => maxSampleRate, :enableAccelerometer => true};
        try {
            Sensor.registerSensorDataListener(method(:accelHistoryCallback), options);
        }
        catch(e) {
            System.println(e.getErrorMessage());
        }
    }

    // Prints acclerometer data that is recevied from the system
    function accelHistoryCallback(sensorData) {
        mSamplesX = mFilter.apply(sensorData.accelerometerData.x);
        mSamplesY = sensorData.accelerometerData.y;
        mSamplesZ = sensorData.accelerometerData.z;

        Toybox.System.println("Filtered samples, X axis: " + mSamplesX);
        Toybox.System.println("Raw samples, Y axis: " + mSamplesY);
        Toybox.System.println("Raw samples, Z axis: " + mSamplesZ);
    }

    function disableAccel() {
        Sensor.unregisterSensorDataListener();
    }
}

Logging Accelerometer Data

Accelerometer data can also be logged by the device, and can be played back in the simulator for testing applications that use the Accelerometer Data feature. Logging accelerometer data is done by passing a SensorLogger object to a FIT recording session.


module SensorLogging
{
    class SensorLoggingStats
    {
        var sampleCount;  // The total number of samples logged thus far
        var samplePeriod; // The total number of seconds of data logged thus far
    }

    class SensorLogger
    {
        function initialize(options);

        function getStats();
    }
}

The SensorLogger object with the same options provided to the Sensor.registerSensorDataListener() method. However, the logger will always record at a standard rate for consumption by the simulator.

A simplified usage example can be found below (for a more detailed sample see the PitchCounter sample app distributed with the SDK).


function initialize() {
    mLogger = new SensorLogging.SensorLogger({:enableAccelerometer => true});
    mSession = Fit.createSession({:name=>"mySession", :sport=>Fit.SPORT_GENERIC, :sensorLogger => mLogger});
}

function startLogging() {
    mSession.start();
}

function stopLogging() {
    mSession.stop();
}

function saveLogging() {
    mSession.save();
}

Sensor History (2.1)

The SensorHistory module allows the app to access saved sensor history on the device. Data from sensors can be accessed by getting an iterator.

function getHeartRateHistory(options);
function getTemperatureHistory(options);
function getPressureHistory(options);
function getElevationHistory(options);

All Sensor History types are not available on all devices. Capabilities should be validated using the ‘has’ operator. The get functions will return a SensorHistoryIterator type.

class SensorHistoryIterator {
    function next();
    function getMax();
    function getMin();
    function getNewestSampleTime();
    function getOldestSampleTime();
}

Calling the next() function will iterate through the history values until the end of the data is reached. When there is no more data the iterator will return null. This function returns a SensorSample:

class SensorSample {

    var data; // Sample data. Can be null if not valuid
    var when; // The time this sample occurred
}

The iterator can be adjusted to provide the newest data values first or the oldest values first by using the enumeration values ORDER_NEWEST_FIRST and ORDER_OLDEST_FIRST

Fit File Recording

Imagine you are trying to create a Yoga app. You’d like the app to record heart rate and calories burned during your Yoga workout, just like other Garmin apps. You’d also like the recording to be represented on Garmin Connect. Monkey C allows for apps to start and stop recording of FIT files. Controlling the FIT file recording requires a few steps:

  1. Enable the sensors to be recorded
  2. Use Fit.createSession(options) to create a session object
  3. Use the start() method of the FIT session to begin recording. Data from the enabled sensors will be recorded into the FIT file.
  4. Use stop() to pause the recording
  5. Use save() to save the recording, or discard() to delete the recording

The FIT file will sync with Garmin Connect. You can use the Garmin Connect API to process the FIT file from a web service.

Recording FIT Files (CIQ 1.3)

In addition to being able to play back existing FIT files, the simulator can also record files using data obtained via the Fit Data>Simulate Data menu option. To begin recording a session you must start the timer; this can be done via the Data Fields>Timer>Start Activity menu option, or by clicking the corresponding start button on the device you have selected. The following table describes the different options available for activity recording and provides instructions for how to access them via the menu bar, or the device buttons (where available).

Option Menu Button Device Button Notes
Start Activity Data Fields>Timer>Start Activity Start Button The device start button starts and stops an activity.
Stop Activity Data Fields>Timer>Stop Activity Start Button The device start button starts and stops an activity, and the Start Activity menu item is renamed to Stop Activity when recording is active.
Lap Activity Data Fields>Timer>Lap Activity Back Button Records a lap record in the FIT file.
Pause Activity Data Fields>Timer>Pause Activity N/A Pauses activity recording. Note this option is only available when FIT data simulation is enabled.
Resume Activity Data Fields>Timer>Resume Activity N/A Resumes activity recording. Note this option is only available when FIT data simulation is enabled and activity recording is paused.
Discard Activity Data Fields>Timer>Discard Activity N/A Discards the current recording and deletes the corresponding .fit file. Note this is only enabled if activity recording has been started and subsequently stopped.
Save Activity Data Fields>Timer>Save Activity N/A Saves the recorded activity into a .fit file and resets the timer state. Note this is only enabled if activity recording has been started and subsequently stopped.

Note: the device simulator will not automatically start activity recording when a data field is being run. You must excplitily start and stop activity recording via the menu options described here.

FIT Developer Fields (CIQ 1.3)

Now imagine you want to add a new metric - Namastes - to the Yoga app[1]. This metric will combine heart rate, accelerometer, and other sensor data into a single value, and does not have an analog in any Garmin recording metric. To do this we need the FitContributor API, which allows you to add new metrics to a FIT recording and display it on Garmin Connect.

First, you must enable the FitContributor permission in the manifest file. Next, you need to add your field definitions in your resources using the fitContributions block:

    <strings>
        <string id="namaste_label">Namastes</string>
        <string id="namaste_graph_label">Namastes</string>
        <string id="namaste_units">N(s)</string>
    </strings>
    <fitContributions>
        <fitField id="0" displayInChart="true" sortOrder = "0" precision="2"
        chartTitle="@Strings.namaste_graph_label" dataLabel="@Strings.namaste_label"
        unitLabel="@Strings.namaste_units" fillColor="#FF0000" />
    </fitContributions>

The fitField block has a number of configurable options:

Attribute Value Notes
id A numeric value from 0 to 255 used to refer to your field No duplicates within an app are allowed
displayInChart Indicates whether or not record level connect IQ data should be rendered in a chart true if you want this entry to be displayed as a chart, false otherwise. Graph fields can only support numeric data.
displayInActivityLaps Indicates whether or not lap level connect IQ data should be rendered in the Activity Laps section in the Garmin Connect Activity Details page true if you want this entry to be displayed in the activity laps, false otherwise
displayInActivitySummary Indicates whether or not activity (fit session) level connect IQ data should be rendered in the Activity Summary section in the Garmin Connect Activity Details page true if you want this entry to be displayed in the activity summary data, false otherwise
sortOrder Determines the order in which the Connect IQ data will appear in the Summary or Lap Sections of the Activity Details page and what order charts will be displayed on the Activity Details page No duplicates are allowed
precision Decimal point precision for numeric data 0 for integer, 1 for one decimal point, 2 for two decimal points. Without this attribute the default is no rounding
chartTitle This is the resources string key to use to render the title of the chart Optional if displayInChart is false. Must be a string resource.
dataLabel This is the resources string key to use to render the label of the data field in the Activity Summary or Activity Laps section of the Activity Details page (ex. Cadence, or Heart Rate). Must be a string resource
unitLabel This is the key to use to render the unit of the data field in the Activity Summary or Activity Laps section of the Activity Details page (ex. kph, or miles). Must be a string resource
fillColor RRGGBB value of color to use for chart Optional if displayInChart is false

This will communicate the metadata to display our chart to Garmin Connect. Now we need to create our Field in the code. You do this by using the createField method of the Session object within your source:

// Field ID from resources.
const NAMASTE_FIELD_ID = 0;
hidden var mNamasteField;

// Initializes the new Namaste field in the activity file
function setupField(session) {
    // Create a new field in the session.
    // Current namastes provides an file internal definition of the field
    // Field id _must_ match the fitField id in resources or your data will not display!
    // The field type specifies the kind of data we are going to store. For Record data this must be numeric, for others it can also be a string.
    // The mesgType allows us to say what kind of FIT record we are writing.
    //    FitContributor.MESG_TYPE_RECORD for graph information
    //    FitContributor.MESG_TYPE_LAP for lap information
    //    FitContributor.MESG_TYPE_SESSION` for summary information.
    // Units provides a file internal units field.
    mNamasteField = session.createField("current_namastes", NAMASTE_FIELD_ID, FitContributor.DATA_TYPE_FLOAT, { :mesgType=>Fit.MESG_TYPE_RECORD, :units=>"N" });
}

Now when you call setData on mNamasteField the value will be recorded into either the Record, Lap, or Session information of the FIT file based on the message type specified when calling createField. If you are creating a Record (graph), you should update this value once a second. For lap and summary, you should provide constant updates to the current lap or workout value for the metric.

After setting this up, you will want to preview how this will look on Garmin Connect. We can use the Monkeygraph tool to create a preview. The Monkeygraph tool requires the following: a FIT file with the recorded developer data, and an IQ file of the app (you can acquire an IQ file via the [App Export Wizard]). The tool will allow you to test how charts will look before uploading your app for review.

There are two ways that you can use the Monkeygraph tool included with the SDK:

Launching the Mokeygraph tool with the Eclipse Plugin:

  1. Navigate to “Connect IQ” in the Eclipse menu
  2. Click “Start Monkeygraph”
  1. This launches a new window for the Monkeygraph tool

Launching the Monkeygraph tool with the command line:

You can start the Monkeygraph tool with following from the command line:

$ monkeygraph

Using the Monkeygraph tool:

  1. Click on “File”
  1. Click “Open IQ File”
  2. Select the IQ File generated from your project
  3. Click “Open FIT File”
  4. Select the Fit file with the recorded developer data

A new view will launch a graph showing the data:

And here we see some secondary data on another page:

Communicating With ANT/ANT+ Sensors

Generic ANT channels

Connect IQ provides a low level interface for communication with ANT and ANT+ sensors. With this interface, an ANT channel can be created to send and receive ANT packets. The MO2Display sample provides a sample application that implements the Muscle Oxygen ANT profile. The ANT Generic interface is not available to watch faces. Low and High priority search timeout for sensors differs from the basic ANT radio specification to allow for interoperation with native ANT behavior on devices. These are limited to a maximum timeout of 30 seconds and 5 seconds respectively.

Burst Data (CIQ 2.2)

Burst data transmission provides a mechanism for large amounts of data to be sent between devices over an ANT Generic Channel. Developers are notified through a listener of the success/failure of burst transmit/receive events. Burst data transmission is limited to up to 8Kb of data at a time.

Common use cases for this include passkey authentication or sending/receiving configuration data between devices.

The GenericChannelBurst sample provides a demonstration of transmitting and receiving burst data.

ANT+ Profiles (CIQ 2.2)

The AntPlus module allows access to information about ANT+ sensors that are paired to a user’s device without requiring you to set up and manage the ANT channel yourself. All management of ANT+ sensors such as adding, removing, enabling, disabling, and calibrating is managed by the user via the device’s regular sensor menus.

An extension of a sensor-specific listener is passed into the constructor for a sensor-specific extension of Device. If there is a sensor of the given type paired to the user’s device, information about that sensor can be retrieved using the sensor-specific getters, or through the common data getters such as GetBatteryStatus(identifier). Null can be passed in as the identifier for sensor types (like most) that do not support multi-components.

Callbacks in the DeviceListener and extensions of it will be called automatically if a sensor of the given type is paired and the corresponding information is updated via ANT. For example, onDeviceStateUpdate() will be called if a sensor’s ANT channel goes from connected to searching, or if the user switches the sensor ID of a given type that their device is connected to. Callbacks like onCalculatedPowerUpdate() will be called when new pieces of information about a power sensor are received via ANT.

Certain ANT+ sensors, such as bike lights, have special callbacks. For example, the onLightNetworkStateUpdate() callback should be used to understand the light network’s state rather than onDeviceStateUpdate(). The LightNetwork class will allow you to make changes to bike light modes, given there are bike lights paired to the user’s device and a light network is fully formed.

Not all ANT+ profiles provisioned by Monkey C will be supported by every Connect IQ-compatible device.


  1. How can we possibly tell who is more at peace with the universe if we don’t have a metric?  ↩