pebble/src/fw/services/common/ecompass_correction.c

416 lines
13 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ecompass.h"
#include "util/trig.h"
#include "kernel/pbl_malloc.h"
#include "system/logging.h"
#include "util/math.h"
#include <stdint.h>
#include <stdio.h>
#define N_SAMPS 4 // four points define a unique sphere
#define N_AXIS 3
static int16_t s_samples[N_SAMPS][N_AXIS];
//
// Two basic equations of a sphere:
// a) (x - x0)^2 + (y - y0)^2 + (z - z0)^2 = r0^2
// (x^2 - 2*x*x0 + x0^2) + (y^2 - 2*y*x0 + y0^2) + (z^2 - 2*z*z0 + z0^2) = r^2
// -2*x*x0 - 2*y*y0 - 2*z*z0 + (x^2 + y^2 + z^2) + r0^2 = 0
// b) A*(x^2 + y^2 + x^2) + B*x + C*y + D*z + E = 0
//
// Ideally, we would be able to build a large cloud of points and apply a least
// squares or ellipsoid fit to that dataset. However, this quickly becomes
// expensive (from a code space perspective). Therefore, we focus on collecting
// four 'good' data points and fitting those point to a sphere
//
// The sphere fit entails solving a linear system of the form Ax = B
//
// | (x^2 + y^2 + z^2) x y z 1 | | A | | 0 |
// | (x0^2 + y0^2 + z0^2) x0 y0 z0 1 | | B | | 0 |
// | (x1^2 + y1^2 + z1^2) x1 y1 z1 1 | * | C | = | 0 |
// | (x2^2 + y2^2 + z2^2) x2 y2 z2 1 | | D | | 0 |
// | (x3^2 + y3^2 + z3^2) x3 y3 z3 1 | | E | | 0 |
//
// to solve we want to find where det(A) = 0
//
// Using Laplace's formula for determinant expansion we can break this into a
// system of 4x4 determinants. Expanding along row 0 and using Cij to represent
// the cofactor which removes row i and column j:
//
// (x^2 + y^2 + z^2) * C_0,0 + x * C_0,1 + y * C_0,2 + z * C_0,3 + C_0,4 = 0
//
// This solution can be re-written in a form similar to eq a) above as
//
// x * (C_0,1 / C_0,0) + y * (C_0,2 / C_0,0) + z * (C_0,3 / C_0,0) +
// (C_0,4 / C_0,0) + (x^2 + y^2 + z^2) = 0
//
// This gives us our hard iron correction estimates (i.e location of the sphere
// origin, (xo, yo, zo)) as:
//
// (C_0,1 / C_0,0) = -2*x0 ==> x0 = (C_0,1 / C_0,0) / -2
// (C_0,2 / C_0,0) = -2*y0 ==> y0 = (C_0,2 / C_0,0) / -2
// (C_0,3 / C_0,0) = -2*z0 ==> z0 = (C_0,3 / C_0,0) / -2
//
static int32_t sphere_determinant4x4(int32_t **m, int32_t down_samp) {
// assume x, y, z value fits within 14 bits & m[x][3] == 1
// when m[x][0] == x^2 + y^2 + z^2, this val < 30 bits
// so m[x][0] * temp is capped at 44bits leaving plenty of space
// within 64 bits
int64_t res = 0;
// computes the determinant by decomposing the computation into 4 3x3 minor
// determinants formed by removal of row 'skip_row' & the third column
for (int skip_row = 0; skip_row < 4; skip_row++) {
int r0 = (skip_row != 0) ? 0 : 1;
int r1 = (skip_row < 2) ? 2 : 1;
int r2 = (skip_row < 3) ? 3 : 2;
int64_t temp = (m[r1][1] * m[r2][2]) - (m[r1][2] * m[r2][1]);
int64_t det = temp * (int64_t)m[r0][0];
temp = (m[r1][0] * m[r2][2]) - (m[r1][2] * m[r2][0]);
det -= temp * (int64_t)m[r0][1];
temp = (m[r1][0] * m[r2][1]) - (m[r1][1] * m[r2][0]);
det += temp * (int64_t)m[r0][2];
if ((skip_row % 2) == 0) {
res += det;
} else {
res -=det;
}
}
return (res / down_samp);
}
static bool sphere_fit(int16_t *solution) {
bool rv = false;
int32_t **matrix = kernel_malloc(sizeof(int32_t *) * N_SAMPS);
for (int i = 0; i < N_SAMPS; i++) {
matrix[i] = kernel_malloc(sizeof(int32_t) * N_AXIS);
}
// determine average value of x, y, & z coordinates
// and shift by this factor to prevent overflow.
int32_t shift_factor[N_AXIS] = { 0 };
for (int j = 0; j < N_AXIS; j++) {
for (int i = 0; i < N_SAMPS; i++) {
shift_factor[j] += s_samples[i][j];
}
shift_factor[j] /= N_SAMPS;
}
int16_t raw_data[N_SAMPS][N_AXIS];
for (int i = 0; i < N_SAMPS; i++) {
for (int j = 0; j < 3; j++) {
raw_data[i][j] = (s_samples[i][j] - shift_factor[j]);
// TODO: stop here if we have a chance of overflowing
}
}
int32_t r[N_SAMPS];
for (int i = 0; i < N_SAMPS; i++) {
r[i] = raw_data[i][0] * raw_data[i][0] +
raw_data[i][1] * raw_data[i][1] +
raw_data[i][2] * raw_data[i][2];
}
// We now find the origin by solving the linear equation discussed above
for (int i = 0; i < N_SAMPS; i++) {
matrix[i][0] = raw_data[i][0];
matrix[i][1] = raw_data[i][1];
matrix[i][2] = raw_data[i][2];
}
int32_t c00 = sphere_determinant4x4(matrix, 1);
if (c00 == 0) {
goto cleanup;
}
// compute cofactor01 to solve for x0
for (int i = 0; i < N_SAMPS; i++) {
matrix[i][0] = r[i]; // m[i][1] = y, m[i][2] = z
}
solution[0] = shift_factor[0] + sphere_determinant4x4(matrix, c00 * 2);
// compute cofactor02 to solve for y0
for (int i = 0; i < N_SAMPS; i++) {
matrix[i][1] = raw_data[i][0]; // m[i][0] = r^2, m[i][2] = z
}
solution[1] = shift_factor[1] - sphere_determinant4x4(matrix, c00 * 2);
// compute cofactor03 to solve for z0
for (int i = 0; i < N_SAMPS; i++) {
matrix[i][2] = raw_data[i][1]; // m[i][0] = r^2, m[i][1] = x
}
solution[2] = shift_factor[2] + sphere_determinant4x4(matrix, c00 * 2);
rv = true;
cleanup:
for (int i = 0; i < N_SAMPS; i++) {
kernel_free(matrix[i]);
}
kernel_free(matrix);
return (rv);
}
// Earths magnetic field intensity ranges from 25uT (near the equator) to 65uT
// (near the earths poles). A majority of Europe, North America, & Asia ranges
// between 35-50uT.
//
// Assuming magnetometer readings are predominantly influenced by hard iron
// distortions, we seek to find four points and fit them to a sphere in
// order to determine the offset we need to shift the raw data by to correct
// for hard iron distortions.
//
// For points A, B, C, D and a distance threshold, t, we select four points by
// satisfying the following:
// distance_ptA_to_ptB > t
// distance_lineAB_to_ptC > t
// distance_planeABC_to_ptD > t.
//
// Conceptually, it makes sense that the farther points are from one another (>
// t), the less that errors due to noise, fixed point mathematics, & motion
// render bad solutions. (empirically, this seems to be the behavior as
// well). However, the greater the the threshold, the more orientations one
// must put their watch through in order to get solution sets
//
// For now, select a distance metric that should work out of the box for a
// majority of the middle of the world. However, if no solution sets are found
// after 45s, fall back to a less aggressive threshold that will work
// anywhere in the world.
#define THRESH_MAX 370 /* 37 uT */
#define THRESH_MIN 220 /* 22 uT */
// Note: All of the following helper routines operate on the s_samples array
// defined above and populated by add_raw_mag_sample
static bool pt_to_pt_dist_under_thresh(int idx_a, int idx_b, int32_t thresh) {
int32_t v_ab[3] = {
s_samples[idx_b][0] - s_samples[idx_a][0],
s_samples[idx_b][1] - s_samples[idx_a][1],
s_samples[idx_b][2] - s_samples[idx_a][2]
};
int32_t dist_sq = v_ab[0] * v_ab[0] + v_ab[1] * v_ab[1] +
v_ab[2] * v_ab[2];
return (dist_sq > (thresh * thresh));
}
static bool pt_to_line_dist_under_thresh(int idx_line_a, int idx_line_b,
int idx_pt, int32_t thresh) {
int32_t s[3] = {
s_samples[idx_line_b][0] - s_samples[idx_line_a][0],
s_samples[idx_line_b][1] - s_samples[idx_line_a][1],
s_samples[idx_line_b][2] - s_samples[idx_line_a][2]
};
int32_t m1[3] = {
s_samples[idx_line_a][0] - s_samples[idx_pt][0],
s_samples[idx_line_a][1] - s_samples[idx_pt][1],
s_samples[idx_line_a][2] - s_samples[idx_pt][2]
};
int32_t m1xs[3] = {
m1[1] * s[2] - m1[2] *s[1],
-(m1[0] * s[2] - m1[2] * s[0]),
m1[0]*s[1] - m1[1] * s[0]
};
int64_t dist_sq = ((int64_t)m1xs[0]*m1xs[0] + (int64_t)m1xs[1]*m1xs[1] +
(int64_t)m1xs[2]*m1xs[2]) / (s[0]*s[0] + s[1]*s[1] + s[2]*s[2]);
return (dist_sq > (thresh * thresh));
}
static bool pt_to_plane_dist_under_thresh(int idx_pln_a, int idx_pln_b,
int idx_pln_c, int idx_pt, int32_t thresh) {
int32_t v_ab[3];
int32_t v_ac[3];
for (int i = 0; i < 3; i++) {
v_ab[i] = s_samples[idx_pln_b][i] - s_samples[idx_pln_a][i];
v_ac[i] = s_samples[idx_pln_c][i] - s_samples[idx_pln_a][i];
}
int64_t plane_eq[4];
plane_eq[0] = v_ab[1] * v_ac[2] - v_ab[2] * v_ac[1];
plane_eq[1] = -v_ab[0] * v_ac[2] + v_ab[2] * v_ac[0];
plane_eq[2] = v_ab[0] * v_ac[1] - v_ab[1] * v_ac[0];
// solve for d
plane_eq[3] = -(plane_eq[0] * s_samples[0][0] +
plane_eq[1] * s_samples[0][1] + plane_eq[2] * s_samples[0][2]);
// Distance^2 = (a * xo + b * yo + c * zo + d) / (a^2 + b^2 + c^2)
int64_t distance = ABS((plane_eq[0] * s_samples[idx_pt][0] +
plane_eq[1] * s_samples[idx_pt][1] + plane_eq[2] * s_samples[idx_pt][2] +
plane_eq[3]));
int32_t r = integer_sqrt(plane_eq[0] * plane_eq[0] + plane_eq[1] * plane_eq[1]
+ plane_eq[2] * plane_eq[2]);
return ((distance / r) > thresh);
}
static int min_max_diff(int16_t *vals, int n_vals) {
int min = vals[0];
int max = vals[0];
for (int i = 0; i < n_vals; i++) {
if (vals[i] > max) {
max = vals[i];
} else if (vals[i] < min) {
min = vals[i];
}
}
return (max - min);
}
#define N_COMP_SAMPS 3
static MagCalStatus check_correction_value(int16_t *solution,
int16_t *saved_solution) {
const int max_delta_thresh = 50;
// stash several of the most recent calibration results. The idea here is if
// we get multiple readings in a row close to one another than we have locked
// onto a good set of solutions
static int16_t calib_idx = 0;
static int16_t calib_val[N_AXIS][N_COMP_SAMPS];
calib_val[0][calib_idx % 3] = solution[0];
calib_val[1][calib_idx % 3] = solution[1];
calib_val[2][calib_idx % 3] = solution[2];
calib_idx++;
static int saved_sample_match = 0;
int x_delta, y_delta, z_delta;
// is the new solution close to what we already have saved?
if (saved_solution != NULL) {
x_delta = ABS(saved_solution[0] - solution[0]);
y_delta = ABS(saved_solution[1] - solution[1]);
z_delta = ABS(saved_solution[2] - solution[2]);
if ((x_delta < max_delta_thresh) && (y_delta < max_delta_thresh) &&
(z_delta < max_delta_thresh)) {
saved_sample_match++;
if (saved_sample_match == 3) {
saved_sample_match = 0;
calib_idx = 0;
PBL_LOG(LOG_LEVEL_INFO, "Persisting previous values!");
return (MagCalStatusSavedSampleMatch); // locked
}
}
}
// do we have several solutions in a row that are close to one another
if (calib_idx >= N_COMP_SAMPS) {
x_delta = min_max_diff(calib_val[0], 3);
y_delta = min_max_diff(calib_val[1], 3);
z_delta = min_max_diff(calib_val[2], 3);
if ((x_delta < max_delta_thresh) && (y_delta < max_delta_thresh) &&
(z_delta < max_delta_thresh)) {
int corrs[N_AXIS] = { 0 };
for (int i = 0; i < N_AXIS; i++) {
for (int j = 0; j < N_COMP_SAMPS; j++) {
corrs[i] += calib_val[i][j];
}
solution[i] = corrs[i] / N_COMP_SAMPS;
}
calib_idx = 0;
return (MagCalStatusNewLockedSolutionAvail);
}
}
return (MagCalStatusNewSolutionAvail);
}
static int s_sample_idx = 0;
static int s_no_fit_strikes = 0;
static int s_samples_collected_for_fit = 0;
void ecomp_corr_reset(void) {
s_no_fit_strikes = 0;
s_samples_collected_for_fit = 0;
s_sample_idx = 0;
}
MagCalStatus ecomp_corr_add_raw_mag_sample(int16_t *sample,
int16_t *saved_corr, int16_t *solution) {
static int thresh = THRESH_MAX;
s_samples_collected_for_fit++;
// if we haven't gotten good samples points in 15s @ 20Hz (60s @ 5Hz)
if (s_samples_collected_for_fit > 300) {
if (s_sample_idx >= 2) { // there was some kind of motion
s_no_fit_strikes++;
}
if (s_no_fit_strikes == 2) {
PBL_LOG(LOG_LEVEL_INFO, "Lowering magnetometer distance threshold");
thresh = THRESH_MIN;
}
s_samples_collected_for_fit = 0;
s_sample_idx = 0;
}
s_samples[s_sample_idx][0] = sample[0];
s_samples[s_sample_idx][1] = sample[1];
s_samples[s_sample_idx][2] = sample[2];
if (s_sample_idx == 1) {
if (!pt_to_pt_dist_under_thresh(0, 1, thresh)) {
return (MagCalStatusNoSolution);
}
} else if (s_sample_idx == 2) {
if (!pt_to_line_dist_under_thresh(0, 1, 2, thresh)) {
return (MagCalStatusNoSolution);
}
} else if (s_sample_idx == 3) {
if (!pt_to_plane_dist_under_thresh(0, 1, 2, 3, thresh)) {
return (MagCalStatusNoSolution);
}
}
// the sample has passed its distance threshold check so add it
PBL_LOG(LOG_LEVEL_DEBUG, "---> [%d] Adding %d %d %d \n",
s_sample_idx, (int)sample[0], (int)sample[1], (int)sample[2]);
s_sample_idx++;
if (s_sample_idx != 4) {
return (MagCalStatusNoSolution); // we need 4 pts before we can do a fit
}
// reset vars for next potential sphere fit
s_samples_collected_for_fit = 0;
s_sample_idx = 0;
if (!sphere_fit(&solution[0])) {
return (MagCalStatusNoSolution);
}
return (check_correction_value(solution, saved_corr));
}