Passive App Plugin Development Case Study

This guide will take you through the development of a new passive app plugin with examples of how we developed the Faros plugin.

Initialise plugin repository

To get started, clone the Template plugin repository.

The following steps describe how to set up your own device repository by refactoring the template. For renaming the package and classes (steps 3 and 4), use the refactor functionality of Android Studio (shift-F6). This also renames all references to the packages and classes.

  1. Change directory name (radar-android-template => radar-android-faros)
  2. Open the project in Android Studio.
  3. Rename the package (org.radarbase.passive.template => org.radarbase.passive.bittium).
  4. Rename the following classes. e.g.:
    • TemplateDeviceManager => FarosDeviceManager
    • TemplateDeviceServiceProvider => FarosProvider
    • TemplateDeviceService => FarosService
    • TemplateDeviceStatus => FarosDeviceStatus
  5. Edit build.gradle and input the configuration parameters

    ......
    //---------------------------------------------------------------------------//
    // Configuration                                                             //
    //---------------------------------------------------------------------------//
    
    group = 'org.radarbase'
    ext.moduleName = 'radar-android-faros'
    ext.description = 'Faros plugin for RADAR passive remote monitoring app'
    ext.githubRepoName = 'RADAR-BASE/radar-android-faros'
    .....
  6. Edit strings.xml to give your device a label and description (used in the DeviceServiceProvider)

    <resources>
        <string name="farosLabel">Faros</string>
        <string name="farosDescription">Collects data from Faros 90/180/360. The connection is made over Bluetooth using the location permission. Location
            information is not stored.
        </string>
    </resources>
  7. Edit AndroidManifest.xml to put in the references of package and service. 

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="org.radarbase.passive.bittium" >
        <application android:allowBackup="true">
            <service android:name=".FarosService"/>
        </application>
    </manifest>
    • package refers to the name given to the package (step 3)
    • service android:name refers to the DeviceService class (step 4)

When you have completed the refactoring, your project should look like this in Android Studio. Check if everything is renamed properly by building the app with Gradle (./gradlew build).


Develop the schemas

  1. Add the schemas of the data you intend to send to the RADAR-base Schemas repository. For the pRMT app, .avsc schemas for each plugin are stored in a different folder in the RADAR-Schemas/commons/passive directory. Be sure to set the namespace property to org.radarcns.passive.plugin so that generated classes will be put in the right package. All values should have time and timeReceived fields, with type double. These represent the time in seconds since the Unix Epoch (1 January 1970, 00:00:00 UTC). Subsecond precision is possible by using floating point decimals. For instance, the schema for the batterylevel will look as follows:

    {
      "namespace": "org.radarcns.passive.bittium",
      "type": "record",
      "name": "BittiumFarosBatteryLevel",
      "doc": "Battery level of a Bittium Faros device.",
      "fields": [
        { "name": "time", "type": "double", "doc": "Device timestamp in UTC (s)." },
        { "name": "timeReceived", "type": "double", "doc": "Device receiver timestamp in UTC (s)." },
        { "name": "batteryLevel", "type": "float", "doc": "Battery level from 0 to 1. Note that the battery level is a rough estimate." },
        { "name": "exact", "type": "boolean", "doc": "Whether the batteryLevel can be taken as an exact value. True if the battery level is an exact representation, false if it is just an estimate based on the battery status (CRITICAL/LOW/MEDIUM/HIGH)." }
      ]
    }
  2. Create also an .yml file in RADAR-Schemas/specifications/passive/plugin-1.0.0.yml:

    #====================================== Faros 90/180/360 device =====================================#
    vendor: Bittium
    model: Faros
    version: 1.0.0
    app_provider: .bittium.FarosServiceProvider
    data:
      - type: BATTERY
        sample_rate:
          dynamic: true
        unit: PERCENTAGE
        processing_state: RAW
        topic: android_bittium_faros_battery_level
        value_schema: .passive.bittium.BittiumFarosBatteryLevel

    If you need a unit that has not yet been implemented in RADAR-schemas, you can add it to the RADAR-Schemas/commons/catalogue/unit.avsc file.

    After creating all the .avsc files and the .yml file, make sure you can build the project using java version 17 (export JAVA_HOME=’/usr/libexec/java_home -v 17.0.10’).


  3. Publish schemas:


    Publishing schemas to Maven Local:

    When you are still developing your plugin, schemas are not published centrally. To access the java generated file from avro schemas first publish them to Maven Local.

    To publish schemas locally to Maven, please follow these steps:

    • Change your working directory to java-sdk in RADAR-Schemas.
    • Run the following commands in your terminal:

      ./gradlew build 
      ./gradlew publishToMavenLocal
    • In your build.gradle file, add the following to your repositories block:

      repositories {
          mavenLocal()
      }
    • In the same build.gradle file, add the following to your dependencies block, replacing $radarSchemasVersion with its corresponding value:

      dependencies {
          implementation "org.radarbase:radar-schemas-commons:$radarSchemasVersion"
      }
      You can find the version value in the metadata of your local Maven repository. It should end in SNAPSHOT.


