TinyRobotics
Loading...
Searching...
No Matches
MotionController3D.h
1#pragma once
2#include <cmath>
3#include "Arduino.h" // for millis
4#include "TinyRobotics/control/MotionState3D.h"
5#include "TinyRobotics/control/PIDController.h"
6#include "TinyRobotics/coordinates/Coordinate.h"
7#include "TinyRobotics/coordinates/Orientation3D.h"
8#include "TinyRobotics/planning/Path.h"
9#include "TinyRobotics/units/AngularVelocity.h"
10#include "TinyRobotics/units/Speed.h"
11
12namespace tinyrobotics {
13
14/// Action to take when reaching the goal (last waypoint)
15enum class OnGoalAction { Stop, HoldPosition, Circle };
16
17/**
18 * @class MotionController3D
19 * @ingroup control
20 * @brief 3D motion/path controller using PID for position and orientation.
21 *
22 * This class provides high-level 3D path following and pose control for robots,
23 * drones, or vehicles. It uses independent PID controllers for each axis (x, y,
24 * z) and orientation (yaw, pitch, roll).
25 *
26 * ## Features
27 * - Accepts a reference to an IMotionState3D for real-time feedback
28 * - Supports a path of 3D waypoints (Path<Coordinate<DistanceM>>)
29 * - Automatically advances to the next waypoint when the current one is reached
30 * - PID gains and limits are configurable for both position and orientation
31 * - Provides update() to compute new commands, and getters for linear/angular
32 * velocity commands
33 * - begin() initializes the target from the first path coordinate
34 *
35 * ## Usage
36 * 1. Create with a reference to your IMotionState3D implementation (e.g.,
37 * IMU3D)
38 * 2. Set a path of waypoints with setPath()
39 * 3. Call begin() to initialize the first target
40 * 4. In your control loop:
41 * - Call update() to compute new commands
42 * - Use getLinearCommand() and getAngularCommand() for actuation
43 *
44 * ## Limitations
45 * - Only the position is updated from the path; orientation, speed, and angular
46 * velocity are kept from the previous target
47 *
48 * ## Example
49 * @code
50 * MotionController3D controller(imu3d);
51 * controller.setPath(path);
52 * controller.begin();
53 * while (!controller.isPathComplete()) {
54 * controller.update();
55 * auto v = controller.getLinearCommand();
56 * auto w = controller.getAngularCommand();
57 * // send v, w to actuators
58 * controller.advanceWaypoint();
59 * }
60 * @endcode
61 */
63 public:
64 MotionController3D(IMotionState3D& motionStateRef, OnGoalAction onGloal,
65 float positionToleranceM = 2.0)
66 : positionToleranceM(positionToleranceM),
67 onGoalAction(onGloal),
68 path(),
69 motionState(motionStateRef) {
70 // Default PID: dt=0.1s, max=1.0, min=-1.0, kp=1.0, ki=0.0, kd=0.0
71 pidX.begin(0.1f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f);
72 pidY.begin(0.1f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f);
73 pidZ.begin(0.1f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f);
74 pidYaw.begin(0.1f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f);
75 pidPitch.begin(0.1f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f);
76 pidRoll.begin(0.1f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f);
77 }
78
79 void configurePositionPID(float dt, float maxOut, float minOut, float kp,
80 float ki, float kd) {
81 pidX.begin(dt, minOut, maxOut, kp, ki, kd);
82 pidY.begin(dt, minOut, maxOut, kp, ki, kd);
83 pidZ.begin(dt, minOut, maxOut, kp, ki, kd);
84 }
85 void configureOrientationPID(float dt, float minOut, float maxOut, float kp,
86 float ki, float kd) {
87 pidYaw.begin(dt, minOut, maxOut, kp, ki, kd);
88 pidPitch.begin(dt, minOut, maxOut, kp, ki, kd);
89 pidRoll.begin(dt, minOut, maxOut, kp, ki, kd);
90 }
91
92 void setPath(Path<Coordinate<DistanceM>> path) {
93 this->path = path;
94 circleInitialized = false;
95 }
96
97 /**
98 * @brief Initialize controller and set target from first path coordinate if
99 * available.
100 */
101 void begin() {
102 updateCount = 0;
103 updateStartTimeMs = 0;
104 updateEndTimeMs = 0;
105 dtSetFromUpdates = false;
106
107 if (!path.isEmpty()) {
108 target = MotionState3D(
109 path[0], target.getOrientation(), Speed3D(0, 0, 0, SpeedUnit::MPS),
110 AngularVelocity3D(0, 0, 0, AngularVelocityUnit::RadPerSec));
111 is_active = true;
112 }
113 }
114
115 /// Stop the controller and the vehicle
116 void end() { is_active = false; }
117
118 /**
119 * @brief Update the controller and compute new commands.
120 * @return true if we still have a path to follow, false if path is complete
121 or controller is inactive.
122 */
123 bool update() {
124 if (!is_active) return false;
125 if (!initializeDtFromUpdates()) return true;
126 advanceWaypoint(positionToleranceM);
127 if (path.isEmpty()) {
128 return false;
129 }
130 // Use the referenced motionState for current state
131 float vx =
132 pidX.calculate(target.getPosition().x, motionState.getPosition().x);
133 float vy =
134 pidY.calculate(target.getPosition().y, motionState.getPosition().y);
135 float vz =
136 pidZ.calculate(target.getPosition().z, motionState.getPosition().z);
137 float vyaw =
138 pidYaw.calculate(normalizeAngleRad(target.getOrientation().yaw),
139 normalizeAngleRad(motionState.getOrientation().yaw));
140 float vpitch = pidPitch.calculate(
141 normalizeAngleRad(target.getOrientation().pitch),
142 normalizeAngleRad(motionState.getOrientation().pitch));
143 float vroll =
144 pidRoll.calculate(normalizeAngleRad(target.getOrientation().roll),
145 normalizeAngleRad(motionState.getOrientation().roll));
146
147 linearCmd = Speed3D(vx, vy, vz, SpeedUnit::MPS);
148 angularCmd =
149 AngularVelocity3D(vyaw, vpitch, vroll, AngularVelocityUnit::RadPerSec);
150
151 return true;
152 }
153
154 Speed3D getLinearCommand() const { return linearCmd; }
155
156 AngularVelocity3D getAngularCommand() const { return angularCmd; }
157
158 MotionState3D getTarget() const { return target; }
159
160 bool isActive() const { return is_active; }
161
162 /**
163 * @brief Set the OnGoalAction behavior (Stop, HoldPosition, Circle).
164 * Resets circle mode state if changed.
165 */
166 void setOnGoalAction(OnGoalAction action) {
167 onGoalAction = action;
168 circleInitialized = false;
169 }
170
171 /**
172 * @brief Set a custom callback to be called when reaching the goal. The
173 * callback should return true if it handled the goal action, or false to
174 * allow default handling.
175 */
176 void setOnGoalCallback(bool (*callback)(void*), void* ref = nullptr) {
177 onGoalCallback = callback;
178 if (ref != nullptr) onGoaldRef = ref;
179 }
180
181 /**
182 * @brief Set the radius for circular motion (meters).
183 */
184 void setCircleRadius(float radius) {
185 circleRadius = radius;
186 circleInitialized = false;
187 }
188
189 /**
190 * @brief Set the angular speed for circular motion (radians per update).
191 */
192 void setCircleAngularSpeed(float angularSpeed) {
193 circleAngularSpeed = angularSpeed;
194 }
195
196 protected:
197 bool is_active = false;
198 float positionToleranceM;
199 OnGoalAction onGoalAction;
200 Path<Coordinate<DistanceM>> path;
201 IMotionState3D& motionState;
202 MotionState3D target =
205 PIDController<float> pidX, pidY, pidZ;
206 PIDController<float> pidYaw, pidPitch, pidRoll;
207 Speed3D linearCmd = Speed3D(0, 0, 0, SpeedUnit::MPS);
208 AngularVelocity3D angularCmd =
209 AngularVelocity3D(0, 0, 0, AngularVelocityUnit::RadPerSec);
210 bool (*onGoalCallback)(void*) = nullptr;
211 void* onGoaldRef = this;
212 // For dynamic dt measurement (simplified)
213 int updateCount = 0;
214 unsigned long updateStartTimeMs = 0;
215 unsigned long updateEndTimeMs = 0;
216 bool dtSetFromUpdates = false;
217
218 /// Handles dt initialization from first 10 updates, but does not block
219 /// control logic
221 unsigned long nowMs = millis();
222 if (dtSetFromUpdates) return true;
223 if (updateCount == 0) {
224 updateStartTimeMs = nowMs;
225 }
226 updateCount++;
227 if (updateCount == 11) {
228 updateEndTimeMs = nowMs;
229 float avgMs = (updateEndTimeMs - updateStartTimeMs) / 10.0f;
230 float dt = avgMs / 1000.0f; // convert ms to seconds
231 // Reconfigure all PIDs with new dt, keep other params
232 pidX.setDt(dt);
233 pidY.setDt(dt);
234 pidZ.setDt(dt);
235 pidYaw.setDt(dt);
236 pidPitch.setDt(dt);
237 pidRoll.setDt(dt);
238 dtSetFromUpdates = true;
239 }
240 return dtSetFromUpdates;
241 }
242
243 bool advanceWaypoint(float positionTolerance = 0.1f) {
244 if (path.isEmpty()) return false;
245 auto tgt = path[0];
246 float dx = tgt.x - motionState.getPosition().x;
247 float dy = tgt.y - motionState.getPosition().y;
248 float dz = tgt.z - motionState.getPosition().z;
249 float dist = std::sqrt(dx * dx + dy * dy + dz * dz);
250 if (dist < positionTolerance) {
251 path.removeHead();
252 if (!path.isEmpty()) {
253 // Set next waypoint as new target (keep orientation/vel from previous
254 // target)
255 if (path.size() == 1) {
256 // hover at coordinate
257 target = MotionState3D(
258 path[0], target.getOrientation(),
259 Speed3D(0, 0, 0, SpeedUnit::MPS),
260 AngularVelocity3D(0, 0, 0, AngularVelocityUnit::RadPerSec));
261 } else {
262 target =
263 MotionState3D(path[0], target.getOrientation(), target.getSpeed(),
264 target.getAngularVelocity());
265 }
266 } else {
267 // Path is now empty, trigger on-goal action
268 handleOnGoalAction();
269 }
270 return true;
271 }
272 return false;
273 }
274 // For Circle mode
275 float circlePhase = 0.0f;
276 float circleRadius = 5.0f; // meters
277 float circleAngularSpeed = 0.5f; // radians per update
278 Coordinate<DistanceM> circleCenter;
279 bool circleInitialized = false;
280
281 void handleOnGoalAction() {
282 // process callback
283 if (onGoalCallback) {
284 if (onGoalCallback(onGoaldRef)) {
285 // Callback handled the goal action, so we can return early
286 return;
287 }
288 }
289 switch (onGoalAction) {
290 case OnGoalAction::Stop:
291 // Set all commands to zero and deactivate controller
292 linearCmd = Speed3D(0, 0, 0, SpeedUnit::MPS);
293 angularCmd = AngularVelocity3D(0, 0, 0, AngularVelocityUnit::RadPerSec);
294 is_active = false;
295 break;
296 case OnGoalAction::HoldPosition:
297 // Keep controller active, hold at last target (do nothing)
298 break;
299 case OnGoalAction::Circle:
300 // Move in a circle around the last goal
301 if (!circleInitialized) {
302 circleCenter = motionState.getPosition();
303 circlePhase = 0.0f;
304 circleInitialized = true;
305 } else {
306 circlePhase += circleAngularSpeed;
307 if (circlePhase > 2.0f * static_cast<float>(M_PI))
308 circlePhase -= 2.0f * static_cast<float>(M_PI);
309 }
310 // Compute new target on the circle (XY plane, keep Z constant)
311 float x = circleCenter.x + circleRadius * std::cos(circlePhase);
312 float y = circleCenter.y + circleRadius * std::sin(circlePhase);
313 float z = circleCenter.z;
314 // Optionally, face tangent to the circle (yaw)
315 float yaw = std::atan2(y - circleCenter.y, x - circleCenter.x) +
316 static_cast<float>(M_PI_2);
317 Orientation3D orientation(yaw, target.getOrientation().pitch,
318 target.getOrientation().roll);
319 target = MotionState3D(
320 Coordinate<DistanceM>(x, y, z), orientation,
321 Speed3D(0, 0, 0, SpeedUnit::MPS),
322 AngularVelocity3D(0, 0, 0, AngularVelocityUnit::RadPerSec));
323 break;
324 }
325 }
326
327 // Removed: use normalizeAngleRad directly
328};
329
330} // namespace tinyrobotics
Represents a 3D angular velocity vector with unit support.
Definition: AngularVelocity.h:134
A generic 3D coordinate class for robotics, navigation, and spatial calculations.
Definition: Coordinate.h:57
Interface for representing the motion state of a robot in 3D space.
Definition: MotionState3D.h:34
3D motion/path controller using PID for position and orientation.
Definition: MotionController3D.h:62
void setCircleAngularSpeed(float angularSpeed)
Set the angular speed for circular motion (radians per update).
Definition: MotionController3D.h:192
bool initializeDtFromUpdates()
Definition: MotionController3D.h:220
bool update()
Update the controller and compute new commands.
Definition: MotionController3D.h:123
void end()
Stop the controller and the vehicle.
Definition: MotionController3D.h:116
void begin()
Initialize controller and set target from first path coordinate if available.
Definition: MotionController3D.h:101
void setCircleRadius(float radius)
Set the radius for circular motion (meters).
Definition: MotionController3D.h:184
void setOnGoalAction(OnGoalAction action)
Set the OnGoalAction behavior (Stop, HoldPosition, Circle). Resets circle mode state if changed.
Definition: MotionController3D.h:166
void setOnGoalCallback(bool(*callback)(void *), void *ref=nullptr)
Set a custom callback to be called when reaching the goal. The callback should return true if it hand...
Definition: MotionController3D.h:176
Represents the full 3D motion state of a robot or vehicle.
Definition: MotionState3D.h:53
Simple 3D orientation class (yaw, pitch, roll in radians)
Definition: Orientation3D.h:13
Implements a simple, header-only Proportional-Integral-Derivative (PID) controller.
Definition: PIDController.h:56
AngularVelocityUnit
Supported angular velocity units for conversion and representation.
Definition: AngularVelocity.h:11
SpeedUnit
Supported speed units for conversion and representation.
Definition: Speed.h:10