/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.googlecode.android_scripting.facade; import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Bundle; import com.googlecode.android_scripting.jsonrpc.RpcReceiver; import com.googlecode.android_scripting.rpc.Rpc; import com.googlecode.android_scripting.rpc.RpcDefault; import com.googlecode.android_scripting.rpc.RpcDeprecated; import com.googlecode.android_scripting.rpc.RpcParameter; import com.googlecode.android_scripting.rpc.RpcStartEvent; import com.googlecode.android_scripting.rpc.RpcStopEvent; import java.util.Arrays; import java.util.List; /** * Exposes the SensorManager related functionality.
*
* Guidance notes
* For reasons of economy the sensors on smart phones are usually low cost and, therefore, low * accuracy (usually represented by 10 bit data). The floating point data values obtained from * sensor readings have up to 16 decimal places, the majority of which are noise. On many phones the * accelerometer is limited (by the phone manufacturer) to a maximum reading of 2g. The magnetometer * (which also provides orientation readings) is strongly affected by the presence of ferrous metals * and can give large errors in vehicles, on board ship etc. * * Following a startSensingTimed(A,B) api call sensor events are entered into the Event Queue (see * EventFacade). For the A parameter: 1 = All Sensors, 2 = Accelerometer, 3 = Magnetometer and 4 = * Light. The B parameter is the minimum delay between recordings in milliseconds. To avoid * duplicate readings the minimum delay should be 20 milliseconds. The light sensor will probably be * much slower (taking about 1 second to register a change in light level). Note that if the light * level is constant no sensor events will be registered by the light sensor. * * Following a startSensingThreshold(A,B,C) api call sensor events greater than a given threshold * are entered into the Event Queue. For the A parameter: 1 = Orientation, 2 = Accelerometer, 3 = * Magnetometer and 4 = Light. The B parameter is the integer value of the required threshold level. * For orientation sensing the integer threshold value is in milliradians. Since orientation events * can exceed the threshold value for long periods only crossing and return events are recorded. The * C parameter is the required axis (XYZ) of the sensor: 0 = No axis, 1 = X, 2 = Y, 3 = X+Y, 4 = Z, * 5= X+Z, 6 = Y+Z, 7 = X+Y+Z. For orientation X = azimuth, Y = pitch and Z = roll.
* *
* Example (python) * *
 * import android, time
 * droid = android.Android()
 * droid.startSensingTimed(1, 250)
 * time.sleep(1)
 * s1 = droid.readSensors().result
 * s2 = droid.sensorsGetAccuracy().result
 * s3 = droid.sensorsGetLight().result
 * s4 = droid.sensorsReadAccelerometer().result
 * s5 = droid.sensorsReadMagnetometer().result
 * s6 = droid.sensorsReadOrientation().result
 * droid.stopSensing()
 * 
* * Returns:
* s1 = {u'accuracy': 3, u'pitch': -0.47323511242866517, u'xmag': 1.75, u'azimuth': * -0.26701245009899138, u'zforce': 8.4718560000000007, u'yforce': 4.2495484000000001, u'time': * 1297160391.2820001, u'ymag': -8.9375, u'zmag': -41.0625, u'roll': -0.031366908922791481, * u'xforce': 0.23154590999999999}
* s2 = 3 (Highest accuracy)
* s3 = None ---(not available on many phones)
* s4 = [0.23154590999999999, 4.2495484000000001, 8.4718560000000007] ----(x, y, z accelerations)
* s5 = [1.75, -8.9375, -41.0625] -----(x, y, z magnetic readings)
* s6 = [-0.26701245009899138, -0.47323511242866517, -0.031366908922791481] ---(azimuth, pitch, roll * in radians)
* */ public class SensorManagerFacade extends RpcReceiver { private final EventFacade mEventFacade; private final SensorManager mSensorManager; private volatile Bundle mSensorReadings; private volatile Integer mAccuracy; private volatile Integer mSensorNumber; private volatile Integer mXAxis = 0; private volatile Integer mYAxis = 0; private volatile Integer mZAxis = 0; private volatile Integer mThreshing = 0; private volatile Integer mThreshOrientation = 0; private volatile Integer mXCrossed = 0; private volatile Integer mYCrossed = 0; private volatile Integer mZCrossed = 0; private volatile Float mThreshold; private volatile Float mXForce; private volatile Float mYForce; private volatile Float mZForce; private volatile Float mXMag; private volatile Float mYMag; private volatile Float mZMag; private volatile Float mLight; private volatile Double mAzimuth; private volatile Double mPitch; private volatile Double mRoll; private volatile Long mLastTime; private volatile Long mDelayTime; private SensorEventListener mSensorListener; public SensorManagerFacade(FacadeManager manager) { super(manager); mEventFacade = manager.getReceiver(EventFacade.class); mSensorManager = (SensorManager) manager.getService().getSystemService(Context.SENSOR_SERVICE); } @Rpc(description = "Starts recording sensor data to be available for polling.") @RpcStartEvent("sensors") public void startSensingTimed( @RpcParameter(name = "sensorNumber", description = "1 = All, 2 = Accelerometer, 3 = Magnetometer and 4 = Light") Integer sensorNumber, @RpcParameter(name = "delayTime", description = "Minimum time between readings in milliseconds") Integer delayTime) { mSensorNumber = sensorNumber; if (delayTime < 20) { delayTime = 20; } mDelayTime = (long) (delayTime); mLastTime = System.currentTimeMillis(); if (mSensorListener == null) { mSensorListener = new SensorValuesCollector(); mSensorReadings = new Bundle(); switch (mSensorNumber) { case 1: for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_ALL)) { mSensorManager.registerListener(mSensorListener, sensor, SensorManager.SENSOR_DELAY_FASTEST); } break; case 2: for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER)) { mSensorManager.registerListener(mSensorListener, sensor, SensorManager.SENSOR_DELAY_FASTEST); } break; case 3: for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD)) { mSensorManager.registerListener(mSensorListener, sensor, SensorManager.SENSOR_DELAY_FASTEST); } break; case 4: for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_LIGHT)) { mSensorManager.registerListener(mSensorListener, sensor, SensorManager.SENSOR_DELAY_FASTEST); } } } } @Rpc(description = "Records to the Event Queue sensor data exceeding a chosen threshold.") @RpcStartEvent("threshold") public void startSensingThreshold( @RpcParameter(name = "sensorNumber", description = "1 = Orientation, 2 = Accelerometer, 3 = Magnetometer and 4 = Light") Integer sensorNumber, @RpcParameter(name = "threshold", description = "Threshold level for chosen sensor (integer)") Integer threshold, @RpcParameter(name = "axis", description = "0 = No axis, 1 = X, 2 = Y, 3 = X+Y, 4 = Z, 5= X+Z, 6 = Y+Z, 7 = X+Y+Z") Integer axis) { mSensorNumber = sensorNumber; mXAxis = axis & 1; mYAxis = axis & 2; mZAxis = axis & 4; if (mSensorNumber == 1) { mThreshing = 0; mThreshOrientation = 1; mThreshold = ((float) threshold) / ((float) 1000); } else { mThreshing = 1; mThreshold = (float) threshold; } startSensingTimed(mSensorNumber, 20); } @Rpc(description = "Returns the most recently recorded sensor data.") public Bundle readSensors() { if (mSensorReadings == null) { return null; } synchronized (mSensorReadings) { return new Bundle(mSensorReadings); } } @Rpc(description = "Stops collecting sensor data.") @RpcStopEvent("sensors") public void stopSensing() { mSensorManager.unregisterListener(mSensorListener); mSensorListener = null; mSensorReadings = null; mThreshing = 0; mThreshOrientation = 0; } @Rpc(description = "Returns the most recently received accuracy value.") public Integer sensorsGetAccuracy() { return mAccuracy; } @Rpc(description = "Returns the most recently received light value.") public Float sensorsGetLight() { return mLight; } @Rpc(description = "Returns the most recently received accelerometer values.", returns = "a List of Floats [(acceleration on the) X axis, Y axis, Z axis].") public List sensorsReadAccelerometer() { synchronized (mSensorReadings) { return Arrays.asList(mXForce, mYForce, mZForce); } } @Rpc(description = "Returns the most recently received magnetic field values.", returns = "a List of Floats [(magnetic field value for) X axis, Y axis, Z axis].") public List sensorsReadMagnetometer() { synchronized (mSensorReadings) { return Arrays.asList(mXMag, mYMag, mZMag); } } @Rpc(description = "Returns the most recently received orientation values.", returns = "a List of Doubles [azimuth, pitch, roll].") public List sensorsReadOrientation() { synchronized (mSensorReadings) { return Arrays.asList(mAzimuth, mPitch, mRoll); } } @Rpc(description = "Starts recording sensor data to be available for polling.") @RpcDeprecated(value = "startSensingTimed or startSensingThreshhold", release = "4") public void startSensing( @RpcParameter(name = "sampleSize", description = "number of samples for calculating average readings") @RpcDefault("5") Integer sampleSize) { if (mSensorListener == null) { startSensingTimed(1, 220); } } @Override public void shutdown() { stopSensing(); } private class SensorValuesCollector implements SensorEventListener { private final static int MATRIX_SIZE = 9; private final RollingAverage mmAzimuth; private final RollingAverage mmPitch; private final RollingAverage mmRoll; private float[] mmGeomagneticValues; private float[] mmGravityValues; private float[] mmR; private float[] mmOrientation; public SensorValuesCollector() { mmAzimuth = new RollingAverage(); mmPitch = new RollingAverage(); mmRoll = new RollingAverage(); } private void postEvent() { mSensorReadings.putDouble("time", System.currentTimeMillis() / 1000.0); mEventFacade.postEvent("sensors", mSensorReadings.clone()); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { if (mSensorReadings == null) { return; } synchronized (mSensorReadings) { mSensorReadings.putInt("accuracy", accuracy); mAccuracy = accuracy; } } @Override public void onSensorChanged(SensorEvent event) { if (mSensorReadings == null) { return; } synchronized (mSensorReadings) { switch (event.sensor.getType()) { case Sensor.TYPE_ACCELEROMETER: mXForce = event.values[0]; mYForce = event.values[1]; mZForce = event.values[2]; if (mThreshing == 0) { mSensorReadings.putFloat("xforce", mXForce); mSensorReadings.putFloat("yforce", mYForce); mSensorReadings.putFloat("zforce", mZForce); if ((mSensorNumber == 2) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) { mLastTime = System.currentTimeMillis(); postEvent(); } } if ((mThreshing == 1) && (mSensorNumber == 2)) { if ((Math.abs(mXForce) > mThreshold) && (mXAxis == 1)) { mSensorReadings.putFloat("xforce", mXForce); postEvent(); } if ((Math.abs(mYForce) > mThreshold) && (mYAxis == 2)) { mSensorReadings.putFloat("yforce", mYForce); postEvent(); } if ((Math.abs(mZForce) > mThreshold) && (mZAxis == 4)) { mSensorReadings.putFloat("zforce", mZForce); postEvent(); } } mmGravityValues = event.values.clone(); break; case Sensor.TYPE_MAGNETIC_FIELD: mXMag = event.values[0]; mYMag = event.values[1]; mZMag = event.values[2]; if (mThreshing == 0) { mSensorReadings.putFloat("xMag", mXMag); mSensorReadings.putFloat("yMag", mYMag); mSensorReadings.putFloat("zMag", mZMag); if ((mSensorNumber == 3) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) { mLastTime = System.currentTimeMillis(); postEvent(); } } if ((mThreshing == 1) && (mSensorNumber == 3)) { if ((Math.abs(mXMag) > mThreshold) && (mXAxis == 1)) { mSensorReadings.putFloat("xforce", mXMag); postEvent(); } if ((Math.abs(mYMag) > mThreshold) && (mYAxis == 2)) { mSensorReadings.putFloat("yforce", mYMag); postEvent(); } if ((Math.abs(mZMag) > mThreshold) && (mZAxis == 4)) { mSensorReadings.putFloat("zforce", mZMag); postEvent(); } } mmGeomagneticValues = event.values.clone(); break; case Sensor.TYPE_LIGHT: mLight = event.values[0]; if (mThreshing == 0) { mSensorReadings.putFloat("light", mLight); if ((mSensorNumber == 4) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) { mLastTime = System.currentTimeMillis(); postEvent(); } } if ((mThreshing == 1) && (mSensorNumber == 4)) { if (mLight > mThreshold) { mSensorReadings.putFloat("light", mLight); postEvent(); } } break; } if (mSensorNumber == 1) { if (mmGeomagneticValues != null && mmGravityValues != null) { if (mmR == null) { mmR = new float[MATRIX_SIZE]; } if (SensorManager.getRotationMatrix(mmR, null, mmGravityValues, mmGeomagneticValues)) { if (mmOrientation == null) { mmOrientation = new float[3]; } SensorManager.getOrientation(mmR, mmOrientation); mmAzimuth.add(mmOrientation[0]); mmPitch.add(mmOrientation[1]); mmRoll.add(mmOrientation[2]); mAzimuth = mmAzimuth.get(); mPitch = mmPitch.get(); mRoll = mmRoll.get(); if (mThreshOrientation == 0) { mSensorReadings.putDouble("azimuth", mAzimuth); mSensorReadings.putDouble("pitch", mPitch); mSensorReadings.putDouble("roll", mRoll); if ((mSensorNumber == 1) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) { mLastTime = System.currentTimeMillis(); postEvent(); } } if ((mThreshOrientation == 1) && (mSensorNumber == 1)) { if ((mXAxis == 1) && (mXCrossed == 0)) { if (Math.abs(mAzimuth) > ((double) mThreshold)) { mSensorReadings.putDouble("azimuth", mAzimuth); postEvent(); mXCrossed = 1; } } if ((mXAxis == 1) && (mXCrossed == 1)) { if (Math.abs(mAzimuth) < ((double) mThreshold)) { mSensorReadings.putDouble("azimuth", mAzimuth); postEvent(); mXCrossed = 0; } } if ((mYAxis == 2) && (mYCrossed == 0)) { if (Math.abs(mPitch) > ((double) mThreshold)) { mSensorReadings.putDouble("pitch", mPitch); postEvent(); mYCrossed = 1; } } if ((mYAxis == 2) && (mYCrossed == 1)) { if (Math.abs(mPitch) < ((double) mThreshold)) { mSensorReadings.putDouble("pitch", mPitch); postEvent(); mYCrossed = 0; } } if ((mZAxis == 4) && (mZCrossed == 0)) { if (Math.abs(mRoll) > ((double) mThreshold)) { mSensorReadings.putDouble("roll", mRoll); postEvent(); mZCrossed = 1; } } if ((mZAxis == 4) && (mZCrossed == 1)) { if (Math.abs(mRoll) < ((double) mThreshold)) { mSensorReadings.putDouble("roll", mRoll); postEvent(); mZCrossed = 0; } } } } } } } } } static class RollingAverage { private final int mmSampleSize; private final double mmData[]; private int mmIndex = 0; private boolean mmFilled = false; private double mmSum = 0.0; public RollingAverage() { mmSampleSize = 5; mmData = new double[mmSampleSize]; } public void add(double value) { mmSum -= mmData[mmIndex]; mmData[mmIndex] = value; mmSum += mmData[mmIndex]; ++mmIndex; mmIndex %= mmSampleSize; mmFilled = (!mmFilled) ? mmIndex == 0 : mmFilled; } public double get() throws IllegalStateException { if (!mmFilled && mmIndex == 0) { throw new IllegalStateException("No values to average."); } return (mmFilled) ? (mmSum / mmSampleSize) : (mmSum / mmIndex); } } }