PlatformGlobalTrajectoryController.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <cstdint>
6#include <string>
7
8#include <Ice/Current.h>
9#include <IceUtil/Time.h>
10
11#include <SimoxUtility/math/convert/mat4f_to_rpy.h>
12#include <VirtualRobot/VirtualRobot.h>
13
18#include <ArmarXCore/interface/core/ManagedIceObjectDefinitions.h>
19#include <ArmarXCore/interface/observers/ObserverInterface.h>
20#include <ArmarXCore/interface/observers/VariantBase.h>
23
30#include <RobotAPI/interface/units/RobotUnit/NJointController.h>
31#include <RobotAPI/interface/visualization/DebugDrawerInterface.h>
32
33#include <armarx/control/interface/ConfigurableNJointControllerInterface.h> // IWYU pragma: keep
35#include <armarx/navigation/platform_controller/aron/PlatformGlobalTrajectoryControllerConfig.aron.generated.h>
38
40{
43
44 namespace
45 {
46 /// Nominal rates of the two loops, used only to turn a diagnostics duration into a
47 /// sample count. `rtRun` is driven by the RobotUnit; `additionalTask` by `CycleUtil(10)`.
48 /// A slower loop than this only means the ring holds *more* than the configured
49 /// duration; `meta.json` reports the span actually recorded.
50 constexpr float rtRate = 1000.F; // [Hz]
51 constexpr float controlRate = 100.F; // [Hz]
52
53 float
54 yawOf(const Eigen::Isometry3f& pose)
55 {
56 return std::atan2(pose.linear()(1, 0), pose.linear()(0, 0));
57 }
58 } // namespace
59
61 const NJointControllerConfigPtr& config,
63 {
64 ARMARX_IMPORTANT << "Creating "
67 // config
68 ConfigPtrT cfg = ConfigPtrT::dynamicCast(config);
70 // ARMARX_CHECK_EXPRESSION(!cfg->nodeSetName.empty());
71
72 ARMARX_CHECK_EXPRESSION(robotUnit);
73
74 const auto robot = useSynchronizedRtRobot();
75
76 // Control target
77 {
78 const std::string controlTargetName = robotUnit->getRobotPlatformName();
79 platformName_ = controlTargetName;
80
81 ARMARX_INFO << "Using control target " << controlTargetName;
82 auto* ct = useControlTarget(controlTargetName, ControlModes::HolonomicPlatformVelocity);
83 ARMARX_CHECK_NOT_NULL(ct) << "Cannot use control target " << QUOTED(controlTargetName);
84
85 platformTarget = ct->asA<ControlTargetHolonomicPlatformVelocity>();
86
87 ARMARX_CHECK_EXPRESSION(platformTarget)
88 << "The actuator " << controlTargetName << " has no control mode "
89 << ControlModes::HolonomicPlatformVelocity;
90
91 const auto* sv = useSensorValue(controlTargetName);
92 ARMARX_CHECK_NOT_NULL(sv) << "No sensor value for " << QUOTED(controlTargetName);
93
94 platformSensor = sv->asA<SensorValueHolonomicPlatformVelocity>();
95 ARMARX_CHECK_EXPRESSION(platformSensor)
96 << "The sensor value for " << controlTargetName << " has no platform velocity";
97 }
98
99
100 const auto configData = ::armarx::fromAron<arondto::Config, Config>(cfg->config);
101 const auto trajectoryFollowingControllerParams = configData.params;
102
104 configBuffer_updateConfigToAdditionalTask.reinitAllBuffers(configData);
105 configBuffer_updateConfigToOnPublish.reinitAllBuffers(configData);
106 configBuffer_updateConfigToDiagnostics.reinitAllBuffers(configData);
107
108 trajectoryFollowingController.emplace(trajectoryFollowingControllerParams);
109
110 rtMaxLinearAcceleration.store(trajectoryFollowingControllerParams.maxLinearAcceleration);
111 rtMaxAngularAcceleration.store(trajectoryFollowingControllerParams.maxAngularAcceleration);
112
113 // See `rtMaxSlewDeltaT`. On the real robot the loop is ~1 ms, so 2 ms only ever guards
114 // the first cycle after an activation. In simulation it is ~10 ms, where that cap binds
115 // every cycle and throttles the slew to a fifth of the configured acceleration; a bound
116 // far above any real cycle keeps it a guard rather than a limit there.
117 isSimulation_ = robotUnit->isSimulation();
118 rtMaxSlewDeltaT = isSimulation_ ? 1.0F : 0.002F;
119
120 ARMARX_INFO << "Slew dt bound: " << rtMaxSlewDeltaT << " s ("
121 << (isSimulation_ ? "simulation" : "real robot") << ").";
122
123 // The rings are sized here and never again: `rtRun` writes into them, so a later resize
124 // would allocate on the real-time thread. `updateConfig` therefore ignores the
125 // diagnostics block -- switching this on is a config edit plus a restart.
126 diagnosticsParams_ = configData.diagnostics;
127
128 if (diagnosticsParams_.enabled)
129 {
130 const auto capacity = [](const float seconds, const float rate) -> std::size_t
131 { return static_cast<std::size_t>(std::max(0.F, seconds) * rate); };
132
133 rtSamples_.init(capacity(diagnosticsParams_.rtDurationSeconds, rtRate));
134 controlSamples_.init(capacity(diagnosticsParams_.controlDurationSeconds, controlRate));
135
136 recordedTrajectories.reserve(
137 static_cast<std::size_t>(std::max(0, diagnosticsParams_.maxTrajectoryRevisions)));
138
139 ARMARX_IMPORTANT << "[nav-diag] Execution diagnostics ENABLED: keeping "
140 << rtSamples_.capacity() << " real-time and "
141 << controlSamples_.capacity() << " control samples, dumping to "
142 << diagnosticsParams_.outputDirectory
143 << " when a navigation request finishes.";
144 }
145 else
146 {
147 // Reported even when off, so "was it recording?" is one grep rather than an inference
148 // from silence -- which is also what an outdated binary looks like.
149 ARMARX_IMPORTANT << "[nav-diag] Execution diagnostics DISABLED. Set "
150 "`diagnostics.enabled` in the controller config this controller "
151 "was built from and restart the navigator to record a run.";
152 }
153
154 ARMARX_INFO << "Init done.";
155 }
156
157 std::string
163
164 void
165 Controller::rtRun(const IceUtil::Time& sensorValuesTimestamp,
166 const IceUtil::Time& timeSinceLastIteration)
167 {
169
170 // `additionalTask()` produces the twist at 100 Hz (`CycleUtil c(10)` in
171 // `onInitNJointController`) while this runs at 1 kHz, so copying it straight through
172 // hands the device a staircase that the joint controller's ramp then has to absorb.
173 // Walking towards it keeps the command continuous between non-RT updates.
174 //
175 // This is a bridge, not a limit: the joint controller's ramp remains the authoritative
176 // bound, because the same control mode is shared with the platform unit and the
177 // gamepad. The rates default to that ramp's own values, so in steady motion this never
178 // binds -- it only takes the edges off, which is where the jerk was.
179 const float dt = static_cast<float>(timeSinceLastIteration.toSecondsDouble());
180
181 // Watchdog. `additionalTask()` publishes at 100 Hz; if its sequence number stops
182 // advancing the task is gone (or wedged) and `rtGetControlStruct()` is a frozen value
183 // that would otherwise be executed forever. Ramp to zero instead of holding it -- via the
184 // same slew, so this is a controlled stop rather than a step to zero.
185 constexpr float staleAfterSeconds = 0.1F; // 10x the 100 Hz task period
186
187 const std::uint64_t sequence = additionalTaskSequence.load(std::memory_order_acquire);
188
189 if (sequence != rtLastSequence)
190 {
191 rtLastSequence = sequence;
192 rtSecondsSinceTargetUpdate = 0.F;
193 controlTargetStaleCycles.store(0, std::memory_order_relaxed);
194 }
195 else
196 {
197 rtSecondsSinceTargetUpdate += std::max(0.F, dt);
198 }
199
200 const bool targetIsStale = rtSecondsSinceTargetUpdate > staleAfterSeconds;
201
202 if (targetIsStale)
203 {
204 // No logging here: this is the 1 kHz real-time thread. `onPublish` reports it.
205 controlTargetStaleCycles.fetch_add(1, std::memory_order_relaxed);
207 }
208 else
209 {
211 }
212
213 // update control devices
214 platformTarget->velocityX = rtCommandedTwist.linear.x();
215 platformTarget->velocityY = rtCommandedTwist.linear.y();
216 platformTarget->velocityRotation = rtCommandedTwist.angular;
217
218 // read data (for non-rt)
219 const std::int64_t timestampUs = sensorValuesTimestamp.toMicroSeconds();
220
221 // Read once into a local: `commitWrite()` swaps the write and hidden buffers, so
222 // `getWriteBuffer()` afterwards names a *different* buffer holding an older cycle.
223 Eigen::Isometry3f global_T_robot;
224 global_T_robot.matrix() = rtGetRobot()->getGlobalPose();
225
226 robotStateBuffer_rtToAdditionalTask.getWriteBuffer().global_T_robot = global_T_robot;
227 robotStateBuffer_rtToAdditionalTask.getWriteBuffer().timestampUs = timestampUs;
228 robotStateBuffer_rtToAdditionalTask.commitWrite();
229
230 // Diagnostics. Inert unless enabled: `record` returns immediately on a zero-capacity
231 // ring. When enabled it is a single assignment into preallocated storage plus one atomic
232 // store -- no allocation, no lock, no I/O, which is what this thread requires.
233 if (rtSamples_.capacity() > 0)
234 {
235 // Deliberately the pre-slew value the RT thread is acting on, so the recorded target
236 // matches what the watchdog decided, not what the 100 Hz task last wrote.
237 const Target target = targetIsStale ? Target{} : rtGetControlStruct();
238
240 sample.timestampUs = timestampUs;
241 sample.episode = diagnosticsEpisode.load(std::memory_order_relaxed);
242 sample.sequence = sequence;
243 sample.x = global_T_robot.translation().x();
244 sample.y = global_T_robot.translation().y();
245 sample.yaw = yawOf(global_T_robot);
246 sample.vMeasX = platformSensor->velocityX;
247 sample.vMeasY = platformSensor->velocityY;
248 sample.wMeas = platformSensor->velocityRotation;
249 sample.vTgtX = target.linear.x();
250 sample.vTgtY = target.linear.y();
251 sample.wTgt = target.angular;
252 sample.vCmdX = rtCommandedTwist.linear.x();
253 sample.vCmdY = rtCommandedTwist.linear.y();
254 sample.wCmd = rtCommandedTwist.angular;
255 sample.dt = dt;
256 sample.stale = targetIsStale;
257 sample.slewLimited = rtSlewLimited;
258
259 rtSamples_.record(sample);
260 }
261 }
262
263 void
264 Controller::rtSlewTowards(const Target& target, float dt)
265 {
266 // Clamped for the same reason the device ramp clamps its own: the first cycle after an
267 // activation reports whatever elapsed since the controller was built, and an unbounded
268 // dt would let the whole step through in one go -- exactly what this exists to avoid.
269 //
270 // The bound has to sit above the loop period or it stops being a guard and becomes the
271 // acceleration limit itself; `rtMaxSlewDeltaT` is chosen for the loop this controller is
272 // actually running in.
273 dt = std::max(0.F, std::min(dt, rtMaxSlewDeltaT));
274
275 // Both bounds are forced non-negative. The rates come from the aron config, so a
276 // negative one is reachable by configuration alone, and neither use below survives it:
277 // the linear branch would scale the step by a negative factor and drive the command
278 // *away* from its target, and the angular one is an interval whose ends would be
279 // reversed. `std::max` also absorbs a NaN rate, which would otherwise reach both.
280 const float linearBound =
281 std::max(0.F, rtMaxLinearAcceleration.load(std::memory_order_relaxed) * dt);
282 const float angularBound =
283 std::max(0.F, rtMaxAngularAcceleration.load(std::memory_order_relaxed) * dt);
284
285 // The norm of the linear change is bounded, not each axis: two independent bounds would
286 // rotate the commanded direction while the slew runs, which is the same reason the
287 // device ramp couples vx and vy (`directionPreservingRamp`).
288 const Eigen::Vector2f linearDelta = target.linear - rtCommandedTwist.linear;
289 const float linearDistance = linearDelta.norm();
290
291 rtCommandedTwist.linear +=
292 linearDistance > linearBound
293 ? Eigen::Vector2f{linearDelta * (linearBound / linearDistance)}
294 : linearDelta;
295
296 // Not `std::clamp`: its interval precondition makes a reversed range undefined
297 // behaviour with no diagnostic, which is the wrong failure mode in the RT thread.
298 const float angularDelta = target.angular - rtCommandedTwist.angular;
299
300 rtCommandedTwist.angular += std::max(-angularBound, std::min(angularDelta, angularBound));
301
302 // Recorded rather than derived offline: reconstructing it from the command alone needs
303 // the rates *and* the clamped dt, and gets the equality case wrong.
304 rtSlewLimited = linearDistance > linearBound or std::abs(angularDelta) > angularBound;
305 }
306
307 void
308 Controller::updateConfig(const ::armarx::aron::data::dto::DictPtr& dto,
309 const Ice::Current& iceCurrent)
310 {
311 // TODO maybe update pid controller
312
314
315 rtMaxLinearAcceleration.store(updateConfig.params.maxLinearAcceleration);
316 rtMaxAngularAcceleration.store(updateConfig.params.maxAngularAcceleration);
317
318 configBuffer_updateConfigToAdditionalTask.getWriteBuffer() = updateConfig;
319 configBuffer_updateConfigToAdditionalTask.commitWrite();
320
321 configBuffer_updateConfigToOnPublish.getWriteBuffer() = updateConfig;
322 configBuffer_updateConfigToOnPublish.commitWrite();
323
324 configBuffer_updateConfigToDiagnostics.getWriteBuffer() = updateConfig;
325 configBuffer_updateConfigToDiagnostics.commitWrite();
326
327 // Released after the config writes, so the 100 Hz task cannot see a new revision number
328 // ahead of the trajectory it refers to.
329 trajectoryRevision.fetch_add(1, std::memory_order_release);
330
331 // The rest of `updateConfig.diagnostics` stays ignored -- the rings were sized in the
332 // constructor and the RT thread writes into them -- but `dumpRequest` is a request, not a
333 // size, and it is what makes the record independent of whether this controller is ever
334 // deactivated. The navigator bumps it when the goal is reached.
335 if (diagnosticsParams_.enabled and updateConfig.diagnostics.dumpRequest != lastDumpRequest)
336 {
337 lastDumpRequest = updateConfig.diagnostics.dumpRequest;
338 diagnosticsDumpRequested.store(true, std::memory_order_release);
339 }
340
341 ARMARX_VERBOSE << "Trajectory with " << updateConfig.targets.trajectory.points().size();
342 }
343
344 void
346 {
348 ARMARX_CHECK(trajectoryFollowingController.has_value());
349
350 const auto& configBuffer =
351 configBuffer_updateConfigToAdditionalTask.getUpToDateReadBuffer();
352
353
354 // if trajectory is empty, set velocity to 0
355 if (configBuffer.targets.trajectory.points().empty())
356 {
357 ARMARX_INFO << deactivateSpam(1) << "Trajectory is empty!";
358
359 filteredTwist.reset();
360
361 getWriterControlStruct().reset();
363 additionalTaskSequence.fetch_add(1, std::memory_order_release);
364 return;
365 }
366
367 // update controller
369
370 // make sure the parameters are up to date
371 // i.e. velocityFactor and limits, as they can be changed on the fly
372 trajectoryFollowingController->updateParams(configBuffer.params);
373
374 // run the controller, resulting in the required twist and other values
376 trajectoryFollowingController->control(
377 configBuffer.targets.trajectory,
378 robotStateBuffer_rtToAdditionalTask.getUpToDateReadBuffer().global_T_robot);
379
380 // low-pass filter the twist
381 {
382 const float alpha = configBuffer.params.alpha;
383 filteredTwist.linear =
384 alpha * filteredTwist.linear + (1. - alpha) * result.twist.linear.head<2>();
385 filteredTwist.angular =
386 alpha * filteredTwist.angular + (1. - alpha) * result.twist.angular.z();
387 }
388
389 // store result
390 getWriterControlStruct() = filteredTwist;
393
394 // Tells the RT watchdog this thread is still alive. Released after the write so the RT
395 // thread cannot observe a fresh sequence number ahead of the twist it refers to.
396 additionalTaskSequence.fetch_add(1, std::memory_order_release);
397
398 // store results (onPublish)
399 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().target = filteredTwist;
400 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().dropPointVelocity =
401 result.dropPoint.velocity;
402 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().currentOrientation =
403 result.currentOrientation;
404 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().desiredOrientation =
405 result.desiredOrientation;
406 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().orientationError =
407 result.orientationError;
408 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().positionError =
409 result.positionError;
410 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().isFinalSegment =
411 result.isFinalSegment;
412 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().ffAngular = result.ffAngular;
413 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().cappedFfVel = result.cappedFfVel;
414 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().projectionIndex =
415 result.projectionIndex;
416 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().angularFeedforwardSaturated =
418 targetBuffer_additionalTaskToOnPublish.getWriteBuffer().global_T_robot =
419 robotStateBuffer_rtToAdditionalTask.getReadBuffer().global_T_robot;
420 targetBuffer_additionalTaskToOnPublish.commitWrite();
421
422 recordControlSample(result, filteredTwist, configBuffer.targets.trajectory,
423 configBuffer.params);
424 }
425
426 void
428 const Twist2D& commandedTwist,
431 params)
432 {
433 if (controlSamples_.capacity() == 0)
434 {
435 return;
436 }
437
438 const RobotState& robotState = robotStateBuffer_rtToAdditionalTask.getReadBuffer();
439
440 // The trajectory the samples below refer to. Copied on change rather than at dump time:
441 // the navigator replans during a request, so the last one is not the one most of the
442 // episode was tracking.
443 const std::uint64_t revision = trajectoryRevision.load(std::memory_order_acquire);
444
445 // Shared with the dump thread. Held only around the bookkeeping, never around file I/O,
446 // and the common case is an uncontended lock on a branch that does nothing.
447 const std::scoped_lock lock{diagnosticsTrajectoryMutex};
448
449 if (revision != lastRecordedTrajectoryRevision or recordedTrajectories.empty())
450 {
451 lastRecordedTrajectoryRevision = revision;
452
453 // Every `updateConfig` bumps the revision, including one that carries nothing but a
454 // dump request, so the revision alone would fill the list with duplicates. Compared
455 // on size and end point, the same cheap identity the control law uses to notice a new
456 // trajectory.
457 const std::size_t size = trajectory.points().size();
458 const Eigen::Vector3f end =
459 trajectory.points().empty()
460 ? Eigen::Vector3f{Eigen::Vector3f::Zero()}
461 : Eigen::Vector3f{trajectory.points().back().waypoint.pose.translation()};
462
463 const bool unchanged = not recordedTrajectories.empty() and
464 size == lastRecordedTrajectorySize and
465 end == lastRecordedTrajectoryEnd;
466
467 if (unchanged)
468 {
469 // Nothing to record, but the samples still belong to the entry already there.
470 lastRecordedTrajectoryRevision = recordedTrajectories.back().revision;
471 }
472 else if (recordedTrajectories.size() <
473 static_cast<std::size_t>(
474 std::max(0, diagnosticsParams_.maxTrajectoryRevisions)))
475 {
476 recordedTrajectories.push_back({.revision = revision, .trajectory = trajectory});
477 lastRecordedTrajectorySize = size;
478 lastRecordedTrajectoryEnd = end;
479 }
480 else
481 {
482 droppedTrajectoryRevisions++;
483 }
484 }
485
486 const std::uint64_t sampleRevision =
487 recordedTrajectories.empty() ? revision : recordedTrajectories.back().revision;
488
489 const core::Pose& reference = result.dropPoint.waypoint.pose;
490
492 sample.timestampUs = robotState.timestampUs;
493 sample.episode = diagnosticsEpisode.load(std::memory_order_relaxed);
494 sample.sequence = additionalTaskSequence.load(std::memory_order_relaxed);
495 sample.trajectoryRevision = sampleRevision;
496 sample.x = robotState.global_T_robot.translation().x();
497 sample.y = robotState.global_T_robot.translation().y();
498 sample.yaw = yawOf(robotState.global_T_robot);
499 sample.refX = reference.translation().x();
500 sample.refY = reference.translation().y();
501 sample.refYaw = yawOf(reference);
502 sample.refVelocity = result.dropPoint.velocity;
503 sample.projectionIndex = static_cast<std::uint32_t>(result.projectionIndex);
504 sample.finalSegment = result.isFinalSegment;
505 sample.positionError = result.positionError;
506 sample.orientationError = result.orientationError;
509 sample.ffAngular = result.ffAngular;
510 sample.cappedFfVelocity = result.cappedFfVel;
512 sample.guard = static_cast<std::int32_t>(result.guard);
513 sample.vRawX = result.twist.linear.x();
514 sample.vRawY = result.twist.linear.y();
515 sample.wRaw = result.twist.angular.z();
516 sample.vFiltX = commandedTwist.linear.x();
517 sample.vFiltY = commandedTwist.linear.y();
518 sample.wFilt = commandedTwist.angular;
519 sample.limitLinear = params.limits.linear;
520 sample.limitAngular = params.limits.angular;
521 sample.velocityFactor = params.velocityFactor;
522
523 controlSamples_.record(sample);
524 }
525
526 void
528 {
530
531 // Everything shared with the control task is taken here, and the episode is closed in the
532 // same critical section: after the counter is bumped the control task and the RT thread
533 // record into the next episode, so the snapshot below cannot miss a sample or take one
534 // that belongs to the next request.
535 const std::uint64_t episode = [&]
536 {
537 const std::scoped_lock lock{diagnosticsTrajectoryMutex};
538
539 const std::uint64_t current = diagnosticsEpisode.load(std::memory_order_acquire);
540
541 record.trajectories = std::move(recordedTrajectories);
542 record.trajectoryRevisionsDropped = droppedTrajectoryRevisions;
543
544 recordedTrajectories.clear();
545 lastRecordedTrajectoryRevision = 0;
546 lastRecordedTrajectorySize = 0;
547 lastRecordedTrajectoryEnd = Eigen::Vector3f::Zero();
548 droppedTrajectoryRevisions = 0;
549
550 // A dump ends the episode, whatever asked for it. Without this a controller that
551 // stays active across two navigation requests would write the second request's
552 // record with the first one's samples still in it.
553 diagnosticsEpisode.fetch_add(1, std::memory_order_release);
554
555 // Latched here rather than after the file is written. The navigator deactivates the
556 // controller within milliseconds of asking for the dump, while writing it takes far
557 // longer; latching at the end let that deactivation queue a second request, which
558 // came back as a stray dump holding the handful of cycles in between. Three of those
559 // appear in the first robot recordings (`ep005`, `ep027`, `ep032`).
560 diagnosticsDumpedSinceActivation.store(true, std::memory_order_release);
561
562 return current;
563 }();
564
565 record.episode = episode;
567 record.platform = platformName_;
568 record.params = configBuffer_updateConfigToDiagnostics.getUpToDateReadBuffer().params;
569
570 rtSamples_.snapshot(episode, record.rtSamples);
571 controlSamples_.snapshot(episode, record.controlSamples);
572
573 record.simulation = isSimulation_;
574 record.slewDtBound = rtMaxSlewDeltaT;
575 record.rtTruncated = rtSamples_.truncated(episode);
576 record.controlTruncated = controlSamples_.truncated(episode);
577 record.rtOverwritten = rtSamples_.overwritten();
578 record.controlOverwritten = controlSamples_.overwritten();
579 record.controlTaskExceptions = controlTaskExceptions.load(std::memory_order_relaxed);
580 record.controlTargetStaleCycles = controlTargetStaleCycles.load(std::memory_order_relaxed);
581
582 if (record.rtSamples.empty() and record.controlSamples.empty())
583 {
584 // Diagnostics are on -- `rtPostDeactivateController` only asks for a dump then -- so
585 // an empty episode is worth a line: it means the controller was activated and
586 // deactivated without ever running.
587 ARMARX_INFO << "Execution diagnostics: episode " << episode
588 << " recorded no samples, nothing written.";
589 }
590 else
591 {
592 diagnostics::writeDump(record, diagnosticsParams_);
593 }
594
595 }
596
597 void
599 const DebugDrawerInterfacePrx& /*debugDrawer*/,
600 const DebugObserverInterfacePrx& debugObservers)
601 {
602 StringVariantBaseMap datafields;
603
604 const auto& debugStuff = targetBuffer_additionalTaskToOnPublish.getUpToDateReadBuffer();
605 const auto& config = configBuffer_updateConfigToOnPublish.getUpToDateReadBuffer();
606
607
608 datafields["vx"] = new Variant(debugStuff.target.linear.x());
609 datafields["vy"] = new Variant(debugStuff.target.linear.y());
610 datafields["v_linear"] = new Variant(debugStuff.target.linear.norm());
611 datafields["vyaw"] = new Variant(debugStuff.target.angular);
612 datafields["trajectory_points"] = new Variant(config.targets.trajectory.points().size());
613
614 datafields["drop_point_velocity"] = new Variant(debugStuff.dropPointVelocity);
615
616 datafields["orientationError"] = new Variant(debugStuff.orientationError);
617 datafields["desiredOrientation"] = new Variant(debugStuff.desiredOrientation);
618 datafields["currentOrientation"] = new Variant(debugStuff.currentOrientation);
619 datafields["isFinalSegment"] = new Variant(debugStuff.isFinalSegment);
620
621 datafields["positionError"] = new Variant(debugStuff.positionError);
622
623 datafields["ffAngular"] = new Variant(debugStuff.ffAngular);
624 datafields["cappedFfVel"] = new Variant(debugStuff.cappedFfVel);
625
626 datafields["global_T_robot.x"] = new Variant(debugStuff.global_T_robot.translation().x());
627 datafields["global_T_robot.y"] = new Variant(debugStuff.global_T_robot.translation().y());
628 datafields["global_T_robot.o"] =
629 new Variant(simox::math::mat4f_to_rpy(debugStuff.global_T_robot.matrix()).z());
630
631 datafields["limitLinear"] = new Variant(config.params.limits.linear);
632 datafields["limitAngular"] = new Variant(config.params.limits.angular);
633
634 datafields["projectionIndex"] = new Variant(static_cast<int>(debugStuff.projectionIndex));
635 datafields["angularFeedforwardSaturated"] =
636 new Variant(debugStuff.angularFeedforwardSaturated);
637
638 const std::uint32_t staleCycles = controlTargetStaleCycles.load(std::memory_order_relaxed);
639 const std::uint64_t taskExceptions = controlTaskExceptions.load(std::memory_order_relaxed);
640
641 datafields["controlTargetStaleCycles"] = new Variant(static_cast<int>(staleCycles));
642 datafields["controlTaskExceptions"] = new Variant(static_cast<int>(taskExceptions));
643
644 // A recording that has outrun its ring is only visible in `meta.json` otherwise, i.e.
645 // after the fact. Reported live so the durations can be raised before the next run.
646 if (diagnosticsParams_.enabled)
647 {
648 datafields["diagnosticsRtOverwritten"] =
649 new Variant(static_cast<int>(rtSamples_.overwritten()));
650 datafields["diagnosticsControlOverwritten"] =
651 new Variant(static_cast<int>(controlSamples_.overwritten()));
652 }
653
654 // The RT thread cannot log, so the warning is emitted here instead.
655 if (staleCycles > 0 and not publishedStaleWarning)
656 {
657 publishedStaleWarning = true;
658 ARMARX_WARNING << "[nav-guard] control-target-stale: the 100 Hz control task stopped "
659 "publishing, so the platform is being ramped to a stop. "
660 << "Swallowed control-task exceptions so far: " << taskExceptions
661 << ". The robot would previously have kept executing the last "
662 "commanded twist indefinitely.";
663 }
664 else if (staleCycles == 0)
665 {
666 publishedStaleWarning = false;
667 }
668
669 debugObservers->setDebugChannel(
671 datafields);
672 }
673
674 void
676 {
678 ARMARX_INFO << "PlatformGlobalTrajectoryController::onInitNJointController";
679
680 runTask(
681 "PlatformGlobalTrajectoryControllerAdditionalTask",
682 [&]
683 {
684 CycleUtil c(10);
685 getObjectScheduler()->waitForObjectStateMinimum(eManagedIceObjectStarted);
687 << "Create a new thread alone PlatformGlobalTrajectoryController controller";
688 while (getState() == eManagedIceObjectStarted)
689 {
690 if (isControllerActive() and rtReady.load())
691 {
692 ARMARX_VERBOSE << "additional task";
693
694 // `RunningTaskBase::run()` wraps this whole callback in a single
695 // try/catch, so without this an exception from one cycle leaves the loop
696 // and the thread never runs again -- while `rtRun()` keeps commanding the
697 // twist it was last handed. One bad cycle must cost one cycle.
698 try
699 {
701 }
702 catch (const std::exception& e)
703 {
704 const std::uint64_t count =
705 controlTaskExceptions.fetch_add(1, std::memory_order_relaxed) + 1;
706
707 if (count == 1)
708 {
710 << "[nav-guard] control-task-exception: " << e.what()
711 << ". The control cycle was skipped; the RT watchdog will "
712 "ramp the platform to a stop if this persists.";
713 }
714 else
715 {
717 << "[nav-guard] control-task-exception: " << count
718 << " so far, latest: " << e.what();
719 }
720 }
721 catch (...)
722 {
723 controlTaskExceptions.fetch_add(1, std::memory_order_relaxed);
725 << "[nav-guard] control-task-exception: non-standard "
726 "exception. The control cycle was skipped.";
727 }
728 }
729 c.waitForCycleDuration();
730 }
731 });
732
733 // A thread of its own, and not the control task above, because writing a full buffer takes
734 // on the order of 100 ms. The goal-reached dump happens while the controller may still be
735 // active, and a control task blocked that long stops advancing `additionalTaskSequence`
736 // -- which the RT watchdog reads as a dead task after 100 ms and answers by ramping the
737 // platform to a stop, with a `[nav-guard] control-target-stale` warning. A diagnostics
738 // feature must not be able to manufacture a fault report.
739 if (diagnosticsParams_.enabled)
740 {
741 runTask("PlatformGlobalTrajectoryControllerDiagnosticsTask",
742 [&]
743 {
744 CycleUtil c(50);
745 getObjectScheduler()->waitForObjectStateMinimum(eManagedIceObjectStarted);
746
747 while (getState() == eManagedIceObjectStarted)
748 {
749 if (diagnosticsDumpRequested.exchange(false, std::memory_order_acquire))
750 {
751 // `writeDump` swallows its own failures; this covers the
752 // assembly around it, so one bad episode cannot end the thread.
753 try
754 {
756 }
757 catch (const std::exception& e)
758 {
759 ARMARX_WARNING << "Could not write the execution diagnostics: "
760 << e.what();
761 }
762 catch (...)
763 {
764 ARMARX_WARNING << "Could not write the execution "
765 "diagnostics: non-standard exception.";
766 }
767 }
768
769 c.waitForCycleDuration();
770 }
771 });
772 }
773
774 ARMARX_INFO << "PlatformGlobalTrajectoryController::onInitNJointController done.";
775 }
776
777 void
779 {
780 // additionalTask() is not executed. Thus, we can access lastTwist without mutex.
781 filteredTwist.reset();
782
783 // Seed the slew from what the base is actually doing, not from zero: a controller switch
784 // while moving would otherwise walk the command down from a velocity the robot never
785 // had. After an emergency stop this reads ~0, which is exactly the ramp-up we want.
786 rtCommandedTwist.linear = {platformSensor->velocityX, platformSensor->velocityY};
787 rtCommandedTwist.angular = platformSensor->velocityRotation;
788
789 robotStateBuffer_rtToAdditionalTask.getWriteBuffer().global_T_robot.matrix() =
790 rtGetRobot()->getGlobalPose();
791 robotStateBuffer_rtToAdditionalTask.commitWrite();
792
793 rtSlewLimited = false;
794
795 // A plain increment, which is all the diagnostics need from this thread: every sample
796 // recorded from here on carries the new id, and the dump filters on it.
797 diagnosticsEpisode.fetch_add(1, std::memory_order_release);
798 diagnosticsDumpedSinceActivation.store(false, std::memory_order_release);
799
800 rtReady.store(true);
801 }
802
803 void
805 {
806 rtReady.store(false);
807
808 // Only a flag: writing the dump is file I/O and must not happen here. The 100 Hz task
809 // thread picks it up within one cycle. This hook also runs on
810 // `rtDeactivateControllerBecauseOfError`, so a fault stop is recorded too.
811 //
812 // `enabled` is written once, in the constructor, so reading it here is a plain load.
813 //
814 // Skipped when a dump was already written since this activation: that is the goal-reached
815 // case, where deactivation follows within milliseconds and has nothing to add but a
816 // second directory holding the few cycles in between.
817 if (diagnosticsParams_.enabled and
818 not diagnosticsDumpedSinceActivation.load(std::memory_order_acquire))
819 {
820 diagnosticsDumpRequested.store(true, std::memory_order_release);
821 }
822 }
823
824 Controller::~Controller() = default;
825
827 Controller::getConfig(const ::Ice::Current&)
828 {
829 ARMARX_ERROR << "NYI";
830 return nullptr;
831 }
832} // namespace armarx::navigation::platform_controller::platform_global_trajectory
#define QUOTED(x)
constexpr T c
constexpr T dt
Brief description of class ControlTargetHolonomicPlatformVelocity.
This util class helps with keeping a cycle time during a control cycle.
Definition CycleUtil.h:41
SpamFilterDataPtr deactivateSpam(float deactivationDurationSec=10.0f, const std::string &identifier="", bool deactivate=true) const
disables the logging for the current line for the given amount of seconds.
Definition Logging.cpp:99
ArmarXObjectSchedulerPtr getObjectScheduler() const
int getState() const
Retrieve current state of the ManagedIceObject.
bool isControllerActive(const Ice::Current &=Ice::emptyCurrent) const final override
const SensorValueBase * useSensorValue(const std::string &sensorDeviceName) const
Get a const ptr to the given SensorDevice's SensorValue.
void runTask(const std::string &taskName, Task &&task)
Executes a given task in a separate thread from the Application ThreadPool.
const VirtualRobot::RobotPtr & useSynchronizedRtRobot(bool updateCollisionModel=false)
Requests a VirtualRobot for use in rtRun *.
std::string getInstanceName(const Ice::Current &=Ice::emptyCurrent) const final override
const VirtualRobot::RobotPtr & rtGetRobot()
TODO make protected and use attorneys.
ControlTargetBase * useControlTarget(const std::string &deviceName, const std::string &controlMode)
Declares to calculate the ControlTarget for the given ControlDevice in the given ControlMode when rtR...
The Variant class is described here: Variants.
Definition Variant.h:224
::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.
#define ARMARX_CHECK_EXPRESSION(expression)
This macro evaluates the expression and if it turns out to be false it will throw an ExpressionExcept...
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_NOT_NULL(ptr)
This macro evaluates whether ptr is not null and if it turns out to be false it will throw an Express...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_IMPORTANT
The logging level for always important information, but expected behaviour (in contrast to ARMARX_WAR...
Definition Logging.h:188
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:194
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:185
std::shared_ptr< class Robot > RobotPtr
Definition Bus.h:19
::IceInternal::Handle< Dict > DictPtr
const simox::meta::EnumNames< ControllerType > ControllerTypeNames
Eigen::Isometry3f Pose
Definition basic_types.h:31
std::string writeDump(const Episode &episode, const Params &params)
Write episode as <outputDirectory>/<DateTime>-ep<NNN>/.
const NJointControllerRegistration< Controller > Registration(common::ControllerTypeNames.to_name(common::ControllerType::PlatformGlobalTrajectory))
::IceInternal::ProxyHandle<::IceProxy::armarx::DebugObserverInterface > DebugObserverInterfacePrx
std::map< std::string, VariantBasePtr > StringVariantBaseMap
void fromAron(const arondto::PackagePath &dto, PackageFileLocation &bo)
IceUtil::Handle< class RobotUnit > RobotUnitPtr
Definition FTSensor.h:34
::IceInternal::ProxyHandle<::IceProxy::armarx::DebugDrawerInterface > DebugDrawerInterfacePrx
detail::ControlThreadOutputBufferEntry SensorAndControl
One cycle of the 100 Hz control task, i.e. one TrajectoryFollowingController::control.
float refX
The trajectory point the controller projected onto – the reference it is tracking.
std::uint64_t trajectoryRevision
Which trajectory this cycle tracked. Indexes trajectory.json.
float vRawX
The controller's own output, base frame, before the alpha low-pass.
float vFiltX
After the low-pass, i.e. what was handed to the real-time thread.
std::int32_t guard
traj_ctrl::global::GuardVerdict as an integer.
float positionError
Distance to the last trajectory point, which is what the controller calls positionError.
Everything one dump is written from. Assembled off the real-time thread.
std::uint64_t rtOverwritten
Lifetime overwrite counts, for context only. See Ring::overwritten.
traj_ctrl::global::TrajectoryFollowingControllerParams params
float slewDtBound
The dt bound the real-time slew was actually using [s].
bool rtTruncated
Whether the ring overwrote samples of this episode before the dump read them.
bool simulation
Whether this ran against a simulated RobotUnit.
float vCmdX
What was written to the control target, i.e. after rtSlewTowards.
Definition Diagnostics.h:96
std::uint64_t sequence
additionalTaskSequence as observed this cycle. Joins this row to control.csv.
Definition Diagnostics.h:78
float vMeasX
Measured platform velocity, base frame.
Definition Diagnostics.h:86
bool slewLimited
A slew bound was binding this cycle, i.e. the command could not follow its target.
bool stale
The watchdog fired: the 100 Hz task stopped publishing and the command is ramping down.
float vTgtX
The twist the 100 Hz task published, before the slew. Zero while the watchdog holds.
Definition Diagnostics.h:91
bool angularFeedforwardSaturated
True while the angular feed-forward is pinned at the limit, which leaves the orientation feedback no ...
std::size_t projectionIndex
Index of the trajectory segment the controller is tracking.
#define ARMARX_TRACE
Definition trace.h:75