Publishing schemas to Docker:

If your plugin is ready for production, you can publish your schemas using Docker. To do so, create a Docker account and download the Docker application. Make sure docker daemon is running. In the /RADAR-Schemas directory, run the following commands:


docker buildx build . --tag username/radar-schemas:0.1.0 

docker push username/radar-schemas:0.1.0

After your schemas have been pushed to Docker, they can be deployed on the RADAR-base instance (see Deploy Schemas on RADAR-base)

4. Deploy schemas on the RADAR-base instance:

After your schemas have been pushed to Docker, they can be deployed on the RADAR-base instance, by following these steps:

  • Add the created schemas image to the catolog_server deployment by running:

    kubectl edit deployment/catalog_server
  • Add the username/radar-schemas:0.1.0 to the file after schemas → image. Make sure you add this line in two different places, and save the changes you made to the deployment file.
  • After the catalog_server deployment has been edited, make sure you start the following pods again in this order. Wait until each pod has restarted, before restarting the next one (you can see the current pod status using: kubectl get deployments).

    kubectl rollout restart deployment catalog-server
    kubectl rollout restart deployment management-portal
    kubectl rollout restart deployment radar-s3-connector
  • To check whether the schemas have been correctly deployed, you can run the following command (make sure to use the correct catalog-server pod name, using kubectl get pods):

    kubectl logs catalog-server-7b9845b459-kc2pw --container kafka-init

Develop the plugin

The plugin contains at least four classes, that each extend base classes from the RADAR-Android-Commons repository.

We explain the purpose of each class below.

DeviceManager

The heart and lungs of the plugin. The connection and data handling logic should be placed here.

The DeviceManager is implemented from org.radarbase.android.source.SourceManager and will connect to a device and collect its data, register all Kafka topics in its constructor. 

  • Make sure your schemas are imported using: 

    import org.radarcns.passive.bittium*
  • Create a private val for each topic, using the following format:

    private val batteryTopic: DataCache<ObservationKey, BittiumFarosBatteryLevel> = createCache("android_bittium_faros_battery_level", BittiumFarosBatteryLevel())
  • Make sure the connection to the device is established in the override fun start(acceptableIds: Set<String>) function.
  • Collected data can be sent using the ‘send’ function, using the following code:

    send(batteryTopic, BittiumFarosBatteryLevel(timestamp, currentTime, level, false))

DeviceService

Creates the DeviceManager.

The subclass org.radarbase.android.source.SourceService is used to run the device manager. This SourceService manages the SourceManager and a TableDataHandler to store the data of a wearable device, phone or API and send it to a Kafka REST proxy. Specific wearables should extend this class.

DeviceServiceProvider

The provider talks to the main pRMT app and contains the device configuration parameters.

The subclass org.radarbase.android.source.SourceProvider exposes the new service. After the plugin has been created, ‘plugin.SourceProvider’ should be added to the global ‘plugins’ configuration of the pRMT application client in AppConfig

DeviceStatus

Reports the device values to the pRMT app. For example the battery level should be configured here.

The subclass org.radarbase.android.source.SourceState is used to keep the current state of the device, you could also use org.radarbase.android.source.BaseSourceState.

Add the plugin to the pRMT app

To add the created plugin to the pRMT App, follow the following steps:

  1. In the radar-prmt-android/app/build.gradle include the following line:

    dependencies {
    
    	implementation "org.radarbase:radar-android-faros:$radar_commons_android_version"
    
    }
  2. In radar-prmt-android/app/src/playStore/java/org/radarcns/detail/RadarServiceImpl.kt import the SourceProvider of the plugin, like:

    import org.radarbase.passive.bittium.FarosProvider

    And also make sure that the PluginProvider is added to the plugins variable, like:

    override val plugins: List<SourceProvider<*>> = listOf(
    	FarosProvider(this)
    )

Existing Plugins

As an example, you could look at the plugins that have already been developed.