четвртак, 20. септембар 2012.

Welcome to My Blog, Folks!

All right, so here it is - my very first blog and it is about Android. Being firstly educated as a "classic" engineer I am fascinated by Android as a platform that glues together ease of coding with access to web technologies, mobile communications and various sensors and stuff. In short I consider Android to be the "Swiss Army Knife" in mobile communications platforms, the real successor of "Computer Everywhere" idea that kickstarted with appearance of IBM PC. Sure, it might not be as slick and shiny and polished up as its main competitor(s), but it is good enough to create valuable applications and user experience, and it will only get better and better. But most of all, to me personally, it represents a valuable source of mental relaxation, a joy of programming being rediscovered, after a hard day filled with people-customer-support problems and issues.
Enough talk, let's go to work! The first example here will do a couple of things with sensors.

The idea: Tiltmeter (inclinometer)

By this tilt I mean to determine how horizontal (or vertical) an edge or surface is. In construction jobs inclination is usually measured by air bubble meter. Measuring tilt should be simple - all you need to do is measure the angle that one of the edges of your Android phone forms with horizontal plane. So, it is to do with orientation, right? Well... yes, it could be accomplished with the orientation sensor (it is actually software that combines readings from several hardware sensors) but it seems too complex for such a simple task. Could it be done another way? Perhaps by using direct readings from one of the hardware sensors? Actually... yes.

Gravity to the rescue

The question: Is there a sensor onboard Android phone that can give us a horizontal or vertical direction? First we must take into consideration sensors that are used to mimic the direction sensor - the acceleration and magnetic field sensors. And there it is - acceleration sensor, as developer documentation says, "Measures the acceleration force in m/s2that is applied to a device on all three physical axes (x, y, and z), including the force of gravity...." A-ha. And the force of gravity is the one that gives us vertical direction. 
Ok, so we have picked up a sensor that we will be using, now, what do we do with it? Basically, I want to use the longest edge of the phone casing to lean against the surface whose inclination I wish to measure. So let us see how are the axes placed for the Android's acceleration sensor:
Android gravity sensor coordinate system
z-axis is pointing straight upwards. 
So now it is all clear about the axes: I want the phone edge along the y-axis to be the one I measure inclination with. I will measure the angle that this edge (y-axis) builds with horizontal plane - that is, inclination. I also want to avoid small wobbling of the phone to influence results, once I lean the phone up against the measured surface, therefore I have to take into consideration only things that are happening within the x-y plane, and to reject the z-component of gravity vector. 
The inclination is then calculated as:
public void onSensorChanged(SensorEvent event) {
float _valX = event.values[0];
float _valY = event.values[1];
float _valZ = event.values[2];
... 
double _rat = _valX / _valY;
float _sign = 1.0f
float _angle = (float) (Math.atan(_rat) * (360.0/2/Math.PI));
...
onSensorChanged() is standard callback method for various sensor and their events. It is naturally registered within the Main activity because we want to do the measurements only when our application is on top! 

Showing the Result

We can display the angle being measured in two chief forms, analog and digital. Each one has its (dis)advantages compared to the other one, therefore the best way is to display them both, combined. For analog display I picked a "classic" degrees/radians division circle:
Degrees/Radians Division Circle
I will rotate this image for the amount of measured angle that gravity vector builds with horizontal plane. The digital information (inclination angle in deg.) will be put in the middle of this circle - there is enough empty space there for this purpose, and this kind of layout will allow the deg/rad circle to bi as big as possible. The final result goes like this:

Analog and Digital display

Fine Tuning

So far things have been "rude and crude", it is now time to add some fine-tuning to this little application:

  • It would be nice to be able to measure vertical tilt as well
  • In order to facilitate reading a "freeze" function can be added
  • To make readings more smooth an averaging filter can be introduced. I chose a simple yet effective "Exponential moving average" filter, google for it and you will find plenty of information
  • When the tilt angle becomes too steep you can obtain inacurate results or get yourself an Exception, so when this happens measurement calculations must stop and user needs to be informed on this.
  • Also if the z-component of gravity vector becomes predominant, x-y component will become small, unstable and inaccurate. This happens, for example, if you put your phone on the table. Again, in this situation calculations must stop and user must be informed. 
Here is the entire sensor change callback function with these things added:
public void onSensorChanged(SensorEvent event) {
    float _valX = event.values[0];
    float _valY = event.values[1];
    float _valZ = event.values[2];

    //if Z-component prevails, measurement becomes unstable and inaccurate
    isOutOfRange = (_valZ > 3* android.util.FloatMath.sqrt(_valX*_valX + _valY*_valY)); 

    if (!isOutOfRange) {
        double _rat = !isPortrait ? _valY / _valX : _valX / _valY;
        float _sign = isPortrait ? 1.0f : -1.0f;
        float _angle = (float) (Math.atan(_rat) * (360.0/2/Math.PI));
        if (_angle != Float.NaN) {
            averageTilt.average(_angle);
        }

    //tilt over 75 degrees is dangerously close to atan() going to infinity
    if (averageTilt.getCurrentValue() != null && Math.abs(averageTilt.getCurrentValue()) > 75) {
        setValueError(true);
        isOutOfRange = true;
    } else {
        setValueError(false);
        isOutOfRange = false;
    }

    if(!isOutOfRange && !isFrozen) {
        tvTiltValue.setText(mNumberFormat.format(averageTilt.average(_angle)));

        RotateAnimation rotation = new RotateAnimation(_sign*averageTilt.getOldValue(), _sign*averageTilt.getCurrentValue(), ivCircleDeg.getWidth()/2.0f, ivCircleDeg.getHeight()/2.0f);
        rotation.setFillAfter(true);
        rotation.setDuration(0L);
        rotation.setStartOffset(0L);
        ivCircleDeg.startAnimation(rotation);
        } 
    } else {
       setValueError(true);
    }
}
The Activity is registered as listener for sensor events at system SensorManager object inside the onResume() method.

Future work

Here are some more suggestions on possible improvements of this little app:
  • Memorize last 5 (10, 20, 50, 100...) measurements and show them in a list.
  • Send measurement to an email or SMS
  • Make the exponential filter better, e.g. use variable dampening ("alpha") coefficient, that changes with rotation speed of the deg/rad circle
  • Combine inclination angle with GPS coordinates - this is for all those that love building Ancient Roman aqueducts :) 
  • Add callibration utility - the acceleration sensor is mounted on the phone by the pick'n'place machine, so it is pretty well alligned with phone casing but is probably off by couple of degrees. 
  • :) Design a phone that is built speciffically for tilt measurements:
    Brickmasters' favourite phone

Conclusion

That's it for this post. This application is more about sensors and data acquisition, filtering and interpretation than it is about Android, but sensors are part of everyday Android life and deserve an application every now and then. In the next post I will try to cover something a bit more conventional - making photos, doing GPS location, working with RESTful and standard web services etc.

P.S. You can take finished version of this application from here. Tested on Android 2.3 and 3.0. You must enable "unknown sources" at Settings to be able to install this application... but you know that already!