TinyRobotics
Loading...
Searching...
No Matches
Fusion2D.h
1
2#pragma once
3#include <stdint.h>
4
5#include <cmath>
6
7#include "TinyRobotics/communication/Message.h"
8#include "TinyRobotics/communication/MessageHandler.h"
9#include "TinyRobotics/communication/MessageSource.h"
10#include "TinyRobotics/control/KalmanFilter.h"
11#include "TinyRobotics/control/MotionState2D.h"
12#include "TinyRobotics/coordinates/Coordinate.h"
13#include "TinyRobotics/coordinates/GPSCoordinate.h"
14
15namespace tinyrobotics {
16
17/**
18 * @class Fusion2D
19 * @ingroup fusion
20 * @brief 2D sensor fusion EKF for position, heading, and speed estimation.
21 *
22 * Supports optional sensors:
23 * - IMU (gyro + optional acceleration)
24 * - Wheel speed
25 * - Absolute heading (magnetometer or GPS-derived)
26 * - GPS position
27 *
28 * The EKF automatically adapts to the available sensors.
29 */
30class Fusion2D : public IMotionState2D {
31 public:
32 /**
33 * @brief Holds the estimated 2D state.
34 */
35 struct State2D {
36 uint32_t timeMs; ///< Timestamp of the state in milliseconds
37 float x, y; ///< Position in meters
38 float heading; ///< Heading in radians
39 float speed; ///< Speed in meters per second
40 float gyroBias; ///< Estimated gyro bias (radians/sec)
41 };
42
43 /**
44 * @brief Simple 2D pose (x, y, heading).
45 */
46 struct Pose2D {
47 float x, y;
48 float heading;
49 };
50
51 /**
52 * @brief Constructor. Resets the EKF state to zeros.
53 */
54 Fusion2D() { reset(0); }
55
56 bool begin(const Coordinate<DistanceM>& initialPosition = {0, 0},
57 float initialHeadingDeg = 0) {
58 // Set initial state
59 reset(0); // optional timestamp
60 state.x = 10.0f; // initial X position in meters
61 state.y = 5.0f; // initial Y position in meters
62 state.heading = M_PI / 4; // initial heading in radians (45 degrees)
63 state.speed = 0.0f; // optional initial speed
64 state.timeMs = millis();
65 return true;
66 }
67
68 bool begin(Transform2D initialTransform) {
69 return begin(initialTransform.pos,
70 initialTransform.getHeading(AngleUnit::DEG));
71 }
72
73 // =====================
74 // PREDICTION
75 // =====================
76 /**
77 * @brief Predict the next state based on elapsed time.
78 *
79 * This method performs dead-reckoning using the last speed and heading.
80 * If no wheel speed is available but IMU acceleration is provided, it will
81 * integrate ax/ay to estimate speed.
82 *
83 * @param timeMs Current timestamp in milliseconds.
84 * @param ax Optional IMU acceleration along body-forward axis (m/s²). Used
85 * only if no wheel speed.
86 * @param ay Optional IMU acceleration along body-sideways axis (m/s²). Used
87 * only if no wheel speed.
88 */
89 void predict(uint32_t timeMs, float ax = 0.0f, float ay = 0.0f) {
90 uint32_t dtMs = timeMs - lastPredictTime;
91 lastPredictTime = timeMs;
92 float dt = dtMs * 0.001f;
93 if (dt <= 0.0f) return;
94 if (dt > 0.1f) dt = 0.1f;
95
96 float c = std::cos(state.heading);
97 float s = std::sin(state.heading);
98
99 // Determine velocity
100 float v = state.speed;
101
102 // Use IMU acceleration if wheel speed not available
103 if (!hasWheelSpeed && hasIMU) {
104 float ax_world = ax * c - ay * s;
105 float ay_world = ax * s + ay * c;
106 float dv = std::sqrt(ax_world * ax_world + ay_world * ay_world) * dt;
107 v += dv;
108 state.speed = v;
109 }
110
111 // Heading integration
112 float omega = hasIMU ? (omegaMeasured - state.gyroBias) : 0.0f;
113 state.heading += omega * dt;
114 state.heading = normalizeAngleRad(state.heading);
115
116 // Position integration (requires some velocity)
117 if (v > 0.0f) {
118 state.x += v * c * dt;
119 state.y += v * s * dt;
120 }
121
122 // EKF covariance propagation
123 float F[5][5] = {{1, 0, -v * s * dt, c * dt, 0},
124 {0, 1, v * c * dt, s * dt, 0},
125 {0, 0, 1, 0, -dt},
126 {0, 0, 0, 1, 0},
127 {0, 0, 0, 0, 1}};
128
129 float q_pos = 0.05f, q_heading = 0.01f, q_speed = 0.1f, q_bias = 0.0005f;
130 float Q[5][5] = {{q_pos, 0, 0, 0, 0},
131 {0, q_pos, 0, 0, 0},
132 {0, 0, q_heading, 0, 0},
133 {0, 0, 0, q_speed, 0},
134 {0, 0, 0, 0, q_bias}};
135
136 float FP[5][5] = {}, FPFt[5][5] = {};
137 for (int i = 0; i < 5; i++)
138 for (int j = 0; j < 5; j++)
139 for (int k = 0; k < 5; k++) FP[i][j] += F[i][k] * P[k][j];
140 for (int i = 0; i < 5; i++)
141 for (int j = 0; j < 5; j++)
142 for (int k = 0; k < 5; k++) FPFt[i][j] += FP[i][k] * F[j][k];
143 for (int i = 0; i < 5; i++)
144 for (int j = 0; j < 5; j++) P[i][j] = FPFt[i][j] + Q[i][j];
145
146 state.timeMs = timeMs;
147 }
148
149 // =====================
150 // SENSOR UPDATES
151 // =====================
152 /**
153 * @brief Update the EKF with IMU measurements.
154 *
155 * @param timeMs Timestamp of the IMU measurement.
156 * @param yawRate Gyro z-axis measurement (yaw rate, rad/s).
157 * @param ax Optional IMU acceleration along body x-axis (m/s²).
158 * @param ay Optional IMU acceleration along body y-axis (m/s²).
159 *
160 * The yawRate is used for heading prediction, and optional ax/ay
161 * are used to integrate speed when wheel encoder is missing.
162 */
163 void updateIMU(uint32_t timeMs, float yawRate, float ax = 0.0f,
164 float ay = 0.0f) {
165 hasIMU = true;
166 omegaMeasured = yawRate;
167 predict(timeMs, ax, ay);
168 }
169
170 // Update filter using an IMU2D instance
171 void updateIMU(const IMU2D<float>& imu) {
172 uint32_t t = imu.getTimestamp(); // timestamp in ms or µs
173 float omega = imu.getAngularVelocity(); // angular velocity (rad/s)
174 float v = imu.getLinearVelocity(); // linear velocity (m/s)
175
176 // 1. Integrate angular velocity into orientation
177 updateIMU(t, omega);
178
179 // 2. Integrate linear velocity into position/speed
181 }
182
183 /**
184 * @brief Update the EKF with wheel speed.
185 *
186 * @param timeMs Timestamp of the speed measurement.
187 * @param speed Wheel speed in meters per second.
188 *
189 * Overrides IMU acceleration-based speed if available.
190 */
191 void updateSpeed(uint32_t timeMs, float speed) {
192 hasWheelSpeed = true;
193 predict(timeMs);
194 float y = speed - state.speed;
195 float R = 0.5f;
196 float S = P[3][3] + R;
197 float K[5];
198 for (int i = 0; i < 5; i++) K[i] = P[i][3] / S;
199 applyUpdate(K, y);
200 updateCovariance(K, 3);
201 }
202
203 void updateSpeed(uint32_t timeMs, Speed newSpeed) {
204 updateSpeed(timeMs, newSpeed.getValue(SpeedUnit::MPS));
205 }
206
207 /**
208 * @brief Update the EKF with an absolute heading measurement.
209 *
210 * @param timeMs Timestamp of the heading measurement.
211 * @param heading Absolute heading in radians (e.g., from magnetometer or
212 * GPS-derived).
213 *
214 * This corrects the heading estimate and helps reduce gyro drift.
215 */
216 void updateHeading(uint32_t timeMs, float heading) {
217 predict(timeMs);
218 float y = normalizeAngleRad(heading - state.heading);
219 float R = 0.3f;
220 float S = P[2][2] + R;
221 float K[5];
222 for (int i = 0; i < 5; i++) K[i] = P[i][2] / S;
223 applyUpdate(K, y);
224 state.heading = normalizeAngleRad(state.heading);
225 updateCovariance(K, 2);
226 }
227
228 void updateHeading(uint32_t timeMs, Angle newHeading) {
229 updateHeading(timeMs, newHeading.getValue(AngleUnit::RAD));
230 }
231
232 /**
233 * @brief Update the EKF with GPS position.
234 *
235 * @param timeMs Timestamp of the GPS measurement.
236 * @param x GPS X position in meters (or local coordinates).
237 * @param y GPS Y position in meters (or local coordinates).
238 * @param accuracy Standard deviation of the GPS measurement (meters).
239 *
240 * Corrects the EKF position estimate. Works independently of IMU or wheel
241 * speed.
242 */
243 void updateGPS(uint32_t timeMs, float x, float y, float accuracy) {
244 predict(timeMs);
245 float R = accuracy * accuracy;
246 // X
247 {
248 float residual = x - state.x;
249 float S = P[0][0] + R;
250 float K[5];
251 for (int i = 0; i < 5; i++) K[i] = P[i][0] / S;
252 applyUpdate(K, residual);
253 updateCovariance(K, 0);
254 }
255 // Y
256 {
257 float residual = y - state.y;
258 float S = P[1][1] + R;
259 float K[5];
260 for (int i = 0; i < 5; i++) K[i] = P[i][1] / S;
261 applyUpdate(K, residual);
262 updateCovariance(K, 1);
263 }
264 }
265
266 void updateGPS(uint32_t timeMs, const Coordinate<DistanceM>& gpsCoord,
267 float accuracyM) {
268 updateGPS(timeMs, gpsCoord.x, gpsCoord.y, accuracyM);
269 }
270
271 // =====================
272 // OUTPUTS
273 // =====================
274 /**
275 * @brief Get the full estimated state.
276 * @return Current State2D struct.
277 */
278 State2D getState() const { return state; }
279
280 /**
281 * @brief Get 2D pose (position + heading).
282 * @return Pose2D struct with x, y, heading.
283 */
284 Pose2D getPose() const { return {state.x, state.y, state.heading}; }
285
286 /**
287 * @brief Get the current speed estimate.
288 * @return Speed in meters per second.
289 */
290 float getSpeedMPS() const { return state.speed; }
291
292 /**
293 * @brief Get the current estimated gyro bias.
294 * @return Gyro bias in rad/s.
295 */
296 float getGyroBias() const { return state.gyroBias; }
297
298 Coordinate<DistanceM> getPosition() const {
299 return Coordinate<DistanceM>(state.x, state.y);
300 }
301 Angle getHeading() const { return Angle(state.heading, AngleUnit::RAD); }
302 Speed getSpeed() const { return Speed(state.speed, SpeedUnit::MPS); }
303 Transform2D getTransform() const {
304 return Transform2D(
305 Coordinate<DistanceM>(state.x, state.y),
306 Angle(state.heading, AngleUnit::RAD).getValue(AngleUnit::DEG));
307 }
308
309 void end() {
310 // No resources to clean up in this implementation
311 }
312
313 protected:
314 State2D state;
315 float P[5][5] = {};
316 uint32_t lastPredictTime = 0;
317 float omegaMeasured = 0.0f;
318 bool hasWheelSpeed = false;
319 bool hasIMU = false;
320
321 void applyUpdate(float K[5], float residual) {
322 state.x += K[0] * residual;
323 state.y += K[1] * residual;
324 state.heading += K[2] * residual;
325 state.speed += K[3] * residual;
326 state.gyroBias += K[4] * residual;
327 state.heading = normalizeAngleRad(state.heading);
328 }
329
330 void updateCovariance(float K[5], int idx) {
331 float newP[5][5];
332 for (int i = 0; i < 5; i++)
333 for (int j = 0; j < 5; j++) newP[i][j] = P[i][j] - K[i] * P[idx][j];
334 for (int i = 0; i < 5; i++)
335 for (int j = 0; j < 5; j++) P[i][j] = newP[i][j];
336 }
337
338 /**
339 * @brief Reset the EKF state and covariance.
340 * @param timeMs Optional timestamp to set the initial state time.
341 */
342 void reset(uint32_t timeMs = millis()) {
343 state = {};
344 state.timeMs = timeMs;
345 state.speed = 0.0f;
346 state.heading = 0.0f;
347 state.gyroBias = 0.0f;
348
349 hasWheelSpeed = false;
350 hasIMU = false;
351
352 for (int i = 0; i < 5; i++)
353 for (int j = 0; j < 5; j++) P[i][j] = (i == j) ? 1.0f : 0.0f;
354
355 lastPredictTime = timeMs;
356 omegaMeasured = 0.0f;
357 }
358};
359
360} // namespace tinyrobotics
Represents an angle with unit conversion and wrap-around support.
Definition: Angle.h:42
A generic 3D coordinate class for robotics, navigation, and spatial calculations.
Definition: Coordinate.h:57
2D sensor fusion EKF for position, heading, and speed estimation.
Definition: Fusion2D.h:30
Fusion2D()
Constructor. Resets the EKF state to zeros.
Definition: Fusion2D.h:54
void updateSpeed(uint32_t timeMs, float speed)
Update the EKF with wheel speed.
Definition: Fusion2D.h:191
void updateGPS(uint32_t timeMs, float x, float y, float accuracy)
Update the EKF with GPS position.
Definition: Fusion2D.h:243
void updateHeading(uint32_t timeMs, float heading)
Update the EKF with an absolute heading measurement.
Definition: Fusion2D.h:216
Pose2D getPose() const
Get 2D pose (position + heading).
Definition: Fusion2D.h:284
void predict(uint32_t timeMs, float ax=0.0f, float ay=0.0f)
Predict the next state based on elapsed time.
Definition: Fusion2D.h:89
void reset(uint32_t timeMs=millis())
Reset the EKF state and covariance.
Definition: Fusion2D.h:342
State2D getState() const
Get the full estimated state.
Definition: Fusion2D.h:278
float getSpeedMPS() const
Get the current speed estimate.
Definition: Fusion2D.h:290
float getGyroBias() const
Get the current estimated gyro bias.
Definition: Fusion2D.h:296
void updateIMU(uint32_t timeMs, float yawRate, float ax=0.0f, float ay=0.0f)
Update the EKF with IMU measurements.
Definition: Fusion2D.h:163
Interface for representing the navigation state of a robot in 2D space.
Definition: MotionState2D.h:35
Represents a speed measurement with unit conversion support.
Definition: Speed.h:40
Represents a 2D rigid body transform (SE(2)): translation and rotation.
Definition: FrameMgr2D.h:42
float getHeading(AngleUnit unit) const
Get heading in specified unit (degrees or radians)
Definition: FrameMgr2D.h:54
AngleUnit
Supported angle units for conversion and representation.
Definition: Angle.h:11
SpeedUnit
Supported speed units for conversion and representation.
Definition: Speed.h:10
Simple 2D pose (x, y, heading).
Definition: Fusion2D.h:46
Holds the estimated 2D state.
Definition: Fusion2D.h:35
float gyroBias
Estimated gyro bias (radians/sec)
Definition: Fusion2D.h:40
float speed
Speed in meters per second.
Definition: Fusion2D.h:39
uint32_t timeMs
Timestamp of the state in milliseconds.
Definition: Fusion2D.h:36
float y
Position in meters.
Definition: Fusion2D.h:37
float heading
Heading in radians.
Definition: Fusion2D.h:38