Files
twislaandClaude Opus 5.5 da311a11b6 GNSS: a heading from the gyroscope, the course and the sun
The device has no magnetometer (issue #16): north is given by the course
over ground while walking, or by aiming at the sun, and the gyroscope
follows the turn around the vertical in between. With the sun's position
from the time and the place, the bearing to a point, the 16 points and
distances as text. Host-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
2026-10-11 17:01:57 +02:00

124 lines
4.7 KiB
C++

#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