1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
//! Code releated to magnetometer reading and calibration.
use core::f32::consts::TAU;
// todo: Assess mag health using consistency of mag vector len (calibrated). It should be
// todo close to 1. THrow out readings that aren't close to 1.
use lin_alg::f32::{Quaternion, Vec3};
use num_traits::float::Float; // abs etc
#[cfg(feature = "defmt")]
use defmt::println;
use crate::{
attitude::{make_nudge, Ahrs, AhrsCal},
blend,
mag_ellipsoid_fitting::{
self, MAG_SAMPLES_PER_CAT, SAMPLE_VERTEX_ANGLE, SAMPLE_VERTICES, TOTAL_MAG_SAMPLE_PTS,
},
print_quat,
util::map_linear,
FORWARD, RIGHT, UP,
};
impl AhrsCal {
/// Apply the hard and soft iron offsets to our readings.
pub fn apply_cal_mag(&self, data: Vec3) -> Vec3 {
// todo: Why this clone?
// todo: Temp removing soft iron to TS
self.soft_iron.clone() * (data - self.hard_iron)
}
// /// Estimate hard iron offset from the filled data buffer.
// /// todo: How should we do this? Best-fit ellipse, then find its center?
// /// todo: To keep your buffers from running you out of flash, consider a few independent estimates,
// /// todo then averaging (etc) them.
// pub fn estimate_hard_iron(&mut self) {
// // self.hard_iron =
// }
/// Run this once every x updates.
/// Stores a calibration point into a directional category. The aim is to evenly mix samples taked
/// at ~evenly-spaced attitudes, despite attitudes not being distributed evenly over time.
/// This collects readings, then performs calibration once enough are taken in each category.
pub fn log_mag_cal_pt(&mut self, att: Quaternion, mag_raw: Vec3) {
if mag_raw == Vec3::new_zero() {
return; // todo: Not sure why we occasionally (and at start) get this.
}
// Indexed by the sample vec vertices. We use 2 perpendicular vector
// to remove a rotational ambiguity using a single would do.
for (data, direction, i, count) in [
(
&mut self.mag_cal_data_up,
UP,
&mut self.mag_sample_i_up,
&mut self.mag_sample_count_up,
),
(
&mut self.mag_cal_data_fwd,
FORWARD,
&mut self.mag_sample_i_fwd,
&mut self.mag_sample_count_fwd,
),
] {
let mut sample_category = 0;
for (cat, sample) in SAMPLE_VERTICES.iter().enumerate() {
// `UP` is arbitrary; any Vec will do as long as we're consistent.
let rotated = att.rotate_vec(direction);
if (rotated.dot(*sample)).acos() < SAMPLE_VERTEX_ANGLE {
sample_category = cat;
break;
}
}
data[sample_category][i[sample_category]] = mag_raw;
// This loops around; we always update the oldest value in each category
i[sample_category] = (i[sample_category] + 1) % MAG_SAMPLES_PER_CAT;
if count[sample_category] < MAG_SAMPLES_PER_CAT as u8 {
count[sample_category] += 1;
}
}
}
/// Update mag calibration based on sample points.
/// Only run this when there is recent data of each attitude category of points.
pub fn update_mag_cal(&mut self, update_amt: f32) {
// Flatten our sample data organized by points; that format is only used to ensure a
// good selection of points, ie fairly weighted on all sides.
let mut sample_pts = [Vec3::new_zero(); TOTAL_MAG_SAMPLE_PTS];
const EPS: f32 = 0.0000001;
#[cfg(feature = "defmt")]
println!("\nMag cal complete\n");
let mut i = 0;
// println!("\n\n\n[");
for data in &[self.mag_cal_data_up, self.mag_cal_data_fwd] {
for cat in data {
for point in cat {
if point.x.abs() < EPS && point.y.abs() < EPS && point.z.abs() < EPS {
continue;
}
sample_pts[i] = *point;
// println!("({}, {}, {}),", point.x, point.y, point.z);
i += 1;
}
}
}
// println!("]");
// todo: Put back once working.
let poly_terms = mag_ellipsoid_fitting::ls_ellipsoid(&sample_pts);
let (hard_iron, soft_iron) = mag_ellipsoid_fitting::poly_to_params_3d(&poly_terms);
// // todo: axes and inve; what are they? Check the web page.
// self.hard_iron = center;
// self.soft_iron = inve;
// println!(
// "Hard iron: x{} y{} z{}",
// hard_iron.x, hard_iron.y, hard_iron.z
// );
// println!(
// "Soft iron diag: {} {} {} {} {} {}",
// soft_iron.data[0],
// soft_iron.data[1],
// soft_iron.data[2],
// soft_iron.data[4],
// soft_iron.data[5],
// soft_iron.data[8]
// );
let update_amt_inv = 1. - update_amt;
self.hard_iron = self.hard_iron * update_amt_inv + hard_iron * update_amt;
self.soft_iron = self.soft_iron.clone() * update_amt_inv + soft_iron * update_amt;
// Reset our sample counters. We leave the sample data in place, since we can still
// use the readings in the next calibration. (Since we initiate cal without completely
// filling it)
self.mag_sample_i_up = Default::default();
self.mag_sample_i_fwd = Default::default();
self.mag_sample_count_up = Default::default();
self.mag_sample_count_fwd = Default::default();
self.mag_cal_updated = true; // set to false in firmware once written to flash.
}
}
impl Ahrs {
/// We use magnetometer information in two ways. Note that its ambiguity axis is close to
/// the ambitguity access of the accelerometer, but it provides some heading information, while
/// the acc provides none. We apply corrections in two ways:
///
/// 1: Create an attitude using the assessed inclination vector, and the magnetometer reading.
/// 2: Extract heading directly, and apply a correction: This is our primary absolute heading reference.
pub(crate) fn handle_mag(&mut self, mag_raw: Vec3, att_fused: &mut Quaternion) {
let mag = self.cal.apply_cal_mag(mag_raw);
self.mag_calibrated = Some(mag);
const EPS: f32 = 0.0000001;
if mag.x.abs() < EPS && mag.y.abs() < EPS && mag.z.abs() < EPS {
// todo: We get this on the first run; not sure why.
return;
}
let mag_norm = mag.to_normalized();
let incl_rot = Quaternion::from_axis_angle(RIGHT, -self.mag_inclination_estimate);
let mag_field_absolute = incl_rot.rotate_vec(FORWARD);
let att_mag = att_from_mag(mag_norm, mag_field_absolute);
self.att_from_mag = Some(att_mag);
let magnetometer_magnitude = mag.magnitude(); // Say that 10 times fast?
let update_amt_mag_var = 0.10 * self.dt; // todo: Store this const as a struct param.
// let mag_variance = (mag.magnitude() - 1.).powi(2);
let mag_variance = (mag.magnitude() - 1.).abs();
self.recent_mag_variance =
blend(self.recent_mag_variance, mag_variance, update_amt_mag_var);
// Symmetry with acc here; similar logic etc.
// todo: Move this update_gyro_from_acc logic elsewhere, like a dedicated fn; or, rework it.
let mut update_gyro_from_mag = false;
// If it appears there is negligible non-calibrated-away interference, update our
// gyro readings as appropriate.
if (magnetometer_magnitude - 1.).abs() < self.config.total_mag_thresh
&& self.mag_inclination_estimate < self.config.mag_incl_max
{
update_gyro_from_mag = true;
}
if update_gyro_from_mag {
let rot_correction_from_att = make_nudge(
self.attitude,
// *att_fused,
mag_norm,
mag_field_absolute,
self.config.update_amt_att_from_mag * self.dt,
);
// We extract the pure heading from the magnetometer, since that is what we primarily don't get
// from the accelerometer, and we only get a small part from the magnetometer directly.
// todo: We don't want just acc here, due to linear-acc problems, but do it for now to test.
let tilt_compensated = self.att_from_acc.inverse().rotate_vec(mag_norm);
let hdg = tilt_compensated.x.atan2(tilt_compensated.y) - self.mag_declination;
let heading_rotation = Quaternion::from_unit_vecs(
self.attitude.rotate_vec(FORWARD).project_to_plane(UP),
Vec3::new(hdg.sin(), hdg.cos(), 0.),
);
// todo: make sure you apply an instantaneous correction on init.
let rot_correction_from_heading = Quaternion::new_identity().slerp(
heading_rotation,
self.config.update_amt_hdg_from_mag * self.dt,
);
*att_fused = rot_correction_from_att * rot_correction_from_heading * *att_fused;
#[cfg(feature = "defmt")]
// if self.num_updates % ((1. / self.dt) as u32) == 0 {
if false {
println!("HDG: {:?}", hdg);
print_quat(heading_rotation, "HDG ROT");
println!(" FWD mag: x{} y{}", hdg.sin(), hdg.cos());
let a = att_fused.rotate_vec(FORWARD).project_to_plane(UP);
println!(" FWD att: x{} y{} z: {}", a.x, a.y, a.z);
// print_quat(rot_correction_from_heading * self.att_from_mag.unwrap(), "Att mag w hdg");
}
}
self.update_mag_incl(mag_norm);
#[cfg(feature = "defmt")]
// if self.num_updates % ((1. / self.dt) as u32) == 0 {
if false {
#[cfg(feature = "defmt")]
println!(
"\n\nMag raw: x{} y{} z{} len{}",
mag_raw.x,
mag_raw.y,
mag_raw.z,
mag_raw.magnitude()
);
#[cfg(feature = "defmt")]
println!(
"Mag: x{} y{} z{} len{}",
mag.x,
mag.y,
mag.z,
mag.magnitude()
);
#[cfg(feature = "defmt")] {
println!("Estimated mag incl: {}", self.mag_inclination_estimate);
println!("Recent mag var: {}", self.recent_mag_variance);
print_quat(att_mag, "Att mag");
}
}
if self.num_updates % self.config.update_ratio_mag_cal_log as u32 == 0 {
self.update_mag_cal(mag_raw);
}
}
/// Estimate magnetic inclination, by calculating the angle between Up in the aircraft, and the magnetometer
/// vector. The resulting value is in radians, down from the horizon.
fn update_mag_incl(&mut self, mag_norm: Vec3) {
const TAU_DIV_4: f32 = TAU / 4.;
let up = self.att_from_acc.rotate_vec(UP);
// Angle between up and the mag reading. We subtract tau/4 to get angle below the horizon.
let inclination_estimate = up.dot(mag_norm).acos() - TAU_DIV_4;
// No need to update the ratio each time.
if self.num_updates % self.config.update_ratio_mag_incl as u32 == 0 {
if self.initialized {
// Weighted average of current inclination estimate with stored.
let incl_ratio = self.config.update_amt_mag_incl_estimate
* self.dt
* self.config.update_ratio_mag_incl as f32;
self.mag_inclination_estimate = blend(
self.mag_inclination_estimate,
inclination_estimate,
incl_ratio,
);
} else {
// Take the full update on the first run.
// todo: That doesn't appear to work; showing 0 start. use a sane default.
self.mag_inclination_estimate = 1.2;
// self.mag_inclination_estimate = inclination_estimate;
}
}
}
/// Log a magnetic calibration point. Check if we've logged enough points to apply the calibration,
/// and do if we do.
fn update_mag_cal(&mut self, mag_raw: Vec3) {
self.cal.log_mag_cal_pt(self.attitude, mag_raw);
// If we've filled up most of our sample slots, divided by directional category, initiate cal.
// Note that additional samples in a filled cat don't count towards this value.
let mut samples_taken = 0;
for cat_count in self.cal.mag_sample_count_up {
samples_taken += cat_count;
}
for cat_count in self.cal.mag_sample_count_fwd {
samples_taken += cat_count;
}
if samples_taken as f32 / TOTAL_MAG_SAMPLE_PTS as f32 >= self.config.mag_cal_portion_req {
// Ie, if the recent magnetometer magnitude is too high, take all or almost all of the update.
// todo: Store these consts in the main struct?
let mut update_amt_mag_cal = map_linear(self.recent_mag_variance, (0., 0.3), (0.1, 1.));
if update_amt_mag_cal > 1. {
update_amt_mag_cal = 1.;
}
self.cal.update_mag_cal(update_amt_mag_cal);
}
}
}
/// Estimate attitude from magnetometer. This will fail when experiencing magnetic
/// interference, and is noisy in general. Apply calibration prior to this step.
/// The magnetic field vector points points towards magnetic earth, and into the earth IOC inlination.
/// It is points forward and down in our coordinate system.
pub fn att_from_mag(mag_norm: Vec3, mag_field_vec_absolute: Vec3) -> Quaternion {
Quaternion::from_unit_vecs(mag_field_vec_absolute, mag_norm)
}