Measurement API

This page describes how to create a measurement plugin for NEON Signal Mapper using the NEON Measurement API.

For a complete example of a NEON Signal Mapper plugin that can simulate data or read from an Android’s GPS receiver, look at our example code on GitHub.

Note: The ability to load a plugin into NEON Signal Mapper must be enabled in your subscription. Please contact support@trxsystems.com if you are interested in this feature.

Introduction

NEON Signal Mapper combines real-time locations with signal data in order to produce heatmaps indoors and out. By creating a measurement plugin, custom sensors can be integrated with the NEON Signal Mapper application, allowing heatmaps to be generated for any signal.

In order to add custom measurement data to NEON Signal Mapper, the SignalDefinitionBuilder is used to define the data that will be heatmapped, displayed, and exported to reports.

Then a MeasurementPlugin class can be registered so the NEON Signal Mapper can request the signal definitions defined by the plugin, and log signal measurements when the system is mapping.

Defining a signal

A signal definition provides everything needed for NEON Signal Mapper to display heatmaps and signal information on Android and NEON Command. A signal definition builder is provided to assist you in creating a signal.

A simple signal

Let’s start by looking at the simplest signal you can create. Assume your phone has a GPS receiver and you’d like to measure the carrier-to-noise-density ratio (C/N0) and map it using NEON Signal Mapper. You could create a signal that would look something like this:

public static SignalDefinition SatelliteInformation = new SignalDefinitionBuilder("Manufacturer", "GPS", "Satellite Information")
        .AddHeatmapDefinition("c/n0", "C/N0", "%.1f dB-Hz", "%f", "dB-Hz", 0, 50, 40, false, 0)
        .Build();

The SignalDefinitionBuilder takes 3 arguments which are used to create a key for your signal - and the combination needs to be unique. The arguments are:

Argument Description
Provider Usually the company name, e.g., TRX Systems
Technology Technology used to receive the signal, such as Wi-Fi, BLE, USB, GPS etc.
Signal Signal name such as 2.4 GHz for Wi-Fi or iBeacon for BLE

For our example we’ve created a Signal named: “Manufacturer GPS Satellite Information.”

The next step is to define a value that we can map as a heatmap by recording measurements at various locations. For this example we want to map the received carrier-to-noise-density ratio at our current location as we walk outdoors. For this, we add a Heatmap definition. The arguments needed are:

Argument Description
FieldName The name of this value, which should be unique in the signal, such as “cn0”
DisplayName The string to use when displaying this signal in NEON Signal Mapper and Command, such as “C/N0”
DisplayFormat A Java-style format string for display purposes - for instance “%.1f dB-Hz” to display a dB-Hz measurement to two decimal places (37.5 dB-Hz)
ExportFormat A Java-style format string for CSV export purposes - for instance “%.1f” to display a dB-Hz measurement to two decimal places (37.5)
Units The units for the measurement, such as “dB-Hz”
Min Minimum value for this measurement
Max Maximum value for this measurement
Threshold Value at which this measurement transitions from a “bad” to a “good” value, used in grid testing
LocateSources If set to true, NEON Command will attempt to use heatmaps to localize the source of this signal. This should only be used for local sources such as iBeacons or Wi-Fi access points for good results.
SortIndex Indicates where this value should be listed relative to other details of the signal

Signal Definitions with identifiers

The simple signal described above is sufficient to generate a heatmap of channel power on Command, but to prevent all heatmaps from being merged together you can separate measurements from different sources by adding an Identifier to the signal definition. For instance, you could register the satellite ID and constellation type as identifiers, and then different satellites would produce different heatmaps in Command. Let’s revisit our GPS receiver example, but this time add identifiers

