Android: measuring speed and distance with an accelerometer

Ever since I got a google phone, the thoughts “have been wandering around in my head periodically,“ what would be so funny to do with this very phone? ” After playing with accelerometer control toys, I thought - what else can I do with this sensor? Of course, measure acceleration! And, as a result, calculate the speed and the distance traveled. Of course, the use of only an accelerometer imposes a number of limitations on the measured: firstly, the movement should be rectilinear, secondly, the orientation of the apparatus in space should not change, and thirdly, it is desirable to calibrate the sensor before starting the measurement. I must say right away - there are ways to mitigate these requirements, but more on that later.

The main question, as usual, is “why?”. Why is it if there is GPS? Well, a true point. However, GPS does not work everywhere, but an accelerometer - it is with you on the phone. For example,

did you try to catch the satellites in the subway? .. With "Why," figured out, go to "How" ...

In order to respond to changes in acceleration, you need to implement the SensorEventListener interface somewhere. Since we have not yet figured out what to do with it, we will create an abstract class
public abstract class Accelerometer implements SensorEventListener {
    protected float lastX;
    protected float lastY;
    protected float lastZ;
    public abstract Point getPoint();
    public void onAccuracyChanged(Sensor arg0, int arg1) {
    }
}

And, at the same time, a class for storing the sensor counter reading :
public class Point {
    private float x = 0;
    private float y = 0;
    private float z = 0;
    private int cnt = 1;
    public float getX() {
        return x/(float)cnt;
    }
    public float getY() {
        return y/(float)cnt;
    }
    public float getZ() {
        return z/(float)cnt;
    }
    public Point(float x, float y, float z, int cnt) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.cnt = cnt;
    }
}


And think about what to do next. The period of updating information from the sensor in SENSOR_DELAY_GAME mode is approximately 20 milliseconds. This is often enough, our task does not require this. On the other hand, taking readings less frequently, we run the risk of getting into "emissions" and losing accuracy. It is logical to somehow regularly receive the average acceleration value, say, in the last second. To store an array and calculate the average value is expensive, it is much easier to add all the values ​​obtained and divide by the number. Also, we provide dX, dY, dZ - our calibration that has not yet been implemented.
Here is the result:
public class XYZAccelerometer extends Accelerometer {
      private static final int BUFFER_SIZE = 500;
    // calibration
    private  float dX = 0;
    private  float dY = 0;
    private  float dZ = 0;
    // buffer variables
    private float X;
    private float Y;
    private float Z;
    private int cnt = 0;
    // returns last SenorEvent parameters
    public Point getLastPoint(){
        return new Point(lastX, lastY, lastZ, 1);
    }
    // returrns parameters, using buffer: average acceleration
    // since last call of getPoint(). 
    public Point getPoint(){
        if (cnt == 0){
            return new Point(lastX, lastY, lastZ, 1);
        }
        Point p =  new Point(X, Y, Z, cnt);
        reset();
        return p;
    }
    // resets buffer
    public void reset(){
        cnt = 0;
        X = 0;
        Y = 0;
        Z = 0;
    }
    public void onSensorChanged(SensorEvent se) {
        float x = se.values[SensorManager.DATA_X] + dX;
        float y = se.values[SensorManager.DATA_Y] + dY;
        float z = se.values[SensorManager.DATA_Z] + dZ;
        lastX = x;
        lastY = y;
        lastZ = z;
        X+= x;
        Y+= y;
        Z+= z;
        if (cnt < BUFFER_SIZE-1) {
            cnt++;
        } else
        {
            reset();
        }
    }
    public  void setdX(float dX) {
        this.dX = dX;
    }
    public  void setdY(float dY) {
        this.dY = dY;
    }
    public  void setdZ(float dZ) {
        this.dZ = dZ;
    }
}


With your permission, I will skip the description of sensor calibration methods. It is enough to say that it is necessary to remove the repentance for some time, then install the appropriate dX, dY, dZ in our XYZAccelerometer. You can not neglect this procedure, because while we sleep , the acceleration of gravity is constantly acting, and the sensor measures it.

For the most important, let's get a class for storing and calculating motion parameters on an interval:

