PlatformGlobalTrajectoryController.h
Go to the documentation of this file.
1/**
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @author Fabian Reister ( fabian dot reister at kit dot edu )
17 * @date 2022
18 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
19 * GNU General Public License
20 */
21
22#pragma once
23
24#include <atomic>
25#include <cstdint>
26#include <map>
27#include <mutex>
28#include <optional>
29#include <string>
30#include <vector>
31
32#include <Ice/Current.h>
33#include <Ice/Object.h>
34#include <IceUtil/Time.h>
35
36#include <VirtualRobot/VirtualRobot.h>
37
38#include <ArmarXCore/interface/observers/ObserverInterface.h>
40
43#include <RobotAPI/interface/aron/Aron.h>
44#include <RobotAPI/interface/units/RobotUnit/NJointController.h>
45#include <RobotAPI/interface/visualization/DebugDrawerInterface.h>
46
47#include <armarx/control/interface/ConfigurableNJointControllerInterface.h>
51
52namespace armarx
53{
56 class SensorValueHolonomicPlatformWithAbsolutePosition;
57} // namespace armarx
58
60{
65
74
75 struct Twist2D
76 {
77 Eigen::Vector2f linear = Eigen::Vector2f::Zero();
78 float angular = 0;
79
80 void
82 {
83 linear = Eigen::Vector2f::Zero();
84 angular = 0;
85 }
86 };
87
88 using Target = Twist2D;
89
90 struct Debug
91 {
93
97 double orientationError = 0;
98 bool isFinalSegment = false;
99
100 double positionError = 0;
101
102 double ffAngular = 0;
103 double cappedFfVel = 0;
104
105 std::size_t projectionIndex = 0;
106
108
109 Eigen::Isometry3f global_T_robot = Eigen::Isometry3f::Identity();
110 };
111
116
117 using NameValueMap = std::map<std::string, float>;
118
120 virtual public NJointControllerWithTripleBuffer<Target>,
121 virtual public armarx::control::ConfigurableNJointControllerInterface
122 {
123 public:
124 using ConfigPtrT = control::ConfigurableNJointControllerConfigPtr;
125
126 Controller(const RobotUnitPtr& robotUnit,
127 const NJointControllerConfigPtr& config,
129
130 ~Controller() override;
131
132 std::string getClassName(const Ice::Current& iceCurrent = Ice::emptyCurrent) const override;
133
134 void rtRun(const IceUtil::Time& sensorValuesTimestamp,
135 const IceUtil::Time& timeSinceLastIteration) override;
136
137 void updateConfig(const ::armarx::aron::data::dto::DictPtr& dto,
138 const Ice::Current& iceCurrent = Ice::emptyCurrent) override;
139
141 getConfig(const ::Ice::Current& = ::Ice::emptyCurrent) override;
142
143 protected:
144 void additionalTask();
145 void onPublish(const SensorAndControl& sac,
146 const DebugDrawerInterfacePrx& debugDrawer,
147 const DebugObserverInterfacePrx& debugObservers) override;
148
149
150 void onInitNJointController() override;
151 void rtPreActivateController() override;
152 void rtPostDeactivateController() override;
153
154 /// Walk `rtCommandedTwist` towards `target`, by at most the configured rate times `dt`.
155 void rtSlewTowards(const Target& target, float dt);
156
157 /// Record one control cycle. Runs on the 100 Hz task thread; no-op when disabled.
160 const Twist2D& commandedTwist,
163
164 /// Assemble and write the episode that just ended. Non-real-time; never throws.
166
167 private:
168 struct RobotState
169 {
170 Eigen::Isometry3f global_T_robot;
171
172 /// `sensorValuesTimestamp` of the cycle this pose was read in. The 100 Hz task has no
173 /// clock of its own, and both diagnostics streams have to share one to be comparable.
174 std::int64_t timestampUs = 0;
175 };
176
177 TripleBuffer<Config> configBuffer_updateConfigToAdditionalTask;
178 TripleBuffer<Config> configBuffer_updateConfigToOnPublish;
179
180 /// A separate arrow, per `NJointControllerBase`'s rule that each reader needs its own
181 /// buffer: `getUpToDateReadBuffer()` is single-reader, and the diagnostics task reads the
182 /// parameters into the dump's metadata while `onPublish` reads them for its datafields.
183 TripleBuffer<Config> configBuffer_updateConfigToDiagnostics;
184 TripleBuffer<Debug> targetBuffer_additionalTaskToOnPublish;
185 TripleBuffer<RobotState> robotStateBuffer_rtToAdditionalTask;
186
187
188 // internal
189 std::atomic_bool rtFirstRun = true;
190 std::atomic_bool rtReady = false;
191
193
194 /// Seeds `rtCommandedTwist` on activation, so a controller switch while the base is
195 /// moving does not start the slew from a velocity the robot does not have.
196 const SensorValueHolonomicPlatformVelocity* platformSensor;
197
198 std::optional<traj_ctrl::global::TrajectoryFollowingController>
199 trajectoryFollowingController;
200
201
202 Devices getDevices(const VirtualRobot::RobotNodeSet& rns);
203
204 // additional task has exclusive access to this variable
205 Twist2D filteredTwist;
206
207 /// What was last written to the control target. RT thread only.
208 Twist2D rtCommandedTwist;
209
210 /**
211 * @brief Upper bound on the `dt` the slew integrates, in seconds.
212 *
213 * The bound exists so the first cycle after an activation -- whose
214 * `timeSinceLastIteration` can be however long the controller sat idle -- cannot let the
215 * whole velocity step through at once.
216 *
217 * It is not a constant, because 2 ms is only a *guard* while the loop period is well
218 * under it. On the real robot at 1 kHz that holds. `RobotUnitSimulation` runs the loop at
219 * ~10 ms, where a 2 ms cap binds on every cycle instead and silently throttles the slew
220 * to `rate * 2 ms / 10 ms`, i.e. a fifth of the configured acceleration -- measured at
221 * 60 mm/s^2 against a configured 300, which is enough to overshoot a goal by 45 cm.
222 *
223 * Set once, in the constructor, from `RobotUnit::isSimulation()`. Read by the RT thread
224 * only after that, so a plain float is sufficient.
225 */
226 float rtMaxSlewDeltaT{0.002F};
227
228 /// Slew rates, mirrored out of the config so the RT thread never reads the config
229 /// buffer. Written by `updateConfig` on an Ice thread, read every RT cycle.
230 std::atomic<float> rtMaxLinearAcceleration{250.F};
231 std::atomic<float> rtMaxAngularAcceleration{0.4F};
232
233 /**
234 * @brief Advanced by `additionalTask()` every time it publishes a twist.
235 *
236 * `rtRun()` keeps slewing towards whatever `rtGetControlStruct()` holds, and nothing else
237 * tells it whether that value is still being produced. `RunningTaskBase::run()` wraps the
238 * whole task callback in one try/catch, so a single escaping exception ended the 100 Hz
239 * thread for good while the robot carried on executing the last twist -- a small linear
240 * command in the *base* frame plus a yaw rate is a tight circle, i.e. a robot that turns
241 * on the spot until someone stops it.
242 */
243 std::atomic<std::uint64_t> additionalTaskSequence{0};
244
245 /// How many exceptions `additionalTask()` has swallowed. Published, so a controller that
246 /// is silently failing every cycle is visible without the log.
247 std::atomic<std::uint64_t> controlTaskExceptions{0};
248
249 /// Set by the RT thread when the command it is executing has gone stale, read by
250 /// `onPublish`. The RT thread must not log, so it only records.
251 std::atomic<std::uint32_t> controlTargetStaleCycles{0};
252
253 /// RT-thread-only: the sequence number last observed, and how long it has been unchanged.
254 std::uint64_t rtLastSequence{0};
255
256 float rtSecondsSinceTargetUpdate{0.F};
257
258 /// `onPublish`-thread-only latch, so the stale warning is reported once per episode.
259 bool publishedStaleWarning{false};
260
261 // ------------------------------------------------------------------------------------ //
262 // Diagnostics. Everything below is inert unless `diagnosticsParams_.enabled` was set in
263 // the config the controller was *built* with -- the rings are sized once, in the
264 // constructor, because the real-time thread writes into them.
265 // ------------------------------------------------------------------------------------ //
266
267 diagnostics::Params diagnosticsParams_;
268
269 /// Whether the RobotUnit this runs against is simulated. Read once, in the constructor:
270 /// it decides `rtMaxSlewDeltaT` and is recorded in every dump, because a reader cannot
271 /// otherwise tell a 10 ms simulated loop from a 1 kHz real one.
272 bool isSimulation_{false};
273
274 /// Only for the dump's metadata -- the RobotUnit is not reachable from the task thread.
275 std::string platformName_;
276
277 diagnostics::Ring<diagnostics::RtSample> rtSamples_;
278 diagnostics::Ring<diagnostics::ControlSample> controlSamples_;
279
280 /// Bumped on every activation, stamped into every sample. Lets `snapshot()` separate one
281 /// episode from the next without the ring ever being reset under a running reader.
282 std::atomic<std::uint64_t> diagnosticsEpisode{0};
283
284 /**
285 * @brief Serviced by the 100 Hz task thread, which is the only place the file I/O may
286 * run: not the RT thread, not the shared publisher thread.
287 *
288 * Set from two places, because either alone loses records. Deactivation alone is not
289 * enough: reaching the goal does not necessarily stop this controller -- the navigator's
290 * `stop()` only deactivates one that reports active -- so an episode could end with its
291 * data still sitting in the ring. A goal-reached request alone is not enough either: an
292 * aborted or emergency-stopped run never reaches the goal.
293 */
294 std::atomic_bool diagnosticsDumpRequested{false};
295
296 /// Whether a dump has already been written since the controller was last activated. Stops
297 /// the deactivation that normally *follows* a goal-reached dump from writing a second,
298 /// near-empty record for the handful of cycles in between.
299 std::atomic_bool diagnosticsDumpedSinceActivation{false};
300
301 /// `updateConfig`-thread only: the last `dumpRequest` observed, so only a change triggers.
302 int lastDumpRequest{0};
303
304 /// Bumped by `updateConfig`. `additionalTask` copies the trajectory whenever it changes,
305 /// because the navigator replans during a request.
306 std::atomic<std::uint64_t> trajectoryRevision{0};
307
308 /// RT-thread-only: whether a slew bound was binding in the cycle just computed.
309 bool rtSlewLimited{false};
310
311 /// Guards the trajectory bookkeeping below, which the 100 Hz control task appends to and
312 /// the diagnostics task takes away. Held around that bookkeeping only, never around the
313 /// file I/O.
314 std::mutex diagnosticsTrajectoryMutex;
315
316 std::vector<diagnostics::TrajectoryRevision> recordedTrajectories;
317 std::uint64_t lastRecordedTrajectoryRevision{0};
318 std::uint64_t droppedTrajectoryRevisions{0};
319
320 /// Cheap identity of the last trajectory recorded, so a config update that carries only a
321 /// dump request does not append a duplicate of the trajectory already there.
322 std::size_t lastRecordedTrajectorySize{0};
323 Eigen::Vector3f lastRecordedTrajectoryEnd{Eigen::Vector3f::Zero()};
324 };
325
326} // namespace armarx::navigation::platform_controller::platform_global_trajectory
constexpr T dt
Brief description of class ControlTargetHolonomicPlatformVelocity.
NJointControllerWithTripleBuffer(const Target &initialCommands=Target())
A simple triple buffer for lockfree comunication between a single writer and a single reader.
::armarx::aron::data::dto::DictPtr getConfig(const ::Ice::Current &=::Ice::emptyCurrent) override
void recordControlSample(const traj_ctrl::global::TrajectoryControllerResult &result, const Twist2D &commandedTwist, const core::GlobalTrajectory &trajectory, const traj_ctrl::global::TrajectoryFollowingControllerParams &params)
Record one control cycle. Runs on the 100 Hz task thread; no-op when disabled.
Controller(const RobotUnitPtr &robotUnit, const NJointControllerConfigPtr &config, const VirtualRobot::RobotPtr &)
void rtSlewTowards(const Target &target, float dt)
Walk rtCommandedTwist towards target, by at most the configured rate times dt.
void rtPostDeactivateController() override
This function is called after the controller is deactivated.
void rtRun(const IceUtil::Time &sensorValuesTimestamp, const IceUtil::Time &timeSinceLastIteration) override
TODO make protected and use attorneys.
void onPublish(const SensorAndControl &sac, const DebugDrawerInterfacePrx &debugDrawer, const DebugObserverInterfacePrx &debugObservers) override
void updateConfig(const ::armarx::aron::data::dto::DictPtr &dto, const Ice::Current &iceCurrent=Ice::emptyCurrent) override
void writeDiagnosticsDump()
Assemble and write the episode that just ended. Non-real-time; never throws.
std::string getClassName(const Ice::Current &iceCurrent=Ice::emptyCurrent) const override
void rtPreActivateController() override
This function is called before the controller is activated.
std::shared_ptr< class Robot > RobotPtr
Definition Bus.h:19
::IceInternal::Handle< Dict > DictPtr
This file offers overloads of toIce() and fromIce() functions for STL container types.
::IceInternal::ProxyHandle<::IceProxy::armarx::DebugObserverInterface > DebugObserverInterfacePrx
IceUtil::Handle< class RobotUnit > RobotUnitPtr
Definition FTSensor.h:34
::IceInternal::ProxyHandle<::IceProxy::armarx::DebugDrawerInterface > DebugDrawerInterfacePrx
detail::ControlThreadOutputBufferEntry SensorAndControl
What the diagnostics mode records and where it writes it.
Definition Diagnostics.h:46