mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
Each file that contains copied code names its copyright holder and license and points at THIRD_PARTY_NOTICES.md; zlib code keeps its full notice and the mark that it was altered. Comments and probe text only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
6.6 KiB
C
154 lines
6.6 KiB
C
// Contains code Copyright (c) 2026 MacNativeGames contributors (MIT; see THIRD_PARTY_NOTICES.md).
|
|
// Six-axis sensor states (nn::hid::SixAxisSensorState) from gyro and accelerometer samples. Input per sample, in the
|
|
// Switch controller frame (x to the right, y toward the top edge, z out of the face): acceleration in G including
|
|
// gravity, as gravity reads (lying face up z = -1 G), and rotation rate in rad/s. Output per sample, as the Switch
|
|
// reports it: acceleration in G, angular velocity in revolutions per second, angle = the integrated revolutions per
|
|
// axis, direction = the controller's three axes in a reference frame whose z is up. The orientation is integrated from
|
|
// the gyro and, while the controller is not being accelerated (|a| within 15 % of 1 G), pulled toward the measured
|
|
// gravity (Mahony, gain 0.5/s: about 2 s), as the Switch's sensor fusion does; yaw has no reference and drifts with the
|
|
// gyro's bias. The newest MOTION_RING samples are kept; reads are newest first. A restart (StartSixAxisSensor) drops
|
|
// the samples before it: reads return nothing until the next sample, which starts a fresh frame and angle; sampling
|
|
// numbers go on. The feeding thread writes, guest threads read, both under the instance's SRW lock.
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "host.h"
|
|
#include "input.h"
|
|
#include "platform/platform.h"
|
|
|
|
enum { MOTION_RING = 64 };
|
|
#define STALE_NS 200000000ull // no sample for 200 ms: no motion
|
|
|
|
struct MotionFusion {
|
|
PlatMutex lock;
|
|
MotionState ring[MOTION_RING];
|
|
uint64_t produced, last_ns;
|
|
uint64_t first; // produced at the last restart: older samples are not read
|
|
double R[3][3]; // controller -> reference; row i = reference axis i in controller coordinates
|
|
double angle[3];
|
|
int restart;
|
|
};
|
|
|
|
MotionFusion *motion_new(void) {
|
|
MotionFusion *m = calloc(1, sizeof *m);
|
|
if (!m) rt_fatal("motion: out of memory");
|
|
plat_mutex_init(&m->lock);
|
|
m->restart = 1;
|
|
return m;
|
|
}
|
|
|
|
static MotionFusion *global;
|
|
|
|
static void global_init(void *arg) {
|
|
(void)arg;
|
|
global = motion_new();
|
|
}
|
|
|
|
MotionFusion *motion_global(void) {
|
|
static PlatOnce once = PLAT_ONCE_INIT;
|
|
plat_once(&once, global_init, NULL);
|
|
return global;
|
|
}
|
|
|
|
static double norm3(const double v[3]) { return sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); }
|
|
static void cross3(const double a[3], const double b[3], double out[3]) {
|
|
out[0] = a[1] * b[2] - a[2] * b[1];
|
|
out[1] = a[2] * b[0] - a[0] * b[2];
|
|
out[2] = a[0] * b[1] - a[1] * b[0];
|
|
}
|
|
// Rows, from the up row (the one the gravity correction keeps right).
|
|
static void orthonormalize(double R[3][3]) {
|
|
double n = norm3(R[2]);
|
|
for (int j = 0; j < 3; j++) R[2][j] /= n;
|
|
double d = R[0][0] * R[2][0] + R[0][1] * R[2][1] + R[0][2] * R[2][2];
|
|
for (int j = 0; j < 3; j++) R[0][j] -= d * R[2][j];
|
|
n = norm3(R[0]);
|
|
for (int j = 0; j < 3; j++) R[0][j] /= n;
|
|
cross3(R[2], R[0], R[1]);
|
|
}
|
|
// A fresh orientation: z up against the measured gravity, x as close to the controller's x as that allows.
|
|
static void align_to(double R[3][3], const double acc[3]) {
|
|
double n = norm3(acc), up[3] = {0, 0, 1}, x[3] = {1, 0, 0};
|
|
if (n > 0.5)
|
|
for (int j = 0; j < 3; j++) up[j] = -acc[j] / n;
|
|
if (fabs(up[0]) > 0.9) { x[0] = 0; x[1] = 1; }
|
|
memcpy(R[2], up, sizeof up);
|
|
memcpy(R[0], x, sizeof x);
|
|
orthonormalize(R);
|
|
}
|
|
// Body-frame rate: R = R * exp([w] dt), the rate plus the gravity pull.
|
|
static void integrate(double R[3][3], const double gyro[3], const double acc[3], double dt) {
|
|
double w[3] = {gyro[0], gyro[1], gyro[2]}, n = norm3(acc);
|
|
if (fabs(n - 1) < 0.15) { // not being accelerated: turn the estimated down toward the measured one
|
|
const double kp = 0.5;
|
|
double measured[3] = {acc[0] / n, acc[1] / n, acc[2] / n}, estimated[3] = {-R[2][0], -R[2][1], -R[2][2]}, e[3];
|
|
cross3(measured, estimated, e);
|
|
for (int j = 0; j < 3; j++) w[j] += kp * e[j];
|
|
}
|
|
double rate = norm3(w), theta = rate * dt;
|
|
if (theta < 1e-12) return;
|
|
double k[3] = {w[0] / rate, w[1] / rate, w[2] / rate}, s = sin(theta), c = 1 - cos(theta);
|
|
double K[3][3] = {{0, -k[2], k[1]}, {k[2], 0, -k[0]}, {-k[1], k[0], 0}}, E[3][3], out[3][3];
|
|
for (int i = 0; i < 3; i++)
|
|
for (int j = 0; j < 3; j++) {
|
|
double kk = 0;
|
|
for (int q = 0; q < 3; q++) kk += K[i][q] * K[q][j];
|
|
E[i][j] = (i == j) + s * K[i][j] + c * kk;
|
|
}
|
|
for (int i = 0; i < 3; i++)
|
|
for (int j = 0; j < 3; j++) {
|
|
double v = 0;
|
|
for (int q = 0; q < 3; q++) v += R[i][q] * E[q][j];
|
|
out[i][j] = v;
|
|
}
|
|
memcpy(R, out, sizeof out);
|
|
orthonormalize(R);
|
|
}
|
|
|
|
void motion_feed(MotionFusion *m, const double acc[3], const double gyro[3], uint64_t now) {
|
|
plat_mutex_lock(&m->lock);
|
|
double dt = m->last_ns && now > m->last_ns ? (double)(now - m->last_ns) / 1e9 : 0.004;
|
|
if (dt > 0.05) dt = 0.004; // a gap (no controller, sensors were off): no integration across it
|
|
m->last_ns = now;
|
|
if (m->restart) {
|
|
align_to(m->R, acc);
|
|
memset(m->angle, 0, sizeof m->angle);
|
|
m->restart = 0;
|
|
} else integrate(m->R, gyro, acc, dt);
|
|
MotionState *s = &m->ring[m->produced % MOTION_RING];
|
|
*s = (MotionState){.delta_ns = (int64_t)(dt * 1e9), .sampling = (int64_t)m->produced + 1, .attributes = 1};
|
|
for (int i = 0; i < 3; i++) {
|
|
double rps = gyro[i] / (2 * M_PI);
|
|
m->angle[i] += rps * dt;
|
|
s->acc[i] = (float)acc[i];
|
|
s->gyro[i] = (float)rps;
|
|
s->angle[i] = (float)m->angle[i];
|
|
for (int j = 0; j < 3; j++) s->dir[i * 3 + j] = (float)m->R[j][i]; // direction i = controller axis i in the reference frame
|
|
}
|
|
m->produced++;
|
|
plat_mutex_unlock(&m->lock);
|
|
}
|
|
|
|
int motion_read(MotionFusion *m, MotionState *out, int max, uint64_t now) {
|
|
if (max <= 0) return 0;
|
|
int n = 0;
|
|
plat_mutex_lock_shared(&m->lock);
|
|
if (m->produced > m->first && (now < m->last_ns || now - m->last_ns < STALE_NS)) {
|
|
uint64_t have = m->produced - m->first < MOTION_RING ? m->produced - m->first : MOTION_RING;
|
|
n = (int)(have < (uint64_t)max ? have : (uint64_t)max);
|
|
for (int i = 0; i < n; i++) out[i] = m->ring[(m->produced - 1 - (uint64_t)i) % MOTION_RING];
|
|
}
|
|
plat_mutex_unlock_shared(&m->lock);
|
|
return n;
|
|
}
|
|
|
|
void motion_restart(MotionFusion *m) {
|
|
plat_mutex_lock(&m->lock);
|
|
m->restart = 1;
|
|
m->first = m->produced;
|
|
m->last_ns = 0; // the first sample after the restart integrates nothing across the stop
|
|
memset(m->angle, 0, sizeof m->angle);
|
|
plat_mutex_unlock(&m->lock);
|
|
}
|