TinyRobotics
Loading...
Searching...
No Matches
WheelEncoder.h
1#pragma once
2#include <cmath>
3#include <vector>
4
5#include "TinyRobotics/communication/Message.h"
6#include "TinyRobotics/communication/MessageSource.h"
7#include "TinyRobotics/control/Scheduler.h"
8#include "TinyRobotics/odometry/ISpeedSource.h"
9#include "TinyRobotics/units/Distance.h"
10#include "TinyRobotics/units/Units.h"
11
12namespace tinyrobotics {
13
14/**
15 * @class WheelEncoder
16 * @ingroup sensors
17 * @brief Measures wheel rotation and computes per-wheel distance and speed
18 * using encoder ticks for mobile robots.
19 *
20 * The WheelEncoder class provides a multi-wheel, vectorized interface for
21 * tracking the movement of a robot's wheels using incremental encoders. It
22 * accumulates encoder ticks for each wheel to estimate the distance traveled
23 * and calculates speed based on tick timing, supporting robust odometry for
24 * differential, skid-steer, and multi-motor vehicles.
25 *
26 * Key features:
27 * - Supports any number of wheels (configurable at construction).
28 * - Vectorized state for distance, speed, and tick timing per wheel.
29 * - Interface-compliant with ISpeedSource for modular odometry integration.
30 * - Configurable wheel diameter and ticks per revolution for accurate distance
31 * estimation.
32 * - Periodic reporting of distance and speed via the MessageSource interface.
33 * - Slip calibration support to compensate for wheel slip or surface effects.
34 * - Designed for use with interrupt-driven tick updates (call setTick() in your
35 * ISR, specifying the wheel index).
36 *
37 * Usage:
38 * 1. Create a WheelEncoder instance, specifying the number of wheels if needed,
39 * and configure the wheel diameter and ticks per revolution.
40 * 2. Call begin() to reset and start periodic reporting.
41 * 3. In your encoder interrupt handler, call setTick(motor) to update the
42 * encoder state for the correct wheel.
43 * 4. Use getDistanceM(motor), getSpeedMPS(motor), or getDistance(unit, motor)
44 * to retrieve odometry data for each wheel.
45 * 5. Optionally, calibrate slip using calibrateSlip() if you observe systematic
46 * odometry errors.
47 *
48 * Example:
49 * @code
50 * WheelEncoder encoder(2); // Two wheels (differential drive)
51 * encoder.setWheelDiameter(0.065); // 65mm wheel
52 * encoder.setTicksPerRevolution(20);
53 * encoder.begin();
54 * // In your interrupt:
55 * encoder.setTick(0); // Left wheel
56 * encoder.setTick(1); // Right wheel
57 * // In your main loop:
58 * float leftDistance = encoder.getDistanceM(0);
59 * float rightDistance = encoder.getDistanceM(1);
60 * float leftSpeed = encoder.getSpeedMPS(0);
61 * float rightSpeed = encoder.getSpeedMPS(1);
62 * @endcode
63 *
64 * This class is intended for embedded robotics applications (Arduino, ESP32,
65 * etc.) and integrates with the TinyRobotics messaging framework. It is
66 * suitable for use as a modular speed/distance source in extensible odometry
67 * pipelines.
68 */
69class WheelEncoder : public MessageSource, public ISpeedSource {
70 public:
71 /**
72 * @brief Default constructor. Wheel diameter and ticks per revolution must be
73 * set before use.
74 */
75 WheelEncoder(size_t numWheels = 1)
76 : speedMPS(numWheels, 0.0f),
77 distanceM(numWheels, 0.0f),
81
82 /**
83 * @brief Constructor with wheel diameter and ticks per revolution.
84 * @param wheelDiameterM The diameter of the wheel (Distance object).
85 * @param ticksPerRevolution Number of encoder ticks per full wheel
86 * revolution.
87 */
88 WheelEncoder(Distance wheelDiameterM, int ticksPerRevolution = 1,
89 size_t numWheels = 1)
91 setWheelDiameter(wheelDiameterM);
92 setTicksPerRevolution(ticksPerRevolution);
93 }
94
95 /**
96 * @brief Set the wheel diameter using a Distance object.
97 * @param wheelDiameter The wheel diameter.
98 */
99 void setWheelDiameter(Distance wheelDiameter) {
100 wheelDiameterM = wheelDiameter.getValue(DistanceUnit::M);
101 }
102
103 /**
104 * @brief Set the wheel diameter using a float value and unit.
105 * @param diameter The wheel diameter value.
106 * @param unit The unit of the diameter (default: meters).
107 */
108 void setWheelDiameter(float diameter, DistanceUnit unit = DistanceUnit::M) {
109 setWheelDiameter(Distance(diameter, unit));
110 }
111
112 /**
113 * @brief Set the number of encoder ticks per wheel revolution.
114 * @param ticks Number of ticks per revolution.
115 */
116 void setTicksPerRevolution(int ticks) {
117 ticksPerRevolution = ticks;
118 distancePerTickM =
119 static_cast<float>(M_PI) * wheelDiameterM / ticksPerRevolution;
120 }
121
122 /**
123 * @brief Defines the reporint frequency for distance and speed messages.
124 * @param ms Reporting interval in milliseconds.
125 */
126 void setReportingFrequencyMs(uint16_t ms) {
127 reportingScheduler.begin(ms, sendMessageCB, this);
128 }
129
130 /**
131 * @brief Get the total distance traveled since last reset.
132 * @return Distance object representing the traveled distance in meters.
133 */
134 // Removed: getDistance() returning Distance from a vector (invalid)
135
136 /**
137 * @brief Get the total distance traveled in meters.
138 * @return Distance in meters.
139 */
140 float getDistanceM(size_t motor = 0) const {
141 if (motor >= numWheels) return 0.0f;
142 return distanceM[motor] * slipFactor;
143 }
144
145 /**
146 * @brief Get the total distance traveled in the specified unit.
147 * @param unit The distance unit.
148 * @return Distance in the specified unit.
149 */
150 float getDistance(DistanceUnit unit, size_t motor = 0) const {
151 Distance dist(getDistanceM(motor), DistanceUnit::M);
152 return dist.getValue(unit);
153 }
154
155 /**
156 * @brief Get the distance corresponding to a given number of ticks (in
157 * meters).
158 * @param ticks Number of encoder ticks.
159 * @return Distance in meters.
160 */
161 float getDistanceForTicksM(size_t ticks) const {
162 return ticks * distancePerTickM;
163 }
164
165 /**
166 * @brief Get the distance for a given number of ticks in the specified unit.
167 * @param ticks Number of encoder ticks.
168 * @param unit The distance unit.
169 * @return Distance in the specified unit.
170 */
171 float getDistanceForTicks(size_t ticks, DistanceUnit unit) const {
172 Distance dist(getDistanceForTicksM(ticks), DistanceUnit::M);
173 return dist.getValue(unit);
174 }
175
176 /**
177 * @brief Resets the encoder counts and distance.
178 *
179 * Also starts periodic reporting if not already active.
180 * @return true if the encoder is properly configured (wheel diameter and
181 * ticks per revolution are set).
182 */
183 bool begin() {
184 distanceM.resize(numWheels, 0.0f);
185 lastSpeedCalcTimeMs.resize(numWheels, 0);
186 lastDistanceM.resize(numWheels, 0.0f);
187 speedMPS.resize(numWheels, 0.0f);
188 for (size_t i = 0; i < numWheels; ++i) {
189 distanceM[i] = 0;
190 lastSpeedCalcTimeMs[i] = millis();
191 lastDistanceM[i] = 0;
192 speedMPS[i] = 0;
193 }
194 if (!reportingScheduler.isActive()) {
196 }
197 return wheelDiameterM > 0 && ticksPerRevolution > 0;
198 }
199
200 /**
201 * @brief To be called by the pin interrupt handler when a tick is detected.
202 *
203 * Updates the distance and triggers reporting if needed.
204 */
205 void setTick(size_t motor = 0) {
206 if (motor >= numWheels) return;
207 distanceM[motor] += distancePerTickM;
208 reportingScheduler.run();
209 }
210
211 Speed getSpeed(uint8_t motor = 0) const override {
212 if (motor >= numWheels) return Speed(0.0f, SpeedUnit::MPS);
213 return Speed(const_cast<WheelEncoder*>(this)->getSpeedMPS(motor),
214 SpeedUnit::MPS);
215 }
216
217 /// Just provide the last reported speed without inertia modeling
218
219 Speed updateSpeed(uint32_t deltaTimeMs, uint8_t motor = 0) override {
220 if (motor >= numWheels) return Speed(0.0f, SpeedUnit::MPS);
221 return Speed(speedMPS[motor], SpeedUnit::MPS);
222 }
223
224 /**
225 * @brief Trigger sending messages for distance and speed.
226 *
227 * Sends the current distance and speed as messages.
228 */
229 void sendMessage() {
230 for (size_t i = 0; i < numWheels; ++i) {
231 Message<float> msg(MessageContent::Distance, getDistanceM(i),
232 Unit::Meters);
233 msg.origin = MessageOrigin::Sensor;
234 MessageSource::sendMessage(msg);
235
236 Message<float> speedMsg(MessageContent::Speed, getSpeedMPS(i),
237 Unit::MetersPerSecond);
238 speedMsg.origin = MessageOrigin::Sensor;
239 MessageSource::sendMessage(speedMsg);
240 }
241 }
242
243 /**
244 * @brief Set the slip correction factor.
245 *
246 * The slip factor is used to correct encoder-based distance and speed
247 * estimates. It should be set to (actual_distance / encoder_distance) after
248 * calibration. Default is 1.0 (no slip).
249 * @param slipFactor The slip correction factor (typically <= 1.0).
250 */
251 void setSlipFactor(float slipFactor) { this->slipFactor = slipFactor; }
252
253 /**
254 * @brief Calibrate slip by providing a known actual distance.
255 *
256 * Call this after moving the robot a known distance and accumulating encoder
257 * ticks. The slip factor will be set to (actual_distance / encoder_distance).
258 * @param actualDistanceM The actual distance traveled in meters.
259 */
260 void calibrateSlip(float actualDistanceM) {
261 float encoderDistance = getRawDistanceM();
262 if (encoderDistance > 0) {
263 slipFactor = actualDistanceM / encoderDistance;
264 }
265 }
266
267 /**
268 * @brief Get the slip correction factor.
269 * @return The slip factor (1.0 = no slip, <1.0 = slip present).
270 */
271 float getSlipFactor() const { return slipFactor; }
272
273 /**
274 * @brief Get the raw (uncorrected) distance in meters from the encoder.
275 * @return Raw encoder distance in meters.
276 */
277 float getRawDistanceM(size_t motor = 0) const {
278 if (motor >= numWheels) return 0.0f;
279 return distanceM[motor];
280 }
281
282 /// Not used
283 void setThrottlePercent(float value, uint8_t motor = 0) override {}
284
285 size_t getMotorCount() const override { return numWheels; }
286
287 protected:
288 std::vector<float> speedMPS;
289 std::vector<float> distanceM;
290 // Returns the distance for a specific motor (default 0)
291 Distance getDistance(size_t motor = 0) const {
292 if (motor >= numWheels) return Distance(0.0f, DistanceUnit::M);
293 return Distance(distanceM[motor], DistanceUnit::M);
294 }
295 float wheelDiameterM = 0.0f;
296 float distancePerTickM = 0.0f;
297 std::vector<float> lastDistanceM;
298 float slipFactor = 1.0f;
299 uint32_t ticksPerRevolution = 0;
300 std::vector<uint32_t> lastSpeedCalcTimeMs;
301 Scheduler reportingScheduler;
302 size_t numWheels = 1;
303
304 /**
305 * @brief Static callback for the reporting scheduler to send messages.
306 * @param ref Pointer to the WheelEncoder instance.
307 */
308 static void sendMessageCB(void* ref) {
309 WheelEncoder* encoder = static_cast<WheelEncoder*>(ref);
310 encoder->sendMessage();
311 }
312
313 /**
314 * @brief Calculate speed in meters per second based on distance traveled
315 * since last calculation and elapsed time.
316 * @return Speed in meters per second.
317 */
318 float getSpeedMPS(size_t motor = 0) {
319 if (motor >= numWheels) return 0.0f;
320 speedMPS[motor] = 0;
321 uint32_t currentTimeMs = millis();
322 uint32_t elapsedTimeMs = currentTimeMs - lastSpeedCalcTimeMs[motor];
323 if (elapsedTimeMs > 0) {
324 float distanceTraveledM =
325 (distanceM[motor] - lastDistanceM[motor]) * slipFactor;
326 speedMPS[motor] = distanceTraveledM / (elapsedTimeMs / 1000.0f);
327 lastSpeedCalcTimeMs[motor] = currentTimeMs;
328 lastDistanceM[motor] = distanceM[motor];
329 }
330 return speedMPS[motor];
331 }
332};
333
334} // namespace tinyrobotics
Represents a distance measurement with unit conversion support.
Definition: Distance.h:40
Interface for speed sources.
Definition: ISpeedSource.h:9
Base class for message sources in the TinyRobotics communication framework.
Definition: MessageSource.h:35
Simple periodic task scheduler for embedded and Arduino environments.
Definition: Scheduler.h:57
Represents a speed measurement with unit conversion support.
Definition: Speed.h:40
Measures wheel rotation and computes per-wheel distance and speed using encoder ticks for mobile robo...
Definition: WheelEncoder.h:69
void setSlipFactor(float slipFactor)
Set the slip correction factor.
Definition: WheelEncoder.h:251
static void sendMessageCB(void *ref)
Static callback for the reporting scheduler to send messages.
Definition: WheelEncoder.h:308
float getDistanceM(size_t motor=0) const
Get the total distance traveled since last reset.
Definition: WheelEncoder.h:140
float getDistance(DistanceUnit unit, size_t motor=0) const
Get the total distance traveled in the specified unit.
Definition: WheelEncoder.h:150
void setWheelDiameter(float diameter, DistanceUnit unit=DistanceUnit::M)
Set the wheel diameter using a float value and unit.
Definition: WheelEncoder.h:108
WheelEncoder(Distance wheelDiameterM, int ticksPerRevolution=1, size_t numWheels=1)
Constructor with wheel diameter and ticks per revolution.
Definition: WheelEncoder.h:88
bool begin()
Resets the encoder counts and distance.
Definition: WheelEncoder.h:183
float getSlipFactor() const
Get the slip correction factor.
Definition: WheelEncoder.h:271
Speed getSpeed(uint8_t motor=0) const override
Get the current speed.
Definition: WheelEncoder.h:211
void setThrottlePercent(float value, uint8_t motor=0) override
Not used.
Definition: WheelEncoder.h:283
void setReportingFrequencyMs(uint16_t ms)
Defines the reporint frequency for distance and speed messages.
Definition: WheelEncoder.h:126
Speed updateSpeed(uint32_t deltaTimeMs, uint8_t motor=0) override
Just provide the last reported speed without inertia modeling.
Definition: WheelEncoder.h:219
float getDistanceForTicks(size_t ticks, DistanceUnit unit) const
Get the distance for a given number of ticks in the specified unit.
Definition: WheelEncoder.h:171
float getRawDistanceM(size_t motor=0) const
Get the raw (uncorrected) distance in meters from the encoder.
Definition: WheelEncoder.h:277
float getSpeedMPS(size_t motor=0)
Calculate speed in meters per second based on distance traveled since last calculation and elapsed ti...
Definition: WheelEncoder.h:318
float getDistanceForTicksM(size_t ticks) const
Get the distance corresponding to a given number of ticks (in meters).
Definition: WheelEncoder.h:161
void setTick(size_t motor=0)
To be called by the pin interrupt handler when a tick is detected.
Definition: WheelEncoder.h:205
void sendMessage()
Trigger sending messages for distance and speed.
Definition: WheelEncoder.h:229
void setTicksPerRevolution(int ticks)
Set the number of encoder ticks per wheel revolution.
Definition: WheelEncoder.h:116
void calibrateSlip(float actualDistanceM)
Calibrate slip by providing a known actual distance.
Definition: WheelEncoder.h:260
void setWheelDiameter(Distance wheelDiameter)
Set the wheel diameter using a Distance object.
Definition: WheelEncoder.h:99
WheelEncoder(size_t numWheels=1)
Default constructor. Wheel diameter and ticks per revolution must be set before use.
Definition: WheelEncoder.h:75
DistanceUnit
Supported distance units for conversion and representation.
Definition: Distance.h:10
SpeedUnit
Supported speed units for conversion and representation.
Definition: Speed.h:10