public static SignalDefinition SatelliteInformation = new SignalDefinitionBuilder("Phone", "GPS", "Satellite Information")
        .AddIdentifierDefinition("id", "Satellite ID", "%d", "%d", false, DisplayType.Integer, 1)
        .AddIdentifierDefinition("type", "Constellation", "%s", "%s", false, DisplayType.String, 0)

        .AddHeatmapDefinition("c/n0", "C/N0", "%.1f dB-Hz", "%f", "dB-Hz", 0, 50, 40, false, 2)

        .Build();

We’ve created two identifiers for this signal that correspond to the center frequency and span frequency in MHz. The arguments for an identifier are the following:

Argument Description
FieldName The name of this value, which should be unique in the signal, such as “id”
DisplayName The string to use when displaying this signal in NEON Signal Mapper and Command, such as “Satellite ID”
DisplayFormat A Java-style format string for display purposes - for instance “%.1f dB-Hz” to display a dB-Hz measurement to two decimal places (37.5 dB-Hz)
ExportFormat A Java-style format string for CSV export purposes - for instance “%.1f” to display a dB-Hz measurement to two decimal places (37.5)
SeparateExportFiles If set to true, CSV export files from command will be separated based on this identifier. For instance, if there is a channel index as an identifier than each channel index present will get a separate CSV file
DisplayType The field type of this identifer - currently String, Integer, or Float. This type must match with the format display and export format strings.
SortIndex Indicates where this value should be listed relative to other details of the signal

By adding these identifiers, if you track multiple satellites, you will be able to select and view each heatmap separately, and generate separate CSV export files for each combination.

Signal Definitions with additional data

If you have additional data that you would like to associate with a measurement that is neither a heatmap value nor an identifier, you can add it with a display value definition. This would be any information that may change from measurement to measurement, such as status. For instance, say you wanted to display the elevation, azimuth, and whether the particular satellite is being used in the location fix. You could add a display definitions such as this:

public static SignalDefinition SatelliteInformation = new SignalDefinitionBuilder("Phone", "GPS", "Satellite Information")
        .AddIdentifierDefinition("id", "Satellite ID", "%d", "%d", false, DisplayType.Integer, 1)
        .AddIdentifierDefinition("type", "Constellation", "%s", "%s", false, DisplayType.String, 0)

        .AddHeatmapDefinition("c/n0", "C/N0", "%.1f dB-Hz", "%f", "dB-Hz", 0, 50, 40, false, 2)

        .AddDisplayDefinition("elevation", "Elevation", "%.1f\u00B0", "%.1f", DisplayType.Float, 4)
        .AddDisplayDefinition("azimuth", "Azimuth", "%.1f\u00B0", "%.1f", DisplayType.Float, 5)
        .AddDisplayDefinition("used", "Used In Fix", "%s", "%s", DisplayType.String, 6)

        .Build();

The display definition arguments are as follows:

Argument Description
FieldName The name of this value, which should be unique in the signal, such as “elevation”
DisplayName The string to use when displaying this signal in NEON Signal Mapper and Command, such as “Elevation”
DisplayFormat A Java-style format string for display purposes - for instance “%.1f dB-Hz” to display a dB-Hz measurement to two decimal places (37.5 dB-Hz)
ExportFormat A Java-style format string for CSV export purposes - for instance “%.1f” to display a dB-Hz measurement to two decimal places (37.5)
DisplayType The field type of this identifer - currently String, Integer, or Float. This type must match with the format display and export format strings.
SortIndex Indicates where this value should be listed relative to other details of the signal

Additional data will be displayed in the signal details tab, the signal reading details in Command, and be exported in a CSV file.

Recording a signal measurement from a signal definition

Once you have a signal definition, a single measurement can be recorded using the SignalMeasurementBuilder, which can be generated directly from the definition with SignalDefinition.generateSignalMeasurementBuilder().

A signal measurement must include a timestamp and values for each of the identifiers, heatmap values (always floats), and additional data in the signal definition. Values are added with the FieldName of each data type.

An example signal measurement for the above signal definition would look like this:

