Public Access
GNSS App: a Compass, with Places and a level #98
@@ -0,0 +1,123 @@
|
|||||||
|
#include "compass.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
namespace roro::gnss {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr double kRad = M_PI / 180.0;
|
||||||
|
// The sensor's axes are right-handed, so a positive turn around "up" is anticlockwise seen from
|
||||||
|
// above, and the heading (clockwise) goes down. Measured on the device: issue #16.
|
||||||
|
constexpr float kTurnSign = -1.0f;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
float wrap360(float degrees) {
|
||||||
|
float d = std::fmod(degrees, 360.0f);
|
||||||
|
return d < 0 ? d + 360.0f : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
float angleDiff(float to, float from) {
|
||||||
|
float d = wrap360(to - from);
|
||||||
|
return d >= 180.0f ? d - 360.0f : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
double bearingDegrees(double lat1, double lon1, double lat2, double lon2) {
|
||||||
|
double p1 = lat1 * kRad, p2 = lat2 * kRad, dl = (lon2 - lon1) * kRad;
|
||||||
|
double y = std::sin(dl) * std::cos(p2);
|
||||||
|
double x = std::cos(p1) * std::sin(p2) - std::sin(p1) * std::cos(p2) * std::cos(dl);
|
||||||
|
double b = std::atan2(y, x) / kRad;
|
||||||
|
return b < 0 ? b + 360.0 : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* cardinal(float degrees) {
|
||||||
|
static const char* const kPoints[16] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
|
||||||
|
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"};
|
||||||
|
return kPoints[static_cast<int>(wrap360(degrees) / 22.5f + 0.5f) % 16];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string formatDistance(double meters) {
|
||||||
|
char buf[24];
|
||||||
|
if (meters < 999.5) std::snprintf(buf, sizeof buf, "%.0f m", meters);
|
||||||
|
else if (meters < 9950.0) std::snprintf(buf, sizeof buf, "%.1f km", meters / 1000.0);
|
||||||
|
else std::snprintf(buf, sizeof buf, "%.0f km", meters / 1000.0);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
SunPosition sunPosition(int64_t utcSeconds, double latitude, double longitude) {
|
||||||
|
double d = utcSeconds / 86400.0 - 10957.5; // days since 2000-01-01 12:00 UTC
|
||||||
|
double g = (357.529 + 0.98560028 * d) * kRad; // mean anomaly
|
||||||
|
double q = 280.459 + 0.98564736 * d; // mean longitude
|
||||||
|
double l = (q + 1.915 * std::sin(g) + 0.020 * std::sin(2 * g)) * kRad; // ecliptic longitude
|
||||||
|
double e = (23.439 - 0.00000036 * d) * kRad; // the ecliptic's tilt
|
||||||
|
double rightAscension = std::atan2(std::cos(e) * std::sin(l), std::cos(l));
|
||||||
|
double declination = std::asin(std::sin(e) * std::sin(l));
|
||||||
|
double siderealDeg = std::fmod(18.697374558 + 24.06570982441908 * d, 24.0) * 15.0; // at Greenwich
|
||||||
|
double hourAngle = (siderealDeg + longitude) * kRad - rightAscension;
|
||||||
|
double lat = latitude * kRad;
|
||||||
|
double elevation = std::asin(std::sin(lat) * std::sin(declination) +
|
||||||
|
std::cos(lat) * std::cos(declination) * std::cos(hourAngle));
|
||||||
|
double azimuth = std::atan2(-std::sin(hourAngle),
|
||||||
|
std::tan(declination) * std::cos(lat) - std::sin(lat) * std::cos(hourAngle));
|
||||||
|
return {wrap360(static_cast<float>(azimuth / kRad)), static_cast<float>(elevation / kRad)};
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadingFilter::motion(const float gyro[3], const float accel[3], float dt) {
|
||||||
|
if (dt <= 0) return;
|
||||||
|
// "Up" is where the accelerometer points at rest, smoothed so a shake doesn't move it.
|
||||||
|
float norm = std::sqrt(accel[0] * accel[0] + accel[1] * accel[1] + accel[2] * accel[2]);
|
||||||
|
if (norm > 0.5f && norm < 1.5f) {
|
||||||
|
float k = any_ ? std::fmin(1.0f, dt / kUpSeconds) : 1.0f;
|
||||||
|
float length = 0;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
up_[i] += k * (accel[i] / norm - up_[i]);
|
||||||
|
length += up_[i] * up_[i];
|
||||||
|
}
|
||||||
|
length = std::sqrt(length);
|
||||||
|
if (length > 0.01f)
|
||||||
|
for (float& u : up_) u /= length;
|
||||||
|
any_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float rate[3], turning = 0;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
rate[i] = gyro[i] - bias_[i];
|
||||||
|
turning += rate[i] * rate[i];
|
||||||
|
}
|
||||||
|
if (std::sqrt(turning) < kStillDps) {
|
||||||
|
stillFor_ += dt;
|
||||||
|
if (stillFor_ >= kStillSeconds) { // lying still: what the gyroscope still reads is its offset
|
||||||
|
float k = std::fmin(1.0f, dt / kBiasSeconds);
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
bias_[i] += k * rate[i];
|
||||||
|
rate[i] = gyro[i] - bias_[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else stillFor_ = 0;
|
||||||
|
|
||||||
|
if (!known()) return;
|
||||||
|
float aroundUp = rate[0] * up_[0] + rate[1] * up_[1] + rate[2] * up_[2];
|
||||||
|
heading_ = wrap360(heading_ + kTurnSign * aroundUp * dt);
|
||||||
|
held_ += dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadingFilter::course(float courseDeg, float speedKmh) {
|
||||||
|
if (speedKmh < kMinSpeedKmh) return;
|
||||||
|
heading_ = known() ? wrap360(heading_ + kCourseGain * angleDiff(courseDeg, heading_)) : wrap360(courseDeg);
|
||||||
|
source_ = Source::Course;
|
||||||
|
held_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadingFilter::sun(float azimuthDeg) {
|
||||||
|
heading_ = wrap360(azimuthDeg);
|
||||||
|
source_ = Source::Sun;
|
||||||
|
held_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadingFilter::forget() {
|
||||||
|
source_ = Source::None;
|
||||||
|
held_ = 0;
|
||||||
|
stillFor_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace roro::gnss
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace roro::gnss {
|
||||||
|
|
||||||
|
// Angles are degrees from true north, clockwise, 0 up to 360.
|
||||||
|
float wrap360(float degrees);
|
||||||
|
// How far to turn from `from` to `to`: -180 up to 180, positive clockwise.
|
||||||
|
float angleDiff(float to, float from);
|
||||||
|
// The initial great-circle bearing from the first point to the second.
|
||||||
|
double bearingDegrees(double lat1, double lon1, double lat2, double lon2);
|
||||||
|
// One of the 16 points: "N", "NNE", "NE"...
|
||||||
|
const char* cardinal(float degrees);
|
||||||
|
// "340 m", "1.2 km", "12 km".
|
||||||
|
std::string formatDistance(double meters);
|
||||||
|
|
||||||
|
// Where the sun is in the sky, from the time and the place (the low-precision formulas of the
|
||||||
|
// Astronomical Almanac: within a hundredth of a degree until 2050, far finer than a hand can aim).
|
||||||
|
struct SunPosition {
|
||||||
|
float azimuthDeg;
|
||||||
|
float elevationDeg; // above the horizon; refraction is left out
|
||||||
|
};
|
||||||
|
SunPosition sunPosition(int64_t utcSeconds, double latitude, double longitude);
|
||||||
|
|
||||||
|
// The Compass's heading (issue #16): where the top of the device points. There is no
|
||||||
|
// magnetometer, so north comes from outside (the course over ground while walking, or the sun
|
||||||
|
// when the user aims at it) and the gyroscope follows the turns in between. Host-tested.
|
||||||
|
class HeadingFilter {
|
||||||
|
public:
|
||||||
|
enum class Source : uint8_t { None, Course, Sun };
|
||||||
|
// Below a walking pace the receiver's course is noise.
|
||||||
|
static constexpr float kMinSpeedKmh = 3.0f;
|
||||||
|
// Each course reading pulls the heading this much of the way: a few seconds to settle,
|
||||||
|
// without jumping at every reading.
|
||||||
|
static constexpr float kCourseGain = 0.3f;
|
||||||
|
// The gyroscope's own offset is learnt while the device lies still: turning slower than
|
||||||
|
// this for kStillSeconds counts as still.
|
||||||
|
static constexpr float kStillDps = 2.0f;
|
||||||
|
static constexpr float kStillSeconds = 1.0f;
|
||||||
|
static constexpr float kBiasSeconds = 4.0f;
|
||||||
|
// How fast the measured "up" follows the accelerometer.
|
||||||
|
static constexpr float kUpSeconds = 0.3f;
|
||||||
|
|
||||||
|
// One reading of the IMU: degrees a second and g, on the sensor's axes, `dt` seconds after
|
||||||
|
// the one before. Only the turn around the vertical counts, however the device is held.
|
||||||
|
void motion(const float gyro[3], const float accel[3], float dt);
|
||||||
|
// The receiver's course over ground, once a second: used when moving fast enough.
|
||||||
|
void course(float courseDeg, float speedKmh);
|
||||||
|
// The top of the device is aimed at the sun, which is at this azimuth.
|
||||||
|
void sun(float azimuthDeg);
|
||||||
|
void forget(); // the device was not watched for a while: north has to come again
|
||||||
|
|
||||||
|
bool known() const { return source_ != Source::None; }
|
||||||
|
float heading() const { return heading_; }
|
||||||
|
Source source() const { return source_; }
|
||||||
|
// Seconds the gyroscope has carried the heading alone since north was last given.
|
||||||
|
float heldSeconds() const { return held_; }
|
||||||
|
// "Up" along the sensor's x and y axes, -1 to 1: both 0 when the device lies flat.
|
||||||
|
float tiltX() const { return up_[0]; }
|
||||||
|
float tiltY() const { return up_[1]; }
|
||||||
|
bool still() const { return stillFor_ >= kStillSeconds; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
float heading_ = 0, held_ = 0, stillFor_ = 0;
|
||||||
|
float up_[3] = {0, 0, 1}, bias_[3] = {0, 0, 0};
|
||||||
|
bool any_ = false;
|
||||||
|
Source source_ = Source::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace roro::gnss
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "compass.h"
|
||||||
|
|
||||||
|
using namespace roro::gnss;
|
||||||
|
|
||||||
|
void setUp() {}
|
||||||
|
void tearDown() {}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const float kFlat[3] = {0, 0, 1};
|
||||||
|
const float kNoTurn[3] = {0, 0, 0};
|
||||||
|
|
||||||
|
// `seconds` of readings at 100 Hz.
|
||||||
|
void run(HeadingFilter& f, const float gyro[3], const float accel[3], float seconds) {
|
||||||
|
for (int i = 0; i < static_cast<int>(seconds * 100 + 0.5f); i++) f.motion(gyro, accel, 0.01f);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void test_angles_wrap() {
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.001, 350.0, wrap360(-10.0f));
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.001, 10.0, wrap360(370.0f));
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.001, 20.0, angleDiff(10.0f, 350.0f));
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.001, -20.0, angleDiff(350.0f, 10.0f));
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.001, -180.0, angleDiff(180.0f, 0.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_bearing_to_a_point() {
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 0.0, bearingDegrees(50.0, 4.0, 51.0, 4.0));
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 180.0, bearingDegrees(50.0, 4.0, 49.0, 4.0));
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.5, 90.0, bearingDegrees(50.0, 4.0, 50.0, 4.1));
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.5, 270.0, bearingDegrees(50.0, 4.0, 50.0, 3.9));
|
||||||
|
// Brussels to Paris: south-south-west.
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(1.0, 214.0, bearingDegrees(50.8503, 4.3517, 48.8566, 2.3522));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_cardinal_points() {
|
||||||
|
TEST_ASSERT_EQUAL_STRING("N", cardinal(0));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("N", cardinal(359));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("NNE", cardinal(22.5f));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("E", cardinal(90));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("WSW", cardinal(247));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("NNW", cardinal(340));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("N", cardinal(-5));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_distances_read_well() {
|
||||||
|
TEST_ASSERT_EQUAL_STRING("0 m", formatDistance(0.2).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("340 m", formatDistance(340.4).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1.0 km", formatDistance(999.6).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("1.2 km", formatDistance(1234).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("12 km", formatDistance(12345).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_the_sun_at_an_equinox() {
|
||||||
|
// 2024-03-20 03:06 UTC, the March equinox: the sun is over the equator. Twelve hours of
|
||||||
|
// turning later, at 12:07:30 UTC (the equation of time is -7.5 min), it is over Greenwich.
|
||||||
|
int64_t noon = 1710936450;
|
||||||
|
SunPosition s = sunPosition(noon, 0.0, 0.0);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.3, 90.0, s.elevationDeg);
|
||||||
|
s = sunPosition(noon, 50.0, 0.0); // from 50° north: due south, 40° up
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.5, 180.0, s.azimuthDeg);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.3, 40.0, s.elevationDeg);
|
||||||
|
s = sunPosition(noon, -50.0, 0.0); // from 50° south: due north
|
||||||
|
TEST_ASSERT_TRUE(s.azimuthDeg < 1.0f || s.azimuthDeg > 359.0f);
|
||||||
|
// Six hours earlier it rises, due east at an equinox, anywhere.
|
||||||
|
s = sunPosition(noon - 6 * 3600, 50.0, 0.0);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(1.0, 90.0, s.azimuthDeg);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(1.0, 0.0, s.elevationDeg);
|
||||||
|
s = sunPosition(noon + 6 * 3600, 50.0, 0.0); // and sets due west
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(1.0, 270.0, s.azimuthDeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_the_sun_at_a_solstice() {
|
||||||
|
// 2024-06-20 20:51 UTC: 23.44° north of the equator. At noon in Brussels that day
|
||||||
|
// (11:44 UTC by the sun) it stands 90 - 50.85 + 23.44 = 62.6° up, in the south.
|
||||||
|
SunPosition s = sunPosition(1718883840, 50.85, 4.35);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(1.0, 180.0, s.azimuthDeg);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.3, 62.6, s.elevationDeg);
|
||||||
|
// At midnight it is under the horizon, in the north.
|
||||||
|
s = sunPosition(1718883840 + 12 * 3600, 50.85, 4.35);
|
||||||
|
TEST_ASSERT_TRUE(s.elevationDeg < 0);
|
||||||
|
TEST_ASSERT_TRUE(s.azimuthDeg < 3.0f || s.azimuthDeg > 357.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_no_heading_until_north_is_given() {
|
||||||
|
HeadingFilter f;
|
||||||
|
const float turn[3] = {0, 0, 30};
|
||||||
|
run(f, turn, kFlat, 1);
|
||||||
|
TEST_ASSERT_FALSE(f.known());
|
||||||
|
f.course(120, 1.0f); // too slow: the course means nothing
|
||||||
|
TEST_ASSERT_FALSE(f.known());
|
||||||
|
f.course(120, 4.0f);
|
||||||
|
TEST_ASSERT_TRUE(f.known());
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 120.0, f.heading());
|
||||||
|
TEST_ASSERT_TRUE(f.source() == HeadingFilter::Source::Course);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_the_gyroscope_follows_a_turn() {
|
||||||
|
HeadingFilter f;
|
||||||
|
f.course(100, 5.0f);
|
||||||
|
// A positive turn around "up" is anticlockwise from above: the heading goes down.
|
||||||
|
const float left[3] = {0, 0, 45};
|
||||||
|
run(f, left, kFlat, 2);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.5, 10.0, f.heading());
|
||||||
|
const float right[3] = {0, 0, -45};
|
||||||
|
run(f, right, kFlat, 4);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.5, 190.0, f.heading());
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.05, 6.0, f.heldSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_only_the_turn_around_the_vertical_counts() {
|
||||||
|
HeadingFilter f;
|
||||||
|
f.course(0, 5.0f);
|
||||||
|
// Held upright, the screen facing the user: "up" is along the sensor's y.
|
||||||
|
const float upright[3] = {0, 1, 0};
|
||||||
|
run(f, kNoTurn, upright, 1);
|
||||||
|
const float roll[3] = {0, 0, 90}; // around the screen's own axis: no change of heading
|
||||||
|
run(f, roll, upright, 1);
|
||||||
|
TEST_ASSERT_TRUE(std::fabs(angleDiff(f.heading(), 0)) < 0.5f);
|
||||||
|
const float yaw[3] = {0, -90, 0}; // around the vertical
|
||||||
|
run(f, yaw, upright, 1);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(1.0, 90.0, f.heading());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_the_course_pulls_the_heading_gently() {
|
||||||
|
HeadingFilter f;
|
||||||
|
f.course(350, 5.0f);
|
||||||
|
f.course(10, 5.0f); // through north, the short way
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 356.0, f.heading());
|
||||||
|
for (int i = 0; i < 20; i++) f.course(10, 5.0f);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.1, 10.0, f.heading());
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.001, 0.0, f.heldSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_the_sun_sets_north() {
|
||||||
|
HeadingFilter f;
|
||||||
|
f.sun(203);
|
||||||
|
TEST_ASSERT_TRUE(f.known());
|
||||||
|
TEST_ASSERT_TRUE(f.source() == HeadingFilter::Source::Sun);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 203.0, f.heading());
|
||||||
|
f.forget();
|
||||||
|
TEST_ASSERT_FALSE(f.known());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_the_gyroscope_offset_is_learnt_at_rest() {
|
||||||
|
HeadingFilter f;
|
||||||
|
f.sun(90);
|
||||||
|
const float offset[3] = {0.3f, -0.2f, 0.8f}; // what a BMI270 reads lying still
|
||||||
|
run(f, offset, kFlat, 30);
|
||||||
|
TEST_ASSERT_TRUE(f.still());
|
||||||
|
float before = f.heading();
|
||||||
|
run(f, offset, kFlat, 60); // a minute later it has not drifted
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.2, before, f.heading());
|
||||||
|
// A real turn is still measured, with the offset taken off.
|
||||||
|
const float turn[3] = {0.3f, -0.2f, 0.8f - 90.0f};
|
||||||
|
run(f, turn, kFlat, 1);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.5, before + 90.0f, f.heading());
|
||||||
|
TEST_ASSERT_FALSE(f.still());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_tilt_follows_the_accelerometer() {
|
||||||
|
HeadingFilter f;
|
||||||
|
run(f, kNoTurn, kFlat, 1);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 0.0, f.tiltX());
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 0.0, f.tiltY());
|
||||||
|
const float tilted[3] = {0.5f, 0, 0.866f}; // 30° over
|
||||||
|
run(f, kNoTurn, tilted, 2);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 0.5, f.tiltX());
|
||||||
|
const float shaken[3] = {3, 3, 3}; // not gravity: ignored
|
||||||
|
run(f, kNoTurn, shaken, 1);
|
||||||
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 0.5, f.tiltX());
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_angles_wrap);
|
||||||
|
RUN_TEST(test_bearing_to_a_point);
|
||||||
|
RUN_TEST(test_cardinal_points);
|
||||||
|
RUN_TEST(test_distances_read_well);
|
||||||
|
RUN_TEST(test_the_sun_at_an_equinox);
|
||||||
|
RUN_TEST(test_the_sun_at_a_solstice);
|
||||||
|
RUN_TEST(test_no_heading_until_north_is_given);
|
||||||
|
RUN_TEST(test_the_gyroscope_follows_a_turn);
|
||||||
|
RUN_TEST(test_only_the_turn_around_the_vertical_counts);
|
||||||
|
RUN_TEST(test_the_course_pulls_the_heading_gently);
|
||||||
|
RUN_TEST(test_the_sun_sets_north);
|
||||||
|
RUN_TEST(test_the_gyroscope_offset_is_learnt_at_rest);
|
||||||
|
RUN_TEST(test_tilt_follows_the_accelerometer);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user