├── AndroidManifest.xml
├── README.md
├── ic_launcher-web.png
├── java
└── com
│ └── goatstone
│ ├── sensorFusion
│ ├── BubbleLevelCompass.java
│ └── MainActivity.java
│ └── util
│ └── SensorFusion.java
└── res
├── drawable-ldpi
└── ic_launcher.png
├── drawable-mdpi
└── ic_launcher.png
├── ic_launcher
└── ic_launcher.png
├── layout
└── main.xml
└── values
└── strings.xml
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | AndroidSensorFusion
2 | com.goatstone.util.SensorFusion
3 | ===================
4 |
5 | Get, fuse and display sensor information for Android.
6 |
7 | Usage:
8 |
9 | private SensorFusion sensorFusion;
10 | sensorFusion = new SensorFusion();
11 | sensorFusion.setMode(SensorFusion.Mode.ACC_MAG);
12 |
13 | double azimuthValue = sensorFusion.getAzimuth();
14 | double rollValue = sensorFusion.getRoll();
15 | double pitchValue = sensorFusion.getPitch();
16 |
--------------------------------------------------------------------------------
/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goatstone/AndroidSensorFusion/2fc70e56c24efc229cd4d5327d8e65b2bb6e6753/ic_launcher-web.png
--------------------------------------------------------------------------------
/java/com/goatstone/sensorFusion/BubbleLevelCompass.java:
--------------------------------------------------------------------------------
1 | /************************************************************************************
2 | * Copyright (c) 2014 Jose Collas
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining
5 | * a copy of this software and associated documentation files (the "Software"),
6 | * to deal in the Software without restriction, including without limitation
7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 | * and/or sell copies of the Software, and to permit persons to whom the
9 | * Software is furnished to do so, subject to the following conditions:
10 | *
11 | *
12 | * The above copyright notice and this permission notice shall be included
13 | * in all copies or substantial portions of the Software.
14 | *
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
19 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
20 | * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
21 | * OR OTHER DEALINGS IN THE SOFTWARE.
22 | ************************************************************************************/
23 | package com.goatstone.sensorFusion;
24 |
25 |
26 | import android.content.Context;
27 | import android.graphics.Canvas;
28 | import android.graphics.Color;
29 | import android.graphics.Paint;
30 | import android.os.Handler;
31 | import android.util.AttributeSet;
32 | import android.util.Log;
33 | import android.view.View;
34 | import android.view.ViewGroup;
35 |
36 | import java.util.Date;
37 |
38 | /**
39 | * BubbleLevelCompass Widget : Takes input from sensors and displays a bubble level and compass.
40 | * @author Jose Collas
41 | *
42 | * @version 0.7 1/26/14.
43 | */
44 | public class BubbleLevelCompass extends ViewGroup {
45 |
46 | public Pointer pointer;
47 |
48 | private Paint mainPaint;
49 | private String message = "";
50 |
51 | public BubbleLevelCompass(Context context) {
52 | super(context);
53 | float pointerRadius = 12.0f;
54 | requestFocus();
55 |
56 | // Set up the paint for the label text
57 | mainPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
58 | mainPaint.setTextSize(20);
59 | mainPaint.setColor(Color.BLUE);
60 | pointer = new Pointer(getContext());
61 | pointer.setPointerRadius((int) pointerRadius);
62 | pointer.setPointerColor(Color.GRAY);
63 |
64 | addView(pointer);
65 | }
66 |
67 | public BubbleLevelCompass(Context context, AttributeSet attrs) {
68 | super(context, attrs);
69 | float pointerRadius = 12.0f;
70 | requestFocus();
71 | setPadding(20, 20, 20, 20);
72 | setBackgroundColor(Color.GRAY);
73 |
74 | mainPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
75 | mainPaint.setTextSize(20);
76 | mainPaint.setColor(Color.BLUE);
77 | pointer = new Pointer(getContext());
78 | pointer.setPointerRadius((int) pointerRadius);
79 | addView(pointer);
80 |
81 | }
82 |
83 | /**
84 | * Pointer : represents a UI element that will point to another UI element in order to select it
85 | */
86 | private class Pointer extends View {
87 |
88 | private int pointerRadius = 22;
89 | private int left = 200;
90 | private int top = 120;
91 | private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
92 | private Paint pointerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
93 | private int azimuth;
94 | int viewWidth;
95 | int viewHeight;
96 | int distanceMult = 1;
97 |
98 | public void setPointerRadius(int r) {
99 | pointerRadius = r;
100 | }
101 |
102 | public void setPointerColor(int color) {
103 | paint.setColor(color);
104 | invalidate();
105 | }
106 |
107 | public void setPointerLeft(int left) {
108 | this.left = (left * distanceMult);
109 | invalidate();
110 | }
111 |
112 | public int getPointerLeft() {
113 | return left;
114 | }
115 |
116 | public void setPointerTop(int top) {
117 | this.top = (top * distanceMult);
118 | invalidate();
119 | }
120 |
121 | public void setAzimuth(int azimuth) {
122 | this.azimuth = azimuth;
123 | }
124 |
125 | //private Handler mainHandler;
126 | private Handler handler;
127 |
128 | public Pointer(Context context) {
129 | super(context);
130 | paint.setStrokeWidth(6.0f);
131 | paint.setColor(Color.argb(250, 0, 0, 250));
132 |
133 | pointerPaint.setStrokeWidth(3.0f);
134 | pointerPaint.setColor(Color.argb(200, 250, 10, 10));
135 | viewWidth = getWidth();
136 | viewHeight = getHeight();
137 | left = viewWidth / 2;
138 | top = viewHeight / 2;
139 | setBackgroundColor(Color.argb(200, 250, 200, 200));
140 |
141 | // handler = new Handler();
142 | // handler.postDelayed(runnable, 100);
143 | }
144 |
145 | private Runnable runnable = new Runnable() {
146 | @Override
147 | public void run() {
148 | Log.i("tag", "run: " + new Date());
149 | //invalidate();
150 | handler.postDelayed(this, 1000);
151 | }
152 | };
153 |
154 | @Override
155 | protected void onDraw(Canvas canvas) {
156 | int viewWidth = getWidth();
157 | int viewHeight = getHeight();
158 |
159 | canvas.translate((viewWidth / 2), (viewHeight / 2));
160 | canvas.drawCircle(0 + left, 0 + top - 150, pointerRadius, pointerPaint);
161 | canvas.rotate(azimuth * -1 - 90);
162 | canvas.drawLine(0, 0, 50, 0, paint);
163 |
164 | }
165 | } // end Pointer
166 |
167 | public void setPLeft(int left) {
168 | pointer.setPointerLeft(left);
169 | }
170 |
171 | public void setPTop(int top) {
172 | pointer.setPointerTop(top * -1);
173 | }
174 |
175 | public void setMessage(String message) {
176 | this.message = message;
177 | invalidate();
178 | }
179 |
180 | public void setAzimuth(int azimuth1) {
181 | pointer.setAzimuth(azimuth1);
182 | }
183 |
184 | @Override
185 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
186 | // Do nothing. Do not call the superclass method--that would start a layout pass
187 | // on this view's children. ?? - RoundDial lays out its children in onSizeChanged(). ??
188 | }
189 |
190 | @Override
191 | protected void onDraw(Canvas canvas) {
192 | super.onDraw(canvas);
193 | // canvas.drawText("CustomView : pointer.getPointerLeft() : " + pointer.getPointerLeft(), 10, 300, mainPaint);
194 | }
195 |
196 | @Override
197 | protected int getSuggestedMinimumWidth() {
198 | return 400;
199 | }
200 |
201 | @Override
202 | protected int getSuggestedMinimumHeight() {
203 | return (int) 400;
204 | }
205 |
206 | @Override
207 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
208 | setMeasuredDimension(400, 500);
209 | }
210 |
211 | @Override
212 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
213 | super.onSizeChanged(w, h, oldw, oldh);
214 | pointer.layout(0, 0, w, h);
215 | }
216 |
217 | }
218 |
--------------------------------------------------------------------------------
/java/com/goatstone/sensorFusion/MainActivity.java:
--------------------------------------------------------------------------------
1 | /************************************************************************************
2 | * Copyright (c) 2014 Jose Collas
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining
5 | * a copy of this software and associated documentation files (the "Software"),
6 | * to deal in the Software without restriction, including without limitation
7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 | * and/or sell copies of the Software, and to permit persons to whom the
9 | * Software is furnished to do so, subject to the following conditions:
10 | *
11 | *
12 | * The above copyright notice and this permission notice shall be included
13 | * in all copies or substantial portions of the Software.
14 | *
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
19 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
20 | * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
21 | * OR OTHER DEALINGS IN THE SOFTWARE.
22 | ************************************************************************************/
23 | package com.goatstone.sensorFusion;
24 |
25 | import android.app.Activity;
26 | import android.hardware.Sensor;
27 | import android.hardware.SensorEvent;
28 | import android.hardware.SensorEventListener;
29 | import android.hardware.SensorManager;
30 | import android.os.Bundle;
31 | import android.widget.RadioGroup;
32 | import android.widget.TextView;
33 |
34 | import com.goatstone.util.SensorFusion;
35 |
36 | import java.text.DecimalFormat;
37 |
38 | public class MainActivity extends Activity
39 | implements SensorEventListener, RadioGroup.OnCheckedChangeListener {
40 |
41 | private SensorFusion sensorFusion;
42 | private BubbleLevelCompass bubbleLevelCompass;
43 | private SensorManager sensorManager = null;
44 |
45 | private RadioGroup setModeRadioGroup;
46 | private TextView azimuthText, pithText, rollText;
47 | private DecimalFormat d = new DecimalFormat("#.##");
48 |
49 | @Override
50 | public void onCreate(Bundle savedInstanceState) {
51 | super.onCreate(savedInstanceState);
52 | setContentView(R.layout.main);
53 |
54 | sensorManager = (SensorManager) this.getSystemService(SENSOR_SERVICE);
55 | registerSensorManagerListeners();
56 |
57 | d.setMaximumFractionDigits(2);
58 | d.setMinimumFractionDigits(2);
59 |
60 | sensorFusion = new SensorFusion();
61 | sensorFusion.setMode(SensorFusion.Mode.ACC_MAG);
62 |
63 | bubbleLevelCompass = (BubbleLevelCompass) this.findViewById(R.id.SensorFusionView);
64 | setModeRadioGroup = (RadioGroup) findViewById(R.id.radioGroup1);
65 | azimuthText = (TextView) findViewById(R.id.azmuth);
66 | pithText = (TextView) findViewById(R.id.pitch);
67 | rollText = (TextView) findViewById(R.id.roll);
68 | setModeRadioGroup.setOnCheckedChangeListener(this);
69 | }
70 |
71 | public void updateOrientationDisplay() {
72 |
73 | double azimuthValue = sensorFusion.getAzimuth();
74 | double rollValue = sensorFusion.getRoll();
75 | double pitchValue = sensorFusion.getPitch();
76 |
77 | azimuthText.setText(String.valueOf(d.format(azimuthValue)));
78 | pithText.setText(String.valueOf(d.format(pitchValue)));
79 | rollText.setText(String.valueOf(d.format(rollValue)));
80 |
81 | bubbleLevelCompass.setPLeft((int) rollValue);
82 | bubbleLevelCompass.setPTop((int) pitchValue);
83 | bubbleLevelCompass.setAzimuth((int) azimuthValue);
84 |
85 | }
86 |
87 | public void registerSensorManagerListeners() {
88 | sensorManager.registerListener(this,
89 | sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
90 | SensorManager.SENSOR_DELAY_FASTEST);
91 |
92 | sensorManager.registerListener(this,
93 | sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
94 | SensorManager.SENSOR_DELAY_FASTEST);
95 |
96 | sensorManager.registerListener(this,
97 | sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
98 | SensorManager.SENSOR_DELAY_FASTEST);
99 | }
100 |
101 | @Override
102 | public void onStop() {
103 | super.onStop();
104 | sensorManager.unregisterListener(this);
105 | }
106 |
107 | @Override
108 | protected void onPause() {
109 | super.onPause();
110 | sensorManager.unregisterListener(this);
111 | }
112 |
113 | @Override
114 | public void onResume() {
115 | super.onResume();
116 | registerSensorManagerListeners();
117 | }
118 |
119 | @Override
120 | public void onAccuracyChanged(Sensor sensor, int accuracy) {
121 | }
122 |
123 | @Override
124 | public void onSensorChanged(SensorEvent event) {
125 | switch (event.sensor.getType()) {
126 | case Sensor.TYPE_ACCELEROMETER:
127 | sensorFusion.setAccel(event.values);
128 | sensorFusion.calculateAccMagOrientation();
129 | break;
130 |
131 | case Sensor.TYPE_GYROSCOPE:
132 | sensorFusion.gyroFunction(event);
133 | break;
134 |
135 | case Sensor.TYPE_MAGNETIC_FIELD:
136 | sensorFusion.setMagnet(event.values);
137 | break;
138 | }
139 | updateOrientationDisplay();
140 | }
141 |
142 | @Override
143 | public void onCheckedChanged(RadioGroup group, int checkedId) {
144 |
145 | switch (checkedId) {
146 | case R.id.radio0:
147 | sensorFusion.setMode(SensorFusion.Mode.ACC_MAG);
148 | break;
149 | case R.id.radio1:
150 | sensorFusion.setMode(SensorFusion.Mode.GYRO);
151 | break;
152 | case R.id.radio2:
153 | sensorFusion.setMode(SensorFusion.Mode.FUSION);
154 | break;
155 | }
156 | }
157 |
158 | }
--------------------------------------------------------------------------------
/java/com/goatstone/util/SensorFusion.java:
--------------------------------------------------------------------------------
1 | /************************************************************************************
2 | * Copyright (c) 2012 Paul Lawitzki
3 | * Copyright (c) 2014 Jose Collas : Wrapper code
4 | *
5 | * Permission is hereby granted, free of charge, to any person obtaining
6 | * a copy of this software and associated documentation files (the "Software"),
7 | * to deal in the Software without restriction, including without limitation
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 | * and/or sell copies of the Software, and to permit persons to whom the
10 | * Software is furnished to do so, subject to the following conditions:
11 | *
12 | *
13 | * The above copyright notice and this permission notice shall be included
14 | * in all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
20 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
22 | * OR OTHER DEALINGS IN THE SOFTWARE.
23 | ************************************************************************************/
24 |
25 | package com.goatstone.util;
26 |
27 | import android.hardware.SensorEvent;
28 | import android.hardware.SensorManager;
29 | import android.util.Log;
30 |
31 | import java.util.Timer;
32 | import java.util.TimerTask;
33 |
34 | /**
35 | * Provides a wrapper around code that fuses sensor data.
36 | * @author 2012 Paul Lawitzki
37 | * @author 2014 Jose Collas
38 | *
39 | * @version 0.7 1/26/14.
40 | *
41 | */
42 | public class SensorFusion {
43 |
44 | // angular speeds from gyro
45 | private float[] gyro = new float[3];
46 |
47 | // rotation matrix from gyro data
48 | private float[] gyroMatrix = new float[9];
49 |
50 | // orientation angles from gyro matrix
51 | private float[] gyroOrientation = new float[3];
52 |
53 | // magnetic field vector
54 | private float[] magnet = new float[3];
55 |
56 | // accelerometer vector
57 | private float[] accel = new float[3];
58 |
59 | // orientation angles from accel and magnet
60 | private float[] accMagOrientation = new float[3];
61 |
62 | // final orientation angles from sensor fusion
63 | private float[] fusedOrientation = new float[3];
64 |
65 | private float[] selectedOrientation = fusedOrientation;
66 |
67 | public enum Mode {
68 | ACC_MAG, GYRO, FUSION
69 | }
70 |
71 | // accelerometer and magnetometer based rotation matrix
72 | private float[] rotationMatrix = new float[9];
73 |
74 | public static final float EPSILON = 0.000000001f;
75 | private static final float NS2S = 1.0f / 1000000000.0f;
76 | private long timestamp;
77 | private boolean initState = true;
78 |
79 | public static final int TIME_CONSTANT = 30;
80 | public static final float FILTER_COEFFICIENT = 0.98f;
81 | private Timer fuseTimer = new Timer();
82 |
83 | public SensorFusion() {
84 |
85 | gyroOrientation[0] = 0.0f;
86 | gyroOrientation[1] = 0.0f;
87 | gyroOrientation[2] = 0.0f;
88 |
89 | // initialise gyroMatrix with identity matrix
90 | gyroMatrix[0] = 1.0f;
91 | gyroMatrix[1] = 0.0f;
92 | gyroMatrix[2] = 0.0f;
93 | gyroMatrix[3] = 0.0f;
94 | gyroMatrix[4] = 1.0f;
95 | gyroMatrix[5] = 0.0f;
96 | gyroMatrix[6] = 0.0f;
97 | gyroMatrix[7] = 0.0f;
98 | gyroMatrix[8] = 1.0f;
99 |
100 | // wait for one second until gyroscope and magnetometer/accelerometer
101 | // data is initialised then scedule the complementary filter task
102 | fuseTimer.scheduleAtFixedRate(new calculateFusedOrientationTask(),
103 | 1000, TIME_CONSTANT);
104 |
105 | }
106 |
107 | public double getAzimuth() {
108 | return selectedOrientation[0] * 180 / Math.PI;
109 | }
110 |
111 | public double getPitch() {
112 | return selectedOrientation[1] * 180 / Math.PI;
113 | }
114 |
115 | public double getRoll() {
116 | return selectedOrientation[2] * 180 / Math.PI;
117 | }
118 |
119 | public void setMagnet(float[] sensorValues) {
120 | System.arraycopy(sensorValues, 0, magnet, 0, 3);
121 | }
122 |
123 | public void setAccel(float[] sensorValues) {
124 | System.arraycopy(sensorValues, 0, accel, 0, 3);
125 |
126 | }
127 |
128 | public void setMode(Mode mode) {
129 | Log.i("tag", "msg 0"+ mode);
130 | Log.i("tag", "msg 00000"+ Mode.ACC_MAG);
131 | switch (mode) {
132 | case ACC_MAG:
133 | Log.i("tag", "msg 1"+ mode);
134 | selectedOrientation = accMagOrientation;
135 | break;
136 | case GYRO:
137 | Log.i("tag", "msg 2"+ mode);
138 | selectedOrientation = gyroOrientation;
139 | break;
140 | case FUSION:
141 | Log.i("tag", "msg 3"+ mode);
142 | selectedOrientation = fusedOrientation;
143 | break;
144 | default:
145 | Log.i("tag", "msg 4"+ mode);
146 | selectedOrientation = fusedOrientation;
147 | break;
148 | }
149 | }
150 |
151 | // calculates orientation angles from accelerometer and magnetometer output
152 | public void calculateAccMagOrientation() {
153 | if (SensorManager.getRotationMatrix(rotationMatrix, null, accel, magnet)) {
154 | SensorManager.getOrientation(rotationMatrix, accMagOrientation);
155 | }
156 | }
157 |
158 | // This function is borrowed from the Android reference
159 | // at http://developer.android.com/reference/android/hardware/SensorEvent.html#values
160 | // It calculates a rotation vector from the gyroscope angular speed values.
161 | private void getRotationVectorFromGyro(float[] gyroValues,
162 | float[] deltaRotationVector,
163 | float timeFactor) {
164 | float[] normValues = new float[3];
165 |
166 | // Calculate the angular speed of the sample
167 | float omegaMagnitude =
168 | (float) Math.sqrt(gyroValues[0] * gyroValues[0] +
169 | gyroValues[1] * gyroValues[1] +
170 | gyroValues[2] * gyroValues[2]);
171 |
172 | // Normalize the rotation vector if it's big enough to get the axis
173 | if (omegaMagnitude > EPSILON) {
174 | normValues[0] = gyroValues[0] / omegaMagnitude;
175 | normValues[1] = gyroValues[1] / omegaMagnitude;
176 | normValues[2] = gyroValues[2] / omegaMagnitude;
177 | }
178 |
179 | // Integrate around this axis with the angular speed by the timestep
180 | // in order to get a delta rotation from this sample over the timestep
181 | // We will convert this axis-angle representation of the delta rotation
182 | // into a quaternion before turning it into the rotation matrix.
183 | float thetaOverTwo = omegaMagnitude * timeFactor;
184 | float sinThetaOverTwo = (float) Math.sin(thetaOverTwo);
185 | float cosThetaOverTwo = (float) Math.cos(thetaOverTwo);
186 | deltaRotationVector[0] = sinThetaOverTwo * normValues[0];
187 | deltaRotationVector[1] = sinThetaOverTwo * normValues[1];
188 | deltaRotationVector[2] = sinThetaOverTwo * normValues[2];
189 | deltaRotationVector[3] = cosThetaOverTwo;
190 | }
191 |
192 | // This function performs the integration of the gyroscope data.
193 | // It writes the gyroscope based orientation into gyroOrientation.
194 | public void gyroFunction(SensorEvent event) {
195 | // don't start until first accelerometer/magnetometer orientation has been acquired
196 | if (accMagOrientation == null)
197 | return;
198 |
199 | // initialisation of the gyroscope based rotation matrix
200 | if (initState) {
201 | float[] initMatrix = new float[9];
202 | initMatrix = getRotationMatrixFromOrientation(accMagOrientation);
203 | float[] test = new float[3];
204 | SensorManager.getOrientation(initMatrix, test);
205 | gyroMatrix = matrixMultiplication(gyroMatrix, initMatrix);
206 | initState = false;
207 | }
208 |
209 | // copy the new gyro values into the gyro array
210 | // convert the raw gyro data into a rotation vector
211 | float[] deltaVector = new float[4];
212 | if (timestamp != 0) {
213 | final float dT = (event.timestamp - timestamp) * NS2S;
214 | System.arraycopy(event.values, 0, gyro, 0, 3);
215 | getRotationVectorFromGyro(gyro, deltaVector, dT / 2.0f);
216 | }
217 |
218 | // measurement done, save current time for next interval
219 | timestamp = event.timestamp;
220 |
221 | // convert rotation vector into rotation matrix
222 | float[] deltaMatrix = new float[9];
223 | SensorManager.getRotationMatrixFromVector(deltaMatrix, deltaVector);
224 |
225 | // apply the new rotation interval on the gyroscope based rotation matrix
226 | gyroMatrix = matrixMultiplication(gyroMatrix, deltaMatrix);
227 |
228 | // get the gyroscope based orientation from the rotation matrix
229 | SensorManager.getOrientation(gyroMatrix, gyroOrientation);
230 | }
231 |
232 | private float[] getRotationMatrixFromOrientation(float[] o) {
233 | float[] xM = new float[9];
234 | float[] yM = new float[9];
235 | float[] zM = new float[9];
236 |
237 | float sinX = (float) Math.sin(o[1]);
238 | float cosX = (float) Math.cos(o[1]);
239 | float sinY = (float) Math.sin(o[2]);
240 | float cosY = (float) Math.cos(o[2]);
241 | float sinZ = (float) Math.sin(o[0]);
242 | float cosZ = (float) Math.cos(o[0]);
243 |
244 | // rotation about x-axis (pitch)
245 | xM[0] = 1.0f;
246 | xM[1] = 0.0f;
247 | xM[2] = 0.0f;
248 | xM[3] = 0.0f;
249 | xM[4] = cosX;
250 | xM[5] = sinX;
251 | xM[6] = 0.0f;
252 | xM[7] = -sinX;
253 | xM[8] = cosX;
254 |
255 | // rotation about y-axis (roll)
256 | yM[0] = cosY;
257 | yM[1] = 0.0f;
258 | yM[2] = sinY;
259 | yM[3] = 0.0f;
260 | yM[4] = 1.0f;
261 | yM[5] = 0.0f;
262 | yM[6] = -sinY;
263 | yM[7] = 0.0f;
264 | yM[8] = cosY;
265 |
266 | // rotation about z-axis (azimuth)
267 | zM[0] = cosZ;
268 | zM[1] = sinZ;
269 | zM[2] = 0.0f;
270 | zM[3] = -sinZ;
271 | zM[4] = cosZ;
272 | zM[5] = 0.0f;
273 | zM[6] = 0.0f;
274 | zM[7] = 0.0f;
275 | zM[8] = 1.0f;
276 |
277 | // rotation order is y, x, z (roll, pitch, azimuth)
278 | float[] resultMatrix = matrixMultiplication(xM, yM);
279 | resultMatrix = matrixMultiplication(zM, resultMatrix);
280 | return resultMatrix;
281 | }
282 |
283 | private float[] matrixMultiplication(float[] A, float[] B) {
284 | float[] result = new float[9];
285 |
286 | result[0] = A[0] * B[0] + A[1] * B[3] + A[2] * B[6];
287 | result[1] = A[0] * B[1] + A[1] * B[4] + A[2] * B[7];
288 | result[2] = A[0] * B[2] + A[1] * B[5] + A[2] * B[8];
289 |
290 | result[3] = A[3] * B[0] + A[4] * B[3] + A[5] * B[6];
291 | result[4] = A[3] * B[1] + A[4] * B[4] + A[5] * B[7];
292 | result[5] = A[3] * B[2] + A[4] * B[5] + A[5] * B[8];
293 |
294 | result[6] = A[6] * B[0] + A[7] * B[3] + A[8] * B[6];
295 | result[7] = A[6] * B[1] + A[7] * B[4] + A[8] * B[7];
296 | result[8] = A[6] * B[2] + A[7] * B[5] + A[8] * B[8];
297 |
298 | return result;
299 | }
300 |
301 | class calculateFusedOrientationTask extends TimerTask {
302 | public void run() {
303 | float oneMinusCoeff = 1.0f - FILTER_COEFFICIENT;
304 |
305 | /*
306 | * Fix for 179∞ <--> -179∞ transition problem:
307 | * Check whether one of the two orientation angles (gyro or accMag) is negative while the other one is positive.
308 | * If so, add 360∞ (2 * math.PI) to the negative value, perform the sensor fusion, and remove the 360∞ from the result
309 | * if it is greater than 180∞. This stabilizes the output in positive-to-negative-transition cases.
310 | */
311 |
312 | // azimuth
313 | if (gyroOrientation[0] < -0.5 * Math.PI && accMagOrientation[0] > 0.0) {
314 | fusedOrientation[0] = (float) (FILTER_COEFFICIENT * (gyroOrientation[0] + 2.0 * Math.PI) + oneMinusCoeff * accMagOrientation[0]);
315 | fusedOrientation[0] -= (fusedOrientation[0] > Math.PI) ? 2.0 * Math.PI : 0;
316 | } else if (accMagOrientation[0] < -0.5 * Math.PI && gyroOrientation[0] > 0.0) {
317 | fusedOrientation[0] = (float) (FILTER_COEFFICIENT * gyroOrientation[0] + oneMinusCoeff * (accMagOrientation[0] + 2.0 * Math.PI));
318 | fusedOrientation[0] -= (fusedOrientation[0] > Math.PI) ? 2.0 * Math.PI : 0;
319 | } else {
320 | fusedOrientation[0] = FILTER_COEFFICIENT * gyroOrientation[0] + oneMinusCoeff * accMagOrientation[0];
321 | }
322 |
323 | // pitch
324 | if (gyroOrientation[1] < -0.5 * Math.PI && accMagOrientation[1] > 0.0) {
325 | fusedOrientation[1] = (float) (FILTER_COEFFICIENT * (gyroOrientation[1] + 2.0 * Math.PI) + oneMinusCoeff * accMagOrientation[1]);
326 | fusedOrientation[1] -= (fusedOrientation[1] > Math.PI) ? 2.0 * Math.PI : 0;
327 | } else if (accMagOrientation[1] < -0.5 * Math.PI && gyroOrientation[1] > 0.0) {
328 | fusedOrientation[1] = (float) (FILTER_COEFFICIENT * gyroOrientation[1] + oneMinusCoeff * (accMagOrientation[1] + 2.0 * Math.PI));
329 | fusedOrientation[1] -= (fusedOrientation[1] > Math.PI) ? 2.0 * Math.PI : 0;
330 | } else {
331 | fusedOrientation[1] = FILTER_COEFFICIENT * gyroOrientation[1] + oneMinusCoeff * accMagOrientation[1];
332 | }
333 |
334 | // roll
335 | if (gyroOrientation[2] < -0.5 * Math.PI && accMagOrientation[2] > 0.0) {
336 | fusedOrientation[2] = (float) (FILTER_COEFFICIENT * (gyroOrientation[2] + 2.0 * Math.PI) + oneMinusCoeff * accMagOrientation[2]);
337 | fusedOrientation[2] -= (fusedOrientation[2] > Math.PI) ? 2.0 * Math.PI : 0;
338 | } else if (accMagOrientation[2] < -0.5 * Math.PI && gyroOrientation[2] > 0.0) {
339 | fusedOrientation[2] = (float) (FILTER_COEFFICIENT * gyroOrientation[2] + oneMinusCoeff * (accMagOrientation[2] + 2.0 * Math.PI));
340 | fusedOrientation[2] -= (fusedOrientation[2] > Math.PI) ? 2.0 * Math.PI : 0;
341 | } else {
342 | fusedOrientation[2] = FILTER_COEFFICIENT * gyroOrientation[2] + oneMinusCoeff * accMagOrientation[2];
343 | }
344 |
345 | // overwrite gyro matrix and orientation with fused orientation
346 | // to comensate gyro drift
347 | gyroMatrix = getRotationMatrixFromOrientation(fusedOrientation);
348 | System.arraycopy(fusedOrientation, 0, gyroOrientation, 0, 3);
349 |
350 |
351 | // update sensor output in GUI
352 | //mainHandler.post(updateOreintationDisplayTask);
353 | }
354 | }
355 |
356 |
357 | }
358 |
--------------------------------------------------------------------------------
/res/drawable-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goatstone/AndroidSensorFusion/2fc70e56c24efc229cd4d5327d8e65b2bb6e6753/res/drawable-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goatstone/AndroidSensorFusion/2fc70e56c24efc229cd4d5327d8e65b2bb6e6753/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/ic_launcher/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goatstone/AndroidSensorFusion/2fc70e56c24efc229cd4d5327d8e65b2bb6e6753/res/ic_launcher/ic_launcher.png
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
18 |
19 |
25 |
26 |
34 |
35 |
36 |
37 |
38 |
43 |
44 |
50 |
51 |
59 |
60 |
61 |
62 |
63 |
64 |
70 |
71 |
77 |
78 |
86 |
87 |
88 |
89 |
93 |
94 |
95 |
96 |
97 |
101 |
102 |
109 |
110 |
116 |
117 |
123 |
124 |
125 |
134 |
135 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello
5 | SensorFusion
6 |
7 |
--------------------------------------------------------------------------------