SignalMeasurement satelliteMeasurement = SatelliteInformation.generateSignalMeasurementBuilder()
                .AddTimestamp(System.currentTimeMillis())
                .AddIdentifier("id",1)
                .AddIdentifier("type","Galileo")
                .AddHeatmapValue("c/n0",37.5f)
                .AddAdditionalData("elevation",23)
                .AddAdditionalData("azimuth",42)
                .AddAdditionalData("used","Yes")
                .Build();

Creating a Measurement Plugin

Once you’ve defined the signals (or set of signals) that will be sent to NEON Signal Mapper, create a measurement plugin by extending the MeasurementPlugin class in the NEON Measurement API. Your plugin should handle the following functions:

import com.trx.neon.api.neonMeasurement.MeasurementPlugin;
import com.trx.neon.api.neonMeasurement.model.definitions.SignalDefinition;

public class GPSReceiverPlugin extends MeasurementPlugin
{
    public MeasurementPlugin GetPlugin() 
    {
        return this;
    }

    @Override
    public void StartLogging() {} 

    @Override
    public void StopLogging() { }

    @Override
    public List<SignalDefinition> GetSignalDefinitions() { }

    @Override
    public boolean hasSettingsActivity() { }

    @Override
    public String getSettingsTitle() { }

    @Override
    public void StartSettingsActivity() { }
}

GetPlugin() allows NEON Signal Mapper to retrieve and call functions in your plugin. You should always return the plugin class.

GetSignalDefinitions() should return the list of SignalDefinitions that are defined by your plugin. NEON Signal Mapper will retrieve this list when the plugin is loaded on starting the application.

hasSettingsActivity() should return true if a settings page is available to start. This settings page will be included in the NEON Signal Mapper menu and can be used to store parameters such as an IP address or start-up configuration parameters for a custom measurement device.

getSettingsTitle() should return the name for the settings page if there is one.

StartSettingsActivity() will be called when the user clicks to bring up the settings page. See below for starting a local activity from this remote context.

StartLogging() will be called when the user begins mapping in NEON Signal Mapper. This is a good time to start a local service that will continuously read signal measurements and pass them to NEON Signal Mapper with the provided measurementCallback.OnMeasurementAvailable(signalMeasurement).

Stop Logging() will be called when the user ends the mapping session. At this time, all logging services should be stopped.

Configuring your manifest

In order for your plugin to be loaded by NEON Signal Mapper, the class that extends MeasurementPlugin must register as a plugin in the manifest.

Here is a sample manifest that registers the GPSReceiverPlugin with the category “com.trx.neonsignalmapper.PLUGIN_APPLICATION,” and also registers a settings activity and a logging service. Note that the settings activity and logger service must be marked with android:exported="true" in order to be opened by NEON Signal Mapper.

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.gpsplugin">

    <application
        android:allowBackup="false"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:theme="@style/AppTheme">

        <activity android:name=".GPSReceiverPlugin">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="com.trx.neonsignalmapper.PLUGIN_APPLICATION"/>
            </intent-filter>
        </activity>

        <activity
            android:name=".SettingsActivity"
            android:exported="true"
            android:label="GPS Plugin Settings"
            android:configChanges="orientation|screenSize">
        </activity>

        <service
            android:name=".LoggerService"
            android:exported="true"
            android:label="Logger Service"
            android:configChanges="orientation|screenSize">
        </service>
    </application>

</manifest>

Starting a settings activity

Since your plugin and NEON Signal Mapper are in different processes, starting your local settings activity needs to be done remotely. A helper method has been provided that can be used to start your settings activity if provided with the package and full class names. If a settings activity is started this way, preferences will be saved to the local context, and will not be available in the measurement plugin’s signalMapperContext. To access these settings, start a local logging service and read them in that context.

 @Override
public void StartSettingsActivity()
{
    Intent intent = GetRemoteActivityIntent("com.example.gpsplugin","com.example.gpsplugin.SettingsActivity");
    signalMapperContext.startActivity(intent);
}

Using a logging service to take measurements

