@wemap/positioning 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +479 -0
- package/.nvmrc +1 -0
- package/babel.config.js +11 -0
- package/config.json +7 -0
- package/debug/index.html +15 -0
- package/debug/index.old.html +37 -0
- package/package.json +82 -0
- package/scripts/release-github.js +216 -0
- package/src/Constants.js +11 -0
- package/src/NavigationHandler.js +244 -0
- package/src/Pose.js +8 -0
- package/src/attitude/Attitude.js +65 -0
- package/src/attitude/AttitudeHandler.js +343 -0
- package/src/attitude/EkfAttitude.js +238 -0
- package/src/attitude/EkfAttitude.spec.js +116 -0
- package/src/components/AbsoluteAttitude.jsx +136 -0
- package/src/components/Imu.jsx +89 -0
- package/src/components/LocationSource.jsx +434 -0
- package/src/components/Logger.jsx +113 -0
- package/src/components/NavigationDebugApp.jsx +106 -0
- package/src/components/Others.jsx +121 -0
- package/src/components/RelativeAttitude.jsx +104 -0
- package/src/components/Utils.js +35 -0
- package/src/components/index.js +13 -0
- package/src/index.js +9 -0
- package/src/providers/FixedLocationImuLocationSource.js +66 -0
- package/src/providers/GnssLocationSource.js +118 -0
- package/src/providers/GnssPdrLocationSource.js +182 -0
- package/src/providers/IPLocationSource.js +96 -0
- package/src/providers/LocationSource.js +290 -0
- package/src/providers/PdrLocationSource.js +312 -0
- package/src/providers/ProvidersLogger.js +77 -0
- package/src/providers/pdr/HeadingUnlocker.js +41 -0
- package/src/providers/pdr/HeadingUnlocker.spec.js +26 -0
- package/src/providers/pdr/Smoother.js +90 -0
- package/src/providers/pdr/Smoother.spec.js +424 -0
- package/src/providers/pdr/ThugDetector.js +37 -0
- package/src/providers/steps/StepDetection.js +7 -0
- package/src/providers/steps/StepDetectionLadetto.js +67 -0
- package/src/providers/steps/StepDetectionMinMaxPeaks.js +80 -0
- package/src/providers/steps/StepDetectionMinMaxPeaks2.js +108 -0
- package/src/sensors/SensorsCompatibility.js +484 -0
- package/src/sensors/SensorsCompatibility.spec.js +270 -0
- package/src/sensors/SensorsLogger.js +94 -0
- package/src/sensors/SensorsLoggerUtils.js +35 -0
- package/src.new/NavigationHandler.js +62 -0
- package/src.new/index.js +3 -0
- package/src.new/providers/FakeLocationSource.js +39 -0
- package/webpack/webpack.common.js +20 -0
- package/webpack/webpack.dev.js +24 -0
- package/webpack/webpack.prod.js +15 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import geomagnetism from 'geomagnetism';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Quaternion, Utils as MathUtils
|
|
5
|
+
} from '@wemap/maths';
|
|
6
|
+
|
|
7
|
+
import Attitude from './Attitude';
|
|
8
|
+
import EkfAttitude from './EkfAttitude';
|
|
9
|
+
import SensorsCompatibility from '../sensors/SensorsCompatibility';
|
|
10
|
+
|
|
11
|
+
const { deg2rad } = MathUtils;
|
|
12
|
+
|
|
13
|
+
const RelativeMethod = {
|
|
14
|
+
AUTOMATIC: 0,
|
|
15
|
+
BROWSER: 1,
|
|
16
|
+
INTERNAL_EKF: 2
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const AbsoluteMethod = {
|
|
20
|
+
AUTOMATIC: 0,
|
|
21
|
+
BROWSER: 1,
|
|
22
|
+
INTERNAL_EKF: 2,
|
|
23
|
+
INTERNAL_CUSTOM: 3
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AttitudeHandler {
|
|
28
|
+
|
|
29
|
+
isRelativeStarted = false;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
this.sensorsCompatibility = new SensorsCompatibility();
|
|
33
|
+
this.relativeOffsetQuaternion = [1, 0, 0, 0];
|
|
34
|
+
this.ekfAttitude = new EkfAttitude();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static get RelativeMethod() {
|
|
38
|
+
return RelativeMethod;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static get AbsoluteMethod() {
|
|
42
|
+
return AbsoluteMethod;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setLogger(logger) {
|
|
46
|
+
this.sensorsCompatibility.setLogger(logger);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
startRelative(callback, method = RelativeMethod.INTERNAL_EKF) {
|
|
50
|
+
|
|
51
|
+
if (method === RelativeMethod.AUTOMATIC) {
|
|
52
|
+
throw new Error('Do not use automatic method for the moment');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let promise;
|
|
56
|
+
|
|
57
|
+
this.relativeMethodAnswer = false;
|
|
58
|
+
|
|
59
|
+
const fn = (quaternion, timestamp) => {
|
|
60
|
+
if (quaternion) {
|
|
61
|
+
callback(new Attitude(quaternion), timestamp);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.relativeMethod = method;
|
|
66
|
+
|
|
67
|
+
if (method === RelativeMethod.AUTOMATIC) {
|
|
68
|
+
|
|
69
|
+
promise = new Promise((resolve, reject) => {
|
|
70
|
+
// Try internal EKF first
|
|
71
|
+
this.startRelative(callback, RelativeMethod.INTERNAL_EKF)
|
|
72
|
+
.then(resolve)
|
|
73
|
+
.catch(() => {
|
|
74
|
+
this.stopRelative();
|
|
75
|
+
// If EKF did not work, try Browser method
|
|
76
|
+
this.startRelative(callback, RelativeMethod.BROWSER)
|
|
77
|
+
.then(resolve)
|
|
78
|
+
.catch(() => {
|
|
79
|
+
this.stopRelative();
|
|
80
|
+
reject();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
} else if (method === RelativeMethod.BROWSER) {
|
|
86
|
+
|
|
87
|
+
promise = this.sensorsCompatibility.startRelativeOrientation((quaternion, timestamp) => {
|
|
88
|
+
quaternion = Quaternion.multiply(this.relativeOffsetQuaternion, quaternion);
|
|
89
|
+
fn(quaternion, timestamp);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
} else if (method === RelativeMethod.INTERNAL_EKF) {
|
|
93
|
+
|
|
94
|
+
let lastTimestamp = 0;
|
|
95
|
+
promise = this.sensorsCompatibility.startImu(accGyrEvent => {
|
|
96
|
+
const timestamp = accGyrEvent.timestamp;
|
|
97
|
+
const acc = accGyrEvent.acc;
|
|
98
|
+
const gyr = accGyrEvent.gyr;
|
|
99
|
+
|
|
100
|
+
// Handle timestamps and dt
|
|
101
|
+
if (lastTimestamp === 0) {
|
|
102
|
+
lastTimestamp = timestamp;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const diffTime = timestamp - lastTimestamp;
|
|
106
|
+
lastTimestamp = timestamp;
|
|
107
|
+
const quaternion = this.ekfAttitude.update(diffTime, acc, gyr);
|
|
108
|
+
fn(quaternion, timestamp);
|
|
109
|
+
}, {
|
|
110
|
+
accelerometer: true,
|
|
111
|
+
gyroscope: true
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
return Promise.reject();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.isRelativeStarted = true;
|
|
118
|
+
|
|
119
|
+
return promise;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
stopRelative() {
|
|
124
|
+
this.isRelativeStarted = false;
|
|
125
|
+
|
|
126
|
+
if (this.relativeMethod === RelativeMethod.BROWSER) {
|
|
127
|
+
this.sensorsCompatibility.stopRelativeOrientation();
|
|
128
|
+
} else if (this.relativeMethod === RelativeMethod.INTERNAL_EKF) {
|
|
129
|
+
this.sensorsCompatibility.stopImu();
|
|
130
|
+
}
|
|
131
|
+
this.relativeMethod = null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
*
|
|
137
|
+
* Set yaw offset, this value will be used as the filter does not use magnetometer
|
|
138
|
+
* @param {Number} heading heading offset in radians and clockwise
|
|
139
|
+
*/
|
|
140
|
+
setRelativeHeading(heading) {
|
|
141
|
+
|
|
142
|
+
// Minus before "heading" is here because ENU attitude is counter-clockwise whereas WGS84 heading is clockwise.
|
|
143
|
+
heading = -heading;
|
|
144
|
+
|
|
145
|
+
// Offset from window orientation
|
|
146
|
+
heading += deg2rad(window.orientation || 0);
|
|
147
|
+
|
|
148
|
+
// // We don't know the method used, so we set both
|
|
149
|
+
this.relativeOffsetQuaternion = Quaternion.fromAxisAngle([0, 0, 1], heading);
|
|
150
|
+
|
|
151
|
+
// We don't know the method used, so we set both
|
|
152
|
+
this.ekfAttitude.setOrientationYaw(heading);
|
|
153
|
+
|
|
154
|
+
if (this.relativeMethod === RelativeMethod.BROWSER && this.relativeMethodAnswer) {
|
|
155
|
+
this.stopRelative();
|
|
156
|
+
return this.startRelative(this.relativeCallback, RelativeMethod.BROWSER);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
startAbsolute(callback, method = AbsoluteMethod.BROWSER) {
|
|
164
|
+
|
|
165
|
+
if (method === AbsoluteMethod.AUTOMATIC) {
|
|
166
|
+
throw new Error('Do not use automatic method for the moment');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let promise;
|
|
170
|
+
|
|
171
|
+
this.absoluteMethodAnswer = false;
|
|
172
|
+
|
|
173
|
+
const fn = (quaternion, timestamp, externalData) => {
|
|
174
|
+
if (!quaternion || !this.declinationQuaternion) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const trueQuaternion = Quaternion.multiply(this.declinationQuaternion, quaternion);
|
|
178
|
+
callback(new Attitude(trueQuaternion), timestamp, externalData);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.absoluteMethod = method;
|
|
182
|
+
|
|
183
|
+
if (method === AbsoluteMethod.AUTOMATIC) {
|
|
184
|
+
|
|
185
|
+
promise = new Promise((resolve, reject) => {
|
|
186
|
+
|
|
187
|
+
// Try internal EKF first
|
|
188
|
+
this.startAbsolute(callback, AbsoluteMethod.INTERNAL_EKF).catch(() => {
|
|
189
|
+
this.stopAbsolute();
|
|
190
|
+
|
|
191
|
+
// If EKF did not work, try internal custom method
|
|
192
|
+
this.startAbsolute(callback, AbsoluteMethod.INTERNAL_CUSTOM).catch(() => {
|
|
193
|
+
this.stopAbsolute();
|
|
194
|
+
|
|
195
|
+
// If internal custom did not work, try Browser method
|
|
196
|
+
this.startAbsolute(callback, AbsoluteMethod.BROWSER).catch(() => {
|
|
197
|
+
|
|
198
|
+
this.stopAbsolute();
|
|
199
|
+
reject();
|
|
200
|
+
|
|
201
|
+
}).then(resolve);
|
|
202
|
+
|
|
203
|
+
}).then(resolve);
|
|
204
|
+
|
|
205
|
+
}).then(resolve);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
} else if (method === AbsoluteMethod.INTERNAL_CUSTOM) {
|
|
209
|
+
|
|
210
|
+
let lastAttitude;
|
|
211
|
+
let isFirst = true;
|
|
212
|
+
|
|
213
|
+
promise = new Promise((resolve, reject) => {
|
|
214
|
+
|
|
215
|
+
const startRelativeFn = (heading) => {
|
|
216
|
+
this.stopAbsolute();
|
|
217
|
+
|
|
218
|
+
// Mandatory, otherwise this.absoluteMethod is null and cannot be stopped
|
|
219
|
+
this.absoluteMethod = AbsoluteMethod.INTERNAL_CUSTOM;
|
|
220
|
+
|
|
221
|
+
this.startRelative(callback)
|
|
222
|
+
.then(resolve)
|
|
223
|
+
.catch(() => {
|
|
224
|
+
this.stopRelative();
|
|
225
|
+
reject();
|
|
226
|
+
});
|
|
227
|
+
this.setRelativeHeading(heading);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const customCallback = (attitude, timestamp, externalData) => {
|
|
231
|
+
lastAttitude = attitude;
|
|
232
|
+
|
|
233
|
+
if (isFirst) {
|
|
234
|
+
this.timeoutAbsoluteFirstMillis = setTimeout(() => {
|
|
235
|
+
startRelativeFn(externalData && externalData.webkitCompassHeading
|
|
236
|
+
? deg2rad(externalData.webkitCompassHeading)
|
|
237
|
+
: lastAttitude.heading);
|
|
238
|
+
}, 300);
|
|
239
|
+
|
|
240
|
+
isFirst = false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
this.startAbsolute(customCallback, AbsoluteMethod.BROWSER)
|
|
246
|
+
.catch(() => {
|
|
247
|
+
this.stopAbsolute();
|
|
248
|
+
reject();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
} else if (method === AbsoluteMethod.BROWSER) {
|
|
253
|
+
promise = this.sensorsCompatibility.startAbsoluteOrientation((quaternion, timestamp, externalData) => {
|
|
254
|
+
fn(quaternion, timestamp, externalData);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
} else if (method === AbsoluteMethod.INTERNAL_EKF) {
|
|
258
|
+
let lastTimestamp = 0;
|
|
259
|
+
promise = this.sensorsCompatibility.startImu(accGyrMagEvent => {
|
|
260
|
+
const timestamp = accGyrMagEvent.timestamp;
|
|
261
|
+
const acc = accGyrMagEvent.acc;
|
|
262
|
+
const gyr = accGyrMagEvent.gyr;
|
|
263
|
+
const mag = accGyrMagEvent.mag;
|
|
264
|
+
|
|
265
|
+
// Handle timestamps and dt
|
|
266
|
+
if (lastTimestamp === 0) {
|
|
267
|
+
lastTimestamp = timestamp;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const diffTime = timestamp - lastTimestamp;
|
|
271
|
+
lastTimestamp = timestamp;
|
|
272
|
+
const quaternion = this.ekfAttitude.update(diffTime, acc, gyr, mag);
|
|
273
|
+
fn(quaternion, timestamp);
|
|
274
|
+
});
|
|
275
|
+
} else {
|
|
276
|
+
promise = Promise.reject();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return promise;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
stopAbsolute() {
|
|
283
|
+
if (this.absoluteMethod === AbsoluteMethod.BROWSER) {
|
|
284
|
+
this.sensorsCompatibility.stopAbsoluteOrientation();
|
|
285
|
+
} else if (this.absoluteMethod === AbsoluteMethod.INTERNAL_EKF) {
|
|
286
|
+
this.sensorsCompatibility.stopImu();
|
|
287
|
+
} else if (this.absoluteMethod === AbsoluteMethod.INTERNAL_CUSTOM) {
|
|
288
|
+
this.stopRelative();
|
|
289
|
+
}
|
|
290
|
+
if (this.timeoutAbsoluteFirstMillis) {
|
|
291
|
+
clearTimeout(this.timeoutAbsoluteFirstMillis);
|
|
292
|
+
this.timeoutAbsoluteFirstMillis = null;
|
|
293
|
+
}
|
|
294
|
+
this.absoluteMethod = null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// This method should be theoretically called every time the user moves.
|
|
298
|
+
// But in reality declination does not change as much.
|
|
299
|
+
setUserLocationForAbsolute(userLocation) {
|
|
300
|
+
const wmmResult = geomagnetism.model().point([userLocation.lat, userLocation.lng]);
|
|
301
|
+
// Declination is given in NED frame and our code use ENU, that is why we have: "-decl"
|
|
302
|
+
this.declinationQuaternion = Quaternion.fromAxisAngle([0, 0, 1], - deg2rad(wmmResult.decl));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
startMonitoringInclination(callback) {
|
|
306
|
+
|
|
307
|
+
this.sensorsCompatibilityInclination = new SensorsCompatibility();
|
|
308
|
+
|
|
309
|
+
return this.sensorsCompatibility.startImu(({ acc }) => {
|
|
310
|
+
|
|
311
|
+
const screenOrientation = window.orientation || 0;
|
|
312
|
+
|
|
313
|
+
const sizeAcc = Math.sqrt(acc[0] * acc[0] + acc[1] * acc[1] + acc[2] * acc[2]);
|
|
314
|
+
const accNormalized = [acc[0] / sizeAcc, acc[1] / sizeAcc, acc[2] / sizeAcc];
|
|
315
|
+
|
|
316
|
+
const q = [accNormalized[2] + 1, accNormalized[1], -accNormalized[0], 0];
|
|
317
|
+
const qSize = Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2]);
|
|
318
|
+
const qNormalized = [q[0] / qSize, q[1] / qSize, q[2] / qSize, 0];
|
|
319
|
+
|
|
320
|
+
let inclination;
|
|
321
|
+
if (screenOrientation === 0) {
|
|
322
|
+
inclination = Math.asin(2 * qNormalized[1] * qNormalized[0]);
|
|
323
|
+
} else if (screenOrientation === 90) {
|
|
324
|
+
inclination = -Math.asin(2 * qNormalized[2] * qNormalized[0]);
|
|
325
|
+
} else if (screenOrientation === -90) {
|
|
326
|
+
inclination = Math.asin(2 * qNormalized[2] * qNormalized[0]);
|
|
327
|
+
} else if (screenOrientation === 180) {
|
|
328
|
+
inclination = -Math.asin(2 * qNormalized[1] * qNormalized[0]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
callback(inclination);
|
|
332
|
+
|
|
333
|
+
}, { accelerometer: true });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
stopMonitoringInclination() {
|
|
337
|
+
if (this.sensorsCompatibilityInclination) {
|
|
338
|
+
this.sensorsCompatibilityInclination.stopImu();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export default AttitudeHandler;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Matrix, Matrix3, Matrix4, Quaternion, Vector, Vector3
|
|
3
|
+
} from '@wemap/maths';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const DEFAULT_RELATIVE_NOISES = {
|
|
7
|
+
acc: 0.5,
|
|
8
|
+
gyr: 0.3
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DEFAULT_ABSOLUTE_NOISES = {
|
|
12
|
+
acc: 0.5,
|
|
13
|
+
gyr: 0.3,
|
|
14
|
+
yc: 2
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class EkfAttitude {
|
|
18
|
+
|
|
19
|
+
constructor(accRef = [0, 0, 1], ycRef = [-1, 0, 0]) {
|
|
20
|
+
|
|
21
|
+
this.accRef = accRef;
|
|
22
|
+
this.cRef = ycRef;
|
|
23
|
+
|
|
24
|
+
this.P = Matrix.diag(Array(4).fill(0.1 ** 2));
|
|
25
|
+
|
|
26
|
+
this.quaternion = null;
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
this.noises = {
|
|
30
|
+
relative: null,
|
|
31
|
+
absolute: null
|
|
32
|
+
};
|
|
33
|
+
this.setRelativeNoises(DEFAULT_RELATIVE_NOISES);
|
|
34
|
+
this.setAbsoluteNoises(DEFAULT_ABSOLUTE_NOISES);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setRelativeNoises(relativeNoises) {
|
|
38
|
+
this.noises.relative = {
|
|
39
|
+
accelerometer: Matrix.diag(Array(3).fill(relativeNoises.acc ** 2)),
|
|
40
|
+
gyroscope: Matrix.diag(Array(3).fill(relativeNoises.gyr ** 2))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
setAbsoluteNoises(absoluteNoises) {
|
|
46
|
+
this.noises.absolute = {
|
|
47
|
+
accelerometer: Matrix.diag(Array(3).fill(absoluteNoises.acc ** 2)),
|
|
48
|
+
gyroscope: Matrix.diag(Array(3).fill(absoluteNoises.gyr ** 2)),
|
|
49
|
+
yc: Matrix.diag(Array(3).fill(absoluteNoises.yc ** 2))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
*
|
|
55
|
+
* Set yaw offset, this value will be used if the filter does not use magnetometer
|
|
56
|
+
* @param {Number} yaw yaw offset in radians and clockwise
|
|
57
|
+
*/
|
|
58
|
+
setOrientationYaw(yaw) {
|
|
59
|
+
this.offsetYawQuaternion = Quaternion.fromAxisAngle(this.accRef, yaw);
|
|
60
|
+
this.quaternion = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Try to initialize filter.
|
|
65
|
+
* To initialize, we need two information: current acceleration (acc) and offset on yaw angle (given by the quaternion: this.offsetYawQuaternion)
|
|
66
|
+
*/
|
|
67
|
+
tryInitialize(acc, mag) {
|
|
68
|
+
|
|
69
|
+
const accNormalized = Vector3.normalize(acc);
|
|
70
|
+
|
|
71
|
+
if (mag) {
|
|
72
|
+
const magNormalized = Vector3.normalize(mag);
|
|
73
|
+
|
|
74
|
+
const H = Vector3.normalize(Vector3.cross(magNormalized, accNormalized));
|
|
75
|
+
const M = Vector3.cross(accNormalized, H);
|
|
76
|
+
|
|
77
|
+
const R = [
|
|
78
|
+
[H[0], M[0], accNormalized[0]],
|
|
79
|
+
[H[1], M[1], accNormalized[1]],
|
|
80
|
+
[H[2], M[2], accNormalized[2]]
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
this.quaternion = Quaternion.fromMatrix3(R);
|
|
84
|
+
|
|
85
|
+
} else {
|
|
86
|
+
|
|
87
|
+
if (!this.offsetYawQuaternion) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const r = Vector3.dot(accNormalized, this.accRef) + 1;
|
|
92
|
+
const v = Vector3.cross(accNormalized, this.accRef);
|
|
93
|
+
|
|
94
|
+
let quaternionWithoutYaw = [r, v[0], v[1], v[2]];
|
|
95
|
+
quaternionWithoutYaw = Quaternion.normalize(quaternionWithoutYaw);
|
|
96
|
+
|
|
97
|
+
this.quaternion = Quaternion.multiply(this.offsetYawQuaternion, quaternionWithoutYaw);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return this.quaternion;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
update(diffTime, acc, gyr, mag) {
|
|
104
|
+
|
|
105
|
+
if (!this.quaternion) {
|
|
106
|
+
return this.tryInitialize(acc, mag);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let q = this.quaternion;
|
|
110
|
+
|
|
111
|
+
/* ------------
|
|
112
|
+
* ESTIMATION
|
|
113
|
+
* ------------*/
|
|
114
|
+
|
|
115
|
+
const qArray = q;
|
|
116
|
+
const gyrInt = Vector3.multiplyScalar(gyr, 0.5 * diffTime);
|
|
117
|
+
const F = this.computeC([1, gyrInt[0], gyrInt[1], gyrInt[2]]);
|
|
118
|
+
const qAPriori = Matrix.multiplyVector(F, q);
|
|
119
|
+
const E1 = Matrix.diag([qArray[0], qArray[0], qArray[0]]);
|
|
120
|
+
const eSkew = Matrix3.skew([qArray[1], qArray[2], qArray[3]]);
|
|
121
|
+
|
|
122
|
+
const qPart = [-1 * qArray[1], -1 * qArray[2], -1 * qArray[3]];
|
|
123
|
+
const E = Matrix.concatRow([qPart], Matrix3.add(eSkew, E1));
|
|
124
|
+
|
|
125
|
+
const Qk = Matrix.multiplyScalar(
|
|
126
|
+
Matrix.multiply(
|
|
127
|
+
Matrix.multiply(E, this.noises[mag ? 'absolute' : 'relative'].gyroscope),
|
|
128
|
+
Matrix.transpose(E)
|
|
129
|
+
),
|
|
130
|
+
(diffTime / 2) ** 2
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const pAPriori = Matrix4.add(
|
|
134
|
+
Matrix.multiply(
|
|
135
|
+
Matrix.multiply(F, this.P),
|
|
136
|
+
Matrix.transpose(F)
|
|
137
|
+
),
|
|
138
|
+
Qk
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
/* ------------
|
|
142
|
+
* CORRECTION
|
|
143
|
+
* ------------*/
|
|
144
|
+
|
|
145
|
+
const accNormalized = Vector3.normalize(acc);
|
|
146
|
+
let dz, K, H;
|
|
147
|
+
|
|
148
|
+
if (mag) {
|
|
149
|
+
|
|
150
|
+
const magNormalized = Vector3.normalize(mag);
|
|
151
|
+
const yc = Vector3.cross(accNormalized, magNormalized);
|
|
152
|
+
const ycNormalized = Vector3.normalize(yc);
|
|
153
|
+
|
|
154
|
+
const dzYc = Vector3.subtract(ycNormalized, Quaternion.rotate(qAPriori, this.cRef));
|
|
155
|
+
const dzAcc = Vector3.subtract(accNormalized, Quaternion.rotate(qAPriori, this.accRef));
|
|
156
|
+
dz = Vector.concat(dzYc, dzAcc);
|
|
157
|
+
|
|
158
|
+
const HYc = this.jacobianES(qAPriori, this.cRef);
|
|
159
|
+
const HAcc = this.jacobianES(qAPriori, this.accRef);
|
|
160
|
+
H = Matrix.concatRow(HYc, HAcc);
|
|
161
|
+
|
|
162
|
+
const RYc = Matrix.concatLine(this.noises.absolute.yc, Matrix3.zeros());
|
|
163
|
+
const RAcc = Matrix.concatLine(Matrix3.zeros(), this.noises.absolute.accelerometer);
|
|
164
|
+
const R = Matrix.concatRow(RYc, RAcc);
|
|
165
|
+
|
|
166
|
+
K = Matrix.multiply(
|
|
167
|
+
Matrix.multiply(pAPriori, Matrix.transpose(H)),
|
|
168
|
+
Matrix.inverse(
|
|
169
|
+
Matrix.add(
|
|
170
|
+
Matrix.multiply(
|
|
171
|
+
Matrix.multiply(H, pAPriori),
|
|
172
|
+
Matrix.transpose(H)
|
|
173
|
+
),
|
|
174
|
+
R
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
dz = Vector3.subtract(accNormalized, Quaternion.rotate(qAPriori, this.accRef));
|
|
180
|
+
H = this.jacobianES(qAPriori, this.accRef);
|
|
181
|
+
const R = this.noises.relative.accelerometer;
|
|
182
|
+
|
|
183
|
+
K = Matrix.multiply(
|
|
184
|
+
Matrix.multiply(pAPriori, Matrix.transpose(H)),
|
|
185
|
+
Matrix3.inverse(
|
|
186
|
+
Matrix3.add(
|
|
187
|
+
Matrix.multiply(
|
|
188
|
+
Matrix.multiply(H, pAPriori),
|
|
189
|
+
Matrix.transpose(H)
|
|
190
|
+
),
|
|
191
|
+
R
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
q = Quaternion.add(
|
|
198
|
+
qAPriori,
|
|
199
|
+
Matrix.multiplyVector(K, dz)
|
|
200
|
+
);
|
|
201
|
+
const P = Matrix.multiply(
|
|
202
|
+
Matrix4.subtract(
|
|
203
|
+
Matrix.identity4(),
|
|
204
|
+
Matrix.multiply(K, H)
|
|
205
|
+
),
|
|
206
|
+
pAPriori
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
q = Quaternion.normalize(q);
|
|
210
|
+
this.quaternion = q;
|
|
211
|
+
this.P = P;
|
|
212
|
+
|
|
213
|
+
return q;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
computeC(b) {
|
|
217
|
+
return [
|
|
218
|
+
[b[0], -b[1], -b[2], -b[3]],
|
|
219
|
+
[b[1], b[0], b[3], -b[2]],
|
|
220
|
+
[b[2], -b[3], b[0], b[1]],
|
|
221
|
+
[b[3], b[2], -b[1], b[0]]
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
jacobianES(q, v) {
|
|
226
|
+
|
|
227
|
+
const [qw, qx, qy, qz] = q;
|
|
228
|
+
const [vx, vy, vz] = v;
|
|
229
|
+
|
|
230
|
+
return [
|
|
231
|
+
[2 * qz * vy - 2 * qy * vz, 2 * qy * vy + 2 * qz * vz, 2 * qx * vy - 2 * qw * vz - 4 * qy * vx, 2 * qw * vy + 2 * qx * vz - 4 * qz * vx],
|
|
232
|
+
[2 * qx * vz - 2 * qz * vx, 2 * qw * vz - 4 * qx * vy + 2 * qy * vx, 2 * qx * vx + 2 * qz * vz, 2 * qy * vz - 2 * qw * vx - 4 * qz * vy],
|
|
233
|
+
[2 * qy * vx - 2 * qx * vy, 2 * qz * vx - 4 * qx * vz - 2 * qw * vy, 2 * qw * vx - 4 * qy * vz + 2 * qz * vy, 2 * qx * vx + 2 * qy * vy]
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export default EkfAttitude;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import chai from 'chai';
|
|
2
|
+
|
|
3
|
+
import { Quaternion } from '@wemap/maths';
|
|
4
|
+
|
|
5
|
+
import EkfAttitude from './EkfAttitude';
|
|
6
|
+
|
|
7
|
+
const expect = chai.expect;
|
|
8
|
+
|
|
9
|
+
const dt = [
|
|
10
|
+
0.02, 0.02, 0.02, 0.02, 0.02
|
|
11
|
+
];
|
|
12
|
+
const accData = [
|
|
13
|
+
[-0.034561157, 3.81073, 8.860977],
|
|
14
|
+
[-0.030700684, 3.814499, 8.818954],
|
|
15
|
+
[-0.018234253, 3.8023376, 8.85762],
|
|
16
|
+
[-0.020080566, 3.8205414, 8.8676605],
|
|
17
|
+
[-0.054519653, 3.8456726, 8.810287]
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const gyrData = [
|
|
21
|
+
[0.0047454834, 0.0028076172, 0.0022888184],
|
|
22
|
+
[0.005218506, 0.0020446777, 0.0012207031],
|
|
23
|
+
[0.0044555664, 0.0023040771, 4.4250488E-4],
|
|
24
|
+
[0.0044555664, 0.0027618408, 0.0011444092],
|
|
25
|
+
[0.0040893555, 0.0020446777, 0.0025787354]
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const magData = [
|
|
29
|
+
[-7.260132, -29.21753, -30.532837],
|
|
30
|
+
[-8.378601, -29.589844, -29.684448],
|
|
31
|
+
[-8.784485, -29.97284, -29.86145],
|
|
32
|
+
[-8.784485, -30.06134, -30.036926],
|
|
33
|
+
[-8.917236, -29.666138, -28.616333]
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const expectationsAbsolute = [
|
|
37
|
+
[0.22501367907095468, 0.048112975227173178, -0.19586497565981961, -0.95325279813673824],
|
|
38
|
+
[0.22521836492577899, 0.048187611771482071, -0.19604062637627487, -0.953164579168738],
|
|
39
|
+
[0.22546875479675244, 0.048146964451479861, -0.19603487950732199, -0.95310861733648156],
|
|
40
|
+
[0.22571872218138991, 0.04816067876157637, -0.19609909097002176, -0.95303554708035731],
|
|
41
|
+
[0.22598409746972753, 0.048415228863568645, -0.1963289784536566, -0.95291242279877153]
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const expectationsRelative = [
|
|
45
|
+
[0.979449872594084, 0.201679452107589, 0.001829118097573, 0],
|
|
46
|
+
[0.979412310855378, 0.201861876976980, 0.001818648953412, 0.000022005516141],
|
|
47
|
+
[0.979416707676713, 0.201841237873455, 0.001739180982152, 0.000051642340567],
|
|
48
|
+
[0.979405268570585, 0.201897124314688, 0.001692389320913, 0.000083114532679],
|
|
49
|
+
[0.979351148979101, 0.202158360999907, 0.001821014027950, 0.000089325563504]
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
describe('initAbsolute', () => {
|
|
53
|
+
it('Should return the good value', () => {
|
|
54
|
+
const ekf = new EkfAttitude();
|
|
55
|
+
ekf.setAbsoluteNoises({
|
|
56
|
+
acc: 0.5,
|
|
57
|
+
gyr: 0.3,
|
|
58
|
+
yc: 2
|
|
59
|
+
});
|
|
60
|
+
const result = ekf.update(dt[0], accData[0], gyrData[0], magData[0]);
|
|
61
|
+
const distance = Quaternion.distance(result, expectationsAbsolute[0]);
|
|
62
|
+
expect(distance).to.below(0.000001);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('updateAbsolute', () => {
|
|
67
|
+
it('Should return the good value', () => {
|
|
68
|
+
const ekf = new EkfAttitude();
|
|
69
|
+
ekf.setAbsoluteNoises({
|
|
70
|
+
acc: 0.5,
|
|
71
|
+
gyr: 0.3,
|
|
72
|
+
yc: 2
|
|
73
|
+
});
|
|
74
|
+
let result;
|
|
75
|
+
let distance;
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < accData.length; i++) {
|
|
78
|
+
result = ekf.update(dt[i], accData[i], gyrData[i], magData[i]);
|
|
79
|
+
distance = Quaternion.distance(result, expectationsAbsolute[i]);
|
|
80
|
+
expect(distance).to.below(0.000001);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('initRelative', () => {
|
|
86
|
+
it('Should return the good value', () => {
|
|
87
|
+
const ekf = new EkfAttitude();
|
|
88
|
+
ekf.setOrientationYaw(0);
|
|
89
|
+
ekf.setRelativeNoises({
|
|
90
|
+
acc: 0.5,
|
|
91
|
+
gyr: 0.3
|
|
92
|
+
});
|
|
93
|
+
const result = ekf.update(dt[0], accData[0], gyrData[0]);
|
|
94
|
+
const distance = Quaternion.distance(result, expectationsRelative[0]);
|
|
95
|
+
expect(distance).to.below(0.000001);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('updateRelative', () => {
|
|
100
|
+
it('Should return the good value', () => {
|
|
101
|
+
const ekf = new EkfAttitude();
|
|
102
|
+
ekf.setOrientationYaw(0);
|
|
103
|
+
ekf.setRelativeNoises({
|
|
104
|
+
acc: 0.5,
|
|
105
|
+
gyr: 0.3
|
|
106
|
+
});
|
|
107
|
+
let result;
|
|
108
|
+
let distance;
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < accData.length; i++) {
|
|
111
|
+
result = ekf.update(dt[i], accData[i], gyrData[i]);
|
|
112
|
+
distance = Quaternion.distance(result, expectationsRelative[i]);
|
|
113
|
+
expect(distance).to.below(0.000001);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|