воскресенье, 23 октября 2011 г.

Android: computing speed and distance using accelerometer






It's possible to compute distance and speed using only accelerometer, but with three conditions:
1. Linear movement - trajectory must be straight.
2. Slope of the road must be constant.
3. You must perform calibration procedure before start.

Where can you use this method with such restrictions - it's up to you... Now, how to do it:

1. We need something, implementing SensorEventListener interface. For the future use, let's add following 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 this will be our SensorEventListener:

  
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 int getCnt(){
        return cnt;
    }

    public  void setdX(float dX) {
        this.dX = dX;
    }

    public  void setdY(float dY) {
        this.dY = dY;
    }

    public  void setdZ(float dZ) {
        this.dZ = dZ;
    }
}
 

Calibrating accelerometer must be called before each experiment. Phone orientation must not be changed while measuring.

To calibrate accelerometer, i use this class:

  
public class Calibrator {

    final static int UPDATE_INTERVAL = 400;
    final static int ITERATIONS = 5;
    Handler hRefresh;
    XYZAccelerometer acc;
    int eventNumber;
    private LinkedList calData;

    public Calibrator(Handler hRefresh, XYZAccelerometer acc, int eventNumber) {
        this.hRefresh = hRefresh;
        this.acc = acc;
        this.eventNumber = eventNumber;
    }

    public void calibrate() {
        final Timer calTimer = new Timer();
        calData = new LinkedList();
        acc.setdX(0);
        acc.setdY(0);
        acc.setdZ(0);

        calTimer.scheduleAtFixedRate(
                new TimerTask() {

                    public void run() {
                        addCalData(calData);
                        if (calData.size() > ITERATIONS) {
                            calTimer.cancel();
                            try {
                                calSensor(calData);
                            } catch (Exception ex) {
                                try {
                                    throw ex;
                                } catch (Exception ex1) {
                                     hRefresh.sendEmptyMessage(5);
                                }
                            }
                            hRefresh.sendEmptyMessage(eventNumber);
                        }
                    }
                },
                0,
                UPDATE_INTERVAL);
    }

    private void addCalData(LinkedList cD) {
        Point p = acc.getPoint();
        cD.add(p);
        acc.reset();
    }

    private void calSensor(LinkedList cD) throws Exception {
        if (cD.size() < ITERATIONS-1) {
            throw new Exception("not enough data to calibrate");
        }
        float x = 0;
        float y = 0;
        float z = 0;
        // Don't use first measure
        for (int i = 1; i < cD.size(); ++i) {
            x += cD.get(i).getX();
            y += cD.get(i).getY();
            z += cD.get(i).getZ();
        }

        x = x / (cD.size() - 1);
        y = y / (cD.size() - 1);
        z = z / (cD.size() - 1);

        acc.setdX(-x);
        acc.setdY(-y);
        acc.setdZ(-z);
    }
}

 

maintenance class to keep data of one measure

  
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;
    }
    
    
    public float getForce(){
        return getX()*getX()+getY()*getY()+getZ()*getZ();
    }
}
 

And class to process data of measure
  
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;
    private Point averagePoint;

    public MeasurePoint(float x, float y, float z, float speedBefore, long interval, Point averagePoint) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.speedBefore = speedBefore;
        this.interval = interval;
        this.averagePoint = averagePoint;
        speedAfter = 0;
        calc();
    }
    
    private void calc(){
        //Acceleration as projection of current vector on average
        acceleration = this.x*averagePoint.getX() +
                        this.y*averagePoint.getY() +
                        this.z*averagePoint.getZ();
        acceleration = acceleration / ((float)Math.sqrt(averagePoint.getForce()));
        float t = ((float)interval / 1000f);
        speedAfter = speedBefore + acceleration * t;
        distance = speedBefore*t + acceleration*t*t/2;
        
    }
    
    public String getStoreString(){
        String s = "write here whatever you want";
        return s;
    }

// add getters
}
 

This one - to store and save data array
  
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, getAveragePoint()));
        }
    }
    
    public boolean saveExt(Context con, String fname) throws Throwable {

        try {

            File file = new File(con.getExternalFilesDir(null), fname);
            FileOutputStream os = new FileOutputStream(file);
            OutputStreamWriter out = new OutputStreamWriter(os);


            for (int i = 0; i < data.size(); ++i) {
                MeasurePoint m = data.get(i);
                out.write(m.getStoreString());
            }

            out.close();
        } catch (Throwable t) {
            throw (t);
        }
        return true;
    }

    private Point getAveragePoint() {
        float x = 0;
        float y = 0;
        float z = 0;
        
        for(int i = 0; i < accData.size(); ++i){
            Point p = accData.get(i);
            x += p.getX();
            y += p.getY();
            z += p.getZ();
        }
        
        return new Point(x, y, z, 1);
    }
    
    public float getLastSpeed(){
        return data.getLast().getSpeedAfter();
    }
    
    public float getLastSpeedKm(){
        float ms = getLastSpeed();
        return ms*3.6f;
    }
}

 

And, finally, how to use all this in your activity(I cleaned it up a lot, sorry if it will not complie - fill free to write it in comments:

  
public class TestActivity extends Activity {


    static final int TIMER_DONE = 2;
    static final int START = 3;
    static final int CAL_TIMER_DONE = 4;
    static final int ERROR = 5;

    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 + " " + es2 + " \n");
                    enableButtons();
                    break;
                case START:
                    tv.append(" START");
                    timer = new Timer();
                    timer.scheduleAtFixedRate(
                            new TimerTask() {

                                public void run() {
                                    dumpSensor();
                                }
                            },
                            0,
                            UPDATE_INTERVAL);

                    break;
                case ERROR:
                    Toast.makeText(getApplicationContext(), "ERROR", Toast.LENGTH_SHORT).show();
                    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("");
        tv.append("Calibrating");
        Calibrator cal = new Calibrator(hRefresh, xyzAcc, START);
        cal.calibrate();
        
    }

    void dumpSensor() {
        ++counter;
        mdXYZ.addPoint(xyzAcc.getPoint());

        hRefresh.sendEmptyMessage(TICK);
          
        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() {
        try {
            mdXYZ.process();
            long now = System.currentTimeMillis();
            mdXYZ.saveExt(this, Long.toString(now) + ".csv");
        } catch (Throwable ex) {
            Toast.makeText(this, ex.getMessage().toString(), Toast.LENGTH_SHORT);
        }
    }
}


<serviceLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <serviceButton 
        android:id="@+id/btn"
        android:text="TEST"
        android:layout_width="300px"
        android:layout_height="200px"
        android:onClick="onButtonTest"  />

        <serviceTextView  
    android:id = "@+id/txt"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text=":"
    />
<service/LinearLayout>