Create a virtual sensor extension for IoTool
Virtual sensor extension is a extension that gets data from a local database made by IoTool application on your device. It is meant to execute logical operations on recently collected data. This document provides information on what you have to do to create your own custom virtual sensor and demo source code.
General things
Before going further in to the document, it is important to note that this document only extends on information provided by document found on this link.
Reading from database
IoTool application and its library implements simple way of reading from database. To read data from database, use IoToolDataReader class. IoTool application uses multiple databases. Each database is assign to their sensor using service ID and it contains a single table called "data". Out of all columns in data table there are four important columns:
-
timestamp: Indicates when the values where writen (String representation of Long value)
-
vals: Text containing values separated by simbol ";" (Contains String representation of Double values)
-
times: Text containing timestamps separated by simbol ";" (Contains String representation of Long values)
-
type: Text containing type value
Querying from database is similar to the SQLiteDataBase class, difference being that IoToolDataReader has a built in cursor and that we get time or value from calling getTime() or getValue() method.
constructor IoToolReading(String readingID, File appDir):
-
readingID: String that has a format "<reading>@<serviceID>" (In this example: "Reading1@DemoVS")
-
appDir: Directory on the external storage where databases are stored (provided by the IoTool application)
IoToolDataReader db = new IoToolDataReader("AccelerometerX@Device",appDir);
executeSQL: executeSQL(String condition, String order, boolean condensed, int limit):
-
condition: String that is used in "WHERE" statement in sql query. Method automatically pripends "WHERE " if the string is longer than 0.
-
order: String that is used in "ORDER BY" statement in sql query. Method automatically pripends "ORDER BY " if the string is longer than 0.
-
condensed: It condenses the data if true
-
limit (optional): Used in "LIMIT" statement
long now = System.currentTimeMillis();
db.executeSQL("timestamp < " + now + " AND timestamp > " + (now - 1500),"timestamp DESC",false,3);
Moves to the first entry. It return true if it exists.
moveToFirst: moveToFirst()
Moves to the next entry. It return true if it exists.
moveToNext: moveToNext()
Classes
There are three main classes used our demo code package.
Note: Any change to the code may be needed only at the sections which start with "//TODO".
IoToolSensorServiceDemoVS.java
This class defines and implements all necessary setup and processing of data for readings that a sensor supports. In demo code packages this class already implements all service functionality such as reading from database, responding to the application commands etc. This is where you implement logical operation on received data.
package io.senlab.iotool.extension.servicedemovs;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.io.File;
import java.lang.ref.WeakReference;
import io.senlab.iotool.library.base.IoToolConstants;
import io.senlab.iotool.library.base.IoToolDataReader;
import io.senlab.iotool.library.base.IoToolReading;
/**
* Demo service for VS sensors. (c) Senlab.
*/
public class IoToolSensorServiceDemoVS extends Service {
private static final String serviceName = "IoToolSensorServiceDemoVS";
protected static final String serviceID = "DemoVS";
private ConnectServiceCommandReceiver connectReceiver;
private NotificationManager mNM;
private Notification notification;
private PendingIntent contentIntent;
private boolean broadcastData = false;
private boolean useDB = false;
private File appDir;
private Handler reconnectHandler = new Handler();
private Runnable reconnectRunnable = null;
private int reconnectInterval = 1000;
//TODO define all readings
private IoToolReading reading1 = null;
private IoToolReading reading2 = null;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
try {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
CharSequence text = serviceName+" "+getString(R.string.started);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
notification = builder.setContentIntent(contentIntent)
.setSmallIcon(R.drawable.status_disconnected)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setContentTitle(serviceName)
.setContentText(text)
.build();
contentIntent = PendingIntent.getActivity(this, 0, new Intent(), 0);
startForeground(R.string.foreground_service_id, notification);
connectReceiver = new ConnectServiceCommandReceiver(this);
registerReceiver(connectReceiver, new IntentFilter(IoToolConstants.ServiceCommand));
//TODO get settings provided by preference class
String inter = sp.getString(getString(R.string.preference_key_interval), getString(R.string.preference_default_value));
try{
reconnectInterval = Integer.parseInt(inter);
}
catch(Exception e){
reconnectInterval = 1000;
}
broadcastData = intent.getBooleanExtra(IoToolConstants.ServiceSetupBroadcast, false);
useDB = intent.getBooleanExtra(IoToolConstants.ServiceSetupDB, false);
appDir = new File(Environment.getExternalStorageDirectory().toString()+"/"+intent.getStringExtra(IoToolConstants.FileDirExtra));
if (! appDir.exists()) appDir.mkdir();
boolean permissions = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) permissions = false;
}
if (!permissions) {
Intent i = new Intent();
i.setClass(this, IoToolSensorServiceDemoVSPreferences.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
} else {
initServiceReadings();
setupServiceReadings(intent);
reconnectRunnable = new Runnable() {
@Override
public void run() {
queryDB();
requeryDB();
}
};
requeryDB();
}
} catch (Exception e) {
Log.e("Error",e.toString());
stop();
return START_NOT_STICKY;
}
//Log.i("TAGG",intent+"");
return START_STICKY;
}
public void onDestroy() {
super.onDestroy();
unregisterReceiver(connectReceiver);
//TODO null all readings
reading1 = null;
reading2 = null;
}
public IBinder onBind(final Intent intent) {
return null;
}
private void stop() {
try {
reconnectHandler.removeCallbacks(reconnectRunnable);
} catch (Exception e) {
}
reconnectRunnable = null;
//TODO close all readings
if (reading1.isEnabled()) reading1.close();
if (reading2.isEnabled()) reading2.close();
stopForeground(true);
stopSelf();
}
private void writeData(IoToolReading db,long time, double value){
if(db.isEnabled()){
db.writeData(time,value);
}
}
private void queryDB() {
//TODO process received data for each reading. In this example we get value by calling getValue() process it and write into a data base
try {
long now = System.currentTimeMillis();
IoToolDataReader db = new IoToolDataReader("AccelerometerX@Device",appDir);
db.executeSQL("timestamp < " + now + " AND timestamp > " + (now - 1500),"timestamp DESC",false,3);
long time = Long.MAX_VALUE;
double value = 0;
if(db.moveToFirst()){
time = db.getTime();
value = db.getValue();
}
while(db.moveToNext()){
if(time < db.getTime()){
time = db.getTime();
value = db.getValue();
}
else{
break;
}
}
if(time != Long.MAX_VALUE) {
writeData(reading1, now, value);
writeData(reading2, now, value * 10);
}
db.close();
}catch(Exception e){
Log.e("Error",e.toString());
}
}
private void requeryDB() {
reconnectHandler.postDelayed(reconnectRunnable, reconnectInterval);
}
private boolean checkFlag(int flag, int flags) {
if ((flag & flags) != 0) return true;
else return false;
}
private static class ConnectServiceCommandReceiver extends BroadcastReceiver {
private WeakReference<IoToolSensorServiceDemoVS> w;
public ConnectServiceCommandReceiver(IoToolSensorServiceDemoVS a) {
w = new WeakReference<IoToolSensorServiceDemoVS>(a);
}
public void onReceive(Context context, Intent intent) {
if(intent==null || !intent.hasExtra(IoToolConstants.ServiceCommandType))
return;
IoToolSensorServiceDemoVS a = w.get();
if(a!=null) {
if (intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandStopAllServices) ||
intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandStopService+serviceID)) {
a.stop();
}
else if(intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandSensorsChanged+serviceID)) {
a.setupServiceReadings(intent);
}
}
}
}
private void initServiceReadings() {
//TODO initialize all readings
reading1 = new IoToolReading();
reading2 = new IoToolReading();
}
private void setupServiceReadings(Intent intent) {
//TODO setup readings
String typeReading = IoToolReading.dataTypeSingle;
if(reconnectInterval < 1000){
typeReading = IoToolReading.dataTypeTimestamps;
}
if (intent.getBooleanExtra(IoToolConstants.Data+"Reading1@"+serviceID, false)) {
reading1.Setup("Reading1@"+serviceID, appDir, useDB, typeReading, 0, broadcastData, true, this);
reading1.enable();
} else reading1.disable();
if (intent.getBooleanExtra(IoToolConstants.Data+"Reading2@"+serviceID, false)) {
reading2.Setup("Reading2@"+serviceID, appDir, useDB, typeReading, 0, broadcastData, true, this);
reading2.enable();
} else reading2.disable();
}
}
IoToolSensorServiceDemoVSPreferences.java
This class implements preferences for a sensor which are accessible from IoTool app.
package io.senlab.iotool.extension.servicedemovs;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.EditTextPreference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.widget.Toast;
import java.lang.ref.WeakReference;
import io.senlab.iotool.library.base.IoToolConstants;
public class IoToolSensorServiceDemoVSPreferences extends PreferenceActivity {
private PreferenceManager pm;
private EditTextPreference SensorVS;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.servicepreferences);
pm = getPreferenceManager();
initPreferences();
loadSettings();
}
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case IoToolConstants.PERMISSIONS_REQUEST_CODE: {
boolean alert = false;
if (grantResults.length > 0) {
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) alert = true;
}
} else alert = true;
if (alert) {
Toast.makeText(getApplicationContext(), IoToolSensorServiceDemoVS.serviceID + " " + getString(R.string.request_permission_explanation), Toast.LENGTH_LONG).show();
finish();
}
return;
}
}
}
@Override
public void onResume() {
super.onResume();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
boolean requestPermissions = false;
if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) requestPermissions = true;
if(checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) requestPermissions = true;
if(requestPermissions) {
(new Handler()).postDelayed(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.ACCESS_COARSE_LOCATION}, IoToolConstants.PERMISSIONS_REQUEST_CODE);
}
}
}, IoToolConstants.PERMISSIONS_DIALOG_DISPLAY_TIME_MILLIS);
}
}
}
@Override
public void onBackPressed() {
super.onBackPressed();
saveSettings();
}
@Override
public void onDestroy() {
super.onDestroy();
new SaveSettingsInBackground(this).execute();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
private void initPreferences() {
//TODO initialize prefrence
SensorVS = (EditTextPreference) pm.findPreference(getString(R.string.preference_key_interval));
}
private void loadSettings() {
SensorVS.setSummary(getString(R.string.pref_str_summary));
}
private void saveSettings() {
//TODO check for errors in settings
try {
Long.parseLong(SensorVS.getText());
System.gc();
} catch (Exception e) {
SensorVS.setText(getString(R.string.preference_default_value));
System.gc();
}
}
private static class SaveSettingsInBackground extends AsyncTask<Void, Void, Void> {
private WeakReference<IoToolSensorServiceDemoVSPreferences> w;
public SaveSettingsInBackground(IoToolSensorServiceDemoVSPreferences a) {
w = new WeakReference<IoToolSensorServiceDemoVSPreferences>(a);
}
protected Void doInBackground(Void... params) {
IoToolSensorServiceDemoVSPreferences a = w.get();
if(a!=null) {
a.saveSettings();
}
return null;
}
}
}
IoToolSensorServiceDemoVSProvider.java
It must provide info for sensor properties, any dashboard profiles, sensor readings properties. For each of those, info is stored in two arrays, one for headers and one for actual data.
package io.senlab.iotool.extension.servicedemovs;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.SQLException;
import android.net.Uri;
import io.senlab.iotool.library.base.IoToolConstants;
import io.senlab.iotool.library.base.IoToolFunctions;
public class IoToolSensorServiceDemoVSProvider extends ContentProvider {
static final String PROVIDER_NAME = "io.senlab.iotool.extension.servicedemovs.IoToolSensorServiceDemoVSProvider";
//TODO sensor properties
static final String[] properties_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_PROPERTIES_ID, IoToolConstants.PROVIDER_PROPERTIES_NAME, IoToolConstants.PROVIDER_PROPERTIES_DEVELOPER};
static final Object[] properties = new Object[] {1, "DemoVS", "Demo Virtual Sensor", "VSExam"};
//TODO dashboard profiles
static final String[] profiles_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_PROFILES_NAME, IoToolConstants.PROVIDER_PROFILES_READINGS};
static final Object[][] profiles = new Object[][] {
{1, "Demo VS", new String[] {"Reading1@DemoVS", "Reading2@DemoVSS"}}
};
//TODO readings properties
static final String[] readings_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_READINGS_ID, IoToolConstants.PROVIDER_READINGS_NAME, IoToolConstants.PROVIDER_READINGS_SHORTNAME, IoToolConstants.PROVIDER_READINGS_UNIT, IoToolConstants.PROVIDER_READINGS_INTERVAL, IoToolConstants.PROVIDER_READINGS_DECIMALS};
static final Object[][] readings = new Object[][] {
{1, "Reading1@DemoVS", "Reading 1", "Read.1", "unit1", null, 1},
{2, "Reading2@DemoVS", "Reading 2", "Read.2", "unit2", null, 1}
};
static final int PROPERTIES = 1;
static final int PROFILES = 2;
static final int READINGS = 3;
static final UriMatcher uriMatcher;
static{
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(PROVIDER_NAME, "properties", PROPERTIES);
uriMatcher.addURI(PROVIDER_NAME, "profiles", PROFILES);
uriMatcher.addURI(PROVIDER_NAME, "readings", READINGS);
}
@Override
public boolean onCreate() {
return true;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new SQLException("Failed to add a record into " + uri);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
MatrixCursor mc = null;
switch (uriMatcher.match(uri)){
case PROPERTIES:
mc = new MatrixCursor(properties_header);
mc.addRow(properties);
break;
case PROFILES:
mc = new MatrixCursor(profiles_header);
for (int i=0; i<profiles.length; i++) mc.addRow(new Object[] {profiles[i][0], profiles[i][1], IoToolFunctions.providerReadingsToString((String[]) profiles[i][2])});
break;
case READINGS:
mc = new MatrixCursor(readings_header);
for (int i=0; i<readings.length; i++) mc.addRow(new Object[] {readings[i][0], readings[i][1], readings[i][2], readings[i][3], readings[i][4], readings[i][5], readings[i][6]});
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
return mc;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)){
case PROPERTIES:
return "vnd.android.cursor.dir/vnd.iotool.properties";
case PROFILES:
return "vnd.android.cursor.dir/vnd.iotool.profiles";
case READINGS:
return "vnd.android.cursor.dir/vnd.iotool.readings";
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
}
}