Since your plugin and NEON Signal Mapper are in different processes, starting your local logging service also needs to be done remotely. A helper method has been provided that can be used to start/stop your logger service remotely if provided with the package and full class names. Because the service is being started remotely, binding to the service will fail. In order to pass signal measurement data back to the measurement plugin, register a BroadcastReceiver that will take updates from your LoggerService and pass it to NEON Signal Mapper.

public static final String MEASUREMENT_UPDATE =  "com.trx.neon.signalmapperplugin.measurement";
    private BroadcastReceiver loggerServiceReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent)
        {
            SignalMeasurement satelliteMeasurement = SatelliteInformation.generateSignalMeasurementBuilder()
                .AddTimestamp(System.currentTimeMillis())
                .AddIdentifier("id",intent.getIntExtra("id", -1))
                .AddIdentifier("type",intent.getStringExtra("type"))
                .AddHeatmapValue("c/n0",intent.getFloatExtra("c/n0", -1f))
                .AddAdditionalData("elevation",intent.getFloatExtra("elevation", -1f))
                .AddAdditionalData("azimuth",intent.getFloatExtra("azimuth", -1f))
                .AddAdditionalData("used",intent.getStringExtra("used"))
                .Build();
            measurementCallback.OnMeasurementAvailable(channelPowerMeasurement);
        }
    };

@Override
public void StartLogging()
{
    Intent intent = GetRemoteServiceIntent("com.example.gpsplugin","com.example.gpsplugin.LoggerService");
    ContextCompat.startForegroundService(signalMapperContext, intent);
    signalMapperContext.registerReceiver(loggerServiceReceiver, new IntentFilter(MEASUREMENT_UPDATE));
}

@Override
public void StopLogging() {
    Intent intent = GetRemoteServiceIntent("com.example.gpsplugin","com.example.gpsplugin.LoggerService");
    signalMapperContext.stopService(intent);
    signalMapperContext.unregisterReceiver(loggerServiceReceiver);
}

Note that, on Android Oreo or later, this service will have to be started as a foreground service to avoid being killed. The code in the below example in the onCreate() method will start a foreground service if required. This example will broadcast the data required to form a signal measurement back to the receiver in the GPSReceiverPlugin, which will then form a signal measurement and send it to NEON Signal Mapper.

public class LoggerService extends Service
{
    protected final ScheduledExecutorService signalLoggerExec = Executors.newScheduledThreadPool(1);

    @Override
    public IBinder onBind(Intent intent) {

        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        if (Build.VERSION.SDK_INT >= 26) {
            String CHANNEL_ID = "gps_mapper_plugin_channel";
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
                CHANNEL_ID,
                NotificationManager.IMPORTANCE_DEFAULT);

            ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);

            Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_gps)
                .setContentText("Currently logging GPS")
                .setOngoing(true)
                .setOnlyAlertOnce(true)
                .build();

            startForeground(3356, notification);
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId)
    {
        //settings saved in the settings activity can be retrieved here and used to configure your measurement device
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        String testValue = sharedPreferences.getString("plugin_test","no value");
        Log.i("signalplugin","value: "+testValue);
        Toast.makeText(getApplicationContext(),testValue,Toast.LENGTH_SHORT).show();

        signalLoggerExec.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                Random random = new Random();
                for(int i =0; i < 12; i++)
                {
                    Intent intent = new Intent(MEASUREMENT_UPDATE);
                    intent.putExtra("id", i);
                    intent.putExtra("type", "Galileo");
                    intent.putExtra("c/n0", random.nextFloat()*20+30);
                    intent.putExtra("elevation", random.nextFloat()*90);
                    intent.putExtra("azimuth", random.nextFloat()*90);
                    intent.putExtra("used", random.nextFloat() > 0.5f ? "Yes": "No");
                    sendBroadcast(intent);
                }
            }
        },0,1000, TimeUnit.MILLISECONDS);
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        signalLoggerExec.shutdown();
        super.onDestroy();
    }
}