public class MeasurePoint {
    private float x;
    private float y;
    private float z;
    private float speedBefore;
    private float speedAfter;
    private float distance;
    private float acceleration;
    private long interval;
    public MeasurePoint(float x, float y, float z, float speedBefore, long interval) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.speedBefore = speedBefore;
        this.interval = interval;
        speedAfter = 0;
        calc();
    }
    private void calc(){
        //Acceleration as projection of current vector on average
        acceleration = Math.sqrt(this.x*this.x+this.y*this.y*+this.z*this.z);
        float t = ((float)interval / 1000f);
        speedAfter = speedBefore + acceleration * t;
        distance = speedBefore*t + acceleration*t*t/2;
    }
// add getters
}


And a class for storing information about the whole experiment:

public class MeasureData {
    // points from accelerometr
    private LinkedList accData;
    private LinkedList data;
    // timer interval of generating points
    private long interval;
    public MeasureData(long interval) {
        this.interval = interval;
        accData = new LinkedList ();
        data = new LinkedList ();
    }
    public void addPoint(Point p){
        accData.add(p);
    }
    public void process(){
        for(int i = 0; i < accData.size(); ++i){
            Point p = accData.get(i);
            float speed = 0;
            if(i > 0){
                speed = data.get(i-1).getSpeedAfter();
            }
            data.add(new MeasurePoint(p.getX(), p.getY(), p.getZ(), speed, interval));
        }
    }
    public float getLastSpeed(){
        return data.getLast().getSpeedAfter();
    }
    public float getLastSpeedKm(){
        float ms = getLastSpeed();
        return ms*3.6f;
    }
}


I think everything is simple and clear. It remains only to use this in our Activity ... which, by the way, is not there yet. Let's start with layout:



And the code:

public class TestActivity extends Activity {
    static final int TIMER_DONE = 2;
    static final int START = 3;
    private StartCatcher mStartListener;
    private XYZAccelerometer xyzAcc;
    private SensorManager mSensorManager;
    private static final long UPDATE_INTERVAL = 500;
    private static final long MEASURE_TIMES = 20;
    private Timer timer;
    private TextView tv;
    private Button testBtn;
    int counter;
    private MeasureData mdXYZ;
    /** handler for async events*/
    Handler hRefresh = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case TIMER_DONE:
                    onMeasureDone();
                    String es1 = Float.toString(Math.round(mdXYZ.getLastSpeedKm()*100)/100f);
                    tv.append(" END SPEED " + es1  + " \n");
                    enableButtons();
                    break;
                case START:
                    tv.append(" START");
                    timer = new Timer();
                    timer.scheduleAtFixedRate(
                            new TimerTask() {
                                public void run() {
                                    dumpSensor();
                                }
                            },
                            0,
                            UPDATE_INTERVAL);
                    break;
                }
        }
    };
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        tv = (TextView) findViewById(R.id.txt);
        testBtn = (Button) findViewById(R.id.btn);
    }
    @Override
    protected void onResume() {
        super.onResume();
        tv.append("\n ..");
        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        setAccelerometer();
        setStartCatcher();
        mSensorManager.registerListener(xyzAcc,
                mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                SensorManager.SENSOR_DELAY_GAME);
    }
    @Override
    protected void onPause() {
        mSensorManager.unregisterListener(xyzAcc);
        super.onPause();
    }
    public void onButtonTest(View v) {
        disableButtons();
        mdXYZ = new MeasureData(UPDATE_INTERVAL);
        counter = 0;
        tv.setText("");
       hRefresh.sendEmptyMessage(START);  
    }
    void dumpSensor() {
        ++counter;
        mdXYZ.addPoint(xyzAcc.getPoint());
        if (counter > MEASURE_TIMES) {
            timer.cancel();
            hRefresh.sendEmptyMessage(TIMER_DONE);
        }
    }
    private void enableButtons() {
        testBtn.setEnabled(true);
    }
    private void setAccelerometer() {
        xyzAcc = new XYZAccelerometer();
        mSensorManager.registerListener(xyzAcc,
                mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                SensorManager.SENSOR_DELAY_UI);
    }
    private void disableButtons() {
        testBtn.setEnabled(false);
    }
    private void onMeasureDone() {
            mdXYZ.process();
    }
}


That's all. Surprisingly, on a flat path, this method gives a very good measurement accuracy.

I am enclosing a graph of one experiment: the blue line is the speed calculated by the accelerometer, the red line is the one taken from GPS with the maximum frequency. Black blot - speed on the speedometer at the end of the experiment.

image

Also popular now: