TinyRobotics
Loading...
Searching...
No Matches
Odometry2D.h
1#pragma once
2#include <cmath>
3#include <vector>
4
5#include "Arduino.h" // for millis()
6#include "TinyRobotics/communication/MessageHandler.h"
7#include "TinyRobotics/communication/MessageSource.h"
8#include "TinyRobotics/control/MotionState2D.h"
9#include "TinyRobotics/coordinates/Coordinate.h"
10#include "TinyRobotics/odometry/OdometryModel2D.h"
11#include "TinyRobotics/units/Units.h"
12
13namespace tinyrobotics {
14
15/**
16 * @class Odometry2D
17 * @ingroup odometry
18 * @brief Tracks 2D position and orientation of a robot using velocity and
19 * steering data from external sources.
20 *
21 * This class provides 2D odometry for mobile robots, such as differential drive
22 * or Ackermann vehicles. It integrates velocity and steering angle over time to
23 * estimate the robot's position (x, y) in meters.
24 *
25 * ## Supported Kinematics
26 * - Differential drive (default model, via steering angle is provided, via
27 * IOdometryModel2D)
28 *
29 * ## Inputs
30 * - Speed (from an ISpeedSource, e.g., WheelEncoder)
31 * - Steering angle (from messages or sensors)
32 * - Time delta (in milliseconds, or uses millis() if not provided)
33 *
34 * ## Outputs
35 * - 2D position (x, y) in meters
36 *
37 * ## Coordinate Frame
38 * - All positions are in the robot's local/world frame, with x forward and y
39 * left/right.
40 *
41 * ## Update Method
42 * - Call update() in your control loop to update odometry using the latest
43 * speed and steering angle.
44 * - Optionally provide delta time, or let the class compute it using millis().
45 *
46 * ## Integration Method
47 * - Uses simple Euler integration for position updates.
48 *
49 * ## Construction
50 * - Construct with references to an IMessageSource (vehicle), an ISpeedSource
51 * (e.g., WheelEncoder), and an IOdometryModel2D (kinematics model).
52 * - The vehicle is subscribed to receive messages for speed/throttle/steering
53 * updates.
54 *
55 * ## Limitations
56 * - Assumes no wheel slip or drift unless compensated by the speed source.
57 * - Not suitable for holonomic or omnidirectional robots.
58 * - Orientation (theta) is not explicitly tracked in this class.
59 *
60 * ## Example Usage
61 * @code
62 * WheelEncoder encoder(...);
63 * OdometryModelAckerman model(...);
64 * Odometry2D odom(vehicle, encoder, model);
65 * odom.begin(Coordinate<DistanceM>(0, 0));
66 * // In your control loop:
67 * odom.update();
68 * auto pos = odom.getPosition();
69 * Serial.printf("x=%.2f, y=%.2f\n", pos.x, pos.y);
70 * @endcode
71 *
72 * @author TinyRobotics contributors
73 * @date 2026-03-30
74 */
75
76class Odometry2D : public IMotionState2D {
77 public:
78 Odometry2D(MessageSource& vehicle, ISpeedSource& speedSource,
79 IOdometryModel2D& model)
80 : vehicle(vehicle), speedSource(speedSource), model(model) {
81 model.setSpeedSource(speedSource);
82 vehicle.subscribe(model);
84 [](void* userData) {
85 Odometry2D* odometry = static_cast<Odometry2D*>(userData);
86 odometry->update();
87 },
88 this);
89 }
90
91 /**
92 * @brief Initialize the odometry state.
93 *
94 * @param initialPosition The starting position of the robot (x, y) in meters.
95 * @param initialTheta The starting orientation (heading/yaw) in radians.
96 * Default is 0.0.
97 * @return true on success
98 */
99 bool begin(Coordinate<DistanceM> initialPosition, float initialTheta = 0.0f) {
100 position = initialPosition;
101 theta = initialTheta;
102 totalDistance = 0.0f;
103 lastUpdateTimeMs = 0;
104 return true;
105 }
106
107 bool begin(Transform2D transform) {
108 return begin(transform.pos, transform.getHeading(AngleUnit::RAD));
109 }
110
111 /**
112 * @brief Initialize the odometry state from a Frame2D.
113 *
114 * @param frame The Frame2D containing the initial position and orientation.
115 * @return true on success
116 */
117 bool begin(const Frame2D& frame) {
118 // Extract position and orientation from the frame's transform
119 auto tf = frame.getTransform();
120 // tf.translation is a Coordinate<float>, convert to Coordinate<DistanceM>
121 return begin(tf.pos, tf.getHeading(AngleUnit::RAD));
122 }
123
124 void end() {}
125
126 /**
127 * @brief Update the odometry state with new speed and steering angle
128 * Call this method in your control loop
129 */
130 void update() {
131 auto now = millis();
132 float deltaTimeMs = now - lastUpdateTimeMs;
133 if (lastUpdateTimeMs > 0) update(deltaTimeMs);
134 lastUpdateTimeMs = now;
135 }
136
137 /// @brief Get the current 2D position (meters)
138 Coordinate<DistanceM> getPosition() const { return position; }
139 /// @brief Get the current steering angle (radians or angular velocity)
140 Angle getSteeringAngle() const { return model.getSteeringAngle(); }
141 /// @brief Get the current heading as an Angle (radians)
142 Angle getHeading() const { return Angle(theta, AngleUnit::RAD); }
143 /// @brief Get the current speed (meters/second)
144 Speed getSpeed() const { return model.getSpeed(); }
145 /// @brief Get the current orientation (radians)
146 float getTheta() const { return theta; }
147 /// @brief Get the current linear velocity (meters/second)
148 float getLinearVelocity() const {
149 return getSpeed().getValue(SpeedUnit::MPS);
150 }
151 /// @brief Get the current angular velocity (radians/second)
152 float getAngularVelocity() const {
153 return getSteeringAngle().getValue(AngleUnit::RAD);
154 }
155 /// @brief Get the total distance traveled
156 Distance getTotalDistance() const {
157 return Distance(totalDistance, DistanceUnit::M);
158 }
159 /// @brief Get the last delta update (dx, dy, dtheta)
160 Delta2D getLastDelta() const { return lastDelta; }
161
162 /// @brief Get the current speed as a Speed3D (x, y in world frame, z: 0)
163 Speed3D getSpeed3D() const {
164 float v = getSpeed().getValue(SpeedUnit::MPS);
165 float vx = v * std::cos(theta);
166 float vy = v * std::sin(theta);
167 return Speed3D(vx, vy, 0.0f, SpeedUnit::MPS);
168 }
169
170 /// @brief Set the odometry state (position and orientation)
171 void setState(Coordinate<DistanceM> pos, float th) {
172 position = pos;
173 theta = th;
174 }
175
176 /// @brief Get the current motion state as a MotionState3D (with z=0 and
177 /// pitch/roll=0)
179 return MotionState3D(Coordinate<float>(position.x, position.y, 0),
180 Orientation3D(theta, 0, 0), getSpeed3D(),
181 AngularVelocity3D(0.0f, 0.0f, getAngularVelocity(),
182 AngularVelocityUnit::RadPerSec));
183 }
184
185 Transform2D getTransform() const {
186 return Transform2D(position, getHeading().getValue(AngleUnit::DEG));
187 }
188
189 protected:
190 Coordinate<float> position;
191 float theta = 0.0f;
192 float totalDistance = 0.0f;
193 Delta2D lastDelta = {0.0f, 0.0f, 0.0f}; // (dx, dy, dtheta)
194 uint32_t lastUpdateTimeMs = 0;
195 IOdometryModel2D& model;
196 ISpeedSource& speedSource;
197 MessageSource& vehicle;
198 bool is_differential = false;
199
200 void publish() {
201 // Publish position as message
202 Message<Coordinate<float>> msgPos{MessageContent::Position,
203 Coordinate<float>(position.x, position.y),
204 Unit::Meters};
205 msgPos.origin = MessageOrigin::Odometry;
206 sendMessage(msgPos);
207
208 // Publish heading as float (radians)
209 Message<float> msgHeading{MessageContent::Heading, theta,
210 Unit::AngleRadian};
211 msgHeading.origin = MessageOrigin::Odometry;
212 sendMessage(msgHeading);
213
214 // Publish speed as float (meters/second)
215 Message<float> msgSpeed{MessageContent::Speed,
216 getSpeed().getValue(SpeedUnit::MPS),
217 Unit::MetersPerSecond};
218 msgSpeed.origin = MessageOrigin::Odometry;
219 sendMessage(msgSpeed);
220
221 // Publish the full motion state as a MotionState3D message
222 Message<MotionState3D> msgState{MessageContent::MotionState,
223 getMotionState(), Unit::Undefined};
224 msgState.origin = MessageOrigin::Odometry;
225 vehicle.sendMessage(msgState);
226 }
227
228 /**
229 * @brief Update the odometry state with new speed and steering angle.
230 *
231 * @param speed The current speed of the robot (with units).
232 * @param steeringAngle The current steering angle (radians for
233 * Ackermann/rudder, or angular velocity for differential drive).
234 * @param deltaTimeMs Time since last update in milliseconds.
235 *
236 */
237 void update(uint32_t deltaTimeMs) {
238 // Update speed from the speed source (if it has inertia)
239 model.updateSpeed(deltaTimeMs);
240 float steeringAngleRad = model.getSteeringAngle().getValue(AngleUnit::RAD);
241 float deltaTheta = 0.0f;
242 deltaTheta = model.computeDeltaTheta(deltaTimeMs);
243 theta += deltaTheta;
244 // Normalize theta to [-pi, pi)
245 theta = normalizeAngleRad(theta);
246 float deltaX = 0.0f, deltaY = 0.0f;
247 model.computeDeltaXY(theta, deltaTimeMs, deltaX, deltaY);
248 position.x += deltaX;
249 position.y += deltaY;
250 lastDelta = {deltaX, deltaY, deltaTheta};
251 totalDistance += std::sqrt(deltaX * deltaX + deltaY * deltaY);
252
253 publish();
254 }
255};
256
257} // 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
Represents a distance measurement with unit conversion support.
Definition: Distance.h:40
Interface for representing the navigation state of a robot in 2D space.
Definition: MotionState2D.h:35
Abstract interface for 2D odometry models.
Definition: IOdometryModel2D.h:9
virtual Angle getSteeringAngle() const =0
Get the current steering angle of the vehicle.
virtual Speed getSpeed() const =0
Get the current speed of the vehicle.
virtual void registerCallback(void(*callback)(void *), void *userData)=0
Register a callback to be invoked on relevant events (e.g., input change, update).
Interface for speed sources.
Definition: ISpeedSource.h:9
Base class for message sources in the TinyRobotics communication framework.
Definition: MessageSource.h:35
void subscribe(MessageHandler &handler, MessageOrigin origin=MessageOrigin::Undefined, MessageContent content=MessageContent::Undefined)
Subscribe a message handler to this source, with optional filtering.
Definition: MessageSource.h:59
Represents the full 3D motion state of a robot or vehicle.
Definition: MotionState3D.h:53
Tracks 2D position and orientation of a robot using velocity and steering data from external sources.
Definition: Odometry2D.h:76
Speed3D getSpeed3D() const
Get the current speed as a Speed3D (x, y in world frame, z: 0)
Definition: Odometry2D.h:163
bool begin(const Frame2D &frame)
Initialize the odometry state from a Frame2D.
Definition: Odometry2D.h:117
void update(uint32_t deltaTimeMs)
Update the odometry state with new speed and steering angle.
Definition: Odometry2D.h:237
float getLinearVelocity() const
Get the current linear velocity (meters/second)
Definition: Odometry2D.h:148
void setState(Coordinate< DistanceM > pos, float th)
Set the odometry state (position and orientation)
Definition: Odometry2D.h:171
Angle getSteeringAngle() const
Get the current steering angle (radians or angular velocity)
Definition: Odometry2D.h:140
MotionState3D getMotionState() const
Get the current motion state as a MotionState3D (with z=0 and pitch/roll=0)
Definition: Odometry2D.h:178
Delta2D getLastDelta() const
Get the last delta update (dx, dy, dtheta)
Definition: Odometry2D.h:160
bool begin(Coordinate< DistanceM > initialPosition, float initialTheta=0.0f)
Initialize the odometry state.
Definition: Odometry2D.h:99
Angle getHeading() const
Get the current heading as an Angle (radians)
Definition: Odometry2D.h:142
Distance getTotalDistance() const
Get the total distance traveled.
Definition: Odometry2D.h:156
void update()
Update the odometry state with new speed and steering angle Call this method in your control loop.
Definition: Odometry2D.h:130
float getTheta() const
Get the current orientation (radians)
Definition: Odometry2D.h:146
Speed getSpeed() const
Get the current speed (meters/second)
Definition: Odometry2D.h:144
float getAngularVelocity() const
Get the current angular velocity (radians/second)
Definition: Odometry2D.h:152
Coordinate< DistanceM > getPosition() const
Get the current 2D position (meters)
Definition: Odometry2D.h:138
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
DistanceUnit
Supported distance units for conversion and representation.
Definition: Distance.h:10
AngleUnit
Supported angle units for conversion and representation.
Definition: Angle.h:11
SpeedUnit
Supported speed units for conversion and representation.
Definition: Speed.h:10
Represents a 2D incremental motion update (dx, dy, dtheta).
Definition: MotionState2D.h:15
Represents a 2D coordinate frame in a hierarchical frame tree.
Definition: FrameMgr2D.h:130
Generic message structure for communication, parameterized by value type.
Definition: Message.h:72