2021-01-29 12:31:58 +01:00
|
|
|
|
|
|
|
|
|
[![Arduino CI](https://github.com/RobTillaart/runningAngle/workflows/Arduino%20CI/badge.svg)](https://github.com/marketplace/actions/arduino_ci)
|
2021-12-28 09:50:06 +01:00
|
|
|
|
[![Arduino-lint](https://github.com/RobTillaart/runningAngle/actions/workflows/arduino-lint.yml/badge.svg)](https://github.com/RobTillaart/runningAngle/actions/workflows/arduino-lint.yml)
|
|
|
|
|
[![JSON check](https://github.com/RobTillaart/runningAngle/actions/workflows/jsoncheck.yml/badge.svg)](https://github.com/RobTillaart/runningAngle/actions/workflows/jsoncheck.yml)
|
2021-01-29 12:31:58 +01:00
|
|
|
|
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/RobTillaart/runningAngle/blob/master/LICENSE)
|
|
|
|
|
[![GitHub release](https://img.shields.io/github/release/RobTillaart/runningAngle.svg?maxAge=3600)](https://github.com/RobTillaart/runningAngle/releases)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# runningAngle
|
|
|
|
|
|
|
|
|
|
Arduino library to calculate the running average of a series of angles.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Description
|
|
|
|
|
|
|
|
|
|
This library provides a class, `runningAngle`, that computes an
|
|
|
|
|
exponentially weighted running average of a series of angles, such as
|
|
|
|
|
compass readings. It is aware of how angles wrap modulo 360°.
|
|
|
|
|
|
|
|
|
|
The [exponentially weighted running average][ewra] is a type of [running
|
|
|
|
|
average][ra] that averages all the past inputs with weights that
|
|
|
|
|
decrease exponentially as the inputs get older. It is a type of digital
|
|
|
|
|
filter very commonly used for smoothing noisy sensor readings. It is
|
|
|
|
|
more memory efficient than the simple running average, while providing
|
|
|
|
|
similar smoothing capability.
|
|
|
|
|
|
|
|
|
|
Computing an “average” of angular data, such as headings, is inherently
|
|
|
|
|
an ambiguous problem. For example, given the headings 350° and 10°,
|
|
|
|
|
there are two possible “averages” that lie halfway between them, namely
|
|
|
|
|
0° and 180°. This library assumes that the “correct” average is the one
|
|
|
|
|
that lies in the middle of the shorter arc joining the initial headings,
|
|
|
|
|
thus 0°. This is the right choice for smoothing noisy sensor readings,
|
|
|
|
|
assuming the peak-to-peak amplitude of the noise is not too large. Note
|
|
|
|
|
that the regular average of the numbers 350 and 10 is 180, which is not
|
|
|
|
|
the result we expect when averaging angles.
|
|
|
|
|
|
|
|
|
|
This library is a spin off of [AverageAngle][], based on [an issue][]
|
|
|
|
|
raised by Edgar Bonet.
|
|
|
|
|
|
|
|
|
|
[ewra]: https://en.wikipedia.org/wiki/Exponential_smoothing
|
|
|
|
|
[ra]: https://en.wikipedia.org/wiki/Moving_average
|
|
|
|
|
[AverageAngle]: https://github.com/RobTillaart/AverageAngle
|
|
|
|
|
[an issue]: https://github.com/RobTillaart/AverageAngle/issues/1
|
|
|
|
|
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
#### Related
|
|
|
|
|
|
|
|
|
|
- https://github.com/RobTillaart/Angle
|
|
|
|
|
- https://github.com/RobTillaart/AngleConvertor
|
|
|
|
|
- https://github.com/RobTillaart/AverageAngle
|
|
|
|
|
- https://github.com/RobTillaart/RunningAngle
|
|
|
|
|
- https://github.com/RobTillaart/RunningAverage
|
|
|
|
|
- https://github.com/RobTillaart/RunningMedian
|
|
|
|
|
|
|
|
|
|
|
2021-01-29 12:31:58 +01:00
|
|
|
|
## Smoothing coefficient
|
|
|
|
|
|
|
|
|
|
The output of the filter is efficiently computed as a weighted average
|
|
|
|
|
of the current input and the previous output:
|
|
|
|
|
|
|
|
|
|
output = α × current\_input + (1 − α) × previous\_output
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
The smoothing coefficient, α, is the weight of the current input in the average.
|
|
|
|
|
It is called “weight” within the library, and should be set to a value between
|
|
|
|
|
0.001 and 1. The larger the weight (closer to 1), the weaker the smoothing,
|
|
|
|
|
so noise will affect the average quite a bit.
|
|
|
|
|
Closer to zero, new values only affect the average minimal.
|
|
|
|
|
- A weight α = 1 provides no smoothing at all, as the
|
|
|
|
|
filter's output will be a copy of the input.
|
|
|
|
|
- A weight α = 0 will return only the first value added.
|
|
|
|
|
Therefore the weight should be minimal 0.001.
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
|
|
|
|
The filter has a smoothing performance similar to a simple running
|
|
|
|
|
average over N = 2/α − 1 samples. For example, α = 0.2 is similar to
|
|
|
|
|
averaging over the last 9 samples.
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
It is important to do test runs to find the optimal coefficient (range)
|
|
|
|
|
for your application. One should be aware that the frequency of adding
|
|
|
|
|
new angles will affect the time to stabilize the output.
|
|
|
|
|
|
|
|
|
|
Note also that it is possible to change the weight run-time.
|
|
|
|
|
This can be needed e.g. if the accuracy of the input improves over time.
|
|
|
|
|
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
|
|
|
|
## Usage
|
|
|
|
|
|
|
|
|
|
First, create a filter as an instance of `runningAngle`:
|
|
|
|
|
|
|
|
|
|
```c++
|
|
|
|
|
runningAngle my_filter(runningAngle::DEGREES);
|
|
|
|
|
```
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
The parameter of the constructor should be
|
|
|
|
|
`runningAngle::DEGREES`, `runningAngle::RADIANS` or `runningAngle::GRADIANS`.
|
|
|
|
|
This parameter is optional and defaults to degrees.
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
|
|
|
|
Then, set the “weight” smoothing coefficient:
|
|
|
|
|
|
|
|
|
|
```c++
|
|
|
|
|
my_filter.setWeight(0.2);
|
|
|
|
|
```
|
2023-02-22 12:50:23 +01:00
|
|
|
|
Note: the weight defaults to 0.80 so no need to set
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
|
|
|
|
Finally, within the main sketch's loop, feed the raw angle readings to
|
|
|
|
|
the filter's `add()` method:
|
|
|
|
|
|
|
|
|
|
```c++
|
|
|
|
|
float heading = get_a_compass_reading_somehow();
|
|
|
|
|
float smoothed_heading = my_filter.add(heading);
|
|
|
|
|
```
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
The method returns the smoothed reading within ± 180° (i.e. ± π radians) by default.
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
The returned value is easily mapped upon 0..360 by adding 360 degrees ( 2π )
|
|
|
|
|
to the average if the average < 0.
|
|
|
|
|
Other mappings including scaling are of course possible.
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
Note: Degree character ° = ALT-0176 (windows)
|
2022-05-29 11:19:11 +02:00
|
|
|
|
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
|
|
|
|
## Interface
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
```cpp
|
|
|
|
|
#include "runningAngle.h"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### AngleType
|
2022-05-29 11:19:11 +02:00
|
|
|
|
|
|
|
|
|
- **enum AngleType { DEGREES, RADIANS, GRADIANS }** used to get type math right.
|
|
|
|
|
|
|
|
|
|
A full circle is defined as:
|
|
|
|
|
- DEGREES = 360°
|
|
|
|
|
- RADIANS = 2 π = 6.283...
|
|
|
|
|
- GRADIANS = 400°
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
GRADIANS are sometimes called GON.
|
|
|
|
|
|
2022-05-29 11:19:11 +02:00
|
|
|
|
There also exists a type milli-radians which is effectively the
|
2023-02-22 12:50:23 +01:00
|
|
|
|
same as RADIANS \* 1000. This won't be supported.
|
|
|
|
|
mRad and other angle-types can be converted here - https://github.com/RobTillaart/AngleConvertor
|
2022-05-29 11:19:11 +02:00
|
|
|
|
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
#### runningAngle
|
2022-05-29 11:19:11 +02:00
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
- **runningAngle(AngleType type = DEGREES)** constructor, default to DEGREES.
|
|
|
|
|
- **float add(float angle)** adds a new value using a defined weight.
|
|
|
|
|
Except for the first value after a reset, which is used as initial value.
|
2022-11-23 17:07:12 +01:00
|
|
|
|
The **add()** function returns the new average.
|
2023-02-22 12:50:23 +01:00
|
|
|
|
- **void reset()** resets the internal average to 0 and weight to start "clean" again.
|
2022-05-29 11:19:11 +02:00
|
|
|
|
If needed one should call **setWeight()** again!
|
2021-01-29 12:31:58 +01:00
|
|
|
|
- **float getAverage()** returns the current average value.
|
2023-02-22 12:50:23 +01:00
|
|
|
|
- **void setWeight(float weight = RA_DEFAULT_WEIGHT)** sets the weight of the new added value.
|
|
|
|
|
Value will be constrained between 0.001 and 1.0.
|
|
|
|
|
Default weight = RA_DEFAULT_WEIGHT == 0.80.
|
|
|
|
|
- **float getWeight()** returns the current / set weight.
|
2022-05-29 11:19:11 +02:00
|
|
|
|
- **AngleType type()** returns DEGREES, RADIANS or GRADIANS.
|
2023-02-22 12:50:23 +01:00
|
|
|
|
- **float wrap(float angle)** wraps an angle to \[-180..+180> \[-PI..PI>
|
|
|
|
|
or \[-200..200> depending on the type set.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### Mode
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
- **void setMode0()** average interval = \[-180..180>
|
|
|
|
|
- **void setMode1()** average interval = \[0..360>
|
|
|
|
|
- **uint8_t getMode()** returns current mode = 0 or 1.
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
## Performance add()
|
|
|
|
|
|
|
|
|
|
Being the most important worker function, doing float math.
|
|
|
|
|
(based on time-add.ino on UNO)
|
|
|
|
|
|
|
|
|
|
| version | mode | CPU cycles | us per add | relative |
|
|
|
|
|
|:---------:|:------:|-------------:|-------------:|-----------:|
|
|
|
|
|
| 0.1.5 | 0 | 681 | 42.5625 us | 100% |
|
|
|
|
|
| 0.2.0 | 0 | 681 | 42.5625 us | 100% |
|
|
|
|
|
| 0.2.0 | 1 | 681 | 42.5625 us | 100% |
|
2021-01-29 12:31:58 +01:00
|
|
|
|
|
2021-12-28 09:50:06 +01:00
|
|
|
|
|
|
|
|
|
## Future
|
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
#### Must
|
|
|
|
|
|
|
|
|
|
- improve documentation.
|
|
|
|
|
|
|
|
|
|
#### Should
|
2022-11-23 17:07:12 +01:00
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
- should **add()** return the average? (yes)
|
|
|
|
|
- or make a **fastAdd()** that doesn't?
|
2022-11-23 17:07:12 +01:00
|
|
|
|
|
2023-02-22 12:50:23 +01:00
|
|
|
|
#### Could
|
|
|
|
|
|
|
|
|
|
- add examples.
|
|
|
|
|
- compass HMC6352 lib or simulator.
|
|
|
|
|
- AS5600 angle measurement sensor
|
|
|
|
|
- update unit tests.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### Wont
|
|
|
|
|
|
|
|
|
|
- get statistics about the noise in the angles (stats on the delta?).
|
|
|
|
|
- not the goal of this lib ==> use statistics library
|
|
|
|
|
- optimize **wrap()** to be generic => no loop per type.
|
|
|
|
|
- needs variables for -180 / 180 / 360 (RAM vs PROGMEM)
|
|
|
|
|
- derived class for degrees only? (max optimization)
|
|
|
|
|
- runtime change of type
|
|
|
|
|
- no, too specific scenario.
|
|
|
|
|
- conversion needed?
|
|
|
|
|
- add mixed types. 45° + 3 radians = ??
|
|
|
|
|
==> user can do this.
|
2021-12-28 09:50:06 +01:00
|
|
|
|
|