diff --git a/build.gradle b/build.gradle index 8509801e..b67b8475 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ ext.oshCoreVersion = '1.4.0' -ext.compileSdkVersion = 21 -ext.minSdkVersion = 21 -ext.targetSdkVersion = 21 +ext.compileSdkVersion = 31 +ext.minSdkVersion = 26 +ext.targetSdkVersion = 26 ext.buildToolsVersion = "30.0.2" version = oshCoreVersion diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index 1ddef0e1..e6c8d7b7 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -6,37 +6,57 @@ - + + - - + + + + + + + + + + + android:largeHeap="true" + android:theme="@style/AppTheme"> + android:label="@string/app_name" + android:screenOrientation="portrait"> + - - - + android:label="@string/title_activity_user_settings" /> + + + + + - + \ No newline at end of file diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index 9441e669..140bbdbb 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -3,8 +3,25 @@ apply plugin: 'com.android.application' description = 'OSH Demo Android App' ext.details = 'OSH demo app for Android' +repositories { + maven { + url "https://repo.eclipse.org/content/repositories/paho-releases/" + } +} + dependencies { api project(':sensorhub-android-lib') + + implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0' + implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.0' + implementation 'com.android.support:appcompat-v7:26.1.0' + implementation 'com.android.support:design:26.1.0' + implementation 'com.android.support.constraint:constraint-layout:2.0.4' + implementation 'android.arch.navigation:navigation-fragment:1.0.0' + implementation 'android.arch.navigation:navigation-ui:1.0.0' + implementation 'com.android.support:support-v4:26.1.0' +// implementation project(path: ':sensorhub-android-sos-ipc') +// implementation project(path: ':sensorhub-storage-h2') } android { @@ -41,5 +58,8 @@ android { lintOptions { abortOnError false } + buildFeatures { + viewBinding true + } } diff --git a/sensorhub-android-app/ic_launcher-web.png b/sensorhub-android-app/ic_launcher-web.png index 2a05b7e1..b525d261 100644 Binary files a/sensorhub-android-app/ic_launcher-web.png and b/sensorhub-android-app/ic_launcher-web.png differ diff --git a/sensorhub-android-app/res/drawable-hdpi/ic_launcher.png b/sensorhub-android-app/res/drawable-hdpi/ic_launcher.png index befaa77e..b525d261 100644 Binary files a/sensorhub-android-app/res/drawable-hdpi/ic_launcher.png and b/sensorhub-android-app/res/drawable-hdpi/ic_launcher.png differ diff --git a/sensorhub-android-app/res/drawable-mdpi/ic_launcher.png b/sensorhub-android-app/res/drawable-mdpi/ic_launcher.png index 4da9daf4..b525d261 100644 Binary files a/sensorhub-android-app/res/drawable-mdpi/ic_launcher.png and b/sensorhub-android-app/res/drawable-mdpi/ic_launcher.png differ diff --git a/sensorhub-android-app/res/drawable-xhdpi/ic_launcher.png b/sensorhub-android-app/res/drawable-xhdpi/ic_launcher.png index 1575e9de..b525d261 100644 Binary files a/sensorhub-android-app/res/drawable-xhdpi/ic_launcher.png and b/sensorhub-android-app/res/drawable-xhdpi/ic_launcher.png differ diff --git a/sensorhub-android-app/res/drawable-xxhdpi/ic_launcher.png b/sensorhub-android-app/res/drawable-xxhdpi/ic_launcher.png index 104064d5..b525d261 100644 Binary files a/sensorhub-android-app/res/drawable-xxhdpi/ic_launcher.png and b/sensorhub-android-app/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/sensorhub-android-app/res/layout/activity_app_status.xml b/sensorhub-android-app/res/layout/activity_app_status.xml new file mode 100644 index 00000000..f54bffdd --- /dev/null +++ b/sensorhub-android-app/res/layout/activity_app_status.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/menu/main.xml b/sensorhub-android-app/res/menu/main.xml index 673f610b..41eacb89 100644 --- a/sensorhub-android-app/res/menu/main.xml +++ b/sensorhub-android-app/res/menu/main.xml @@ -21,9 +21,15 @@ android:title="@string/action_stop"/> + + diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index d70a0e67..eacd6240 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -1,12 +1,20 @@ - SensorHubConnect - Please configure and start SensorHub using the Options Menu + OpenSensorHub SmartHub + Please configure and start SmartHub using the Options Menu Settings - Start SensorHub - Stop SensorHub + Start SmartHub + Stop SmartHub + App Status About - + Start Proxy + Stop Proxy + SOS Settings (Required) + SOS-T Settings (Optional) + Android Sensor + TruPulse Range Finder Sensor + Angel Sensor + Flirone Sensor JPEG H264 @@ -14,4 +22,187 @@ VP9 VP8 + + + Quaternion + Euler + + + QUATERNION + EULER + + + QUATERNION + + + + GPS + Network + + + GPS + NETWORK + + + GPS + + + + Fetchable + Store Locally + Push Remote + + + FETCH_LOCAL + STORE_LOCAL + PUSH_REMOTE + + + PUSH_REMOTE + + + + Streaming Physical Device + Simulate Virtual Device + + + STREAM + SIMULATED + + STREAM + + + Select Reporting Item + Street Closure + Flooding + Medical + Aid + + + + Nearest Beacon + Trilateration + + + NEAREST + TRILATERATION + + + NEAREST + + + + GPS + Laser Range Finder + Network + + + Spot Report + NAME: + DESCRIPTION: + CAPTURE + RESET + SUBMIT REPORT + A name for the report + + Radius: + Lat: + Lon: + Ft. + + + Select... + Public + All + + + + Select... + Open + Close + + + Action: + Ref. Id: + + + Type: + + + Select... + Channel Drainage + Land Surface + + + + Select... + Meter + Visual + Model + + + Feature Type: + Depth: + Obs. Mode: + + Describe Medical Condition... + Enter measurement (BP, Temp, etc.)... + Emergency (T/F) + + + Select... + Environment + Health + Safety + Services + + + + Select... + 5 + 4 + 3 + 2 + 1 + + + Aid Type: + # Persons: + Urgency: + Describe aid needed... + Enter your name or id + + + Select... + Person + Vehicle + Device + + + + Select... + GPS + BlueTooth Beacon + WiFi + Cellular + UWB + N/A + + + Tracking Resource: + Tracking Method: + Enter resource id + Enter resource label + + + + + AAC + AMR-NB + AMR-WB + FLAC + VORBIS + OPUS + PCM + diff --git a/sensorhub-android-app/res/values/strings_app_status.xml b/sensorhub-android-app/res/values/strings_app_status.xml new file mode 100644 index 00000000..3e5941bb --- /dev/null +++ b/sensorhub-android-app/res/values/strings_app_status.xml @@ -0,0 +1,22 @@ + + + + + + App Status + + + Initializing + Initialized + Starting + Started + Stopping + Stopped + Unknown + + + SOS Service Status + HTTP Server Status + Android Sensor Status + Android Sensor Storage Status + \ No newline at end of file diff --git a/sensorhub-android-app/res/xml/pref_audio.xml b/sensorhub-android-app/res/xml/pref_audio.xml new file mode 100644 index 00000000..f23302f1 --- /dev/null +++ b/sensorhub-android-app/res/xml/pref_audio.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/sensorhub-android-app/res/xml/pref_general.xml b/sensorhub-android-app/res/xml/pref_general.xml index 55f397a2..7ee7b58b 100644 --- a/sensorhub-android-app/res/xml/pref_general.xml +++ b/sensorhub-android-app/res/xml/pref_general.xml @@ -1,5 +1,4 @@ - - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/xml/pref_headers.xml b/sensorhub-android-app/res/xml/pref_headers.xml index d84db848..442cac23 100644 --- a/sensorhub-android-app/res/xml/pref_headers.xml +++ b/sensorhub-android-app/res/xml/pref_headers.xml @@ -9,5 +9,8 @@
+
diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml index 9b19865e..b7df8635 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -1,88 +1,224 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/xml/pref_video.xml b/sensorhub-android-app/res/xml/pref_video.xml index e146590c..96080f4b 100644 --- a/sensorhub-android-app/res/xml/pref_video.xml +++ b/sensorhub-android-app/res/xml/pref_video.xml @@ -25,5 +25,13 @@ android:negativeButtonText="@null" android:positiveButtonText="@null" android:title="Selected Preset" /> + + diff --git a/sensorhub-android-app/res/xml/spotreport_paths.xml b/sensorhub-android-app/res/xml/spotreport_paths.xml new file mode 100644 index 00000000..6a3d2e3c --- /dev/null +++ b/sensorhub-android-app/res/xml/spotreport_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java new file mode 100644 index 00000000..85d29359 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java @@ -0,0 +1,35 @@ +package org.sensorhub.android; + +import android.content.Context; +import android.content.Intent; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.util.Log; +import android.widget.TextView; + +public class AppStatusActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_app_status); + + Intent intent = getIntent(); + Context appContext = getApplicationContext(); + + String sosStatus = intent.getStringExtra("sosService"); + String httpStatus = intent.getStringExtra("httpStatus"); + String sensorStatus = intent.getStringExtra("androidSensorStatus"); + String sensorStorageStatus = intent.getStringExtra("sensorStorageStatus"); + + TextView sosStatusView = (TextView) findViewById(R.id.sos_service_state); + TextView httpStatusView = (TextView) findViewById(R.id.http_service_state); + TextView sensorStatusView = (TextView) findViewById(R.id.sensor_service_state); + TextView storageStatusView = (TextView) findViewById(R.id.storage_service_state); + + sosStatusView.setText(sosStatus); + httpStatusView.setText(httpStatus); + sensorStatusView.setText(sensorStatus); + storageStatusView.setText(sensorStorageStatus); + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index ac47f931..b54be9a1 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -14,33 +14,51 @@ package org.sensorhub.android; +import static android.content.ContentValues.TAG; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; - -import java.net.MalformedURLException; -import java.net.URL; -import java.security.cert.X509Certificate; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; - +import android.hardware.Sensor; +import android.hardware.SensorManager; import android.location.LocationManager; import android.location.LocationProvider; -import android.preference.DialogPreference; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.provider.Settings.Secure; +import android.text.Html; import android.util.Log; -import android.view.*; +import android.view.Menu; +import android.view.MenuItem; +import android.view.TextureView; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.TextView; + import org.sensorhub.android.comm.BluetoothCommProvider; import org.sensorhub.android.comm.BluetoothCommProviderConfig; import org.sensorhub.android.comm.ble.BleConfig; import org.sensorhub.android.comm.ble.BleNetwork; import org.sensorhub.api.common.Event; import org.sensorhub.api.common.IEventListener; +import org.sensorhub.api.common.SensorHubException; import org.sensorhub.api.data.IStreamingDataInterface; +import org.sensorhub.api.module.IModule; import org.sensorhub.api.module.IModuleConfigRepository; import org.sensorhub.api.module.ModuleConfig; import org.sensorhub.api.module.ModuleEvent; @@ -48,35 +66,43 @@ import org.sensorhub.impl.client.sost.SOSTClient; import org.sensorhub.impl.client.sost.SOSTClient.StreamInfo; import org.sensorhub.impl.client.sost.SOSTClientConfig; -//import org.sensorhub.impl.driver.dji.DjiConfig; import org.sensorhub.impl.driver.flir.FlirOneCameraConfig; import org.sensorhub.impl.module.InMemoryConfigDb; +import org.sensorhub.impl.module.ModuleRegistry; +import org.sensorhub.impl.persistence.GenericStreamStorage; +import org.sensorhub.impl.persistence.MaxAgeAutoPurgeConfig; +import org.sensorhub.impl.persistence.StreamStorageConfig; +import org.sensorhub.impl.persistence.h2.MVMultiStorageImpl; +import org.sensorhub.impl.persistence.h2.MVStorageConfig; import org.sensorhub.impl.sensor.android.AndroidSensorsConfig; import org.sensorhub.impl.sensor.android.AndroidSensorsDriver; +import org.sensorhub.impl.sensor.android.audio.AudioEncoderConfig; import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig; import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig.VideoPreset; import org.sensorhub.impl.sensor.angel.AngelSensorConfig; import org.sensorhub.impl.sensor.trupulse.TruPulseConfig; import org.sensorhub.impl.sensor.trupulse.TruPulseWithGeolocConfig; +import org.sensorhub.impl.service.HttpServerConfig; +import org.sensorhub.impl.service.sos.SOSService; +import org.sensorhub.impl.service.sos.SOSServiceConfig; +import org.sensorhub.impl.service.sos.SensorDataProviderConfig; import org.sensorhub.test.sensor.trupulse.SimulatedDataStream; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.ComponentName; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.preference.PreferenceManager; -import android.provider.Settings.Secure; -import android.text.Html; -import android.widget.EditText; -import android.widget.TextView; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; @@ -88,6 +114,9 @@ public class MainActivity extends Activity implements TextureView.SurfaceTextureListener, IEventListener { + public static final String ACTION_BROADCAST_RECEIVER = "org.sensorhub.android.BROADCAST_RECEIVER"; + public static final String ANDROID_SENSORS_MODULE_ID = "ANDROID_SENSORS"; + TextView mainInfoArea; TextView videoInfoArea; SensorHubService boundService; @@ -101,7 +130,27 @@ public class MainActivity extends Activity implements TextureView.SurfaceTexture AndroidSensorsDriver androidSensors; URL sosUrl = null; boolean showVideo; - + + String deviceID; + String deviceName; + String runName; + + // Request codes for permissions + final int FINE_LOC_RC = 101; + final int CAMERA_RC = 102; + final int AUDIO_RC = 103; + + enum Sensors { + Android, + TruPulse, + TruPulseSim, + Angel, + FlirOne, + DJIDrone, + ProxySensor, + BLELocation + } + private ServiceConnection sConn = new ServiceConnection() { @@ -119,10 +168,11 @@ public void onServiceDisconnected(ComponentName className) protected void updateConfig(SharedPreferences prefs, String runName) { + deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); sensorhubConfig = new InMemoryConfigDb(); // get SOS URL from config - String sosUriConfig = prefs.getString("sos_uri", ""); + String sosUriConfig = prefs.getString("sos_uri", "http://127.0.0.1:8585"); String sosUser = prefs.getString("sos_username", null); String sosPwd = prefs.getString("sos_password", null); if (sosUriConfig != null && sosUriConfig.trim().length() > 0) @@ -183,6 +233,9 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.name = "Android Sensors [" + deviceName + "]"; sensorsConfig.id = "ANDROID_SENSORS"; sensorsConfig.autoStart = true; + //TODO: try adding a few options +// sensorsConfig. + sensorsConfig.activateAccelerometer = prefs.getBoolean("accel_enabled", false); sensorsConfig.activateGyrometer = prefs.getBoolean("gyro_enabled", false); sensorsConfig.activateMagnetometer = prefs.getBoolean("mag_enabled", false); @@ -190,8 +243,11 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.activateOrientationEuler = prefs.getBoolean("orient_euler_enabled", false); sensorsConfig.activateGpsLocation = prefs.getBoolean("gps_enabled", false); sensorsConfig.activateNetworkLocation = prefs.getBoolean("netloc_enabled", false); - sensorsConfig.activateBackCamera = prefs.getBoolean("cam_enabled", false); - if (sensorsConfig.activateBackCamera || sensorsConfig.activateFrontCamera) + sensorsConfig.enableCamera = prefs.getBoolean("cam_enabled", false); + sensorsConfig.selectedCameraId = Integer.parseInt(prefs.getString("camera_select", "0")); + /*if (sensorsConfig.activateBackCamera || sensorsConfig.activateFrontCamera) + showVideo = true;*/ + if (sensorsConfig.enableCamera) showVideo = true; // video settings @@ -228,9 +284,67 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.videoConfig.presets = presetList.toArray(new VideoPreset[0]); sensorsConfig.outputVideoRoll = prefs.getBoolean("video_roll_enabled", false); + + // audio + sensorsConfig.activateMicAudio = prefs.getBoolean("audio_enabled", false); + sensorsConfig.audioConfig.codec = prefs.getString("audio_codec", AudioEncoderConfig.AAC_CODEC); + sensorsConfig.audioConfig.sampleRate = Integer.parseInt(prefs.getString("audio_samplerate", "8000")); + sensorsConfig.audioConfig.bitRate = Integer.parseInt(prefs.getString("audio_bitrate", "64")); + sensorsConfig.runName = runName; - sensorhubConfig.add(sensorsConfig); - addSosTConfig(sensorsConfig, sosUser, sosPwd); +// sensorhubConfig.add(sensorsConfig); +// addSosTConfig(sensorsConfig, sosUser, sosPwd); + + // START SOS Config ************************************************************************ +// if(prefs.getBoolean("hub_enable", true)) { + if(shouldServe(prefs)) { + // Setup HTTPServerConfig for enabling more complete node functionality + HttpServerConfig serverConfig = new HttpServerConfig(); + serverConfig.proxyBaseUrl = ""; + serverConfig.httpPort = 8585; + serverConfig.autoStart = true; + sensorhubConfig.add(serverConfig); + } + + // SOS Config +// SOSServiceConfig sosConfig = new SOSServiceWithIPCConfig(); +// sosConfig.moduleClass = SOSServiceWithIPC.class.getCanonicalName(); +// ((SOSServiceWithIPCConfig) sosConfig).androidContext = this.getApplicationContext(); + + // We don't need android context unless we're doing IPC things + SOSServiceConfig sosConfig = new SOSServiceConfig(); + sosConfig.moduleClass = SOSService.class.getCanonicalName(); + sosConfig.id = "SOS_SERVICE"; + sosConfig.name = "SOS Service"; + sosConfig.autoStart = true; + sosConfig.enableTransactional = true; + + // Push Sensors Config + AndroidSensorsConfig androidSensorsConfig = sensorsConfig; + sensorhubConfig.add(androidSensorsConfig); + if (isPushingSensor(Sensors.Android)) { + addSosTConfig(androidSensorsConfig, sosUser, sosPwd); + } +// addSosTConfig(sensorsConfig,sosUser,sosPwd); + + //Storage Configuration +// if(prefs.getBoolean("hub_enable", true) && prefs.getBoolean("hub_enable_local_storage", true)) { + if(shouldStore(prefs)) { + File dbFile = new File(getApplicationContext().getFilesDir() + "/db/"); + dbFile.mkdirs(); + MVStorageConfig basicStorageConfig = new MVStorageConfig(); + basicStorageConfig.moduleClass = "org.sensorhub.impl.persistence.h2.MVObsStorageImpl"; + basicStorageConfig.storagePath = dbFile.getAbsolutePath() + "/${STORAGE_ID}.dat"; + basicStorageConfig.autoStart = true; + sosConfig.newStorageConfig = basicStorageConfig; + + StreamStorageConfig androidStreamStorageConfig = createStreamStorageConfig(androidSensorsConfig); + addStorageConfig(androidSensorsConfig, androidStreamStorageConfig); + } + + SensorDataProviderConfig androidDataProviderConfig = createDataProviderConfig(androidSensorsConfig); + addSosServerConfig(sosConfig, androidDataProviderConfig); + // END SOS CONFIG ************************************************************************** // TruPulse sensor boolean enabled = prefs.getBoolean("trupulse_enabled", false); @@ -326,6 +440,9 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(djiConfig); addSosTConfig(djiConfig, sosUser, sosPwd); }*/ + + // TODO add missing SOS SERVICE config to sensorhub + sensorhubConfig.add(sosConfig); } @@ -372,6 +489,10 @@ protected void onCreate(Bundle savedInstanceState) // handler to refresh sensor status in UI displayHandler = new Handler(Looper.getMainLooper()); + + setupBroadcastReceivers(); + + checkForPermissions(); } @@ -418,6 +539,52 @@ else if (id == R.id.action_about) { showAboutPopup(); } + else if(id == R.id.action_status) + { + Intent statusIntent = new Intent(this, AppStatusActivity.class); + if(boundService.sensorhub != null) { + ModuleRegistry moduleRegistry = boundService.sensorhub.getModuleRegistry(); + Collection modules = moduleRegistry.getAvailableModules(); + + for (ModuleConfig moduleConf: modules) { + IModule module = null; + try { + module = moduleRegistry.getModuleById(moduleConf.id); + String status = module.getCurrentState().name(); + + switch (moduleConf.id){ + case "HTTP_SERVER_0": + statusIntent.putExtra("httpStatus", status); + break; + case "SOS_SERVICE": + statusIntent.putExtra("sosService", status); + break; + case "ANDROID_SENSORS": + statusIntent.putExtra("androidSensorStatus", status); + break; + case "ANDROID_SENSORS#storage": + statusIntent.putExtra("sensorStorageStatus", status); + break; + } + + } catch (SensorHubException e) { + e.printStackTrace(); + } + + } + }else{ + statusIntent.putExtra("sosService", "N/A"); + statusIntent.putExtra("httpStatus", "N/A"); + statusIntent.putExtra("androidSensorStatus", "N/A"); + statusIntent.putExtra("sensorStorageStatus", "N/A"); + } + +// statusIntent.putExtra("boundService", boundService); + + + startActivity(statusIntent); + return true; + } return super.onOptionsItemSelected(item); } @@ -443,22 +610,39 @@ public void onClick(DialogInterface dialog, int whichButton) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); String runName = input.getText().toString(); - newStatusMessage("Starting SensorHub..."); + updateConfig(PreferenceManager.getDefaultSharedPreferences(MainActivity.this), runName); AndroidSensorsConfig androidSensorConfig = (AndroidSensorsConfig) sensorhubConfig.get("ANDROID_SENSORS"); VideoEncoderConfig videoConfig = androidSensorConfig.videoConfig; - if ((androidSensorConfig.activateBackCamera || androidSensorConfig.activateFrontCamera) && videoConfig.selectedPreset < 0 || videoConfig.selectedPreset >= videoConfig.presets.length) { + boolean cameraInUse = (androidSensorConfig.activateBackCamera || androidSensorConfig.activateFrontCamera); + boolean improperVideoSettings = (videoConfig.selectedPreset < 0 || videoConfig.selectedPreset >= videoConfig.presets.length); + + if (cameraInUse && improperVideoSettings) { showVideoConfigErrorPopup(); newStatusMessage("Video Config Error: Check Settings"); } else { + newStatusMessage("Starting SensorHub..."); sostClients.clear(); boundService.startSensorHub(sensorhubConfig, showVideo, MainActivity.this); if (boundService.hasVideo()) mainInfoArea.setBackgroundColor(0x80FFFFFF); + + /*SOSServiceCapabilities caps = null; + try { + GetCapabilitiesRequest getCap = new GetCapabilitiesRequest(); + getCap.setService(SOSUtils.SOS); + getCap.setVersion("V2.0"); + getCap.setGetServer(PreferenceManager.getDefaultSharedPreferences(MainActivity.this).getString("sos_uri", "")); + OWSUtils owsUtils = new OWSUtils(); + caps = owsUtils.sendRequest(getCap, false); + } catch (OWSException e) { +// throw new SensorHubException("Cannot retrieve SOS capabilities", e); + Log.e(TAG, "ERR: Cannot retrieve SOS Capabilities", e); + }*/ } } @@ -641,6 +825,12 @@ else if (dt > stream.getValue().measPeriodMs) mainInfoText.setLength(mainInfoText.length()-5); // remove last
mainInfoText.append("

"); + // Notify we are running when no data is being pushed + boolean serveOrStore = shouldServe(PreferenceManager.getDefaultSharedPreferences(MainActivity.this)) || shouldStore(PreferenceManager.getDefaultSharedPreferences(MainActivity.this)); + if(sostClients.size() == 0 && serveOrStore){ + mainInfoText.append("No Sensors Set to Push Remotely"); + } + // show video info if (androidSensors != null && boundService.hasVideo()) { @@ -716,6 +906,492 @@ protected void hideVideo() { } + private boolean isPushingSensor(Sensors sensor) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + + if (Sensors.Android.equals(sensor)) { + if (prefs.getBoolean("accel_enabled", false) + && prefs.getStringSet("accel_options", Collections.emptySet()).contains("PUSH_REMOTE")) + return true; + if (prefs.getBoolean("gyro_enabled", false) + && prefs.getStringSet("gyro_options", Collections.emptySet()).contains("PUSH_REMOTE")) + return true; + if (prefs.getBoolean("mag_enabled", false) + && prefs.getStringSet("mag_options", Collections.emptySet()).contains("PUSH_REMOTE")) + return true; + if (prefs.getBoolean("orient_quat_enabled", false) + && prefs.getStringSet("orient_quat_options", Collections.emptySet()).contains("PUSH_REMOTE")) + return true; + if (prefs.getBoolean("orient_euler_enabled", false) + && prefs.getStringSet("orient_euler_options", Collections.emptySet()).contains("PUSH_REMOTE")) + return true; + if (prefs.getBoolean("gps_enabled", false) + && prefs.getStringSet("gps_options", Collections.emptySet()).contains("PUSH_REMOTE")) + return true; + if (prefs.getBoolean("netloc_enabled", false) + && prefs.getStringSet("netloc_options", Collections.emptySet()).contains("PUSH_REMOTE")) + return true; + if (prefs.getBoolean("cam_enabled", false) + && prefs.getStringSet("cam_options", Collections.emptySet()).contains("PUSH_REMOTE")) + return true; + if(prefs.getBoolean("audio_enabled", false) + && prefs.getStringSet("audio_options", Collections.emptySet()).contains("PUSH_REMOTE")) + return true; + } else if (Sensors.TruPulse.equals(sensor) || Sensors.TruPulseSim.equals(sensor)) { + return prefs.getBoolean("trupulse_enabled", false) + && prefs.getStringSet("trupulse_options", Collections.emptySet()).contains("PUSH_REMOTE"); + } else if(Sensors.BLELocation.equals(sensor)){ + return prefs.getBoolean("ble_enable", false) && prefs.getStringSet("ble_options", Collections.emptySet()).contains("PUSH_REMOTE"); + } + + return false; + } + + private SensorDataProviderConfig createDataProviderConfig(AndroidSensorsConfig sensorConfig) { + Context androidContext = SensorHubService.getContext(); + SensorDataProviderConfig dataProviderConfig = new SensorDataProviderConfig(); + dataProviderConfig.sensorID = sensorConfig.id; + dataProviderConfig.offeringID = "urn:android:device:" + Secure.getString(androidContext.getContentResolver(), Secure.ANDROID_ID); + dataProviderConfig.storageID = sensorConfig.id + "#storage"; + dataProviderConfig.enabled = true; + dataProviderConfig.liveDataTimeout = 600.0; + dataProviderConfig.maxFois = 10; + + return dataProviderConfig; + } + + private StreamStorageConfig createStreamStorageConfig(AndroidSensorsConfig sensorConfig) { + // H2 Storage Config + File dbFile = new File(getApplicationContext().getFilesDir() + "/db/", deviceID + "_h2.dat"); + dbFile.getParentFile().mkdirs(); + if (!dbFile.exists()) { + try { + dbFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + } + } + MVStorageConfig storageConfig = new MVStorageConfig(); + storageConfig.moduleClass = MVMultiStorageImpl.class.getCanonicalName(); + storageConfig.storagePath = dbFile.getPath(); + storageConfig.autoStart = true; + storageConfig.memoryCacheSize = 102400; + storageConfig.autoCommitBufferSize = 1024; + + // TODO: Base this on size instead of time. This might error when earliest record is purged and then requested. Test if the capabilities updates... + // Auto Purge Config + MaxAgeAutoPurgeConfig autoPurgeConfig = new MaxAgeAutoPurgeConfig(); + autoPurgeConfig.enabled = true; + autoPurgeConfig.purgePeriod = 24.0 * 60.0 * 60.0; + autoPurgeConfig.maxRecordAge = 24.0 * 60.0 * 60.0; + + // Stream Storage Config + StreamStorageConfig streamStorageConfig = new StreamStorageConfig(); + streamStorageConfig.moduleClass = GenericStreamStorage.class.getCanonicalName(); + streamStorageConfig.id = sensorConfig.id + "#storage"; + streamStorageConfig.name = sensorConfig.name + " Storage"; + streamStorageConfig.dataSourceID = sensorConfig.id; + streamStorageConfig.autoStart = true; + streamStorageConfig.processEvents = true; + streamStorageConfig.minCommitPeriod = 10000; + streamStorageConfig.autoPurgeConfig = autoPurgeConfig; + streamStorageConfig.storageConfig = storageConfig; + return streamStorageConfig; + } + + protected void addStorageConfig(SensorConfig sensorConf, StreamStorageConfig storageConf) { + if (sensorConf instanceof AndroidSensorsConfig) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + SensorManager sensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE); + List deviceSensors = sensorManager.getSensorList(Sensor.TYPE_ALL); + + String sensorName; + for (Sensor sensor : deviceSensors) { + if (sensor.isWakeUpSensor()) { + continue; + } + + Log.d(TAG, "addStorageConfig: sensor: " + sensor.getName()); + + switch (sensor.getType()) { + case Sensor.TYPE_ACCELEROMETER: + if (!prefs.getBoolean("accel_enable", false) + || !prefs.getStringSet("accel_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addStorageConfig: excluding accelerometer"); + + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + storageConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addStorageConfig: NOT excluding accelerometer"); + } + break; + case Sensor.TYPE_GYROSCOPE: + if (!prefs.getBoolean("gyro_enable", false) + || !prefs.getStringSet("gyro_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addStorageConfig: excluding gyroscope"); + + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + storageConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addStorageConfig: NOT excluding gyroscope"); + } + break; + case Sensor.TYPE_MAGNETIC_FIELD: + if (!prefs.getBoolean("mag_enable", false) + || !prefs.getStringSet("mag_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addStorageConfig: excluding magnetometer"); + + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + storageConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addStorageConfig: NOT excluding magnetometer"); + } + break; + case Sensor.TYPE_ROTATION_VECTOR: + // TODO: double check, this probably will have an issue when both are checked + if (!prefs.getBoolean("orient_quat_enabled", false) + || !prefs.getStringSet("orient_quat_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addStorageConfig: excluding orientation"); + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + storageConf.excludedOutputs.add(sensorName); + sensorName = "quat_orientation_data"; + storageConf.excludedOutputs.add(sensorName); + + } else if(!prefs.getBoolean("orient_euler_enabled", false) + || !prefs.getStringSet("orient_euler_options", Collections.emptySet()).contains("STORE_LOCAL")){ + + Log.d(TAG, "addStorageConfig: excluding orientation"); + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + storageConf.excludedOutputs.add(sensorName); + sensorName = "euler_orientation_data"; + storageConf.excludedOutputs.add(sensorName); + + } else { + Log.d(TAG, "addStorageConfig: NOT excluding orientation"); + } + break; + } + } + if (!prefs.getBoolean("gps_enabled", false) + || !prefs.getStringSet("gps_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addStorageConfig: excluding gps location"); + + sensorName = "gps_data"; + storageConf.excludedOutputs.add(sensorName); + + } else { + Log.d(TAG, "addStorageConfig: NOT excluding gps location"); + } + if (!prefs.getBoolean("netloc_enabled", false) + || !prefs.getStringSet("netloc_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addStorageConfig: excluding network location"); + + sensorName = "network_data"; + storageConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addStorageConfig: NOT excluding network location"); + } + if (!prefs.getBoolean("cam_enabled", false) + || !prefs.getStringSet("cam_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addStorageConfig: excluding video"); + + // TODO: we need a better way of adding these to storage + sensorName = "camera0_MJPEG"; + storageConf.excludedOutputs.add(sensorName); + sensorName = "camera0_H264"; + storageConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addStorageConfig: NOT excluding video"); + } + if(!prefs.getBoolean("audio_enabled", false) + || !prefs.getStringSet("audio_options", Collections.emptySet()).contains("STORE_LOCAL")){ + sensorName = "audio_aac"; + storageConf.excludedOutputs.add(sensorName); + }else{ + Log.d(TAG, "addStorageConfig: NOT excluding audio"); + } + } + + sensorhubConfig.add(storageConf); + } + + protected void addSosServerConfig(SOSServiceConfig sosConf, SensorDataProviderConfig dataProviderConf) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + SensorManager sensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE); + List deviceSensors = sensorManager.getSensorList(Sensor.TYPE_ALL); + + String sensorName; + for (Sensor sensor : deviceSensors) { + if (sensor.isWakeUpSensor()) { + continue; + } + + Log.d(TAG, "addSosServerConfig: sensor: " + sensor.getName()); + + switch (sensor.getType()) { + case Sensor.TYPE_ACCELEROMETER: + if (!prefs.getBoolean("accel_enable", false) + || !prefs.getStringSet("accel_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addSosServerConfig: excluding accelerometer"); + + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + dataProviderConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addSosServerConfig: NOT excluding accelerometer"); + } + break; + case Sensor.TYPE_GYROSCOPE: + if (!prefs.getBoolean("gyro_enable", false) + || !prefs.getStringSet("gyro_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addSosServerConfig: excluding gyroscope"); + + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + dataProviderConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addSosServerConfig: NOT excluding gyroscope"); + } + break; + case Sensor.TYPE_MAGNETIC_FIELD: + if (!prefs.getBoolean("mag_enable", false) + || !prefs.getStringSet("mag_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addSosServerConfig: excluding magnetometer"); + + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + dataProviderConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addSosServerConfig: NOT excluding magnetometer"); + } + break; + case Sensor.TYPE_ROTATION_VECTOR: + if (!prefs.getBoolean("orient_quat_enabled", false) + || !prefs.getStringSet("orient_quat_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addSosServerConfig: excluding orientation"); + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + dataProviderConf.excludedOutputs.add(sensorName); + sensorName = "quat_orientation_data"; + dataProviderConf.excludedOutputs.add(sensorName); + + } else if(!prefs.getBoolean("orient_euler_enabled", false) + || !prefs.getStringSet("orient_euler_options", Collections.emptySet()).contains("STORE_LOCAL")){ + + Log.d(TAG, "addSosServerConfig: excluding orientation"); + sensorName = sensor.getName().replaceAll(" ", "_") + "_data"; + dataProviderConf.excludedOutputs.add(sensorName); + sensorName = "euler_orientation_data"; + dataProviderConf.excludedOutputs.add(sensorName); + + } else { + Log.d(TAG, "addSosServerConfig: NOT excluding orientation"); + } + break; + } + } + if (!prefs.getBoolean("gps_enabled", false) + || !prefs.getStringSet("gps_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addSosServerConfig: excluding gps location"); + + sensorName = "gps_data"; + dataProviderConf.excludedOutputs.add(sensorName); + + } else { + Log.d(TAG, "addSosServerConfig: NOT excluding gps location"); + } + if (!prefs.getBoolean("netloc_enabled", false) + || !prefs.getStringSet("netloc_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addSosServerConfig: excluding network location"); + + sensorName = "network_data"; + dataProviderConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addSosServerConfig: NOT excluding network location"); + } + if (!prefs.getBoolean("cam_enabled", false) + || !prefs.getStringSet("cam_options", Collections.emptySet()).contains("STORE_LOCAL")) { + + Log.d(TAG, "addSosServerConfig: excluding video"); + + sensorName = "camera0_MJPEG"; + dataProviderConf.excludedOutputs.add(sensorName); + sensorName = "camera0_H264"; + dataProviderConf.excludedOutputs.add(sensorName); + } else { + Log.d(TAG, "addSosServerConfig: NOT excluding video"); + } + if(!prefs.getBoolean("audio_enabled", false) + || !prefs.getStringSet("audio_options", Collections.emptySet()).contains("STORE_LOCAL")){ + sensorName = "audio_aac"; + dataProviderConf.excludedOutputs.add(sensorName); + }else{ + Log.d(TAG, "addSosServerConfig: NOT excluding audio"); + } + // Add BLE back in +// if (!prefs.getBoolean("ble_enable", false) +// || !prefs.getStringSet("ble_location_options", Collections.emptySet()).contains("STORE_LOCAL")) { +// sensorName = "BLEBeacon"; +// dataProviderConf.excludedOutputs.add(sensorName); +// sensorName = "BLEBeaconLocation"; +// dataProviderConf.excludedOutputs.add(sensorName); +// sensorName = "NearestBeacon"; +// dataProviderConf.excludedOutputs.add(sensorName); +// } + + sosConf.dataProviders.add(dataProviderConf); + } + + private SensorConfig createSensorConfig(Sensors sensor) { + SensorConfig sensorConfig; + + if (Sensors.Android.equals(sensor)) { + sensorConfig = new AndroidSensorsConfig(); + sensorConfig.id = ANDROID_SENSORS_MODULE_ID; + sensorConfig.name = "Android Sensors [" + deviceName + "]"; + sensorConfig.autoStart = true; + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + + ((AndroidSensorsConfig) sensorConfig).activateAccelerometer = prefs.getBoolean("accelerometer_enable", false); + ((AndroidSensorsConfig) sensorConfig).activateGyrometer = prefs.getBoolean("gyroscope_enable", false); + ((AndroidSensorsConfig) sensorConfig).activateMagnetometer = prefs.getBoolean("magnetometer_enable", false); + if (prefs.getBoolean("orientation_enable", false)) { + ((AndroidSensorsConfig) sensorConfig).activateOrientationQuat = prefs.getStringSet("orientation_angles", Collections.emptySet()).contains("QUATERNION"); + ((AndroidSensorsConfig) sensorConfig).activateOrientationEuler = prefs.getStringSet("orientation_angles", Collections.emptySet()).contains("EULER"); + } + if (prefs.getBoolean("location_enable", false)) { + ((AndroidSensorsConfig) sensorConfig).activateGpsLocation = prefs.getStringSet("location_type", Collections.emptySet()).contains("GPS"); + ((AndroidSensorsConfig) sensorConfig).activateNetworkLocation = prefs.getStringSet("location_type", Collections.emptySet()).contains("NETWORK"); + } +// if (prefs.getBoolean("video_enable", false)) { +// showVideo = true; +// +// ((AndroidSensorsConfig) sensorConfig).activateBackCamera = true; +// ((AndroidSensorsConfig) sensorConfig).videoCodec = prefs.getString("video_codec", AndroidSensorsConfig.JPEG_CODEC); +// } +// +// ((AndroidSensorsConfig) sensorConfig).androidContext = this.getApplicationContext(); +// ((AndroidSensorsConfig) sensorConfig).camPreviewTexture = boundService.getVideoTexture(); +// ((AndroidSensorsConfig) sensorConfig).runName = runName; + }/* else if (Sensors.TruPulse.equals(sensor)) { + sensorConfig = createTruPulseConfig(); + sensorConfig.id = "TRUPULSE_SENSOR"; + sensorConfig.name = "TruPulse Range Finder [" + deviceName + "]"; + sensorConfig.autoStart = true; + + BluetoothCommProviderConfig btConf = new BluetoothCommProviderConfig(); + btConf.protocol.deviceName = "TP360RB.*"; + btConf.moduleClass = BluetoothCommProvider.class.getCanonicalName(); + ((TruPulseConfig) sensorConfig).commSettings = btConf; + ((TruPulseConfig) sensorConfig).serialNumber = deviceID; + } else if (Sensors.TruPulseSim.equals(sensor)) { + sensorConfig = createTruPulseConfig(); + sensorConfig.id = "TRUPULSE_SENSOR_SIMULATED"; + sensorConfig.name = "Simulated TruPulse Range Finder [" + deviceName + "]"; + sensorConfig.autoStart = true; + + BluetoothCommProviderConfig btConf = new BluetoothCommProviderConfig(); + btConf.protocol.deviceName = "TP360RB.*"; + btConf.moduleClass = SimulatedDataStream.class.getCanonicalName(); + ((TruPulseConfig) sensorConfig).commSettings = btConf; + ((TruPulseConfig) sensorConfig).serialNumber = deviceID; + } else if (Sensors.Angel.equals(sensor)) { + sensorConfig = new AngelSensorConfig(); + sensorConfig.id = "ANGEL_SENSOR"; + sensorConfig.name = "Angel Sensor [" + deviceName + "]"; + sensorConfig.autoStart = true; + + BleConfig bleConf = new BleConfig(); + bleConf.id = "BLE"; + bleConf.moduleClass = BleNetwork.class.getCanonicalName(); + bleConf.androidContext = this.getApplicationContext(); + bleConf.autoStart = true; + sensorhubConfig.add(bleConf); + + ((AngelSensorConfig) sensorConfig).networkID = bleConf.id; + } else if (Sensors.FlirOne.equals(sensor)) { + sensorConfig = new FlirOneCameraConfig(); + sensorConfig.id = "FLIRONE_SENSOR"; + sensorConfig.name = "FLIR One Camera [" + deviceName + "]"; + sensorConfig.autoStart = true; + + ((FlirOneCameraConfig) sensorConfig).androidContext = this.getApplicationContext(); + ((FlirOneCameraConfig) sensorConfig).camPreviewTexture = boundService.getVideoTexture(); + } else if (Sensors.ProxySensor.equals(sensor)) { + sensorConfig = new ProxySensorConfig(); + } else if(Sensors.BLELocation.equals(sensor)){ + sensorConfig = new BLEBeaconConfig(); + sensorConfig.id = "BLE_BEACON_SCANNER"; + sensorConfig.name = "BLE Scanner [" + deviceName + "]"; + sensorConfig.autoStart = true; + }*/else { + sensorConfig = new SensorConfig(); + } + + return sensorConfig; + } + + + private void setupBroadcastReceivers() { + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String origin = intent.getStringExtra("src"); + if (!context.getPackageName().equalsIgnoreCase(origin)) { + String sosEndpointUrl = intent.getStringExtra("sosEndpointUrl"); + String name = intent.getStringExtra("name"); + String sensorId = intent.getStringExtra("sensorId"); + ArrayList properties = intent.getStringArrayListExtra("properties"); + + if (sosEndpointUrl == null || name == null || sensorId == null || properties.size() == 0) { + return; + } + + /* ProxySensorConfig proxySensorConfig = (ProxySensorConfig) createSensorConfig(Sensors.ProxySensor); + proxySensorConfig.androidContext = getApplicationContext(); + proxySensorConfig.sosEndpointUrl = sosEndpointUrl; + proxySensorConfig.name = name; + proxySensorConfig.id = sensorId; + proxySensorConfig.sensorUID = sensorId; + proxySensorConfig.observedProperties.addAll(properties); + proxySensorConfig.sosUseWebsockets = true; + proxySensorConfig.autoStart = true; + proxySensorConfigs.add(proxySensorConfig);*/ + + // register and "start" new sensor, data stream doesn't begin until someone requests data; + try { + boundService.stopSensorHub(); + Thread.sleep(2000); + Log.d("OSHApp", "Starting Sensorhub Again"); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + updateConfig(PreferenceManager.getDefaultSharedPreferences(MainActivity.this), runName); + sostClients.clear(); + boundService.startSensorHub(sensorhubConfig, showVideo, MainActivity.this); + if (boundService.hasVideo()) + mainInfoArea.setBackgroundColor(0x80FFFFFF); + } catch (InterruptedException e) { + Log.e("OSHApp", "Error Loading Proxy Sensor", e); + } + + } + } + }; + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_BROADCAST_RECEIVER); + + registerReceiver(receiver, filter); + } + @Override protected void onStart() @@ -794,4 +1470,50 @@ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { } + + private boolean shouldServe(SharedPreferences prefs){ + Map prefMap = prefs.getAll(); + for(Map.Entry pref : prefMap.entrySet()){ + if(pref.getValue() instanceof HashSet) { + if(((HashSet) pref.getValue()).contains("FETCH_LOCAL")) { + Log.d(TAG, "shouldServe: TRUE"); + return true; + } + } + } + return false; + } + + private boolean shouldStore(SharedPreferences prefs){ + Map prefMap = prefs.getAll(); + for(Map.Entry pref : prefMap.entrySet()){ + if(pref.getValue() instanceof HashSet) { + if(((HashSet) pref.getValue()).contains("STORE_LOCAL")) { + Log.d(TAG, "shouldStore: TRUE"); + return true;} + } + } + return false; + } + + private void checkForPermissions(){ + List permissions = new ArrayList<>(); + + //Check for necessary permissions + if(checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED){ + permissions.add(Manifest.permission.ACCESS_FINE_LOCATION); + } + if(checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_DENIED){ + permissions.add(Manifest.permission.CAMERA); + } + if(checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_DENIED){ + permissions.add(Manifest.permission.RECORD_AUDIO); + } + // Does app actually need storage permissions now? + String[] permARR = new String[permissions.size()]; + permARR = permissions.toArray(permARR); + if(permARR.length >0) { + requestPermissions(permARR, 100); + } + } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java b/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java new file mode 100644 index 00000000..a471ed1c --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java @@ -0,0 +1,98 @@ +package org.sensorhub.android; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import org.sensorhub.api.common.SensorHubException; +import org.sensorhub.impl.service.sos.SOSService; +import org.vast.ows.OWSException; +import org.vast.ows.OWSRequest; +import org.vast.ows.OWSUtils; +import org.vast.xml.DOMHelper; +import org.vast.xml.DOMHelperException; +import org.w3c.dom.Element; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class SOSServiceWithIPC extends SOSService +{ + public static final String SQAN_TEST = "SA"; + private static final String SQAN_EXTRA = "channel"; + public static final String ACTION_SOS = "org.sofwerx.ogc.ACTION_SOS"; + private static final String EXTRA_PAYLOAD = "SOS"; + private static final String EXTRA_ORIGIN = "src"; + private Context androidContext; + + @Override + public void start() throws SensorHubException + { + super.start(); + androidContext = ((SOSServiceWithIPCConfig) config).androidContext; + + BroadcastReceiver receiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + String origin = intent.getStringExtra(EXTRA_ORIGIN); + if (!context.getPackageName().equalsIgnoreCase(origin)) + { + String requestPayload = intent.getStringExtra(EXTRA_PAYLOAD); + handleIPCRequest(requestPayload); + } + } + }; + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_SOS); + + androidContext.registerReceiver(receiver, filter); + } + + private void handleIPCRequest(String body) + { + OWSUtils owsUtils = new OWSUtils(); + ByteArrayInputStream is = new ByteArrayInputStream(body.getBytes()); + + try { + DOMHelper dom = new DOMHelper(is, false); + Element requestElt = dom.getBaseElement(); + OWSRequest request = owsUtils.readXMLQuery(dom, requestElt); + + ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); + request.setResponseStream(responseStream); + servlet.handleRequest(request); + + /** + * request are small usually, but responses can be really large. There is a limit to the size of response + */ + String responsePayload = responseStream.toString(); + Intent responseIntent = new Intent(); + responseIntent.setAction(ACTION_SOS); + responseIntent.putExtra(EXTRA_ORIGIN, androidContext.getPackageName()); + responseIntent.putExtra(EXTRA_PAYLOAD, responsePayload); + + androidContext.sendBroadcast(responseIntent); + } + catch (DOMHelperException e) + { + e.printStackTrace(); + } + catch (IOException e) + { + e.printStackTrace(); + } + catch (OWSException e) + { + e.printStackTrace(); + } + // OGCException e + /** + * TODO: Look how server is handling the this exception + */ + } +} + diff --git a/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPCConfig.java b/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPCConfig.java new file mode 100644 index 00000000..a9701650 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPCConfig.java @@ -0,0 +1,22 @@ +package org.sensorhub.android; + +import android.content.Context; + +import org.sensorhub.api.module.ModuleConfig; +import org.sensorhub.impl.service.sos.SOSServiceConfig; + +public class SOSServiceWithIPCConfig extends SOSServiceConfig +{ + public transient Context androidContext; + + public SOSServiceWithIPCConfig() + { + super(); + } + + @Override + public ModuleConfig clone() + { + return this; // disable clone for now as it crashes Android app + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java b/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java index e54e97e3..df727981 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java @@ -17,7 +17,9 @@ import android.annotation.TargetApi; import android.app.AlertDialog; import android.content.DialogInterface; +import android.content.SharedPreferences; import android.hardware.Camera; +import android.net.wifi.WifiManager; import android.os.Build; import android.os.Bundle; import android.preference.EditTextPreference; @@ -28,14 +30,24 @@ import android.preference.PreferenceFragment; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; +import android.support.v7.view.menu.ListMenuPresenter; import android.text.InputType; +import android.text.PrecomputedText; +import android.util.Log; import android.widget.BaseAdapter; import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig; +import java.math.BigInteger; +import java.net.InetAddress; import java.net.URL; +import java.net.UnknownHostException; +import java.nio.ByteOrder; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; import java.util.List; +import java.util.prefs.Preferences; public class UserSettingsActivity extends PreferenceActivity @@ -152,6 +164,26 @@ public void onCreate(Bundle savedInstanceState) bindPreferenceSummaryToValue(findPreference("device_name")); bindPreferenceSummaryToValue(findPreference("sos_uri")); bindPreferenceSummaryToValue(findPreference("sos_username")); + + WifiManager wifiManager = (WifiManager) getActivity().getApplicationContext().getSystemService(WIFI_SERVICE); + int ipAddress = wifiManager.getConnectionInfo().getIpAddress(); + + // Convert little-endian to big-endianif needed + if (ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)) { + ipAddress = Integer.reverseBytes(ipAddress); + } + + byte[] ipByteArray = BigInteger.valueOf(ipAddress).toByteArray(); + + String ipAddressString; + try { + ipAddressString = InetAddress.getByAddress(ipByteArray).getHostAddress(); + } catch (UnknownHostException ex) { + ipAddressString = "Unable to get IP Address"; + } + + Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress"); + ipAddressLabel.setSummary(ipAddressString); } } @@ -168,6 +200,115 @@ public void onCreate(Bundle savedInstanceState) super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.pref_sensors); bindPreferenceSummaryToValue(findPreference("angel_address")); + + SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); + + Preference accelerometerEnable = getPreferenceScreen().findPreference("accel_enabled"); + Preference accelerometerOptions = getPreferenceScreen().findPreference("accel_options"); + accelerometerOptions.setEnabled(prefs.getBoolean(accelerometerEnable.getKey(), false)); + accelerometerEnable.setOnPreferenceChangeListener((preference, newValue) -> { + accelerometerOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference gyroEnabled = getPreferenceScreen().findPreference("gyro_enabled"); + Preference gyroOptions = getPreferenceScreen().findPreference("gyro_options"); + gyroOptions.setEnabled(prefs.getBoolean(gyroEnabled.getKey(), false)); + gyroEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + gyroOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference magEnabled = getPreferenceScreen().findPreference("mag_enabled"); + Preference magOptions = getPreferenceScreen().findPreference("mag_options"); + magOptions.setEnabled(prefs.getBoolean(magEnabled.getKey(), false)); + magEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + magOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference orientQuatEnabled = getPreferenceScreen().findPreference("orient_quat_enabled"); + Preference orientQuatOptions = getPreferenceScreen().findPreference("orient_quat_options"); + orientQuatOptions.setEnabled(prefs.getBoolean(orientQuatEnabled.getKey(), false)); + orientQuatEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + orientQuatOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference orientEulerEnabled = getPreferenceScreen().findPreference("orient_euler_enabled"); + Preference orientEulerOptions = getPreferenceScreen().findPreference("orient_euler_options"); + orientEulerOptions.setEnabled(prefs.getBoolean(orientEulerEnabled.getKey(), false)); + orientEulerEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + orientEulerOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference gpsEnabled = getPreferenceScreen().findPreference("gps_enabled"); + Preference gpsOptions = getPreferenceScreen().findPreference("gps_options"); + gpsOptions.setEnabled(prefs.getBoolean(gpsEnabled.getKey(), false)); + gpsEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + gpsOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference netlocEnabled = getPreferenceScreen().findPreference("netloc_enabled"); + Preference netlocOptions = getPreferenceScreen().findPreference("netloc_options"); + netlocOptions.setEnabled(prefs.getBoolean(netlocEnabled.getKey(), false)); + netlocEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + netlocOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference camEnabled = getPreferenceScreen().findPreference("cam_enabled"); + Preference camOptions = getPreferenceScreen().findPreference("cam_options"); + camOptions.setEnabled(prefs.getBoolean(camEnabled.getKey(), false)); + camEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + camOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference videoRollEnabled = getPreferenceScreen().findPreference("video_roll_enabled"); + Preference videoRollOptions = getPreferenceScreen().findPreference("video_roll_options"); + videoRollOptions.setEnabled(prefs.getBoolean(videoRollEnabled.getKey(), false)); + videoRollEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + videoRollOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference audioEnabled = getPreferenceScreen().findPreference("audio_enabled"); + Preference audioOptions = getPreferenceScreen().findPreference("audio_options"); + camOptions.setEnabled(prefs.getBoolean(camEnabled.getKey(), false)); + audioEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + audioOptions.setEnabled((boolean) newValue); + return true; + }); + + Preference trupulseEnabled = getPreferenceScreen().findPreference("trupulse_enabled"); + Preference trupulseOptions = getPreferenceScreen().findPreference("trupulse_options"); + Preference trupulseDatasource = getPreferenceScreen().findPreference("trupulse_datasource"); + trupulseOptions.setEnabled(prefs.getBoolean(trupulseEnabled.getKey(), false)); + trupulseDatasource.setEnabled(prefs.getBoolean(trupulseEnabled.getKey(), false)); + trupulseEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + trupulseOptions.setEnabled((boolean) newValue); + trupulseDatasource.setEnabled((boolean) newValue); + return true; + }); + +// Preference bleEnable = getPreferenceScreen().findPreference("ble_enabled"); +// Preference bleLocationMethod = getPreferenceScreen().findPreference("ble_loc_method"); +// Preference bleOptions = getPreferenceScreen().findPreference("ble_options"); +// Preference bleConfigURL = getPreferenceScreen().findPreference("ble_config_url"); +// bleLocationMethod.setEnabled(prefs.getBoolean(bleEnable.getKey(), false)); +// bleOptions.setEnabled((prefs.getBoolean(bleEnable.getKey(), false))); +// bleConfigURL.setEnabled((prefs.getBoolean(bleEnable.getKey(), false))); +// bleEnable.setOnPreferenceChangeListener(((preference, newValue) -> { +// bleLocationMethod.setEnabled((boolean) newValue); +// bleOptions.setEnabled((boolean) newValue); +// bleConfigURL.setEnabled((boolean) newValue); +// return true; +// })); + + // TODO: introduce FLIR and ANGEL sensors } } @@ -178,6 +319,9 @@ public void onCreate(Bundle savedInstanceState) @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static class VideoPreferenceFragment extends PreferenceFragment { + ArrayList frameRateList = new ArrayList<>(); + ArrayList resList = new ArrayList<>(); + @Override public void onCreate(Bundle savedInstanceState) { @@ -185,15 +329,34 @@ public void onCreate(Bundle savedInstanceState) addPreferencesFromResource(R.xml.pref_video); PreferenceScreen videoOptsScreen = getPreferenceScreen(); + + // Create camera selection preference + ArrayList cameras = new ArrayList<>(); + for(int i = 0; i < Camera.getNumberOfCameras(); i++) + { + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(i, info); + cameras.add(Integer.toString(i)); + } + ListPreference cameraSelectList = (ListPreference)videoOptsScreen.findPreference("camera_select"); +// cameraSelectList.setKey("camera_select"); +// cameraSelectList.setTitle("Selected Camera"); + cameraSelectList.setEntries(cameras.toArray(new String[0])); + cameraSelectList.setEntryValues(cameras.toArray(new String[0])); +// cameraSelectList.setDefaultValue(0); + bindPreferenceSummaryToValue(cameraSelectList); + videoOptsScreen.addPreference(cameraSelectList); + bindPreferenceSummaryToValue(findPreference("video_codec")); + // TODO: verify that this works in cases where a camera might not be available, and also works on the default value // get possible video capture frame rates and sizes Camera camera = Camera.open(0); Camera.Parameters camParams = camera.getParameters(); - ArrayList frameRateList = new ArrayList<>(); +// ArrayList frameRateList = new ArrayList<>(); for (int frameRate : camParams.getSupportedPreviewFrameRates()) frameRateList.add(Integer.toString(frameRate)); - ArrayList resList = new ArrayList<>(); +// ArrayList resList = new ArrayList<>(); for (Camera.Size imgSize : camParams.getSupportedPreviewSizes()) resList.add(imgSize.width + "x" + imgSize.height); camera.release(); @@ -252,6 +415,58 @@ public void onCreate(Bundle savedInstanceState) presetIndexes.add("AUTO"); selectedPresetList.setEntries(presetNames.toArray(new String[0])); selectedPresetList.setEntryValues(presetIndexes.toArray(new String[0])); + + // Setup Camera Listener + cameraSelectList.setOnPreferenceChangeListener((preference, newValue) -> { + Log.d("CAMERA_SELECT", "New Camera Selected: " + newValue); + updateCameraSettings(Integer.parseInt((String) newValue)); + cameraSelectList.setSummary(newValue.toString()); + return true; + }); + } + + protected void updateCameraSettings(Integer cameraId){ + Camera camera = Camera.open(cameraId); + Camera.Parameters camParams = camera.getParameters(); + for (int frameRate : camParams.getSupportedPreviewFrameRates()) + frameRateList.add(Integer.toString(frameRate)); + for (Camera.Size imgSize : camParams.getSupportedPreviewSizes()) + resList.add(imgSize.width + "x" + imgSize.height); + camera.release(); + } + } + + + /* + * Fragment for audio settings + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class AudioPreferenceFragment extends PreferenceFragment + { + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.pref_audio); + + PreferenceScreen audioOptsScreen = getPreferenceScreen(); + bindPreferenceSummaryToValue(findPreference("audio_codec")); + + // get possible video capture frame rates and sizes + List sampleRateList = Arrays.asList("8000", "11025", "22050", "44100", "48000"); + List bitRateList = Arrays.asList("32", "64", "96", "128", "160", "192"); + + // add list of supported sample rates + ListPreference sampleRatePrefList = (ListPreference)audioOptsScreen.findPreference("audio_samplerate"); + sampleRatePrefList.setEntries(sampleRateList.toArray(new String[0])); + sampleRatePrefList.setEntryValues(sampleRateList.toArray(new String[0])); + bindPreferenceSummaryToValue(findPreference("audio_samplerate")); + + // add list of supported bitrates + ListPreference bitRatePrefList = (ListPreference)audioOptsScreen.findPreference("audio_bitrate"); + bitRatePrefList.setEntries(bitRateList.toArray(new String[0])); + bitRatePrefList.setEntryValues(bitRateList.toArray(new String[0])); + bindPreferenceSummaryToValue(findPreference("audio_samplerate")); } } diff --git a/sensorhub-android-blebeacon/.gitignore b/sensorhub-android-blebeacon/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/sensorhub-android-blebeacon/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sensorhub-android-blebeacon/build.gradle b/sensorhub-android-blebeacon/build.gradle new file mode 100644 index 00000000..26b596ce --- /dev/null +++ b/sensorhub-android-blebeacon/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'com.android.support:appcompat-v7:28.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation project(path: ':sensorhub-core') + implementation 'org.altbeacon:android-beacon-library:2.16.2' + implementation project(path: ':sensorhub-process-vecmath') + implementation project(path: ':sensorhub-process-geoloc') + implementation 'com.android.volley:volley:1.1.1' +} diff --git a/sensorhub-android-blebeacon/consumer-rules.pro b/sensorhub-android-blebeacon/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-blebeacon/docs/BLEDoc.txt b/sensorhub-android-blebeacon/docs/BLEDoc.txt new file mode 100644 index 00000000..32ebfb29 --- /dev/null +++ b/sensorhub-android-blebeacon/docs/BLEDoc.txt @@ -0,0 +1,12 @@ +The BLE Beacon Sensor is a work in progress module designed to use the URL String gathered from +BLE Beacons using the Eddystone URL protocol with known locations to give an approximate location of +the phone when indoors. + +MODULE MUST: +1. Have a constellation of registered Beacons with UIDs that have known locations +2. Be able to triangulate based on the three strongest signals +3. Have the ability to add a new registered beacon through the App +4. Have the ability to remove a registered beacon through the app + +OPTIONAL: +1. Notify that a beacon has low battery life \ No newline at end of file diff --git a/sensorhub-android-blebeacon/proguard-rules.pro b/sensorhub-android-blebeacon/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/sensorhub-android-blebeacon/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/sensorhub-android-blebeacon/src/androidTest/java/org/sensorhub/impl/sensor/blebeacon/ExampleInstrumentedTest.java b/sensorhub-android-blebeacon/src/androidTest/java/org/sensorhub/impl/sensor/blebeacon/ExampleInstrumentedTest.java new file mode 100644 index 00000000..50c2ab2d --- /dev/null +++ b/sensorhub-android-blebeacon/src/androidTest/java/org/sensorhub/impl/sensor/blebeacon/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package org.sensorhub.impl.sensor.blebeacon; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("org.sensorhub.impl.sensor.blebeacon.test", appContext.getPackageName()); + } +} diff --git a/sensorhub-android-blebeacon/src/main/AndroidManifest.xml b/sensorhub-android-blebeacon/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f6a6a09d --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/AdaptedCellTrilateration.java b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/AdaptedCellTrilateration.java new file mode 100644 index 00000000..d94b5df7 --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/AdaptedCellTrilateration.java @@ -0,0 +1,29 @@ +package org.sensorhub.impl.sensor.blebeacon; + +public class AdaptedCellTrilateration { + static double[] trackPhone(double[][] locations, double[] distances){ + double[] computedLocation = new double[2]; + + double A = 2 * locations[1][0] - 2 * locations[0][0]; + double B = 2 * locations[1][1] - 2 * locations[0][1]; + double C = Math.pow(distances[0], 2) - Math.pow(distances[1], 2) - Math.pow(locations[0][0], 2) + Math.pow(locations[1][0], 2) - Math.pow(locations[0][1], 2) + Math.pow(locations[1][1], 2); + + double D = 2 * locations[2][0] - 2 * locations[1][0]; + double E = 2 * locations[2][1] - 2 * locations[1][1]; + double F = Math.pow(distances[1], 2) - Math.pow(distances[2], 2) - Math.pow(locations[1][0], 2) + Math.pow(locations[2][0], 2) - Math.pow(locations[1][1], 2) + Math.pow(locations[2][1], 2); + + double xCoord = (C*E - F*B) / (E*A - B*D); + double yCoord = (C*D - A*F) / (B*D - A*E); + + computedLocation[0] = yCoord; + computedLocation[1] = xCoord; + + // returns lon, lat + return computedLocation; + } + + private void ddmToENU(){ + // convert to ecef + // convert ecef to enu + } +} diff --git a/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeacon.java b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeacon.java new file mode 100644 index 00000000..eb9c3317 --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeacon.java @@ -0,0 +1,96 @@ +package org.sensorhub.impl.sensor.blebeacon; + +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.RemoteException; +import android.util.Log; + +import org.altbeacon.beacon.Beacon; +import org.altbeacon.beacon.BeaconConsumer; +import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.beacon.BeaconParser; +import org.altbeacon.beacon.Identifier; +import org.altbeacon.beacon.RangeNotifier; +import org.altbeacon.beacon.Region; +import org.altbeacon.beacon.utils.UrlBeaconUrlCompressor; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class BLEBeacon implements BeaconConsumer, RangeNotifier { + private Context parentContext; + private String TAG = "BLEBeacon-DefaultTAG"; + private BeaconManager mBeaconManager; + private Map beacons; + + public BLEBeacon(Context context){ + this.parentContext = context; + this.beacons = new HashMap(); + } + + public void BeaconManagerSetup(){ + mBeaconManager = BeaconManager.getInstanceForApplication(parentContext); + // Detect the URL frame: + mBeaconManager.getBeaconParsers().add(new BeaconParser(). + setBeaconLayout(BeaconParser.EDDYSTONE_URL_LAYOUT)); + mBeaconManager.bind(this); + } + + public void unbind(){ + try { + mBeaconManager.stopRangingBeaconsInRegion(new Region("all-beacons-region", null, null, null)); + } catch (RemoteException e){ + Log.d(TAG, "unbind: " + e); + } + + mBeaconManager.unbind(this); + } + + @Override + public void onBeaconServiceConnect() { + Region region = new Region("all-beacons-region", null, null, null); + try { + mBeaconManager.startRangingBeaconsInRegion(region); + } catch (RemoteException e) { + e.printStackTrace(); + } + mBeaconManager.setRangeNotifier(this); + } + + @Override + public Context getApplicationContext() { + return this.parentContext; + } + + @Override + public void unbindService(ServiceConnection serviceConnection) { + + } + + @Override + public boolean bindService(Intent intent, ServiceConnection serviceConnection, int i) { + return false; + } + + @Override + public void didRangeBeaconsInRegion(Collection beacons, Region region) { + for (Beacon beacon: beacons) { + if (beacon.getServiceUuid() == 0xfeaa && beacon.getBeaconTypeCode() == 0x10) { + // This is an Eddystone-URL frame + String url = UrlBeaconUrlCompressor.uncompress(beacon.getId1().toByteArray()); + Log.d(TAG, "Beacon ID: "+ beacon.getId1() + "Beacon URL: " + url + + " approximately " + beacon.getDistance() + " meters away."); + // TODO: Need to improve this to handle non-EsURL beacons that have info in the other ID slots + this.beacons.put(beacon.getId1(), beacon); + } + } + } + + public void printBeaconList(){ + Log.d(TAG, "printBeaconList: " + this.beacons); + } + + +} diff --git a/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconConfig.java b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconConfig.java new file mode 100644 index 00000000..79772cb5 --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconConfig.java @@ -0,0 +1,37 @@ +package org.sensorhub.impl.sensor.blebeacon; + +import android.content.Context; + +import com.google.gson.annotations.SerializedName; + +import org.sensorhub.api.config.DisplayInfo; +import org.sensorhub.api.module.ModuleConfig; +import org.sensorhub.api.sensor.SensorConfig; + +public class BLEBeaconConfig extends SensorConfig { + public boolean activateBLEBeacon = false; + + public String deviceName; + public String runName; + public String runDescription; + public String configURL; + + public transient Context androidContext; + + @DisplayInfo(label = "Clamp to Nearest", desc = "Shows location as the the Nearest Beacon") + @SerializedName(value = "clampToNearest", alternate = {"enabled"}) + public boolean clampToNearest; + + public BLEBeaconConfig(){ + this.moduleClass = BLEBeaconDriver.class.getCanonicalName(); + this.clampToNearest = true; + } + + // clone disabled as it causes crashes for now + @Override + public ModuleConfig clone(){ + return this; + } + + +} diff --git a/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconDriver.java b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconDriver.java new file mode 100644 index 00000000..f560e34a --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconDriver.java @@ -0,0 +1,356 @@ +package org.sensorhub.impl.sensor.blebeacon; + +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.hardware.Sensor; +import android.os.Build; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; + +import org.altbeacon.beacon.Beacon; +import org.altbeacon.beacon.BeaconConsumer; +import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.beacon.BeaconParser; +import org.altbeacon.beacon.RangeNotifier; +import org.altbeacon.beacon.Region; +import org.altbeacon.beacon.service.RunningAverageRssiFilter; +import org.altbeacon.beacon.utils.UrlBeaconUrlCompressor; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.sensorhub.algo.geoloc.GeoTransforms; +import org.sensorhub.algo.vecmath.Mat3d; +import org.sensorhub.algo.vecmath.Vect3d; +import org.sensorhub.api.common.SensorHubException; +import org.sensorhub.api.sensor.ISensorDataInterface; +import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BLEBeaconDriver extends AbstractSensorModule implements BeaconConsumer, RangeNotifier { + private static final Logger log = LoggerFactory.getLogger(BLEBeaconDriver.class.getSimpleName()); + public static final String LOCAL_REF_FRAME = "LOCAL_FRAME"; + private static final String TAG = "BLEBeaconDriver"; + private static final double pollTimeMillis = 1000; + private static final long runningAveragePollTimeMillis = 5000; + private static final double purgeTimeMillis = 5000; + String localFrameURI; + + // List smlComponents; + private BLEBeaconRawOutput rawOutput; + private BLEBeaconLocationOutput locOutput; + private NearestBeaconOutput nearestBeaconOutput; + + private BeaconManager mBeaconManager; + private List closestBeacons; + private Map url2Locations; + private Map beaconMap; + private Map lastRanged; + private Map url2Room; + private Triangulation triangulation; + Comparator beaconComp; + private double lastPoll; + private double lastRunningAverageTime; + + public BLEBeaconDriver() { + // TODO: implement something better for this down the road (WIP) + beaconMap = new HashMap<>(); + url2Locations = new HashMap<>(); + lastRanged = new HashMap<>(); + url2Room = new HashMap<>(); + +// url2Locations.put("http://rpi4-1", new Vect3d(-86.2012028 * Math.PI / 180, 34.2520736 * Math.PI / 180, 283.0)); +// url2Locations.put("http://opensensorhub.org", new Vect3d(-86.2012018 * Math.PI / 180, 34.2520221 * Math.PI / 180, 283.0)); +// url2Locations.put("http://cardbeacon", new Vect3d(-86.2012561 * Math.PI / 180, 34.2520761 * Math.PI / 180, 283.0)); + + beaconComp = new Comparator() { + @Override + public int compare(Object o1, Object o2) { + if (o1 instanceof Beacon && o2 instanceof Beacon) { + Beacon b1 = (Beacon) o1; + Beacon b2 = (Beacon) o2; + /* if((System.currentTimeMillis() - runningAveragePollTimeMillis) >= lastRunningAverageTime ){ // TODO: Make sure this is a reasonable place to check this + lastRunningAverageTime = System.currentTimeMillis(); + }*/ + return -Double.compare(b1.getRunningAverageRssi(), b2.getRunningAverageRssi()); + } else { + throw new ClassCastException("Arguments must be of type Beacon"); + } + } + }; + } + + @Override + public synchronized void init() throws SensorHubException { + Context androidContext = config.androidContext; + + String deviceID = Settings.Secure.getString(androidContext.getContentResolver(), Settings.Secure.ANDROID_ID); + this.xmlID = "ANDROID_SENSORS_" + Build.SERIAL; // Deprecated in API level 26 + this.uniqueID = "urn:android:ble_beacon:" + deviceID; + this.localFrameURI = this.uniqueID + "#" + LOCAL_REF_FRAME; + lastPoll = System.currentTimeMillis() - pollTimeMillis; + lastRunningAverageTime = System.currentTimeMillis(); + + triangulation = new Triangulation(); + closestBeacons = new ArrayList<>(); + rawOutput = new BLEBeaconRawOutput(this); + locOutput = new BLEBeaconLocationOutput(this); + nearestBeaconOutput = new NearestBeaconOutput(this); + addOutput(rawOutput, false); + addOutput(locOutput, false); + addOutput(nearestBeaconOutput, false); + + // TODO: Switch this to SensorThings API if we want to demonstrate that capability + // Get Beacon data + RequestQueue requestQueue = Volley.newRequestQueue(this.getConfiguration().androidContext); + Log.d(TAG, this.getConfiguration().configURL); + // Use a default URL to start, if the preferences at least start with http or https, then use that + String url = "https://chainreaction31.github.io/scira-ble-page/beacons.json"; + String configURL = this.getConfiguration().configURL; + if(configURL.startsWith("https://") || configURL.startsWith("http://")){ + url = configURL; + } + JsonObjectRequest stringRequest = new JsonObjectRequest(Request.Method.GET, url, null, new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + JSONArray beaconsJSON = null; + + try { + beaconsJSON = response.getJSONArray("beacons"); + + for (int i = 0; i < beaconsJSON.length(); i++) { + JSONObject beaconInfo = beaconsJSON.getJSONObject(i); + String url = beaconInfo.getString("url"); + double[] beaconLocation = new double[]{beaconInfo.getJSONObject("location").getDouble("lon") * Math.PI / 180, + beaconInfo.getJSONObject("location").getDouble("lat") * Math.PI / 180, + beaconInfo.getJSONObject("location").getDouble("alt") + }; + url2Locations.put(url, new Vect3d(beaconLocation[0], beaconLocation[1], beaconLocation[2])); + url2Room.put(url, beaconInfo.optString("roomDesc", "None Provided")); + } + + } catch (JSONException e) { + e.printStackTrace(); + } + + Log.d(TAG, "onResponse: " + beaconsJSON.toString()); + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Log.d(TAG, "onErrorResponse: GET Beacons failed"); + } + }); + requestQueue.add(stringRequest); + } + + @Override + public void start() throws SensorHubException { + BeaconManager.setRssiFilterImplClass(RunningAverageRssiFilter.class); + RunningAverageRssiFilter.setSampleExpirationMilliseconds(runningAveragePollTimeMillis); + + // start the BLEBeacon Manager scanning + // BLE Beacon Initialization + mBeaconManager = BeaconManager.getInstanceForApplication(getConfiguration().androidContext); + mBeaconManager.getBeaconParsers().add(new BeaconParser().setBeaconLayout(BeaconParser.EDDYSTONE_URL_LAYOUT)); + mBeaconManager.bind(this); + rawOutput.start(); + locOutput.start(); + } + + @Override + public void stop() throws SensorHubException { + try { + mBeaconManager.stopRangingBeaconsInRegion(new Region("url-beacons-region", null, null, null)); + } catch (RemoteException e) { + Log.d(TAG, "unbind: " + e); + } + + mBeaconManager.unbind(this); + } + + protected void useSensor(ISensorDataInterface output, Sensor sensor) { + addOutput(output, false); + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public Logger getLogger() { + return log; + } + + // BLE Beacon Consumer/Range Notifier Require Implementations + @Override + public void onBeaconServiceConnect() { + Region region = new Region("url-beacons-region", null, null, null); + try { + mBeaconManager.startRangingBeaconsInRegion(region); + } catch (RemoteException e) { + e.printStackTrace(); + } + mBeaconManager.setRangeNotifier(this); + } + + @Override + public Context getApplicationContext() { + return getConfiguration().androidContext; + } + + @Override + public void unbindService(ServiceConnection serviceConnection) { + + } + + @Override + public boolean bindService(Intent intent, ServiceConnection serviceConnection, int i) { + return false; + } + + @Override + public void didRangeBeaconsInRegion(Collection beacons, Region region) { + Beacon closestBeacon = null; + Log.d(TAG, "didRangeBeaconsInRegion: " + beacons.size()); + if (System.currentTimeMillis() - lastPoll > pollTimeMillis) { + for (Beacon beacon : beacons) { + if (beacon.getServiceUuid() == 0xfeaa && beacon.getBeaconTypeCode() == 0x10) { + // This is an Eddystone-URL frame + String url = UrlBeaconUrlCompressor.uncompress(beacon.getId1().toByteArray()); + Log.d(TAG, "Beacon ID: " + beacon.getId1() + "\nBeacon URL: " + url + +// " approximately " + beacon.getDistance() + " meters away."); + " approximately " + getBeaconDistance(beacon) + " meters away."); + // TODO: Need to improve this to handle non-EsURL beacons that have info in the other ID slots + rawOutput.sendBeaconRecord(beacon); + if (url2Locations.get(url) != null) { + beaconMap.put(url, beacon); + lastRanged.put(url, new Date(System.currentTimeMillis())); + } + } + } + + checkForOldBeacons(); + Beacon[] bArr = beaconMap.values().toArray(new Beacon[0]); + double[] estimatedLocation = determineLocation(); + if (estimatedLocation.length != 0) { + locOutput.sendMeasurement(estimatedLocation, bArr); // Put better checks in place after testing} + } + lastPoll = System.currentTimeMillis(); + } + } + + private double[] determineLocation() { + double[] distances = new double[3]; + Vect3d[] locations = new Vect3d[3]; + double[][] locationArr = new double[3][3]; + GeoTransforms geoTransforms = new GeoTransforms(); + int i = 0; + if (config.clampToNearest && !beaconMap.isEmpty()) { + ArrayList beaconArrayList = new ArrayList<>(); + beaconArrayList.addAll(beaconMap.values()); + beaconArrayList.sort(beaconComp); + Beacon nearest = beaconArrayList.get(0); + String roomDesc = url2Room.get(UrlBeaconUrlCompressor.uncompress(nearest.getId1().toByteArray())); + nearestBeaconOutput.sendMeasurement(nearest, roomDesc); + return new double[]{}; + } + if (beaconMap.size() == 3) { + // TODO: move into a function + + for (Beacon beacon : beaconMap.values()) { + locations[i] = url2Locations.get(UrlBeaconUrlCompressor.uncompress(beacon.getId1().toByteArray())); + // todo: we can skip some repeated conversion by only storing the ecef representations of beacon position but that requires us to repeatedly convert when sending a beacon's location + Vect3d tempVec = new Vect3d(); + geoTransforms.LLAtoECEF(locations[i], tempVec); + + locations[i] = tempVec; + locationArr[i] = new double[]{locations[i].y, locations[i].x, locations[i].z}; + distances[i] = getBeaconDistance(beacon); + i++; + } + + double[] estLocation = AdaptedCellTrilateration.trackPhone(locationArr, distances); + Vect3d estECEF = new Vect3d(estLocation[0], estLocation[1], locations[0].z); + Vect3d estLLA = new Vect3d(); + geoTransforms.ECEFtoLLA(estECEF, estLLA); + Log.d(TAG, "determineLocation: " + estLocation[0] + "," + estLocation[1]); + return new double[]{estLLA.y * 180 / Math.PI, estLLA.x * 180 / Math.PI, estLLA.z}; + } + if (beaconMap.size() > 3) { + // find best three + ArrayList beaconArrayList = new ArrayList<>(); + beaconArrayList.addAll(beaconMap.values()); + beaconArrayList.sort(beaconComp); + // do the same as above + for (Beacon beacon : beaconArrayList.subList(0, 3)) { + locations[i] = url2Locations.get(UrlBeaconUrlCompressor.uncompress(beacon.getId1().toByteArray())); + Vect3d tempVec = new Vect3d(); + geoTransforms.LLAtoECEF(locations[i], tempVec); + locations[i] = tempVec; + locationArr[i] = new double[]{locations[i].y, locations[i].x, locations[i].z}; + distances[i] = getBeaconDistance(beacon); + i++; + } + double[] estLocation = AdaptedCellTrilateration.trackPhone(locationArr, distances); + Vect3d estECEF = new Vect3d(estLocation[0], estLocation[1], locations[0].z); + Vect3d estLLA = new Vect3d(); + geoTransforms.ECEFtoLLA(estECEF, estLLA); + Log.d(TAG, "determineLocation: " + estLocation[0] + "," + estLocation[1]); + return new double[]{estLLA.y * 180 / Math.PI, estLLA.x * 180 / Math.PI, estLLA.z}; + + } + return new double[]{}; // Todo: add better handling of this case + } + + private Vect3d MatrixVectorProduct(Mat3d mat, Vect3d vec) { + Vect3d resultVector = new Vect3d(); + + resultVector.x = mat.m00 * vec.x + mat.m01 * vec.x + mat.m02 * vec.x; + resultVector.y = mat.m10 * vec.y + mat.m11 * vec.y + mat.m12 * vec.y; + resultVector.z = mat.m20 * vec.z + mat.m21 * vec.z + mat.m22 * vec.z; + + return resultVector; + } + + public double[] getBeaconLocation(Beacon beacon) { + Vect3d locationVec = url2Locations.get(UrlBeaconUrlCompressor.uncompress(beacon.getId1().toByteArray())); + if (locationVec != null) + return new double[]{locationVec.y * 180 / Math.PI, locationVec.x * 180 / Math.PI, locationVec.z}; + else return new double[]{0, 0, 0}; + } + + private void checkForOldBeacons() { + for (Map.Entry entry : lastRanged.entrySet()) { + if (entry.getValue().getTime() - lastPoll > purgeTimeMillis) { + beaconMap.remove(entry.getKey()); + } + } + } + + public double getBeaconDistance(Beacon beacon) { + double mPower = -75; + // represents the freespace (how obstructed/congested the area is) + double n = 3.5; + double rssi = beacon.getRssi(); + return Math.pow(10, (mPower - rssi) / (10 * n)); + } +} diff --git a/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconLocationOutput.java b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconLocationOutput.java new file mode 100644 index 00000000..5c3fa8e1 --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconLocationOutput.java @@ -0,0 +1,142 @@ +package org.sensorhub.impl.sensor.blebeacon; + +import android.content.Intent; +import android.util.Log; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.Vector; + +import org.altbeacon.beacon.Beacon; +import org.sensorhub.api.sensor.SensorDataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.vast.swe.helper.GeoPosHelper; + +public class BLEBeaconLocationOutput extends AbstractSensorOutput { + + private static final String ACTION_SUBMIT_TRACK_REPORT = "org.sensorhub.android.intent.SPOT_REPORT_TRACK"; + + private static final String URL_DEF = "http://sensorml.com/ont/swe/property/"; + + String name; + DataComponent posDataStruct; +// DataComponent beaconDataStruct; + DataEncoding dataEncoding; + + + protected BLEBeaconLocationOutput(BLEBeaconDriver parentModule) { + super(parentModule); + this.name = parentModule.getName() + " Location"; + + // output structure (time + location) + GeoPosHelper fac = new GeoPosHelper(); + posDataStruct = fac.newDataRecord(); + posDataStruct.setName(getName()); + posDataStruct.setDefinition("http://sensorml.com/ont/swe/property/BLEBeaconLocation"); + + // detected beacons structure + + + // add fields + Vector vec = fac.newLocationVectorLLA(null); + vec.setLocalFrame(parentSensor.localFrameURI); + + Vector nearBeacon = fac.newVector(URL_DEF + "nearest_beacon", + null, + new String[]{"lat", "lon", "alt", "dist"}, + new String[]{"Geodetic Latitude", "Longitude", "Altitude", "Distance"}, + new String[] {"deg", "deg", "m", "m"}, + new String[] {"Lat", "Long", "h", "dist"}); + + Vector beaconVec = fac.newVector(URL_DEF + "beacons_data", + null, + new String[]{"lat", "lon", "alt", "dist"}, + new String[]{"Geodetic Latitude", "Longitude", "Altitude", "Distance"}, + new String[] {"deg", "deg", "m", "m"}, + new String[] {"Lat", "Long", "h", "dist"}); + +// Vector vec1 = fac.newLocationVectorLLA(URL_DEF + "beacon_location"); +// Vector vec2 = fac.newLocationVectorLLA(URL_DEF + "b2_location"); +// Vector vec3 = fac.newLocationVectorLLA(URL_DEF + "b3_location"); +// +// vec1.setLocalFrame(parentSensor.localFrameURI); +// vec2.setLocalFrame(parentSensor.localFrameURI); +// vec3.setLocalFrame(parentSensor.localFrameURI); + + posDataStruct.addComponent("time", fac.newTimeStampIsoUTC()); + posDataStruct.addComponent("est_location", vec); + posDataStruct.addComponent("nearest_beacon", nearBeacon); + + // TODO: maybe there's a better way to present this data... TESTING new approach + posDataStruct.addComponent("beacon_1", beaconVec); +// posDataStruct.addComponent("beacon_1_range", fac.newQuantity(URL_DEF + "b1_distance", null, null, "m")); + posDataStruct.addComponent("beacon_2", beaconVec); +// posDataStruct.addComponent("beacon_2_range", fac.newQuantity(URL_DEF + "b2_distance", null, null, "m")); + posDataStruct.addComponent("beacon_3", beaconVec); +// posDataStruct.addComponent("beacon_3_range", fac.newQuantity(URL_DEF + "b3_distance", null, null, "m")); + + // output encoding + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + protected void start() { + } + + @Override + public String getName() { + return this.name; + } + + @Override + public DataComponent getRecordDescription() { + return posDataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } + + @Override + public double getAverageSamplingPeriod() { + return 1.0; + } + + protected void sendMeasurement(double[] estLocation, Beacon[] beacons) { + + DataBlock dataBlock = posDataStruct.createDataBlock(); + double sampleTime = System.currentTimeMillis() / 1000; + // TODO: Handle null or {0,0,0} better + double[] b1Loc = parentSensor.getBeaconLocation(beacons[0]); + double[] b2Loc = parentSensor.getBeaconLocation(beacons[1]); + double[] b3Loc = parentSensor.getBeaconLocation(beacons[2]); + + dataBlock.setDoubleValue(0, sampleTime); + dataBlock.setDoubleValue(1, estLocation[0]); + dataBlock.setDoubleValue(2, estLocation[1]); + dataBlock.setDoubleValue(3, estLocation[2]); + + dataBlock.setDoubleValue(4, b1Loc[0]); + dataBlock.setDoubleValue(5, b1Loc[1]); + dataBlock.setDoubleValue(6, b1Loc[2]); +// dataBlock.setDoubleValue(7, beacons[0].getDistance()); + dataBlock.setDoubleValue(7, parentSensor.getBeaconDistance(beacons[0])); + + dataBlock.setDoubleValue(8, b2Loc[0]); + dataBlock.setDoubleValue(9, b2Loc[1]); + dataBlock.setDoubleValue(10, b2Loc[2]); +// dataBlock.setDoubleValue(11, beacons[1].getDistance()); + dataBlock.setDoubleValue(11, parentSensor.getBeaconDistance(beacons[1])); + + dataBlock.setDoubleValue(12, b3Loc[0]); + dataBlock.setDoubleValue(13, b3Loc[1]); + dataBlock.setDoubleValue(14, b3Loc[2]); +// dataBlock.setDoubleValue(15, beacons[2].getDistance()); + dataBlock.setDoubleValue(15, parentSensor.getBeaconDistance(beacons[2])); + + // update latest record and send event + latestRecordTime = System.currentTimeMillis(); + eventHandler.publishEvent(new SensorDataEvent(latestRecordTime, BLEBeaconLocationOutput.this, dataBlock)); + } +} diff --git a/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconRawOutput.java b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconRawOutput.java new file mode 100644 index 00000000..6afad303 --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/BLEBeaconRawOutput.java @@ -0,0 +1,118 @@ +package org.sensorhub.impl.sensor.blebeacon; + +import android.util.Log; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; + +import org.altbeacon.beacon.Beacon; +import org.altbeacon.beacon.BeaconManager; +import org.sensorhub.api.sensor.SensorDataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.vast.swe.SWEHelper; + +public class BLEBeaconRawOutput extends AbstractSensorOutput{ + private static final String BLE_BEACON_DEF = "http://sensorml.com/ont/swe/property/BLEBeacon"; + private static final String URL_DEF = "http://sensorml.com/ont/swe/property/"; + private static final String TAG = "BLEBeaconOutputRaw"; + + String name = "BLE Beacon Raw Data"; + boolean enabled; + DataComponent bleData; + DataEncoding dataEncoding; + double samplingPeriod; + long systemTimeOffset = -1L; + + BeaconManager mBeaconManager; + + + protected BLEBeaconRawOutput(BLEBeaconDriver parent) { + super(parent); + + // create output structure + SWEHelper fac = new SWEHelper(); + bleData = fac.newDataRecord(); + bleData.setName(getName()); + bleData.setDefinition(BLE_BEACON_DEF); + bleData.setDescription("Bluetooth Low Energy Beacon readings for commonly available data"); + + // add fields + bleData.addComponent("time", fac.newTimeStampIsoUTC()); + bleData.addComponent("id", fac.newCategory("http://sensorml.com/ont/swe/property/SensorID", null, null, null)); + bleData.addComponent("name", fac.newText(URL_DEF + "name", null, null)); + bleData.addComponent("ID1", fac.newText(URL_DEF + "Identifier_1", null, "First of the three ids used by most BLE beacons, in ES-URL, the only id")); + bleData.addComponent("ID2", fac.newText(URL_DEF + "Identifier_2", null, null)); + bleData.addComponent("ID3", fac.newText(URL_DEF + "Identifier_3", null, null)); + bleData.addComponent("txPower", fac.newQuantity(URL_DEF + "txPower", null, null, "dBm")); + bleData.addComponent("RSSI", fac.newQuantity(URL_DEF + "rssi", null, null, "dBm")); // Is this value measured, and is it affected by the settings in the beacon + bleData.addComponent("distance", fac.newQuantity(URL_DEF + "distance", null, null, "m")); + + dataEncoding = fac.newTextEncoding(",", "\n"); + + // Other fields that can be added + // Extra Data Fields + // Manufacturer + // Service UUID + // MultiFrameBeacon + // Parser Identifier // Note: this is part of ABL, not the beacon itself + } + + public void start() { + + } + + public void stop() { + + } + + @Override + public String getName() { + return name; + } + + @Override + public DataComponent getRecordDescription() { + return bleData; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } + + @Override + public double getAverageSamplingPeriod() { + return 1.0; // TODO: implement a calculation for this + } + + public void sendBeaconRecord(Beacon beacon) { + double time = System.currentTimeMillis() / 1000.; + DataBlock dataBlock = bleData.createDataBlock(); + + dataBlock.setDoubleValue(0, time); + dataBlock.setStringValue(1, parentSensor.getUniqueIdentifier()); + dataBlock.setStringValue(2, beacon.getBluetoothName()); + dataBlock.setStringValue(3, beacon.getId1().toString()); + /* if (beacon.getId2() != null) { + dataBlock.setStringValue(4, beacon.getId2().toString()); + } else { + dataBlock.setStringValue(4, "NONE"); + } + if (beacon.getId3() != null) { + dataBlock.setStringValue(5, beacon.getId3().toString()); + } else { + dataBlock.setStringValue(5, "NONE"); + }*/ + dataBlock.setStringValue(4, "NONE"); + dataBlock.setStringValue(5, "NONE"); + dataBlock.setDoubleValue(6, beacon.getTxPower()); + dataBlock.setDoubleValue(7, beacon.getRssi()); + dataBlock.setDoubleValue(8, beacon.getDistance()); +// dataBlock.setDoubleValue(8, parentSensor.getBeaconDistance(beacon)); + + // Push the data + latestRecordTime = System.currentTimeMillis(); + eventHandler.publishEvent(new SensorDataEvent(latestRecordTime, BLEBeaconRawOutput.this, dataBlock)); + } +} diff --git a/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/NearestBeaconOutput.java b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/NearestBeaconOutput.java new file mode 100644 index 00000000..ef2f92b8 --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/NearestBeaconOutput.java @@ -0,0 +1,103 @@ +package org.sensorhub.impl.sensor.blebeacon; + +import android.content.Intent; +import android.util.Log; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.Vector; + +import org.altbeacon.beacon.Beacon; +import org.sensorhub.algo.vecmath.Vect3d; +import org.sensorhub.api.sensor.SensorDataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +public class NearestBeaconOutput extends AbstractSensorOutput { + + private static final String ACTION_SUBMIT_TRACK_REPORT = "org.sensorhub.android.intent.SPOT_REPORT_TRACK"; + + private static final String URL_DEF = "http://sensorml.com/ont/swe/property/"; + + String name; + DataComponent posDataStruct; + // DataComponent beaconDataStruct; + DataEncoding dataEncoding; + + + protected NearestBeaconOutput(BLEBeaconDriver parentModule) { + super(parentModule); + this.name = parentModule.getName() + " Nearest Beacon"; + + // output structure (time + location) + GeoPosHelper fac = new GeoPosHelper(); + posDataStruct = fac.newDataRecord(); + posDataStruct.setName(getName()); + posDataStruct.setDefinition("http://sensorml.com/ont/swe/property/NearestBeacon"); + + // detected beacons structure + + + // add fields + Vector vec = fac.newLocationVectorLLA(null); + vec.setLocalFrame(parentSensor.localFrameURI); + + Vector nearBeacon = fac.newVector(URL_DEF + "nearest_beacon", + null, + new String[]{"lat", "lon", "alt", "dist"}, + new String[]{"Geodetic Latitude", "Longitude", "Altitude", "Distance"}, + new String[] {"deg", "deg", "m", "m"}, + new String[] {"Lat", "Long", "h", "dist"}); + + posDataStruct.addComponent("time", fac.newTimeStampIsoUTC()); + posDataStruct.addComponent("nearest_beacon", nearBeacon); + posDataStruct.addComponent("roomDesc", new SWEHelper().newText(URL_DEF + "roomDesc", null, null)); + + // output encoding + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + protected void start() { + } + + @Override + public String getName() { + return this.name; + } + + @Override + public DataComponent getRecordDescription() { + return posDataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } + + @Override + public double getAverageSamplingPeriod() { + return 1.0; + } + + protected void sendMeasurement(Beacon beacon, String roomDesc) { + + DataBlock dataBlock = posDataStruct.createDataBlock(); + double sampleTime = System.currentTimeMillis() / 1000; + double[] locationDecDeg = parentSensor.getBeaconLocation(beacon); + + dataBlock.setDoubleValue(0, sampleTime); + dataBlock.setDoubleValue(1, locationDecDeg[0]); + dataBlock.setDoubleValue(2, locationDecDeg[1]); + dataBlock.setDoubleValue(3, locationDecDeg[2]); +// dataBlock.setDoubleValue(4, beacon.getDistance()); + dataBlock.setDoubleValue(4, parentSensor.getBeaconDistance(beacon)); + dataBlock.setStringValue(5, roomDesc); + + // update latest record and send event + latestRecordTime = System.currentTimeMillis(); + eventHandler.publishEvent(new SensorDataEvent(latestRecordTime, NearestBeaconOutput.this, dataBlock)); + } +} diff --git a/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/Triangulation.java b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/Triangulation.java new file mode 100644 index 00000000..b1afc3a1 --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/java/org/sensorhub/impl/sensor/blebeacon/Triangulation.java @@ -0,0 +1,762 @@ +package org.sensorhub.impl.sensor.blebeacon; + +// TODO: see if there is a better way to import this from the Trek1000 driver or if it should be made available through core +public class Triangulation { + /* Largest nonnegative number still considered zero */ + final static double MAXZERO = 0.001; + + final static int TRIL_3SPHERES = 3; + final static int TRIL_4SPHERES = 4; + + final static int ERR_TRIL_CONCENTRIC = -1; + final static int ERR_TRIL_COLINEAR_2SOLUTIONS = -2; + final static int ERR_TRIL_SQRTNEGNUMB = -3; + final static int ERR_TRIL_NOINTERSECTION_SPHERE4 = -4; + final static int ERR_TRIL_NEEDMORESPHERE = -5; + + private double mu1; + private double mu2; + private int nosolution_count; + private double best_3derror; + private double best_gdoprate; + + protected static class Vec3d + { + public double x; + public double y; + public double z; + + public Vec3d(double x, double y, double z) + { + this.x = x; + this.y = y; + this.z = z; + } + } + + /* Return the difference of two vectors */ + private Vec3d vdiff(Vec3d vector1, Vec3d vector2) + { + return new Vec3d( + vector1.x - vector2.x, + vector1.y - vector2.y, + vector1.z - vector2.z); + } + + /* Return the sum of two vectors */ + private Vec3d vsum(Vec3d vector1, Vec3d vector2) + { + return new Vec3d( + vector1.x + vector2.x, + vector1.y + vector2.y, + vector1.z + vector2.z); + } + + /* Multiply vector by a number */ + private Vec3d vmul(Vec3d vector1, double n) + { + return new Vec3d( + vector1.x * n, + vector1.y * n, + vector1.z * n); + } + + /* Divide vector by a number */ + private Vec3d vdiv(Vec3d vector1, double n) + { + return new Vec3d( + vector1.x / n, + vector1.y / n, + vector1.z / n); + } + + /* Return the Euclidean norm. */ + private double vdist(Vec3d vector1, Vec3d vector2) + { + double xd = vector1.x - vector2.x; + double yd = vector1.y - vector2.y; + double zd = vector1.z - vector2.z; + return Math.sqrt(xd * xd + yd * yd + zd * zd); + } + + /* Return the Euclidean norm. */ + private double vnorm(Vec3d vector) + { + return Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z); + } + + /* Return the dot product of two vectors. */ + private double dot(Vec3d vector1, Vec3d vector2) + { + return vector1.x * vector2.x + vector1.y * vector2.y + vector1.z * vector2.z; + } + + /* Replace vector with its cross product with another vector */ + private Vec3d cross(Vec3d vector1, Vec3d vector2) + { + return new Vec3d( + vector1.y * vector2.z - vector1.z * vector2.y, + vector1.z * vector2.x - vector1.x * vector2.z, + vector1.x * vector2.y - vector1.y * vector2.x); + } + + /* Return the GDOP (Geometric Dilution of Precision) rate between + * 0-1. Lower GDOP rate means better precision of intersection. + **/ + private double gdoprate(Vec3d tag, Vec3d p1, Vec3d p2, Vec3d p3) + { + Vec3d ex, t1, t2, t3; + double h, gdop1, gdop2, gdop3; + + ex = vdiff(p1, tag); + h = vnorm(ex); + t1 = vdiv(ex, h); + + ex = vdiff(p2, tag); + h = vnorm(ex); + t2 = vdiv(ex, h); + + ex = vdiff(p3, tag); + h = vnorm(ex); + t3 = vdiv(ex, h); + + gdop1 = Math.abs(dot(t1, t2)); + gdop2 = Math.abs(dot(t2, t3)); + gdop3 = Math.abs(dot(t3, t1)); + + return Math.max(gdop1, Math.max(gdop2, gdop3)); + } + + /* Intersecting a sphere sc with radius of r, with a line p1-p2. + * Return zero if successful, negative error otherwise. + * mu1 & mu2 are constant to find points of intersection. + **/ + private int sphereline(Vec3d p1, Vec3d p2, Vec3d sc, double r) + { + double a, b, c, bb4ac; + Vec3d dp = new Vec3d(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z); + + a = dp.x * dp.x + dp.y * dp.y + dp.z * dp.z; + b = 2 * (dp.x * (p1.x - sc.x) + dp.y * (p1.y - sc.y) + dp.z * (p1.z - sc.z)); + c = sc.x * sc.x + sc.y * sc.y + sc.z * sc.z; + c += p1.x * p1.x + p1.y * p1.y + p1.z * p1.z; + c -= 2 * (sc.x * p1.x + sc.y * p1.y + sc.z * p1.z); + c -= r * r; + + bb4ac = b * b - 4 * a * c; + + if (Math.abs(a) == 0 || bb4ac < 0) + { + mu1 = 0.0; + mu2 = 0.0; + return -1; + } + + mu1 = (-b + Math.sqrt(bb4ac)) / (2 * a); + mu2 = (-b - Math.sqrt(bb4ac)) / (2 * a); + + return 0; + } + + /* Return TRIL_3SPHERES if it is performed using 3 spheres and return + * TRIL_4SPHERES if it is performed using 4 spheres + * For TRIL_3SPHERES, there are two solutions: result1 and result 2 + * For TRIL_4SPHERES, there is only one solution: best_ solution + * + * Return negative number for other errors + * + * To force the function to work with only 3 spheres, provide a duplicate of + * any sphere at any place among p1, p2, p3, or p4. + * + * The last parameter is the largest nonnegative number considered zero; + * it is somewhat analogous to machine epsilon (but inclusive). + **/ + private int trilateration(Vec3d result1, Vec3d result2, Vec3d best_solution, + Vec3d p1, double r1, + Vec3d p2, double r2, + Vec3d p3, double r3, + Vec3d p4, double r4, + double maxzero) + { + Vec3d ex, ey, ez, t1, t2, t3; + double h, i, j, x, y, z, t, + mu = 0.0; + int result = 0; + + /*** FINDING TWO POINTS FROM THE FIRST THREE SPHERES ***/ + + // if there are at least 2 concentric spheres within the first 3 spheres + // then the calculation may not continue, drop it with error -1 + + /* h = |p3 - p1|, ex = (p3 - p1) / |p3 - p1| */ + ex = vdiff(p3, p1); // vector p13 + h = vnorm(ex); // scalar p13 + if ( h <= maxzero) + { + /* p1 and p3 are concentric, not good to obtain a precise intersection point */ + return ERR_TRIL_CONCENTRIC; + } + + /* h = |p3 - p2|, ex = (p3 - p2) / |p3 - p2| */ + ex = vdiff(p3, p2); // vector p23 + h = vnorm(ex); // scalar p23 + if ( h <= maxzero) + { + /* p2 and p3 are concentric, not good to obtain a precise intersection point */ + return ERR_TRIL_CONCENTRIC; + } + + /* h = |p2 - p1|, ex = (p2 - p1) / |p2 - p1| */ + ex = vdiff(p2, p1); // vector p12 + h = vnorm(ex); // scalar p12 + if ( h <= maxzero) + { + /* p1 and p2 are concentric, not good to obtain a precise intersection point */ + return ERR_TRIL_CONCENTRIC; + } + ex = vdiv(ex, h); // unit vector ex with respect to p1 (new coordinate system) + + /* t1 = p3 - p1, t2 = ex (ex . (p3 - p1)) */ + t1 = vdiff(p3, p1); // vector p13 + i = dot(ex, t1); // the scalar of t1 on the ex direction + t2 = vmul(ex, i); // colinear vector to p13 with the length of i + + /* ey = (t1 - t2), t = |t1 - t2| */ + ey = vdiff(t1, t2); // vector t21 perpendicular to t1 + t = vnorm(ey); // scalar t12 + if (t > maxzero) + { + /* ey = (t1 - t2) / |t1 - t2| */ + ey = vdiv(ey, t); // unit vector ey with respect to p1 (new coordinate system) + + /* j = ey . (p3 - p1) */ + j = dot(ey, t1); // scalar t1 on the ey direction + } else { + j = 0.0; + } + + /* Note: t <= maxzero implies j = 0.0 */ + if (Math.abs(j) <= maxzero) + { + /* Is point p1 + (r1 along the axis) the intersection? */ + t2 = vsum(p1, vmul(ex, r1)); + if (Math.abs(vnorm(vdiff(p2, t2)) - r2) <= maxzero && + Math.abs(vnorm(vdiff(p3, t2)) - r3) <= maxzero) + { + /* Yes, t2 is the only intersection point */ + if (result1 != null) + { + result1.x = t2.x; + result1.y = t2.y; + result1.z = t2.z; + } + if (result2 != null) + { + result2.x = t2.x; + result2.y = t2.y; + result2.z = t2.z; + } + return TRIL_3SPHERES; + } + + /* Is point p1 - (r1 along the axis) the intersection? */ + t2 = vsum(p1, vmul(ex, -r1)); + if (Math.abs(vnorm(vdiff(p2, t2)) - r2) <= maxzero && + Math.abs(vnorm(vdiff(p3, t2)) - r3) <= maxzero) + { + /* Yes, t2 is the only intersection point */ + if (result1 != null) + { + result1.x = t2.x; + result1.y = t2.y; + result1.z = t2.z; + } + if (result2 != null) + { + result2.x = t2.x; + result2.y = t2.y; + result2.z = t2.z; + } + return TRIL_3SPHERES; + } + + /* p1, p2, and p3 are colinear with more than one solution */ + return ERR_TRIL_COLINEAR_2SOLUTIONS; + } + + /* ez = ex * ev */ + ez = cross(ex, ey); // unit vector ez with respect to p1 (new coordinate system) + + x = (r1*r1 - r2*r2) / (2*h) + h /2; + y = (r1*r1 - r3*r3 + i*i) / (2*j) + j / 2 - x * i / j; + z = r1*r1 - x*x - y*y; + if ( z < -maxzero) + { + /* The solution is invalid, square root of a negative number */ + return ERR_TRIL_SQRTNEGNUMB; + } + else if (z > 0.0) + { + z = Math.sqrt(z); + } + else + { + z = 0.0; + } + + /* t2 = p1 + x ex + y ey */ + t2 = vsum(p1, vmul(ex, x)); + t2 = vsum(t2, vmul(ey, y)); + + /* result1 = p1 + x ex + y ey + z ez */ + if (result1 != null) + { + t2 = vsum(t2, vmul(ez, z)); + result1.x = t2.x; + result1.y = t2.y; + result1.z = t2.z; + } + + /* result2 = p1 + x ex + y ey - z ez */ + if (result2 != null) + { + t2 = vsum(t2, vmul(ez, -z)); + result2.x = t2.x; + result2.y = t2.y; + result2.z = t2.z; + } + + /*********** END OF FINDING TWO POINTS FROM THE FIRST THREE SPHERES **********/ + /********* RESULT1 AND RESULT2 ARE SOLUTIONS, OTHERWISE RETURN ERROR *********/ + + + /************* FINDING ONE SOLUTION BY INTRODUCING ONE MORE SPHERE ***********/ + + // check for concentricness of sphere 4 to sphere 1, 2 and 3 + // if it is concentric to one of them, then sphere 4 cannot be used + // to determine the best solution and return -1 + + /* h = |p4 - p1|, ex = (p4 - p1) / |p4 - p1| */ + ex = vdiff(p4, p1); // vector p14 + h = vnorm(ex); // scalar p14 + if (h <= maxzero) { + /* p1 and p4 are concentric, not good to obtain a precise intersection point */ + //printf("concentric14 return 0\n"); + return TRIL_3SPHERES; + } + /* h = |p4 - p2|, ex = (p4 - p2) / |p4 - p2| */ + ex = vdiff(p4, p2); // vector p24 + h = vnorm(ex); // scalar p24 + if (h <= maxzero) { + /* p2 and p4 are concentric, not good to obtain a precise intersection point */ + //printf("concentric24 return 0\n"); + return TRIL_3SPHERES; + } + /* h = |p4 - p3|, ex = (p4 - p3) / |p4 - p3| */ + ex = vdiff(p4, p3); // vector p34 + h = vnorm(ex); // scalar p34 + if (h <= maxzero) { + /* p3 and p4 are concentric, not good to obtain a precise intersection point */ + //printf("concentric34 return 0\n"); + return TRIL_3SPHERES; + } + + // if sphere 4 is not concentric to any sphere, then best solution can be obtained + /* find i as the distance of result1 to p4 */ + t3 = vdiff(result1, p4); + i = vnorm(t3); + /* find h as the distance of result2 to p4 */ + t3 = vdiff(result2, p4); + h = vnorm(t3); + + /* pick the result1 as the nearest point to the center of sphere 4 */ + if (i > h) { + best_solution.x = result1.x; + best_solution.y = result1.y; + best_solution.z = result1.z; + result1.x = result2.x; + result1.y = result2.y; + result1.z = result2.z; + result2.x = best_solution.x; + result2.y = best_solution.y; + result2.z = best_solution.z; + } + + + int count4 = 0; + double rr4 = r4; + result = 1; + /* intersect result1-result2 vector with sphere 4 */ + while(result != 0 && count4 < 10) + { + result = sphereline(result1, result2, p4, rr4); + rr4+=0.1; + count4++; + } + + if (result != 0) + { + /* No intersection between sphere 4 and the line with the gradient of result1-result2! */ + /* result1 is the closer solution to sphere 4 */ + best_solution.x = result1.x; + best_solution.y = result1.y; + best_solution.z = result1.z; + //return ERR_TRIL_NOINTERSECTION_SPHERE4; + } + else + { + if (mu1 < 0 && mu2 < 0) + { + /* if both mu1 and mu2 are less than 0 */ + /* result1-result2 line segment is outside sphere 4 with no intersection */ + mu = (Math.abs(mu1) <= Math.abs(mu2)) ? mu1 : mu2; + /* h = |result2 - result1|, ex = (result2 - result1) / |result2 - result1| */ + ex = vdiff(result2, result1); // vector result1-result2 + // TODO HERE + h = vnorm(ex); // scalar result1-result2 + ex = vdiv(ex, h); // unit vector ex with respect to result1 (new coordinate system) + /* 50-50 error correction for mu */ + mu = 0.5*mu; + /* t2 points to the intersection */ + t2 = vmul(ex, mu*h); + t2 = vsum(result1, t2); + /* the best solution = t2 */ + best_solution.x = t2.x; + best_solution.y = t2.y; + best_solution.z = t2.z; + } + else if ((mu1 < 0 && mu2 > 1) || (mu2 < 0 && mu1 > 1)) + { + /* if mu1 is less than zero and mu2 is greater than 1, or the other way around */ + /* result1-result2 line segment is inside sphere 4 with no intersection */ + mu = (mu1 > mu2) ? mu1 : mu2; + /* h = |result2 - result1|, ex = (result2 - result1) / |result2 - result1| */ + ex = vdiff(result2, result1); // vector result1-result2 + h = vnorm(ex); // scalar result1-result2 + ex = vdiv(ex, h); // unit vector ex with respect to result1 (new coordinate system) + /* t2 points to the intersection */ + t2 = vmul(ex, mu*h); + t2 = vsum(result1, t2); + /* vector t2-result2 with 50-50 error correction on the length of t3 */ + t3 = vmul(vdiff(result2, t2),0.5); + /* the best solution = t2 + t3 */ + best_solution = vsum(t2, t3); + } + else if (((mu1 > 0 && mu1 < 1) && (mu2 < 0 || mu2 > 1)) + || ((mu2 > 0 && mu2 < 1) && (mu1 < 0 || mu1 > 1))) + { + /* if one mu is between 0 to 1 and the other is not */ + /* result1-result2 line segment intersects sphere 4 at one point */ + mu = (mu1 >= 0 && mu1 <= 1) ? mu1 : mu; + /* add or subtract with 0.5*mu to distribute error equally onto every sphere */ + if (mu <= 0.5) mu-=0.5*mu; else mu-=0.5*(1-mu); + /* h = |result2 - result1|, ex = (result2 - result1) / |result2 - result1| */ + ex = vdiff(result2, result1); // vector result1-result2 + h = vnorm(ex); // scalar result1-result2 + ex = vdiv(ex, h); // unit vector ex with respect to result1 (new coordinate system) + /* t2 points to the intersection */ + t2 = vmul(ex, mu*h); + t2 = vsum(result1, t2); + /* the best solution = t2 */ + best_solution = t2; + } + else if (mu1 == mu2) + { + /* if both mu1 and mu2 are between 0 and 1, and mu1 = mu2 */ + /* result1-result2 line segment is tangential to sphere 4 at one point */ + mu = mu1; + /* add or subtract with 0.5*mu to distribute error equally onto every sphere */ + if (mu <= 0.25) mu-=0.5*mu; + else if (mu <=0.5) mu-=0.5*(0.5-mu); + else if (mu <=0.75) mu-=0.5*(mu-0.5); + else mu-=0.5*(1-mu); + /* h = |result2 - result1|, ex = (result2 - result1) / |result2 - result1| */ + ex = vdiff(result2, result1); // vector result1-result2 + h = vnorm(ex); // scalar result1-result2 + ex = vdiv(ex, h); // unit vector ex with respect to result1 (new coordinate system) + /* t2 points to the intersection */ + t2 = vmul(ex, mu*h); + t2 = vsum(result1, t2); + /* the best solution = t2 */ + best_solution = t2; + } + else + { + /* if both mu1 and mu2 are between 0 and 1 */ + /* result1-result2 line segment intersects sphere 4 at two points */ + + //return ERR_TRIL_NEEDMORESPHERE; + + mu = mu1 + mu2; + /* h = |result2 - result1|, ex = (result2 - result1) / |result2 - result1| */ + ex = vdiff(result2, result1); // vector result1-result2 + h = vnorm(ex); // scalar result1-result2 + ex = vdiv(ex, h); // unit vector ex with respect to result1 (new coordinate system) + /* 50-50 error correction for mu */ + mu = 0.5*mu; + /* t2 points to the intersection */ + t2 = vmul(ex, mu*h); + t2 = vsum(result1, t2); + /* the best solution = t2 */ + best_solution = t2; + } + } + + return TRIL_4SPHERES; + /******** END OF FINDING ONE SOLUTION BY INTRODUCING ONE MORE SPHERE *********/ + } + + /* This function calls trilateration to get the best solution. + * + * If any three spheres does not produce valid solution, + * then each distance is increased to ensure intersection to happens. + * + * Return the selected trilateration mode between TRIL_3SPHERES or TRIL_4SPHERES + * For TRIL_3SPHERES, there are two solutions: solution1 and solution2 + * For TRIL_4SPHERES, there is only one solution: best_solution + * + * nosolution_count = the number of failed attempt before intersection is found + * by increasing the sphere diameter. + **/ + int deca_3dlocate ( Vec3d solution1, Vec3d solution2, Vec3d best_solution, + Vec3d p1, double r1, + Vec3d p2, double r2, + Vec3d p3, double r3, + Vec3d p4, double r4, + int combination) + { + Vec3d o1, o2, solution, ptemp; + Vec3d solution_compare1, solution_compare2; + double /*error_3dcompare1, error_3dcompare2,*/ rtemp; + double gdoprate_compare1, gdoprate_compare2; + double ovr_r1, ovr_r2, ovr_r3, ovr_r4; + int overlook_count, combination_counter; + int trilateration_errcounter, trilateration_mode34; + int success, concentric, result; + + trilateration_errcounter = 0; + trilateration_mode34 = 0; + + combination_counter = 4; /* four spheres combination */ + + best_gdoprate = 1; /* put the worst gdoprate init */ + gdoprate_compare1 = 1; gdoprate_compare2 = 1; + solution_compare1 = new Vec3d(0, 0, 0); + o1 = new Vec3d(0, 0, 0); + o2 = new Vec3d(0, 0, 0); + solution = new Vec3d(0, 0, 0); + solution_compare1 = new Vec3d(0, 0, 0); + solution_compare2 = new Vec3d(0, 0, 0); + //error_3dcompare1 = 0; + + do + { + success = 0; + concentric = 0; + overlook_count = 0; + ovr_r1 = r1; ovr_r2 = r2; ovr_r3 = r3; ovr_r4 = r4; + + do + { + result = trilateration(o1, o2, solution, p1, ovr_r1, p2, ovr_r2, p3, ovr_r3, p4, ovr_r4, MAXZERO); + + switch (result) + { + case TRIL_3SPHERES: // 3 spheres are used to get the result + trilateration_mode34 = TRIL_3SPHERES; + success = 1; + break; + case TRIL_4SPHERES: // 4 spheres are used to get the result + trilateration_mode34 = TRIL_4SPHERES; + success = 1; + break; + case ERR_TRIL_CONCENTRIC: + concentric = 1; + break; + default: // any other return value goes here + ovr_r1 += 0.10; + ovr_r2 += 0.10; + ovr_r3 += 0.10; + ovr_r4 += 0.10; + overlook_count++; + break; + } + //qDebug() << "while(!success)" << overlook_count << concentric << "result" << result; + } while (success != 0 && (overlook_count <= 5) && concentric != 0); + + if (success != 0) + { + switch (result) + { + case TRIL_3SPHERES: + solution1.x = o1.x; + solution1.y = o1.y; + solution1.z = o1.z; + solution2.x = o2.x; + solution2.y = o2.y; + solution2.z = o2.z; + nosolution_count = overlook_count; + + combination_counter = 0; + break; + case TRIL_4SPHERES: + /* calculate the new gdop */ + gdoprate_compare1 = gdoprate(solution, p1, p2, p3); + + /* compare and swap with the better result */ + if (gdoprate_compare1 <= gdoprate_compare2) { + solution1.x = o1.x; + solution1.y = o1.y; + solution1.z = o1.z; + solution2.x = o2.x; + solution2.y = o2.y; + solution2.z = o2.z; + best_solution.x = solution.x; + best_solution.x = solution.x; + best_solution.x = solution.x; + nosolution_count = overlook_count; + best_3derror = Math.sqrt((vnorm(vdiff(solution, p1))-r1)*(vnorm(vdiff(solution, p1))-r1) + + (vnorm(vdiff(solution, p2))-r2)*(vnorm(vdiff(solution, p2))-r2) + + (vnorm(vdiff(solution, p3))-r3)*(vnorm(vdiff(solution, p3))-r3) + + (vnorm(vdiff(solution, p4))-r4)*(vnorm(vdiff(solution, p4))-r4)); + best_gdoprate = gdoprate_compare1; + + /* save the previous result */ + solution_compare2.x = solution_compare1.x; + solution_compare2.y = solution_compare1.y; + solution_compare2.z = solution_compare1.z; + //error_3dcompare2 = error_3dcompare1; + gdoprate_compare2 = gdoprate_compare1; + + combination = 5 - combination_counter; + + ptemp = p1; p1 = p2; p2 = p3; p3 = p4; p4 = ptemp; + rtemp = r1; r1 = r2; r2 = r3; r3 = r4; r4 = rtemp; + combination_counter--; + + } + break; + + default: + break; + } + } + else + { + //trilateration_errcounter++; + trilateration_errcounter = 4; + combination_counter = 0; + } + + //ptemp = p1; p1 = p2; p2 = p3; p3 = p4; p4 = ptemp; + //rtemp = r1; r1 = r2; r2 = r3; r3 = r4; r4 = rtemp; + //combination_counter--; + //qDebug() << "while(combination_counter)" << combination_counter; + + } while (combination_counter != 0); + + // if it gives error for all 4 sphere combinations then no valid result is given + // otherwise return the trilateration mode used + if (trilateration_errcounter >= 4) return -1; else return trilateration_mode34; + } + + // TODO: Test that changes made to this do not break the algorithm + public int getLocation(Vec3d best_solution, int use4thAnchor, Vec3d[] anchorArray, double[] distanceArray) + { + + Vec3d o1 = new Vec3d(0, 0, 0), o2 = new Vec3d(0, 0, 0), + p1 = new Vec3d(0, 0, 0), p2 = new Vec3d(0, 0, 0), + p3 = new Vec3d(0, 0, 0), p4 = new Vec3d(0, 0, 0); + double r1 = 0, r2 = 0, r3 = 0, r4 = 0; + int combination = 0; + int result; + + Vec3d t3; + double dist1, dist2; + + /* Anchors coordinate */ + p1.x = anchorArray[0].x; p1.y = anchorArray[0].y; p1.z = anchorArray[0].z; + p2.x = anchorArray[1].x; p2.y = anchorArray[1].y; p2.z = anchorArray[1].z; + p3.x = anchorArray[2].x; p3.y = anchorArray[2].y; p3.z = anchorArray[2].z; + p4.x = anchorArray[0].x; p4.y = anchorArray[0].y; p4.z = anchorArray[0].z; //4th same as 1st - only 3 used for trilateration + + + //Changed to use meters by default + r1 = distanceArray[0]; + r2 = distanceArray[1]; + r3 = distanceArray[2]; + r4 = distanceArray[3]; + +// r1 = (double) distanceArray[0] / 1000.0; +// r2 = (double) distanceArray[1] / 1000.0; +// r3 = (double) distanceArray[2] / 1000.0; +// r4 = (double) distanceArray[3] / 1000.0; + + //qDebug() << "GetLocation" << r1 << r2 << r3 << r4; + + //r4 = r1; + + /* get the best location using 3 or 4 spheres and keep it as know_best_location */ + result = deca_3dlocate(o1, o2, best_solution, + p1, r1, p2, r2, p3, r3, p4, r1, combination); + + + //qDebug() << "GetLocation" << result << "sol1: " << o1.x << o1.y << o1.z << " sol2: " << o2.x << o2.y << o2.z; + + if(result >= 0) + { + if (use4thAnchor == 1) //if have 4 ranging results, then use 4th anchor to pick solution closest to it + { + double diff1, diff2; + /* find dist1 as the distance of o1 to known_best_location */ + t3 = vdiff(o1, anchorArray[3]); + dist1 = vnorm(t3); + + t3 = vdiff(o2, anchorArray[3]); + dist2 = vnorm(t3); + + /* find the distance closest to received range measurement from 4th anchor */ + diff1 = Math.abs(r4 - dist1); + diff2 = Math.abs(r4 - dist2); + + /* pick the closest match to the 4th anchor range */ + if (diff1 < diff2) + { + best_solution.x = o1.x; + best_solution.y = o1.y; + best_solution.z = o1.z; + } + else + { + best_solution.x = o2.x; + best_solution.y = o2.y; + best_solution.z = o2.z; + } + } + else + { + //assume tag is below the anchors (1, 2, and 3) + if(o1.z < p1.z) + { + best_solution.x = o1.x; + best_solution.y = o1.y; + best_solution.z = o1.z; + } + else + { + best_solution.x = o2.x; + best_solution.y = o2.y; + best_solution.z = o2.z; + } + } + } + + if (result >= 0) + { + return result; + } + + //return error + return -1; + } +} diff --git a/sensorhub-android-blebeacon/src/main/res/values/strings.xml b/sensorhub-android-blebeacon/src/main/res/values/strings.xml new file mode 100644 index 00000000..78b089d9 --- /dev/null +++ b/sensorhub-android-blebeacon/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + sensorhub-android-blebeacon + diff --git a/sensorhub-android-blebeacon/src/test/java/org/sensorhub/impl/sensor/blebeacon/ExampleUnitTest.java b/sensorhub-android-blebeacon/src/test/java/org/sensorhub/impl/sensor/blebeacon/ExampleUnitTest.java new file mode 100644 index 00000000..5ee2a7eb --- /dev/null +++ b/sensorhub-android-blebeacon/src/test/java/org/sensorhub/impl/sensor/blebeacon/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package org.sensorhub.impl.sensor.blebeacon; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/sensorhub-android-lib/build.gradle b/sensorhub-android-lib/build.gradle index fbb0c756..70e2b5a7 100644 --- a/sensorhub-android-lib/build.gradle +++ b/sensorhub-android-lib/build.gradle @@ -10,16 +10,18 @@ dependencies { api project(':sensorhub-driver-android') api project(':sensorhub-android-flirone') //api project(':sensorhub-android-dji') + api project(':sensorhub-storage-h2') implementation 'org.slf4j:slf4j-android:1.6.1-RC1' + implementation('javax.xml.stream:stax-api:1.0-2') } configurations { api { // exclude stuff from APK - exclude group: "javax.servlet" +// exclude group: "javax.servlet" exclude group: "xml-apis" - exclude group: "org.eclipse.jetty" +// exclude group: "org.eclipse.jetty" exclude group: "org.n52.amused" exclude group: "ch.qos.logback" exclude group: "org.apache.felix" diff --git a/sensorhub-android-proxysensor/.gitignore b/sensorhub-android-proxysensor/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/sensorhub-android-proxysensor/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sensorhub-android-proxysensor/build.gradle b/sensorhub-android-proxysensor/build.gradle new file mode 100644 index 00000000..050001e4 --- /dev/null +++ b/sensorhub-android-proxysensor/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'com.android.library' +} + +android { + compileSdk 31 + + defaultConfig { + minSdk 21 + targetSdk 31 + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation project(path: ':sensorml-core') + implementation project(path: ':sensorhub-core') + implementation project(path: ':sensorhub-service-swe') + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} \ No newline at end of file diff --git a/sensorhub-android-proxysensor/consumer-rules.pro b/sensorhub-android-proxysensor/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-proxysensor/proguard-rules.pro b/sensorhub-android-proxysensor/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/sensorhub-android-proxysensor/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sensorhub-android-proxysensor/src/androidTest/java/org/sensorhub/impl/swe/proxysensor/ExampleInstrumentedTest.java b/sensorhub-android-proxysensor/src/androidTest/java/org/sensorhub/impl/swe/proxysensor/ExampleInstrumentedTest.java new file mode 100644 index 00000000..8db3f988 --- /dev/null +++ b/sensorhub-android-proxysensor/src/androidTest/java/org/sensorhub/impl/swe/proxysensor/ExampleInstrumentedTest.java @@ -0,0 +1,25 @@ +package org.sensorhub.impl.swe.proxysensor; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("org.sensorhub.impl.swe.proxysensor.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/sensorhub-android-proxysensor/src/main/AndroidManifest.xml b/sensorhub-android-proxysensor/src/main/AndroidManifest.xml new file mode 100644 index 00000000..bad8a116 --- /dev/null +++ b/sensorhub-android-proxysensor/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensor.java b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensor.java new file mode 100644 index 00000000..ba7c24f8 --- /dev/null +++ b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensor.java @@ -0,0 +1,206 @@ +package org.sensorhub.impl.swe.proxysensor; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +import org.sensorhub.android.SOSServiceWithIPCConfig; +import org.sensorhub.api.sensor.ISensorDataInterface; +import org.sensorhub.impl.sensor.swe.SWEVirtualSensor; + +import java.util.ArrayList; +import java.util.List; + +import net.opengis.gml.v32.AbstractFeature; +import net.opengis.sensorml.v20.AbstractPhysicalProcess; +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataChoice; +import net.opengis.swe.v20.DataComponent; + +import org.sensorhub.api.common.SensorHubException; +import org.sensorhub.impl.client.sos.SOSClient; +import org.sensorhub.impl.client.sos.SOSClient.SOSRecordListener; +import org.sensorhub.impl.client.sps.SPSClient; +import org.sensorhub.impl.sensor.AbstractSensorModule; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +import org.vast.ows.GetCapabilitiesRequest; +import org.vast.ows.OWSException; +import org.vast.ows.OWSUtils; +import org.vast.ows.sos.GetResultRequest; +import org.vast.ows.sos.SOSOfferingCapabilities; +import org.vast.ows.sos.SOSServiceCapabilities; +import org.vast.ows.sos.SOSUtils; +import org.vast.util.TimeExtent; + +public class ProxySensor extends SWEVirtualSensor { + // protected static final Logger log = LoggerFactory.getLogger(ProxySensor.class); + private static final String TAG = "OSHProxySensor"; + private static final String SOS_VERSION = "2.0"; + private static final String SPS_VERSION = "2.0"; + private static final double STREAM_END_TIME = 2e9; // + + AbstractFeature currentFoi; + List sosClients; + SPSClient spsClient; + + public static final String ACTION_PROXY = "org.sofwerx.ogc.ACTION_PROXY"; + private static final String EXTRA_PAYLOAD = "PROXY"; + private static final String EXTRA_ORIGIN = "src"; + private Context androidContext; + + public ProxySensor() { + super(); + } + + @Override + public void start() throws SensorHubException { + androidContext = ((ProxySensorConfig) config).androidContext; + + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String origin = intent.getStringExtra(EXTRA_ORIGIN); + if (!context.getPackageName().equalsIgnoreCase(origin)) { + String requestPayload = intent.getStringExtra(EXTRA_PAYLOAD); // TODO: Can be observable property string + try { + stopSOSStreams(); + } catch (SensorHubException e) { + e.printStackTrace(); + } + } + } + }; + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_PROXY); + + androidContext.registerReceiver(receiver, filter); + + Log.d(TAG, "Starting Proxy Sensor"); + + checkConfig(); + removeAllOutputs(); + removeAllControlInputs(); + OWSUtils owsUtils = new OWSUtils(); + + // create SOS clients, to be started by a different event + if (config.sosEndpointUrl != null) { + // find matching offering(s) for sensor UID + SOSServiceCapabilities caps = null; + try { + GetCapabilitiesRequest getCap = new GetCapabilitiesRequest(); + getCap.setService(SOSUtils.SOS); + getCap.setVersion(SOS_VERSION); + getCap.setGetServer(config.sosEndpointUrl); + caps = owsUtils.sendRequest(getCap, false); + } catch (OWSException e) { + throw new SensorHubException("Cannot retrieve SOS capabilities", e); + } + + // scan all offerings and connect to selected ones + int outputNum = 1; + sosClients = new ArrayList(config.observedProperties.size()); + for (SOSOfferingCapabilities offering : caps.getLayers()) { + if (offering.getMainProcedure().equals(config.sensorUID)) { + String offeringID = offering.getIdentifier(); + + for (String obsProp : config.observedProperties) { + if (offering.getObservableProperties().contains(obsProp)) { + // create data request + GetResultRequest req = new GetResultRequest(); + req.setGetServer(config.sosEndpointUrl); + req.setVersion(SOS_VERSION); + req.setOffering(offeringID); + req.getObservables().add(obsProp); + req.setTime(TimeExtent.getPeriodStartingNow(STREAM_END_TIME)); + req.setXmlWrapper(false); + + // create client and retrieve result template + SOSClient sos = new SOSClient(req, config.sosUseWebsockets); + sosClients.add(sos); + sos.retrieveStreamDescription(); + DataComponent recordDef = sos.getRecordDescription(); + if (recordDef.getName() == null) + recordDef.setName("output" + outputNum); + + // retrieve sensor description from remote SOS if available (first time only) + try { + if (outputNum == 1 && config.sensorML == null) + this.sensorDescription = (AbstractPhysicalProcess) sos.getSensorDescription(config.sensorUID); + } catch (SensorHubException e) { + Log.d(TAG, "Cannot get remote sensor description.", e); + } + + // create output + final ProxySensorOutput output = new ProxySensorOutput(this, recordDef, sos.getRecommendedEncoding()); + this.addOutput(output, false); + + // HACK TO PREVENT GETRESULT TIME ERROR +// sos.startStream((data) -> output.publishNewRecord(data)); +// try { +// Thread.sleep(1000); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// sos.stopStream(); + + outputNum++; + } + } + } + } + + if (sosClients.isEmpty()) + throw new SensorHubException("Requested observation data is not available from SOS " + config.sosEndpointUrl + + ". Check Sensor UID and observed properties have valid values."); + } + + } + + public void startSOSStreams() throws SensorHubException { + for (int i = 0; i < sosClients.size(); i++) { + String name = sosClients.get(i).getRecordDescription().getName(); + SOSRecordListener rl = new SOSRecordListener() { + @Override + public void newRecord(DataBlock data) { + ProxySensorOutput output = (ProxySensorOutput) getObservationOutputs().get(name); + output.publishNewRecord(data); + } + }; + sosClients.get(i).startStream(rl); + } + } + + public void startSOSStream(String outputName) throws SensorHubException { + for (SOSClient client : sosClients) { + if (client.getRecordDescription().getName().equals(outputName)) { + SOSRecordListener recordListener = new SOSRecordListener() { + @Override + public void newRecord(DataBlock data) { + ProxySensorOutput output = (ProxySensorOutput) getObservationOutputs().get(outputName); + output.publishNewRecord(data); + } + }; + client.startStream(recordListener); + break; + } + } + } + + public void stopSOSStreams() throws SensorHubException { + for (int i = 0; i < sosClients.size(); i++) { + sosClients.get(i).stopStream(); + } + } + + public void stopSOSStream(String outputName) throws SensorHubException { + for (SOSClient client : sosClients) { + if (client.getRecordDescription().getName().equals(outputName)) { + client.stopStream(); + break; + } + } + } +} diff --git a/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensorConfig.java b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensorConfig.java new file mode 100644 index 00000000..36910192 --- /dev/null +++ b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensorConfig.java @@ -0,0 +1,23 @@ +package org.sensorhub.impl.swe.proxysensor; + +import android.content.Context; + +import org.sensorhub.api.module.ModuleConfig; +import org.sensorhub.impl.sensor.swe.SWEVirtualSensorConfig; + +public class ProxySensorConfig extends SWEVirtualSensorConfig +{ + public transient Context androidContext; + + public ProxySensorConfig() + { + super(); + this.moduleClass = ProxySensor.class.getCanonicalName(); + } + + @Override + public ModuleConfig clone() + { + return this; // disable clone for now as it crashes Android app + } +} diff --git a/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensorDescriptor.java b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensorDescriptor.java new file mode 100644 index 00000000..d643d46b --- /dev/null +++ b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensorDescriptor.java @@ -0,0 +1,33 @@ +package org.sensorhub.impl.swe.proxysensor; + +import org.sensorhub.api.module.IModule; +import org.sensorhub.api.module.IModuleProvider; +import org.sensorhub.api.module.ModuleConfig; +import org.sensorhub.impl.module.JarModuleProvider; + +public class ProxySensorDescriptor extends JarModuleProvider implements IModuleProvider +{ + @Override + public String getModuleName() + { + return "Proxy Sensor"; + } + + @Override + public String getModuleDescription() + { + return "Altered SWEVirtualSensor with changes to event listeners implementation sending data."; + } + + @Override + public Class> getModuleClass() + { + return ProxySensor.class; + } + + @Override + public Class getModuleConfigClass() + { + return ProxySensorConfig.class; + } +} diff --git a/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensorOutput.java b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensorOutput.java new file mode 100644 index 00000000..912c6d33 --- /dev/null +++ b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/ProxySensorOutput.java @@ -0,0 +1,61 @@ +package org.sensorhub.impl.swe.proxysensor; + +import android.util.Log; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; + +import org.sensorhub.api.common.IEventListener; +import org.sensorhub.api.common.SensorHubException; +import org.sensorhub.impl.sensor.swe.SWEVirtualSensor; +import org.sensorhub.impl.sensor.swe.SWEVirtualSensorOutput; + +import static android.content.ContentValues.TAG; + +public class ProxySensorOutput extends SWEVirtualSensorOutput +{ + ProxySensor parentSensor; + DataComponent recordStructure; + DataEncoding recordEncoding; + + public ProxySensorOutput(ProxySensor sensor, DataComponent recordStructure, DataEncoding recordEncoding) { + super(sensor, recordStructure, recordEncoding); + this.parentSensor = sensor; + this.recordStructure = recordStructure; + this.recordEncoding = recordEncoding; + } + + @Override + public void publishNewRecord(DataBlock dataBlock) { + super.publishNewRecord(dataBlock); + Log.d(TAG, "publishNewRecord: "); + } + + @Override + public long getLatestRecordTime() { + return System.currentTimeMillis(); + } + + @Override + public void registerListener(IEventListener listener) + { + Log.d(TAG, "Registering Proxy Sensor Listener for: " + this.name); + try { + this.parentSensor.startSOSStream(this.name); + } catch (SensorHubException e) { + Log.d(TAG, "Error Starting Stream while registering Proxy Sensor", e); + } + eventHandler.registerListener(listener); + } + + @Override + public void unregisterListener(IEventListener listener) { + try { + this.parentSensor.stopSOSStream(this.name); + Log.d(TAG, "unregisterListener: Stopping stream: " + this.name); + } catch (SensorHubException e) { + e.printStackTrace(); + } + } +} diff --git a/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/IMqttSubscriber.java b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/IMqttSubscriber.java new file mode 100644 index 00000000..2265b953 --- /dev/null +++ b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/IMqttSubscriber.java @@ -0,0 +1,21 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2020 Botts Innovative Research, Inc. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ +package org.sensorhub.impl.swe.proxysensor.mqtt; + +public interface IMqttSubscriber { + + void subscribeToTopics(); + + void onMessage(String message); +} diff --git a/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/MqttConnectionListener.java b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/MqttConnectionListener.java new file mode 100644 index 00000000..f53edcef --- /dev/null +++ b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/MqttConnectionListener.java @@ -0,0 +1,56 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2020 Botts Innovative Research, Inc. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ +package org.sensorhub.impl.swe.proxysensor.mqtt; + +import android.util.Log; + +import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions; +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; + +public class MqttConnectionListener implements IMqttActionListener { + + private static final String TAG = "MqttConnectionListener"; + + private MqttHelper mqttHelper; + private IMqttSubscriber subscriber; + + public MqttConnectionListener(MqttHelper helper, IMqttSubscriber subscriber) { + + mqttHelper = helper; + + this.subscriber = subscriber; + } + + @Override + public void onSuccess(IMqttToken asyncActionToken) { + + Log.d(TAG, "Connected"); + + DisconnectedBufferOptions options = new DisconnectedBufferOptions(); + options.setBufferEnabled(true); + options.setBufferSize(100); + options.setPersistBuffer(false); + options.setDeleteOldestMessages(false); + mqttHelper.setDisconnectedBufferOptions(options); + + subscriber.subscribeToTopics(); + } + + @Override + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + + Log.d(TAG, "Connection failure", exception); + } +} diff --git a/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/MqttHelper.java b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/MqttHelper.java new file mode 100644 index 00000000..90b08c3c --- /dev/null +++ b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/MqttHelper.java @@ -0,0 +1,156 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2020 Botts Innovative Research, Inc. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ +package org.sensorhub.impl.swe.proxysensor.mqtt; + +import android.content.Context; +import android.util.Log; + +import org.eclipse.paho.android.service.MqttAndroidClient; +import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions; +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; + +/** + *

+ * Android MQTT wrapper + *

+ * + * @author Nick Garay + * @since Jan 10, 2020 + */ +public class MqttHelper { + + private final String TAG = "MqttHelper"; + private MqttAndroidClient client; + + public MqttHelper() { + } + + public IMqttToken connect(Context context, String url) { + + IMqttToken connectionToken = null; + + String clientId = MqttClient.generateClientId(); + + client = new MqttAndroidClient(context, url, clientId); + + try { + + MqttConnectOptions options = new MqttConnectOptions(); + options.setConnectionTimeout(3000); + options.setKeepAliveInterval(300); + options.setAutomaticReconnect(true); + options.setCleanSession(false); + + connectionToken = client.connect(options); + + }catch(Exception e) { + + Log.d(TAG, "Connection failure", e); + } + + return connectionToken; + } + + public IMqttToken connect(Context context, String username, String password, String url) { + + IMqttToken connectionToken = null; + + String clientId = MqttClient.generateClientId(); + + client = new MqttAndroidClient(context, url, clientId); + + try { + + MqttConnectOptions options = new MqttConnectOptions(); + options.setConnectionTimeout(3000); + options.setKeepAliveInterval(300); + options.setAutomaticReconnect(true); + options.setCleanSession(false); + options.setUserName(username); + options.setPassword(password.toCharArray()); + + connectionToken = client.connect(options); + + }catch(Exception e) { + + Log.d(TAG, "Connection failure", e); + } + + return connectionToken; + } + + public void setDisconnectedBufferOptions(DisconnectedBufferOptions options) { + + client.setBufferOpts(options); + } + + public IMqttToken subscribe(String topic, MqttMessageHandler listener) { + + IMqttToken subscribeToken = null; + + try { + + subscribeToken = client.subscribe(topic, 0, null, listener, listener); + + } catch (MqttException ex) { + + Log.d(TAG, "Subscribe fail", ex); + } + + return subscribeToken; + } + + public void unsubscribe(String topic) { + + try { + + client.unsubscribe(topic); + + } catch (MqttException ex) { + + Log.d(TAG, "Unsubscribe fail", ex); + } + } + + public void disconnect() { + + try { + + client.disconnect().setActionCallback(new IMqttActionListener() { + + @Override + public void onSuccess(IMqttToken asyncActionToken) { + + Log.d(TAG, "Disconnected"); + client.unregisterResources(); + client.close(); + } + + @Override + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + + Log.d(TAG, "Failed to disconnect", exception); + } + }); + + } catch(Exception e) { + + Log.d(TAG, "Failed to disconnect", e); + } + } +} diff --git a/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/MqttMessageHandler.java b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/MqttMessageHandler.java new file mode 100644 index 00000000..547deb90 --- /dev/null +++ b/sensorhub-android-proxysensor/src/main/java/org/sensorhub/impl/swe/proxysensor/mqtt/MqttMessageHandler.java @@ -0,0 +1,68 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + Copyright (C) 2020 Botts Innovative Research, Inc. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ +package org.sensorhub.impl.swe.proxysensor.mqtt; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +public class MqttMessageHandler extends Handler implements IMqttActionListener, IMqttMessageListener { + + private static final String TAG = "MqttMessageHandler"; + + private IMqttSubscriber subscriber; + + public MqttMessageHandler(IMqttSubscriber subscriber) { + + super(); + + this.subscriber = subscriber; + } + + @Override + public void onSuccess(IMqttToken asyncActionToken) { + + Log.d(TAG,"Subscribed - Topic: " + asyncActionToken.getTopics().toString()); + } + + @Override + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + + Log.d(TAG, "Subscribe fail", exception); + } + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + + Log.d(TAG, "Topic: " + topic); + Bundle bundle = new Bundle(); + bundle.putString("mqtt-message", message.toString()); + Message msg = new Message(); + msg.setData(bundle); + this.sendMessage(msg); + } + + @Override + public void handleMessage(Message message) { + + String mqttMessage = message.getData().getString("mqtt-message"); + subscriber.onMessage(mqttMessage); + } +} diff --git a/sensorhub-android-proxysensor/src/test/java/org/sensorhub/impl/swe/proxysensor/ExampleUnitTest.java b/sensorhub-android-proxysensor/src/test/java/org/sensorhub/impl/swe/proxysensor/ExampleUnitTest.java new file mode 100644 index 00000000..1bd8a253 --- /dev/null +++ b/sensorhub-android-proxysensor/src/test/java/org/sensorhub/impl/swe/proxysensor/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package org.sensorhub.impl.swe.proxysensor; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java index c46056d7..d73ae5f2 100644 --- a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java +++ b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java @@ -26,12 +26,14 @@ import android.view.SurfaceView; import android.view.WindowManager; import org.sensorhub.api.common.IEventListener; +import org.sensorhub.api.common.SensorHubException; import org.sensorhub.api.module.IModuleConfigRepository; import org.sensorhub.api.module.ModuleConfig; import org.sensorhub.impl.SensorHub; import org.sensorhub.impl.SensorHubConfig; import org.sensorhub.impl.common.EventBus; import org.sensorhub.impl.module.ModuleRegistry; +import org.sensorhub.impl.service.HttpServer; import org.vast.xml.XMLImplFinder; import android.app.Service; import android.content.Intent; @@ -133,6 +135,15 @@ public void run() { sensorhub.stop(); SensorHub.clearInstance(); sensorhub = null; + + // Make sure the server gets cleaned up + try { + if (HttpServer.getInstance() != null) { + HttpServer.getInstance().cleanup(); + } + } catch (SensorHubException e) { + e.printStackTrace(); + } } }); } diff --git a/sensorhub-driver-android/AndroidManifest.xml b/sensorhub-driver-android/AndroidManifest.xml index a11d911f..e9a28838 100644 --- a/sensorhub-driver-android/AndroidManifest.xml +++ b/sensorhub-driver-android/AndroidManifest.xml @@ -2,4 +2,7 @@ package="org.sensorhub.impl.sensor.android" android:versionCode="1" android:versionName="1.0" > + + + diff --git a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidSensorsConfig.java b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidSensorsConfig.java index eb505e95..7c2558b2 100644 --- a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidSensorsConfig.java +++ b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidSensorsConfig.java @@ -17,6 +17,7 @@ import android.graphics.SurfaceTexture; import org.sensorhub.api.module.ModuleConfig; import org.sensorhub.api.sensor.SensorConfig; +import org.sensorhub.impl.sensor.android.audio.AudioEncoderConfig; import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig; import android.content.Context; @@ -39,16 +40,21 @@ public class AndroidSensorsConfig extends SensorConfig public boolean activateOrientationEuler = true; public boolean activateGpsLocation = true; public boolean activateNetworkLocation = false; + public int selectedCameraId = 0; public boolean activateBackCamera = false; public boolean activateFrontCamera = false; + public boolean enableCamera = false; public VideoEncoderConfig videoConfig = new VideoEncoderConfig(); public boolean outputVideoRoll = false; - + public boolean activateMicAudio = false; + public AudioEncoderConfig audioConfig = new AudioEncoderConfig(); + + public String deviceName; public String runName; public String runDescription; - - + + public AndroidSensorsConfig() { this.moduleClass = AndroidSensorsDriver.class.getCanonicalName(); diff --git a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidSensorsDriver.java b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidSensorsDriver.java index d1e1e4dc..fe4c8215 100644 --- a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidSensorsDriver.java +++ b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidSensorsDriver.java @@ -37,6 +37,9 @@ import org.sensorhub.api.sensor.ISensorDataInterface; import org.sensorhub.api.sensor.SensorException; import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.android.audio.AndroidAudioOutputAAC; +import org.sensorhub.impl.sensor.android.audio.AndroidAudioOutputOPUS; +import org.sensorhub.impl.sensor.android.audio.AudioEncoderConfig; import org.sensorhub.impl.sensor.android.video.AndroidCameraOutputH264; import org.sensorhub.impl.sensor.android.video.AndroidCameraOutputH265; import org.sensorhub.impl.sensor.android.video.AndroidCameraOutputMJPEG; @@ -141,6 +144,9 @@ public synchronized void init() throws SensorHubException if (androidContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) createCameraOutputs(androidContext); + // create data interfaces for audio + if (androidContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MICROPHONE)) + createAudioOutputs(androidContext); } @@ -160,6 +166,8 @@ public void start() throws SensorException @SuppressWarnings("deprecation") protected void createCameraOutputs(Context androidContext) throws SensorException { + int selectedCameraId = config.selectedCameraId; + /*if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.LOLLIPOP) { CameraManager cameraManager = (CameraManager)androidContext.getSystemService(Context.CAMERA_SERVICE); @@ -185,34 +193,37 @@ protected void createCameraOutputs(Context androidContext) throws SensorExceptio } else*/ { + // TODO: check if the rotation issue may actually stem from here for (int cameraId = 0; cameraId < android.hardware.Camera.getNumberOfCameras(); cameraId++) { android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo(); - android.hardware.Camera.getCameraInfo(cameraId, info); +// android.hardware.Camera.getCameraInfo(cameraId, info); + android.hardware.Camera.getCameraInfo(selectedCameraId, info); - if ( (info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK && config.activateBackCamera) || - (info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT && config.activateFrontCamera)) + /*if ( (info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK && config.activateBackCamera) || + (info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT && config.activateFrontCamera))*/ + if(config.enableCamera) { SurfaceTexture camPreviewTexture = SensorHubService.getVideoTexture(); if (VideoEncoderConfig.JPEG_CODEC.equals(config.videoConfig.codec)) { - useCamera(new AndroidCameraOutputMJPEG(this, cameraId, camPreviewTexture), cameraId); + useCamera(new AndroidCameraOutputMJPEG(this, selectedCameraId, camPreviewTexture), selectedCameraId); break; } else if (VideoEncoderConfig.H264_CODEC.equals(config.videoConfig.codec)) { - useCamera(new AndroidCameraOutputH264(this, cameraId, camPreviewTexture), cameraId); + useCamera(new AndroidCameraOutputH264(this, selectedCameraId, camPreviewTexture), selectedCameraId); // try a break to test break; } else if (VideoEncoderConfig.H265_CODEC.equals(config.videoConfig.codec)) { - useCamera(new AndroidCameraOutputH265(this, cameraId, camPreviewTexture), cameraId); + useCamera(new AndroidCameraOutputH265(this, selectedCameraId, camPreviewTexture), selectedCameraId); break; } else if (VideoEncoderConfig.VP9_CODEC.equals(config.videoConfig.codec)) { - useCamera(new AndroidCameraOutputVP9(this, cameraId, camPreviewTexture), cameraId); + useCamera(new AndroidCameraOutputVP9(this, selectedCameraId, camPreviewTexture), selectedCameraId); break; } else if (VideoEncoderConfig.VP8_CODEC.equals(config.videoConfig.codec)) { - useCamera(new AndroidCameraOutputVP8(this, cameraId, camPreviewTexture), cameraId); + useCamera(new AndroidCameraOutputVP8(this, selectedCameraId, camPreviewTexture), selectedCameraId); break; } else @@ -223,6 +234,19 @@ else if (VideoEncoderConfig.VP8_CODEC.equals(config.videoConfig.codec)) { } + protected void createAudioOutputs(Context androidContext) throws SensorException + { + if(config.activateMicAudio) { + if (AudioEncoderConfig.AAC_CODEC.equals(config.audioConfig.codec)) + useAudio(new AndroidAudioOutputAAC(this), "MIC"); + else if (AudioEncoderConfig.OPUS_CODEC.equals(config.audioConfig.codec)) + useAudio(new AndroidAudioOutputOPUS(this), "MIC"); + else + throw new SensorException("Unsupported codec " + config.audioConfig.codec); + } + } + + protected void useSensor(ISensorDataInterface output, Sensor sensor) { addOutput(output, false); @@ -255,6 +279,14 @@ protected void useCamera2(ISensorDataInterface output, String cameraId) } + protected void useAudio(ISensorDataInterface output, String srcName) + { + addOutput(output, false); + smlComponents.add(smlBuilder.getAudioComponentDescription(srcName)); + log.info("Getting data from audio source " + srcName); + } + + @Override public void stop() throws SensorException { diff --git a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/SensorMLBuilder.java b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/SensorMLBuilder.java index 47e2db2e..23f8673c 100644 --- a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/SensorMLBuilder.java +++ b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/SensorMLBuilder.java @@ -1,24 +1,24 @@ -/***************************** BEGIN LICENSE BLOCK *************************** - -The contents of this file are subject to the Mozilla Public License, v. 2.0. -If a copy of the MPL was not distributed with this file, You can obtain one -at http://mozilla.org/MPL/2.0/. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -for the specific language governing rights and limitations under the License. - -Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. - -******************************* END LICENSE BLOCK ***************************/ - +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + package org.sensorhub.impl.sensor.android; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.vast.data.SWEFactory; import org.vast.sensorML.SMLFactory; -import net.opengis.sensorml.v20.PhysicalComponent; +import net.opengis.sensorml.v20.PhysicalComponent; import android.hardware.Sensor; import android.hardware.SensorManager; import android.location.LocationManager; @@ -93,18 +93,27 @@ else if (val instanceof Object[]) }*/ return comp; - } - - - /* - * Version to support older camera API on devices with SDK < 21 - */ - public PhysicalComponent getComponentDescription(int cameraId) - { - PhysicalComponent comp = smlFac.newPhysicalComponent(); - comp.setId("CAM_" + cameraId); - comp.setName("Android Camera #" + cameraId); - return comp; + } + + + /* + * Version to support older camera API on devices with SDK < 21 + */ + public PhysicalComponent getComponentDescription(int cameraId) + { + PhysicalComponent comp = smlFac.newPhysicalComponent(); + comp.setId("CAM_" + cameraId); + comp.setName("Android Camera #" + cameraId); + return comp; + } + + + public PhysicalComponent getAudioComponentDescription(String audioSrcName) + { + PhysicalComponent comp = smlFac.newPhysicalComponent(); + comp.setId("AUDIO_" + audioSrcName); + comp.setName("Android Audio " + audioSrcName); + return comp; } diff --git a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AndroidAudioOutput.java b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AndroidAudioOutput.java new file mode 100644 index 00000000..ea0f6c48 --- /dev/null +++ b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AndroidAudioOutput.java @@ -0,0 +1,400 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.android.audio; + +import android.graphics.ImageFormat; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.Parameters; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaRecorder; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; + +import net.opengis.swe.v20.BinaryBlock; +import net.opengis.swe.v20.BinaryComponent; +import net.opengis.swe.v20.BinaryEncoding; +import net.opengis.swe.v20.ByteEncoding; +import net.opengis.swe.v20.ByteOrder; +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; +import net.opengis.swe.v20.DataStream; +import net.opengis.swe.v20.DataType; +import net.opengis.swe.v20.Quantity; + +import org.sensorhub.algo.vecmath.Vect3d; +import org.sensorhub.api.sensor.SensorDataEvent; +import org.sensorhub.api.sensor.SensorException; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.sensorhub.impl.sensor.android.AndroidSensorsDriver; +import org.sensorhub.impl.sensor.android.IAndroidOutput; +import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig; +import org.sensorhub.impl.sensor.videocam.VideoCamHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.cdm.common.CDMException; +import org.vast.data.AbstractDataBlock; +import org.vast.data.DataBlockMixed; +import org.vast.swe.SWEConstants; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; + + +/** + *

+ * Implementation of data interface capturing audio data using Android + * AudioRecord API. See https://www.codota.com/code/java/classes/android.media.AudioRecord + *

+ * + * @author Alex Robin + * @since Mar 22, 2021 + */ +@SuppressWarnings("deprecation") +public abstract class AndroidAudioOutput extends AbstractSensorOutput implements IAndroidOutput +{ + // keep logger name short because in LogCat it's max 23 chars + static final Logger log = LoggerFactory.getLogger(AndroidAudioOutput.class.getSimpleName()); + + DataComponent dataStruct; + DataEncoding dataEncoding; + int sampleRateHz = 11025; + int pcmEncoding = AudioFormat.ENCODING_PCM_16BIT; + int numSamplesPerRecord; + int bytesPerSample; + int bytesPerRecord; + int bitrate = 64 * 1000; // bits/s + + Looper bgLooper; + AudioRecord audioRecord; + long systemTimeOffset = -1L; + long lastRecordTime = 0; + byte[] codecInfoData; + MediaCodec mCodec; + BufferInfo bufferInfo = new BufferInfo(); + int packetHeaderSize = 0; + + protected abstract String getCodecName(); + + protected abstract void initCodec(int inputBufferSize) throws SensorException; + + protected abstract void addPacketHeader(byte[] packet, int packetLen); + + + protected AndroidAudioOutput(AndroidSensorsDriver parentModule, String name) throws SensorException + { + super(name, parentModule); + + // init audio recorder and codec + initAudio(); + initCodec(bytesPerRecord); + initOutputStructure(); + } + + + protected void initOutputStructure() + { + // create SWE Common data structure and encoding + SWEHelper swe = new SWEHelper(); + String numSamplesId = "NUM_SAMPLES"; + dataStruct = swe.createRecord() + .name(getName()) + .definition(SWEHelper.getPropertyUri("AudioFrame")) + .addField("time", swe.createTime().asPhenomenonTimeIsoUTC().build()) + .addField("sampleRate", swe.createQuantity() + .label("Sample Rate") + .description("Number of audio samples per second") + .definition(SWEHelper.getQudtUri("DataRate")) + .uomCode("Hz")) + .addField("numSamples", swe.createCount() + .id(numSamplesId) + .label("Num Samples") + .description("Number of audio samples packaged in this record")) + .addField("samples", swe.createArray() + .withVariableSize(numSamplesId) + .withElement("sample", swe.createCount() + .label("Audio Sample") + .definition(SWEConstants.DEF_DN) + .dataType(DataType.SHORT)) + .build()) + .build(); + + ////////////////////////// + // binary encoding spec // + ////////////////////////// + BinaryEncoding dataEnc = swe.newBinaryEncoding(); + dataEnc.setByteEncoding(ByteEncoding.RAW); + dataEnc.setByteOrder(ByteOrder.BIG_ENDIAN); + BinaryComponent comp; + + // time stamp + comp = swe.newBinaryComponent(); + comp.setRef("/" + dataStruct.getComponent(0).getName()); + comp.setCdmDataType(DataType.DOUBLE); + dataEnc.addMemberAsComponent(comp); + + // sample rate + comp = swe.newBinaryComponent(); + comp.setRef("/" + dataStruct.getComponent(1).getName()); + comp.setCdmDataType(DataType.INT); + dataEnc.addMemberAsComponent(comp); + + // num samples + comp = swe.newBinaryComponent(); + comp.setRef("/" + dataStruct.getComponent(2).getName()); + comp.setCdmDataType(DataType.INT); + dataEnc.addMemberAsComponent(comp); + + // audio codec + BinaryBlock compressedBlock = swe.newBinaryBlock(); + compressedBlock.setRef("/" + dataStruct.getComponent(3).getName()); + compressedBlock.setCompression(getCodecName()); + dataEnc.addMemberAsBlock(compressedBlock); + + try + { + SWEHelper.assignBinaryEncoding(dataStruct, dataEnc); + } + catch (CDMException e) + { + throw new RuntimeException("Invalid binary encoding configuration", e); + }; + + this.dataEncoding = dataEnc; + } + + + protected void initAudio() throws SensorException + { + AudioEncoderConfig audioConfig = parentSensor.getConfiguration().audioConfig; + sampleRateHz = audioConfig.sampleRate; + bitrate = audioConfig.bitRate * 1000; + + if (numSamplesPerRecord <= 0) + numSamplesPerRecord = (int)(sampleRateHz/10); + bytesPerSample = (pcmEncoding == AudioFormat.ENCODING_PCM_8BIT) ? 1 : 2; + bytesPerRecord = numSamplesPerRecord*bytesPerSample; + + // create audio recorder object + // we use an internal buffer twice as big as the read buffer + // so we have time to compress a chunk while we're recording the next one + audioRecord = new AudioRecord( + MediaRecorder.AudioSource.MIC, + sampleRateHz, + AudioFormat.CHANNEL_IN_MONO, + pcmEncoding, + bytesPerRecord*2); + } + + + @Override + public void start(Handler eventHandler) throws SensorException + { + try + { + // start codec + if (mCodec != null) + mCodec.start(); + + // reset time + lastRecordTime = 0; + systemTimeOffset = -1; + + // read audio samples in background thread + Thread bgThread = new Thread() { + public void run() + { + try + { + byte[] buffer = new byte[numSamplesPerRecord*2]; // 16 bits samples + audioRecord.startRecording(); + + while (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) { + readAndEncodeAudioData(); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + }; + bgThread.start(); + } + catch (Exception e) + { + throw new SensorException("Cannot start codec " + mCodec.getName(), e); + } + } + + + private void readAndEncodeAudioData() + { + // get next available buffer from codec + int inputBufferIndex = mCodec.dequeueInputBuffer(0); + if (inputBufferIndex >= 0) + { + ByteBuffer inputBuffer = mCodec.getInputBuffer(inputBufferIndex); + inputBuffer.clear(); + + int readBytes = audioRecord.read(inputBuffer, bytesPerRecord); + log.debug(lastRecordTime + ": " + readBytes + " audio bytes read"); + mCodec.queueInputBuffer(inputBufferIndex, 0, readBytes, lastRecordTime, 0); + lastRecordTime += (numSamplesPerRecord * 1000000) / sampleRateHz; // in µs + + encode(); + } + } + + + private void encode() + { + int outputBufferIndex; + + do { + outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, 0); + if (outputBufferIndex >= 0) { + ByteBuffer outBuffer = mCodec.getOutputBuffer(outputBufferIndex); + int outDataSize = outBuffer.remaining(); + log.debug(bufferInfo.presentationTimeUs + ": " + outDataSize + " compressed audio bytes"); + int outDataOffset = packetHeaderSize; + + // create buffer to hold packet header + data + byte[] outData = new byte[outDataSize + outDataOffset]; + + // copy encoded data + outBuffer.get(outData, outDataOffset, outDataSize); + addPacketHeader(outData, outDataSize); + + // release output buffer + mCodec.releaseOutputBuffer(outputBufferIndex, false); + + // send it to output (but skip codec config bytes) + if (bufferInfo.flags != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) + sendCompressedData(bufferInfo.presentationTimeUs, outData); + } + } + while (outputBufferIndex >= 0); + } + + + protected void sendCompressedData(long timeStamp, byte[] compressedData) + { + // generate new data record + DataBlock newRecord; + if (latestRecord == null) + newRecord = dataStruct.createDataBlock(); + else + newRecord = latestRecord.renew(); + + // set time stamp + int idx = 0; + double samplingTime = getJulianTimeStamp(timeStamp); + newRecord.setDoubleValue(idx++, samplingTime); + newRecord.setIntValue(idx++, sampleRateHz); + newRecord.setIntValue(idx++, numSamplesPerRecord); + + // set encoded data + AbstractDataBlock audioData = ((DataBlockMixed) newRecord).getUnderlyingObject()[idx++]; + audioData.setUnderlyingObject(compressedData); + + // send event + latestRecord = newRecord; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publishEvent(new SensorDataEvent(latestRecordTime, this, latestRecord)); + } + + + @Override + public void stop() + { + if (audioRecord != null) + { + audioRecord.stop(); + audioRecord = null; + } + + if (mCodec != null) + { + mCodec.stop(); + mCodec.release(); + } + + if (bgLooper != null) + { + bgLooper.quit(); + bgLooper = null; + } + } + + + @Override + public double getAverageSamplingPeriod() + { + return numSamplesPerRecord/(double)sampleRateHz; + } + + + @Override + public DataComponent getRecordDescription() + { + return dataStruct; + } + + + @Override + public DataEncoding getRecommendedEncoding() + { + return dataEncoding; + } + + + @Override + public DataBlock getLatestRecord() + { + return latestRecord; + } + + + @Override + public long getLatestRecordTime() + { + return latestRecordTime; + } + + + protected double getJulianTimeStamp(long sensorTimeStampUs) + { + long sensorTimeMillis = sensorTimeStampUs / 1000; + + if (systemTimeOffset < 0) + systemTimeOffset = System.currentTimeMillis() - sensorTimeMillis; + + return (systemTimeOffset + sensorTimeMillis) / 1000.; + } +} diff --git a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AndroidAudioOutputAAC.java b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AndroidAudioOutputAAC.java new file mode 100644 index 00000000..3772f617 --- /dev/null +++ b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AndroidAudioOutputAAC.java @@ -0,0 +1,115 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.android.audio; + +import android.graphics.SurfaceTexture; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; + +import org.sensorhub.api.sensor.SensorException; +import org.sensorhub.impl.sensor.android.AndroidSensorsDriver; + + +/** + *

+ * Implementation of data interface capturing audio data and compressing it + * using AAC codec. + *

+ * + * @author Alex Robin + * @since March 22, 2021 + */ +@SuppressWarnings("deprecation") +public class AndroidAudioOutputAAC extends AndroidAudioOutput +{ + private static final String CODEC_NAME = "AAC"; + private static int[] ADTS_FREQ_TABLE = { 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350 }; + + int adtsFreqIdx; + + + public AndroidAudioOutputAAC(AndroidSensorsDriver parentModule) throws SensorException + { + super(parentModule, "audio" + "_" + CODEC_NAME); + } + + @Override + protected String getCodecName() + { + return CODEC_NAME; + } + + @Override + protected void initCodec(int inputBufferSize) throws SensorException + { + try { + final String audioCodec = MediaFormat.MIMETYPE_AUDIO_AAC; + mCodec = MediaCodec.createEncoderByType(audioCodec); + MediaFormat mediaFormat = MediaFormat.createAudioFormat(audioCodec, sampleRateHz, 1); + mediaFormat.setInteger("pcm-encoding", pcmEncoding); + mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, inputBufferSize); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); + mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); + mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); + mCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + log.debug("MediaCodec initialized"); + } + catch (Exception e) + { + throw new SensorException("Cannot initialize codec " + mCodec.getName(), e); + } + } + + + @Override + protected void initAudio() throws SensorException + { + // ADTS always includes 1024 samples per packet so align record size to that + this.numSamplesPerRecord = 1024; + super.initAudio(); + + // set ADTS packet header size + packetHeaderSize = 7; + + // compute sample rate index to be used in ADTS headers + adtsFreqIdx = -1; + for (int i = 0; i < ADTS_FREQ_TABLE.length; i++) + { + if (ADTS_FREQ_TABLE[i] == sampleRateHz) + adtsFreqIdx = i; + } + if (adtsFreqIdx < 0) + throw new SensorException("Unsupported sample rate for AAC: " + sampleRateHz); + } + + + protected void addPacketHeader(byte[] packet, int dataLen) + { + int profile = 2; // AAC LC + int freqIdx = adtsFreqIdx; + int chanCfg = 1; // mono + int frameLen = dataLen + packetHeaderSize; + + // fill in ADTS data + packet[0] = (byte)0xFF; + packet[1] = (byte)0xF1; + packet[2] = (byte)(((profile - 1) << 6) + (freqIdx << 2) +(chanCfg >> 2)); + packet[3] = (byte)(((chanCfg & 3) << 6) + (frameLen >> 11)); + packet[4] = (byte)((frameLen & 0x7FF) >> 3); + packet[5] = (byte)(((frameLen & 7) << 5) + 0x1F); + packet[6] = (byte)0xFC; + } +} diff --git a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AndroidAudioOutputOPUS.java b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AndroidAudioOutputOPUS.java new file mode 100644 index 00000000..588e3e8b --- /dev/null +++ b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AndroidAudioOutputOPUS.java @@ -0,0 +1,87 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.android.audio; + +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; + +import org.sensorhub.api.sensor.SensorException; +import org.sensorhub.impl.sensor.android.AndroidSensorsDriver; + + +/** + *

+ * Implementation of data interface capturing audio data and compressing it + * using the Opus codec. + *

+ * + * @author Alex Robin + * @since March 29, 2021 + */ +@SuppressWarnings("deprecation") +public class AndroidAudioOutputOPUS extends AndroidAudioOutput +{ + private static final String CODEC_NAME = "OPUS"; + + int adtsFreqIdx; + + + public AndroidAudioOutputOPUS(AndroidSensorsDriver parentModule) throws SensorException + { + super(parentModule, "audio" + "_" + CODEC_NAME); + } + + @Override + protected String getCodecName() + { + return CODEC_NAME; + } + + @Override + protected void initCodec(int inputBufferSize) throws SensorException + { + try { + final String audioCodec = MediaFormat.MIMETYPE_AUDIO_OPUS; + mCodec = MediaCodec.createEncoderByType(audioCodec); + MediaFormat mediaFormat = MediaFormat.createAudioFormat(audioCodec, sampleRateHz, 1); + mediaFormat.setInteger("pcm-encoding", pcmEncoding); + mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, inputBufferSize); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); + mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); + mCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + log.debug("MediaCodec initialized"); + } + catch (Exception e) + { + throw new SensorException("Cannot initialize codec " + mCodec.getName(), e); + } + } + + + @Override + protected void initAudio() throws SensorException + { + super.initAudio(); + + // no header for opus + packetHeaderSize = 0; + } + + + protected void addPacketHeader(byte[] packet, int dataLen) + { + } +} diff --git a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AudioEncoderConfig.java b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AudioEncoderConfig.java new file mode 100644 index 00000000..7525b4a5 --- /dev/null +++ b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/audio/AudioEncoderConfig.java @@ -0,0 +1,40 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.android.audio; + + +/** + *

+ * Configuration of audio encoding parameters + *

+ * + * @author Alex Robin + * @since Mar 22, 2021 + */ +public class AudioEncoderConfig +{ + public final static String AAC_CODEC = "AAC"; + public final static String AMRNB_CODEC = "AMR-NB"; + public final static String AMRWB_CODEC = "AMR-WB"; + public final static String FLAC_CODEC = "FLAC"; + public final static String OPUS_CODEC = "OPUS"; + public final static String VORBIS_CODEC = "VORBIS"; + public final static String PCM_CODEC = "PCM"; + + + public String codec = AAC_CODEC; + public int sampleRate = 8000; // Hz + public int bitRate = 64; // kbits/sec +} diff --git a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/video/AndroidCameraOutput.java b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/video/AndroidCameraOutput.java index c8240a54..a0c8ef13 100644 --- a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/video/AndroidCameraOutput.java +++ b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/video/AndroidCameraOutput.java @@ -88,7 +88,6 @@ public abstract class AndroidCameraOutput extends AbstractSensorOutput } } +include ':sensorhub-android-proxysensor'