Skip to content

RobTillaart/AverageAngle

Repository files navigation

Arduino CI Arduino-lint JSON check GitHub issues

License: MIT GitHub release PlatformIO Registry

AverageAngle

Arduino library to calculate correctly the average of multiple angles.

Description

AverageAngle is a class to calculate the average of angles.

This is especially useful when angles are around 0 degrees, e.g. from a compass sensor or the resultant of a track. Example, the average angle of 359 and 1 is 0, not 179 (most of the time)

Furthermore the AverageAngle can also include the length (weight) of the angle as if it is a vector. Default this length is set to 1 so all angles are by default of equal weight.

Example: The average angle of 359 (length = 2) and 1 (length = 1) is 359.something not zero.

The average angle is calculated by converting the added angle (in DEGREES, etc) to polar coordinates. These (x,y) are added to a (sumX, sumY) and divided by the number of angles added so far.

Related

AngleType

  • enum AngleType { DEGREES, RADIANS, GRADIANS } idem.
value name range notes
0 DEGREES 0 .. 360
1 RADIANS 0 .. 2PI
2 GRADIANS 0 .. 400 100 GRADIANS == 90 DEGREES.

Interface

#include "AverageAngle.h"

Constructor

  • AverageAngle(AngleType type = DEGREES) constructor, defaults to degrees.
  • AngleType type() returns DEGREES, RADIANS or GRADIANS.
  • void setType(AngleType type) changes type DEGREES, RADIANS or GRADIANS. Type can be changed run time and still continue to add.
  • void reset() clears internal counters.

Core

  • uint32_t add(float alpha, float length = 1.0) add a new angle, optional with length other than 1. Returns the number of elements added so far (count). If the internal sumx or sumy is >= 10000, the error AVERAGE_ANGLE_OVERFLOW is set. This indicates that the internal math is near or over its accuracy limits.
  • uint32_t count() the number of angles added. If count == 0, there is no average.
  • float getAverage() returns the average, unless count == 0 or the internal sums == (0,0) in which case we have a singularity ==> NAN (not a number) If NAN the error AVERAGE_ANGLE_SINGULARITY is set.
  • float getTotalLength() the length of the resulting 'angle' when we see them as vectors. If count == 0 ==> total length = 0.
  • float getAverageLength() returns the average length of the angles added. If count == 0 ==> average length = 0.
  • float getSumX() get internal sumx counter. Rectangular coordinates.
  • float getSumY() get internal sumy counter. Rectangular coordinates.

Error handling

  • int lastError() return the last error detected.
name value
AVERAGE_ANGLE_OK 0
AVERAGE_ANGLE_OVERFLOW -10
AVERAGE_ANGLE_SINGULARITY -20

Experimental Overflow

(since 0.2.0)

When the internal sumx or sumy is large (> 10000) the accuracy of the addition becomes critical, leading to serious errors in the average and length functions. To detect this the function add() sets the error AVERAGE_ANGLE_OVERFLOW. This error can be checked with lastError(). The function add() will add the new angle as good as possible.

Note this condition is independent of the AngleType as the internal math uses radians. The condition will be triggered faster when the length parameter is used.

The overflow threshold of 10000 can be adjusted in the AverageAngle.cpp file if needed.

As this feature is experimental, the trigger condition for overflow will probably be redesigned in the future. See future section below.

Gradians

Gradians a.k.a. gon, is a less often used unit for angles. There are 100 gradians in a right angle. A full circle = 400 gradians.

https://en.wikipedia.org/wiki/Gradian

Other less used units for measuring angles:

Operation

If you want to average 5 compass readings you can just add the angles and do not use the length parameter.

  AA.reset();
  for (int i = 0; i < 5; i++)
  {
    AA.add(compass.readHeading());
    delay(100);    // e.g. compass read needs some time
  }
  Serial.println(AA.getAverage());

If you want to average a track, e.g. 5 steps North, 3 steps west etc, you need to include the length of each step.

  AA.reset();
  AA.add(90, 5);     //  5 steps north  assuming east = 0
  AA.add(180, 3);    //  3 steps west
  Serial.println(AA.getAverage());
  Serial.println(AA.getTotalLength());

If you want to average an angle in DEGREES and an angle in RADIANS, just change the type runtime.

  AA.reset();
  AA.setType(DEGREES);
  AA.add(90);
  AA.setType(RADIANS);
  AA.add(PI/3);
  AA.setType(DEGREES);
  Serial.println(AA.getAverage());

Future

Must

  • investigate if and how the internal math can be made more robust against overflow.
    • use double instead of float (will work on certain platforms) (must) => 0.3.0
    • uint32_t?
    • accuracy threshold depends on float / double usage. (sizeof(double) == 8)
    • threshold depends on the units of length. if all add's are using 10000 as length they have equal weight. normalizing the weight? how? user responsibility?
    • get set threshold via API?
    • use of threshold versus error detection (sum - angle == previous or not)
    • split OVERFLOW error in X and Y

Should

  • add performance example
  • add overflow example
  • add singularity example

Could

  • uint32_t addDegrees(float angle) more explicit.
  • uint32_t addRadians(float angle) idem.
  • uint32_t addGradians(float angle) idem.

Wont

  • add a USER AngleType, in which the user can map 0..360 degrees to any range.
    • float userFactor = 1.0; (default)
    • can even be negative?
    • use cases? e.g 0..4 quadrant?
    • maybe better for the AngleConvertor class.

Support

If you appreciate my libraries, you can support the development and maintenance. Improve the quality of the libraries by providing issues and Pull Requests, or donate through PayPal or GitHub sponsors.

Thank you,