Navigator.cpp
Go to the documentation of this file.
1#include "Navigator.h"
2
4
6
7#include <algorithm>
8#include <cmath>
9#include <cstddef>
10#include <iterator>
11#include <limits>
12#include <memory>
13#include <mutex>
14#include <optional>
15#include <string>
16#include <utility>
17#include <vector>
18
19#include <boost/graph/detail/adjacency_list.hpp>
20#include <boost/graph/dijkstra_shortest_paths.hpp>
21#include <boost/graph/named_function_params.hpp>
22#include <boost/graph/properties.hpp>
23#include <boost/property_map/property_map.hpp>
24
25#include <Eigen/Geometry>
26
27#include <range/v3/algorithm/sort.hpp>
28#include <range/v3/range/conversion.hpp>
29#include <range/v3/view/filter.hpp>
30#include <range/v3/view/reverse.hpp>
31
32#include <SimoxUtility/algorithm/string/string_tools.h>
33#include <VirtualRobot/Robot.h> // IWYU pragma: keep
34
45
67
68#include <SemanticObjectRelations/Shapes/Shape.h>
69
71{
72
78
79 bool
81 {
82 if (subdivision.empty())
83 {
84 // no subdivision
85 return hasLocalPlanner;
86 }
87
88 ARMARX_CHECK(hasLocalPlanner); // we always need local planner for segment subdivision
89 const bool segmentLocalPlanner = subdivision[currentSegment].useLocalPlanner;
90
91 return segmentLocalPlanner;
92 }
93
96 {
97 if (subdivision.empty())
98 {
99 // no subdivision
100 return plan.trajectory;
101 }
103 }
104
107 {
108 if (subdivision.empty())
109 {
110 // no subdivision
111 return plan.trajectory;
112 }
114 }
115
116 namespace
117 {
118 /// Build the configured parametrization, falling back to ramping rather than failing.
119 ///
120 /// The fallback lives here and deliberately *not* in
121 /// `TrajectoryParametrizationFactory`: the analysis application calls the same factory
122 /// and then reads the time parametrization back out of the `Toppra` instance. A silent
123 /// substitution inside the factory would make it write a result labelled `toppra` that
124 /// actually contains a ramping profile -- wrong data in the tool used to validate this.
125 /// A navigation component must not crash; an offline analysis tool must not lie.
126 ///
127 /// Three things can go wrong, and all three land here:
128 /// 1. TOPP-RA was not compiled in (no python environment at configure time).
129 /// 2. No platform dynamics config, or it does not parse / the robot model fails.
130 /// 3. Anything the `Toppra` constructor throws while loading the model.
131 /// Load the drive parameters, or nothing if they are unavailable or unusable.
132 std::optional<algorithms::Toppra::DriveParams>
133 loadDriveParams(const core::GeneralConfig& generalConfig,
134 const Navigator::Config::General& general)
135 {
137 {
138 return std::nullopt;
139 }
140
141 if (not general.platformDynamicsEnabled)
142 {
143 return std::nullopt;
144 }
145
146 try
147 {
150 }
151 catch (const std::exception& e)
152 {
153 ARMARX_WARNING << "Cannot read the platform dynamics config for platform "
154 << QUOTED(general.platform) << ": " << e.what();
155 return std::nullopt;
156 }
157 }
158
160 createParametrization(const core::GeneralConfig& generalConfig,
161 const Navigator::Config::General& general,
162 const std::optional<algorithms::Toppra::DriveParams>& driveParams,
164 {
165 *applied = generalConfig.parametrization;
166
167 const auto rampingInstead = [&](const std::string& reason)
168 {
169 ARMARX_WARNING << "TOPP-RA trajectory parametrization was requested but " << reason
170 << ". Falling back to ramping.";
171
172 core::GeneralConfig ramping = generalConfig;
173 ramping.parametrization = core::TrajectoryParametrization::Ramping;
175
176 return fac::TrajectoryParametrizationFactory::create(ramping);
177 };
178
179 if (generalConfig.parametrization != core::TrajectoryParametrization::Toppra)
180 {
181 return fac::TrajectoryParametrizationFactory::create(generalConfig);
182 }
183
185 {
186 return rampingInstead("it is not available in this build: " +
188 }
189
190 if (not general.platformDynamicsEnabled)
191 {
192 return rampingInstead("its drive parameters are disabled by "
193 "`p.navigator.general.platformDynamicsEnabled`");
194 }
195
196 if (not driveParams.has_value())
197 {
198 return rampingInstead(
199 "its drive parameters could not be read for platform '" + general.platform +
200 "'. Check `p.navigator.general.platform` against the files in "
201 "data/armarx_navigation/config/platform/");
202 }
203
204 try
205 {
206 return fac::TrajectoryParametrizationFactory::create(generalConfig, *driveParams);
207 }
208 catch (const std::exception& e)
209 {
210 return rampingInstead(std::string{"it could not be initialised: "} + e.what());
211 }
212 }
213 /// Report an accepted plan the base cannot follow, on the trajectory that reaches it.
214 ///
215 /// Both checks already existed and neither had a caller.
216 /// `findGeometryViolations` does run inside the SPFA smoother, but on the smoothed
217 /// *candidate*: when smoothing is rejected the raw resampled path ships unchecked, which
218 /// is exactly what happened on ARMAR-7 on 2026-08-29. `findTurnRateViolations` had no
219 /// production caller at all.
220 ///
221 /// Here rather than in a planner because every planner reaches this: SPFA, AStar,
222 /// Point2Point and the graph route all converge on one accepted `globalPlan`, and each
223 /// has early returns that bypass its own final block. It is also upstream of
224 /// `applyParametrization`, so this is provably what TOPP-RA will be handed.
225 ///
226 /// Reporting only. A plan that is awkward to follow still beats no plan, and the
227 /// reparametrization slows down for it rather than failing.
228 void
229 reportPathGeometry(const core::GlobalTrajectory& trajectory)
230 {
231 namespace smoothing = algorithms::spfa::smoothing;
232
233 if (trajectory.points().size() < 2)
234 {
235 return;
236 }
237
238 const smoothing::GeometryLimits limits;
239
240 const std::vector<std::size_t> degenerate =
241 smoothing::findGeometryViolations(trajectory, limits);
242
243 if (not degenerate.empty())
244 {
245 ARMARX_WARNING << "[nav-guard] plan-geometry-violation: " << degenerate.size()
246 << " of " << trajectory.points().size()
247 << " waypoints of the accepted plan reverse direction or sit on a "
248 << "segment shorter than " << limits.minSegmentLength
249 << " mm; the first is waypoint " << degenerate.front()
250 << ". A heading taken across such a segment is numerical noise.";
251 }
252
253 const std::vector<std::size_t> unturnable =
254 smoothing::findTurnRateViolations(trajectory, limits);
255
256 if (not unturnable.empty())
257 {
258 ARMARX_WARNING << "[nav-guard] plan-unturnable: " << unturnable.size() << " of "
259 << trajectory.points().size() - 1
260 << " segments of the accepted plan turn faster than "
261 << limits.maxTurnRate << " rad/mm, the worst at "
262 << smoothing::maxTurnRate(trajectory, limits)
263 << " rad/mm. The reparametrization will slow the whole trajectory "
264 << "down for them.";
265 }
266 }
267 } // namespace
268
269 Navigator::Navigator(const Config& config, const InjectedServices& services) :
270 config{config},
271 srv{services},
272 driveParams{loadDriveParams(config.stack.generalConfig, config.general)},
273 parametrization{createParametrization(config.stack.generalConfig,
274 config.general,
275 driveParams,
276 &parametrizationMode)},
277 rampingFallback{fac::TrajectoryParametrizationFactory::create(
278 [&]
279 {
280 core::GeneralConfig ramping = config.stack.generalConfig;
282 return ramping;
283 }())}
284 {
285 ARMARX_INFO << "Trajectory parametrization: " << core::ToString(parametrizationMode);
286
287 ARMARX_CHECK_NOT_NULL(srv.sceneProvider) << "The scene provider must be set!";
288 // ARMARX_CHECK_NOT_NULL(services.executor) << "The executor service must be set!";
289 ARMARX_CHECK_NOT_NULL(services.publisher) << "The publisher service must be set!";
290 }
291
293 {
294 ARMARX_INFO << "Navigator destructor";
295 stop();
296
297 stopAllThreads();
298 }
299
300 void
301 Navigator::setGraphEdgeCosts(core::Graph& graph) const
302 {
303 const auto cost = [](const core::Pose& p1, const core::Pose& p2)
304 {
305 const core::Pose diff = p1.inverse() * p2;
306
307 // FIXME consider rotation as well
308 return diff.translation().norm();
309 };
310
311 // graph -> trajectory (set costs)
312 for (auto edge : graph.edges())
313 {
314 const core::Pose start = resolveGraphVertex(edge.source());
315 const core::Pose goal = resolveGraphVertex(edge.target());
316
317 switch (edge.attrib().strategy)
318 {
320 {
321 ARMARX_VERBOSE << "Global planning from " << start.translation() << " to "
322 << goal.translation();
323 ARMARX_CHECK_NOT_NULL(config.stack.globalPlanner);
324
325 try
326 {
327 const auto globalPlan = config.stack.globalPlanner->plan(start, goal);
328 if (globalPlan.has_value())
329 {
330 ARMARX_CHECK(globalPlan->trajectory.isValid());
331
332 edge.attrib().trajectory = globalPlan->trajectory;
333 ARMARX_VERBOSE << "Free path length: "
334 << globalPlan->trajectory.length();
335 edge.attrib().cost() = globalPlan->trajectory.length();
336 }
337 else
338 {
339 ARMARX_VERBOSE << "Global planning failed for this edge.";
340 edge.attrib().cost() = std::numeric_limits<float>::max();
341 }
342 }
343 catch (...)
344 {
345 ARMARX_VERBOSE << "Global planning failed due to exception "
347 edge.attrib().cost() = std::numeric_limits<float>::max();
348 }
349
350
351 break;
352 }
354 {
355 edge.attrib().cost() = cost(start, goal);
356 break;
357 }
358 }
359
360 ARMARX_VERBOSE << "Edge cost: " << edge.attrib().cost();
361 }
362 }
363
364 void
365 Navigator::moveTo(const std::vector<core::Pose>& waypoints,
366 core::NavigationFrame navigationFrame)
367 {
368 // Serialized against the periodic run() loop, which reads the state these mutate, and
369 // against the other navigators sharing the scene provider.
370 const std::scoped_lock lock{stateMtx, srv.sceneProvider->sceneMutex()};
371
372 ARMARX_INFO << "Received moveTo() request.";
373
374 std::vector<core::Pose> globalWaypoints;
375 switch (navigationFrame)
376 {
378 globalWaypoints = waypoints;
379 break;
381 globalWaypoints.reserve(waypoints.size());
382
383 ARMARX_CHECK(srv.sceneProvider->synchronize(Clock::Now(), true));
384
385 const core::Pose global_T_robot(srv.sceneProvider->scene().robot->getGlobalPose());
386 ARMARX_VERBOSE << "Initial robot pose: " << global_T_robot.matrix();
387
388 std::transform(std::begin(waypoints),
389 std::end(waypoints),
390 std::back_inserter(globalWaypoints),
391 [&](const core::Pose& p) { return global_T_robot * p; });
392 break;
393 }
394
396 moveToAbsolute(globalWaypoints);
397 }
398
399 void
400 Navigator::moveToAlternatives(const std::vector<core::TargetAlternative>& targets,
401 core::NavigationFrame navigationFrame)
402 {
403 // Serialized against the periodic run() loop, which reads the state these mutate, and
404 // against the other navigators sharing the scene provider.
405 const std::scoped_lock lock{stateMtx, srv.sceneProvider->sceneMutex()};
406
407 ARMARX_INFO << "Received moveToAlternatives() request.";
408
409 std::vector<core::TargetAlternative> globalTargets;
410 switch (navigationFrame)
411 {
413 globalTargets = targets;
414 break;
416 globalTargets.reserve(targets.size());
417
418 ARMARX_CHECK(srv.sceneProvider->synchronize(Clock::Now(), true));
419
420 const core::Pose global_T_robot(srv.sceneProvider->scene().robot->getGlobalPose());
421 ARMARX_VERBOSE << "Initial robot pose: " << global_T_robot.matrix();
422
423 std::transform(std::begin(targets),
424 std::end(targets),
425 std::back_inserter(globalTargets),
426 [&](const core::TargetAlternative& p) {
428 .target = global_T_robot * p.target, .priority = p.priority};
429 });
430 break;
431 }
432
434 moveToAbsoluteAlternatives(globalTargets);
435 }
436
437 void
438 Navigator::update(const std::vector<core::Pose>& waypoints,
439 core::NavigationFrame navigationFrame)
440 {
441 // Serialized against the periodic run() loop, which reads the state these mutate, and
442 // against the other navigators sharing the scene provider.
443 const std::scoped_lock lock{stateMtx, srv.sceneProvider->sceneMutex()};
444
445 ARMARX_INFO << "Received update() request.";
446
447 std::vector<core::Pose> globalWaypoints;
448 switch (navigationFrame)
449 {
451 globalWaypoints = waypoints;
452 break;
454 globalWaypoints.reserve(waypoints.size());
455 std::transform(
456 std::begin(waypoints),
457 std::end(waypoints),
458 std::back_inserter(globalWaypoints),
459 [&](const core::Pose& p)
460 { return core::Pose(srv.sceneProvider->scene().robot->getGlobalPose()) * p; });
461 break;
462 }
463
465 updateAbsolute(globalWaypoints);
466 }
467
468 using GraphPath = std::vector<semrel::ShapeID>;
469
472 const core::Graph::ConstVertex& startVertex,
473 const core::Graph::ConstVertex& goalVertex)
474 {
475 ARMARX_VERBOSE << "Graph consists of " << graph.numVertices() << " vertices and "
476 << graph.numEdges() << " edges.";
477
478 std::vector<core::Graph::VertexDescriptor> predecessors(graph.numVertices());
479 std::vector<int> d(num_vertices(graph));
480
481 // FIXME ARMARX_CHECK(graphBuilder.startVertex.has_value());
482
483 auto weightMap = boost::get(&core::EdgeAttribs::m_value, graph);
484
485 core::Graph::VertexDescriptor start = startVertex.descriptor();
486
487 auto predecessorMap = boost::make_iterator_property_map(
488 predecessors.begin(), boost::get(boost::vertex_index, graph));
489
490 auto params = boost::predecessor_map(predecessorMap)
491 .distance_map(boost::make_iterator_property_map(
492 d.begin(), boost::get(boost::vertex_index, graph)))
493 .weight_map(weightMap);
494
498
499 ARMARX_VERBOSE << "Edge weights:";
500 for (const auto& edge : graph.edges())
501 {
502 ARMARX_VERBOSE << edge.sourceObjectID() << " -> " << edge.targetObjectID() << ": "
503 << edge.attrib().m_value;
504 }
505
506 std::vector<core::Graph::EdgeDescriptor> edgesToBeRemoved;
507 for (const auto edge : graph.edges())
508 {
509 if (edge.attrib().m_value == std::numeric_limits<float>::max())
510 {
511 edgesToBeRemoved.push_back(edge.descriptor());
512 }
513 }
514
515 for (const auto edge : edgesToBeRemoved)
516 {
517 boost::remove_edge(edge, graph);
518 }
519 ARMARX_VERBOSE << "Edge weights after removal of inf edges:";
520
521 for (const auto& edge : graph.edges())
522 {
523 ARMARX_VERBOSE << edge.sourceObjectID() << " -> " << edge.targetObjectID() << ": "
524 << edge.attrib().m_value;
525 ARMARX_VERBOSE << "Edge: " << edge << "cost: " << edge.attrib().cost()
526 << ", type: " << static_cast<int>(edge.attrib().strategy)
527 << " , has traj " << edge.attrib().trajectory.has_value();
528 }
529
530 ARMARX_VERBOSE << "Searching shortest path from vertex with id `"
531 << graph.vertex(start).objectID() << "` to vertex `"
532 << graph.vertex(goalVertex.descriptor()).objectID() << "`";
533
534 boost::dijkstra_shortest_paths(graph, start, params);
535
536 // find shortest path
538
539 // WARNING: shortest path will be from goal to start first
540 GraphPath shortestPath;
541
542 core::Graph::VertexDescriptor currentVertex = goalVertex.descriptor();
543 while (currentVertex != startVertex.descriptor())
544 {
545 shortestPath.push_back(graph.vertex(currentVertex).objectID());
546
548
549 auto parent = predecessorMap[currentVertex];
550 ARMARX_VERBOSE << "Parent id: " << parent;
551
553
554 // find edge between parent and currentVertex
555 auto outEdges = graph.vertex(parent).outEdges();
556 ARMARX_VERBOSE << "Parent has " << std::distance(outEdges.begin(), outEdges.end())
557 << " out edges";
558
559 ARMARX_CHECK_GREATER(std::distance(outEdges.begin(), outEdges.end()), 0)
560 << "Cannot reach another vertex from vertex `"
561 << graph.vertex(parent).objectID(); // not empty
562
563 auto edgeIt = std::find_if(outEdges.begin(),
564 outEdges.end(),
565 [&currentVertex](const auto& edge) -> bool
566 { return edge.target().descriptor() == currentVertex; });
567
568 ARMARX_CHECK(edgeIt != outEdges.end());
569 // shortestPath.edges.push_back(edgeIt->descriptor());
570
571 currentVertex = parent;
572 }
573
574 shortestPath.push_back(startVertex.objectID());
575
576 // ARMARX_CHECK_EQUAL(shortestPath.vertices.size() - 1, shortestPath.edges.size());
577
579 // reverse the range => now it is from start to goal again
580 shortestPath = shortestPath | ranges::views::reverse | ranges::to_vector;
581 // shortestPath.edges = shortestPath.edges | ranges::views::reverse | ranges::to_vector;
582
584 return shortestPath;
585 }
586
588 Navigator::convertToTrajectory(const GraphPath& shortestPath, const core::Graph& graph) const
589 {
591
592 ARMARX_CHECK_GREATER_EQUAL(shortestPath.size(), 2)
593 << "At least start and goal vertices must be available";
594
595 // ARMARX_CHECK_EQUAL(shortestPath.size() - 1, shortestPath.edges.size());
596
597 std::vector<core::GlobalTrajectoryPoint> trajectoryPoints;
598
599 // TODO add the start
600 // trajectoryPoints.push_back(core::TrajectoryPoint{
601 // .waypoint = {.pose = graph.vertex(shortestPath.front()).attrib().getPose()},
602 // .velocity = 0.F});
603
604 ARMARX_VERBOSE << "Shortest path with " << shortestPath.size() << " vertices";
606
607 for (size_t i = 0; i < shortestPath.size() - 1; i++)
608 {
609 // ARMARX_CHECK(graph.hasEdge(shortestPath.edges.at(i).m_source,
610 // shortestPath.edges.at(i).m_target));
611
612 // TODO add method edge(shapeId, shapeId)
613 const core::Graph::ConstEdge edge =
614 graph.edge(graph.vertex(shortestPath.at(i)), graph.vertex(shortestPath.at(i + 1)));
615
617 ARMARX_VERBOSE << "Index " << i;
618 ARMARX_VERBOSE << static_cast<int>(edge.attrib().strategy);
619 ARMARX_VERBOSE << edge;
620 switch (edge.attrib().strategy)
621 {
623 {
624 ARMARX_INFO << "Free navigation on edge";
625
627 ARMARX_CHECK(edge.attrib().trajectory.has_value());
628
630 ARMARX_INFO << "Length: " << edge.attrib().trajectory->length();
631
632
634 ARMARX_INFO << "Free navigation with "
635 << edge.attrib().trajectory->points().size() << " waypoints";
636
637 // we have a trajectory
638 // FIXME trajectory points can be invalid => more points than expected. Why?
640 const std::vector<core::GlobalTrajectoryPoint> edgeTrajectoryPoints =
641 edge.attrib().trajectory->points();
642
643 // if trajectory is being initialized, include the start, otherwise skip it
644 const int offset = trajectoryPoints.empty() ? 0 : 1;
645
646 if (edgeTrajectoryPoints.size() > 2) // not only start and goal position
647 {
649 // append `edge trajectory` to `trajectory` (without start and goal points)
650 trajectoryPoints.insert(trajectoryPoints.end(),
651 edgeTrajectoryPoints.begin() + offset,
652 edgeTrajectoryPoints.end());
653 }
654
655
657 // clang-format off
658 // trajectoryPoints.push_back(
659 // core::TrajectoryPoint
660 // {
661 // .waypoint =
662 // {
663 // .pose = graph.vertex(shortestPath.at(i+1)).attrib().getPose()
664 // },
665 // .velocity = std::numeric_limits<float>::max()
666 // });
667 // clang-format on
668
669 break;
670 }
671
673 {
674 ARMARX_INFO << "Point2Point navigation on edge";
675
676 // FIXME variable
677 const float point2pointVelocity = 400;
678
679 const core::GlobalTrajectoryPoint currentTrajPt = {
680 .waypoint = {.pose = resolveGraphVertex(graph.vertex(shortestPath.at(i)))},
681 .velocity = point2pointVelocity};
682
683 const core::GlobalTrajectoryPoint nextTrajPt{
684 .waypoint = {.pose =
685 resolveGraphVertex(graph.vertex(shortestPath.at(i + 1)))},
686 .velocity = 0};
687
688 // resample event straight lines
689 // ARMARX_CHECK_NOT_EMPTY(trajectoryPoints);
690
691 // const core::Trajectory segmentTraj({trajectoryPoints.back(), nextTrajPt});
692 // ARMARX_INFO << "Segment trajectory length: " << segmentTraj.length();
693
694 // const auto resampledTrajectory = segmentTraj.resample(500); // FIXME param
695
696 // ARMARX_INFO << "Resampled trajectory contains "
697 // << resampledTrajectory.points().size() << " points";
698
699 // this is the same pose as the goal of the previous segment but with a different velocity
700 // FIXME MUST set velocity here.
701 // trajectoryPoints.push_back(
702 // core::TrajectoryPoint{.waypoint = trajectoryPoints.back().waypoint,
703 // .velocity = std::numeric_limits<float>::max()});
704
706 // trajectoryPoints.insert(trajectoryPoints.end(),
707 // resampledTrajectory.points().begin(),
708 // resampledTrajectory.points().end());
709 trajectoryPoints.push_back(currentTrajPt);
710 trajectoryPoints.push_back(nextTrajPt);
711
712 break;
713 }
714 default:
715 {
716 ARMARX_ERROR << "Boom.";
717 }
718 }
719 }
720
721 ARMARX_INFO << "Trajectory consists of " << trajectoryPoints.size() << " points";
722
723 for (const auto& pt : trajectoryPoints)
724 {
725 ARMARX_INFO << pt.waypoint.pose.translation();
726 }
727
728 return {trajectoryPoints};
729 }
730
731 GraphBuilder
732 Navigator::convertToGraph(const std::vector<client::WaypointTarget>& targets) const
733 {
734 //
735 GraphBuilder graphBuilder;
736 graphBuilder.initialize(core::Pose(srv.sceneProvider->scene().robot->getGlobalPose()));
737
738 // std::optional<Graph*> activeSubgraph;
739 for (const auto& target : targets)
740 {
741 ARMARX_INFO << "Adding target " << QUOTED(target) << " to graph";
742 // if the last location was on a subgraph, that we are about to leave
743 // const bool leavingSubgraph = [&]() -> bool
744 // {
745 // if (not activeSubgraph.has_value())
746 // {
747 // return false;
748 // }
749
750 // // if a user specifies a pose, then this pose is not meant to be part of a graph
751 // // -> can be reached directly
752 // if (target.pose.has_value())
753 // {
754 // return true;
755 // }
756
757 // if (not target.locationId->empty())
758 // {
759 // const auto& subgraph = core::getSubgraph(target.locationId.value(),srv.sceneProvider->scene().graph->subgraphs);
760 // return subgraph.name() != activeSubgraph;
761 // }
762
763 // throw LocalException("this line should not be reachable");
764 // }();
765
766 // if(leavingSubgraph)
767 // {
768 // const auto activeNodes = graph.activeVertices();
769 // ARMARX_CHECK(activeNodes.size() == 1);
770 // graph.connect(activeSubgraph->getRoutesFrom(activeNodes.front()));
771 // }
772
773
774 // if a user specifies a pose, then this pose is not meant to be part of a graph
775 // -> can be reached directly
776 if (target.pose.has_value())
777 {
778 graphBuilder.connect(target.pose.value(), target.strategy);
779 continue;
780 }
781
782 // if a user specified a vertex of a subgraph (aka location), then this vertex
783 // might not be reachable directly. It might be needed to navigate on the graph
784 // instead. Thus, we collect all routes to reach the node.
785 if (not target.locationId->empty())
786 {
787 const auto& subgraph = core::getSubgraph(
788 target.locationId.value(), srv.sceneProvider->scene().graph->subgraphs);
789
790 const auto vertex = core::getVertexByName(target.locationId.value(), subgraph);
791
792 ARMARX_INFO << "Vertex " << QUOTED(target.locationId.value()) << " resolved to "
793 << vertex.attrib().getPose();
794
795 const std::vector<core::GraphPath> routes = core::findPathsTo(vertex, subgraph);
796 // const auto routes = subgraph->getRoutesTo(target.locationId);
797
798 ARMARX_INFO << "Found " << routes.size() << " routes to location `"
799 << target.locationId.value();
800
801 ARMARX_CHECK(not routes.empty()) << "The location `" << target.locationId.value()
802 << "` is not a reachable vertex on the graph!";
803
804 // we now add all routes to the graph that take us to the desired location
805 graphBuilder.connect(routes,
806 target.strategy); // TODO: all routes to the same target
807
808 continue;
809 }
810
811 ARMARX_ERROR << "Either `location_id` or `pose` has to be provided!";
812 }
813
814 const auto goalVertex = graphBuilder.getGraph().vertex(graphBuilder.goalVertex());
815 ARMARX_INFO << "Goal vertex is " << QUOTED(goalVertex.attrib().getLocationName());
816
817 return graphBuilder;
818 }
819
821 Navigator::resolveGraphVertex(const core::Graph::ConstVertex& vertex) const
822 {
823 const auto goal = core::resolveLocation(srv.sceneProvider->scene().staticScene->objectMap,
824 srv.sceneProvider->scene().staticScene->objectInfo,
825 vertex.attrib().getPose());
826 ARMARX_CHECK(goal.pose.has_value())
827 << "The location of vertex " << vertex.attrib().getLocationName()
828 << " couldn't be resolved (" << goal.errorMsg << ")";
829 return goal.pose.value();
830 }
831
832 void
833 Navigator::moveTo(const std::vector<client::WaypointTarget>& targets,
834 core::NavigationFrame navigationFrame)
835 {
836 // Serialized against the periodic run() loop, which reads the state these mutate, and
837 // against the other navigators sharing the scene provider.
838 const std::scoped_lock lock{stateMtx, srv.sceneProvider->sceneMutex()};
839
840 // arlt: Is this function deprecated?
842 << "only absolute movement implemented atm.";
843
845
846 validate(targets);
847
848 ARMARX_CHECK_NOT_EMPTY(targets) << "At least the goal has to be provided!";
849 ARMARX_INFO << "Navigating to " << targets.back();
850
851 // update static scene including locations
852 updateScene(true);
853
854 auto graphBuilder = convertToGraph(targets);
855
857
858 core::Graph graph = graphBuilder.getGraph();
859 auto startVertex = graphBuilder.startVertex;
860 auto goalVertex = graphBuilder.getGraph().vertex(graphBuilder.goalVertex());
861
862 ARMARX_INFO << "Goal pose according to graph is " << graphBuilder.goalPose().matrix();
863
864 ARMARX_CHECK(startVertex.has_value());
865
867
868 goalReachedMonitor = std::nullopt;
869 goalReachedMonitor = GoalReachedMonitor(
870 graphBuilder.goalPose(), srv.sceneProvider->scene(), config.goalReachedConfig);
871
872 if (goalReachedMonitor->goalReached(false))
873 {
874 ARMARX_IMPORTANT << "Already at goal position "
875 << goalReachedMonitor->goal().translation().head<2>()
876 << ". Robot won't move.";
877
878 srv.publisher->goalReached(core::GoalReachedEvent{
880 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
881
882 return;
883 }
884
886 setGraphEdgeCosts(graph);
887
889
890 srv.introspector->onGlobalGraph(graph);
891
893 const auto shortestPath = findShortestPath(graph, startVertex.value(), goalVertex);
894
895 // print
897
898 std::vector<core::Pose> vertexPoses;
899 vertexPoses.emplace_back(srv.sceneProvider->scene().robot->getGlobalPose());
900
901 ARMARX_INFO << "Navigating along the following nodes:";
902 for (const semrel::ShapeID& vertex : shortestPath)
903 {
904 ARMARX_INFO << " - " << graph.vertex(vertex).attrib().getLocationName();
905 vertexPoses.push_back(resolveGraphVertex(graph.vertex(vertex)));
906 }
907
908 srv.introspector->onGlobalShortestPath(vertexPoses);
909
910
911 // convert graph / vertex chain to trajectory
913 core::GlobalTrajectory globalPlanTrajectory = convertToTrajectory(shortestPath, graph);
914
915 // globalPlanTrajectory.setMaxVelocity(1000); // FIXME
916
917 // move ...
918
919 // this is our `global plan`
921
922 globalPlan = global_planning::GlobalPlannerResult{.trajectory = globalPlanTrajectory,
923 .helperTrajectory = std::nullopt};
924
925 // the following is similar to moveToAbsolute
926 // TODO(fabian.reister): remove code duplication
927
928 srv.executor->execute(globalPlan->currentGlobalSegment());
929
931 srv.publisher->globalTrajectoryUpdated(core::GlobalTrajectoryUpdatedEvent{
932 {.timestamp = armarx::Clock::Now()}, globalPlan->currentGlobalSegment()});
933
935 srv.introspector->onGlobalPlannerResult(globalPlan->plan);
936
937 ARMARX_INFO << "Global planning completed. Will now start all required threads";
939
940 startStack();
941 }
942
943 void
944 Navigator::startStack()
945 {
946
948
949 ARMARX_INFO << "Starting stack.";
950
951 {
952 // ensure running task is never started and stopped simultaneously
953 const std::scoped_lock<std::mutex> lock{runningTaskMtx};
954
955 shouldRun = true;
956
957 // FIXME instead of PeriodicTask, use RunningTask.
958 if (not runningTask)
959 {
960 runningTask =
962 &Navigator::run,
963 config.general.tasks.replanningUpdatePeriod,
964 false,
965 "PeriodicTask");
966 runningTask->start();
967 }
968 else if (not runningTask->isRunning())
969 {
970 runningTask->start();
971 }
972 }
973
974 // FIXME create separate function for this.
975 if (globalPlan->useLocalPlanner(hasLocalPlanner()))
976 {
977 if (srv.executor != nullptr)
978 {
979 srv.executor->start(ExecutorInterface::ControllerType::LocalTrajectory);
980 }
981 }
982 else
983 {
984 if (srv.executor != nullptr)
985 {
986 srv.executor->start(ExecutorInterface::ControllerType::GlobalTrajectory);
987 }
988 }
989
990 // Could be required if pauseMovement() has been called in the past.
991 resume();
992 srv.publisher->movementStarted(core::MovementStartedEvent{
993 {.timestamp = armarx::Clock::Now()},
994 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
995 }
996
997 void
998 Navigator::moveToAbsolute(const std::vector<core::Pose>& waypoints, bool sceneUpdate)
999 {
1001
1002 // if this navigator is in use, stop the movement ...
1003 pause();
1004 // ... and all threads
1005 stopAllThreads();
1006
1007 // FIXME remove
1008 //std::this_thread::sleep_for(std::chrono::seconds(1));
1009
1011 ARMARX_CHECK_NOT_EMPTY(waypoints);
1012
1013 if (sceneUpdate)
1014 {
1015 updateScene(true);
1016 }
1017
1018 ARMARX_INFO << "Request to move from " << srv.sceneProvider->scene().robot->getGlobalPose()
1019 << " to " << waypoints.back().matrix();
1020
1021 // first we check if we are already at the goal position
1022 goalReachedMonitor = std::nullopt;
1023 goalReachedMonitor = GoalReachedMonitor(
1024 waypoints.back(), srv.sceneProvider->scene(), config.goalReachedConfig);
1025
1026 if (goalReachedMonitor->goalReached(false))
1027 {
1028 ARMARX_INFO << "Already at goal position. Robot won't move.";
1029 ARMARX_CHECK_NOT_NULL(srv.publisher);
1030
1031 ARMARX_VERBOSE << "Finalizing";
1032 srv.publisher->goalReached(core::GoalReachedEvent{
1034 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
1035 ARMARX_VERBOSE << "Finalized";
1036
1037 return;
1038 }
1039
1040 // global planner
1041 ARMARX_INFO << "Planning global trajectory";
1042 ARMARX_CHECK_NOT_NULL(config.stack.globalPlanner);
1043 // TODO plan on multiple waypoints, ignoring waypoints for now
1044 // idea: compute multiple global trajectories, one for each segment between waypoints.
1045
1046 srv.introspector->onGoal(waypoints.back());
1047 globalPlan = config.stack.globalPlanner->plan(waypoints.back());
1048
1049 if (srv.drawer != nullptr)
1050 {
1051 srv.drawer->callGenericDrawFunction(
1052 [&](viz::Client& client)
1053 { config.stack.globalPlanner->visualizeDebugInfo(client); });
1054 }
1055 else
1056 {
1057 ARMARX_WARNING << "No drawer available. Cannot visualize global planner debug info.";
1058 }
1059
1060
1062
1063 if (not globalPlan.has_value())
1064 {
1065 ARMARX_WARNING << "No global trajectory. Cannot move.";
1066 srv.publisher->globalPlanningFailed(core::GlobalPlanningFailedEvent{
1067 {.timestamp = armarx::Clock::Now()}, {"No global trajectory. Cannot move."}});
1068
1069 srv.introspector->failure();
1070
1071 // This request failed, so the robot must not keep driving whatever path a
1072 // previous request left running -- it would be heading for a goal nobody asked
1073 // for anymore.
1074 stop();
1075 return;
1076 }
1077
1078 // create subdivision and start first segment
1079 setupGlobalPlanSubvidision();
1080
1081 startGlobalPathSegment(false, false);
1082 }
1083
1084 void
1085 Navigator::updateAbsolute(const std::vector<core::Pose>& waypoints)
1086 {
1088 ARMARX_CHECK_NOT_EMPTY(waypoints);
1089
1090 // Assume nothing in static obstacles changed -> also don't update costmap
1091 updateScene(false);
1092
1093 // global planner
1094 ARMARX_INFO << "Planning global trajectory";
1095 ARMARX_CHECK_NOT_NULL(config.stack.globalPlanner);
1096 // TODO plan on multiple waypoints, ignoring waypoints for now
1097 // idea: compute multiple global trajectories, one for each segment between waypoints.
1098
1099 srv.introspector->onGoal(waypoints.back());
1100 globalPlan = config.stack.globalPlanner->plan(waypoints.back());
1101
1103
1104 if (not globalPlan.has_value())
1105 {
1106 ARMARX_WARNING << "No global trajectory. Cannot move.";
1107 srv.publisher->globalPlanningFailed(core::GlobalPlanningFailedEvent{
1108 {.timestamp = armarx::Clock::Now()}, {"No global trajectory. Cannot move."}});
1109
1110 srv.introspector->failure();
1111 return;
1112 }
1113
1114 // create subdivision and start first segment
1115 setupGlobalPlanSubvidision();
1116
1117 startGlobalPathSegment(false, true);
1118 }
1119
1120 void
1121 Navigator::moveToAbsoluteAlternatives(const std::vector<core::TargetAlternative>& targets)
1122 {
1124
1125 // if this navigator is in use, stop the movement ...
1126 pause();
1127 // ... and all threads
1128 stopAllThreads();
1129
1131 ARMARX_CHECK_NOT_EMPTY(targets);
1132
1133 updateScene(true);
1134
1135 ARMARX_INFO << "Request to move from " << srv.sceneProvider->scene().robot->getGlobalPose()
1136 << " to " << targets.size() << " alternatives";
1137
1138 lastAllAlternativesImpossible = std::nullopt;
1139
1140 // first, check whether we are already at any target
1141 for (const auto& t : targets)
1142 {
1143 GoalReachedMonitor monitor(
1144 t.target, srv.sceneProvider->scene(), config.goalReachedConfig);
1145
1146 if (monitor.goalReached(false))
1147 {
1148 ARMARX_INFO << "Already at a possible goal position. Robot won't move.";
1149
1150 srv.publisher->goalReached(core::GoalReachedEvent{
1152 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
1153
1154 return;
1155 }
1156 }
1157
1158 // sort alternatives by their priority (in descending order)
1159 targetAlternatives = targets;
1160 ranges::sort(targetAlternatives, std::greater{}, &core::TargetAlternative::priority);
1161
1162 if (setupTargetAlternatives())
1163 {
1164 // create subdivision and start first segment
1165 setupGlobalPlanSubvidision();
1166
1167 startGlobalPathSegment(false, false);
1168 }
1169 }
1170
1171 bool
1172 Navigator::setupTargetAlternatives()
1173 {
1174 ARMARX_CHECK_NOT_EMPTY(targetAlternatives);
1175
1176 globalPlan = std::nullopt;
1177
1178 // Spfa planner plans into the "whole room", i.e. by executing the algorithm with
1179 // a single start position, a path to any goal can be constructed.
1180 // Thus, we only use alternatives with the spfa planner to avoid repeatedly replanning the
1181 // global path.
1182 const auto spfaPlanner =
1183 std::dynamic_pointer_cast<global_planning::SPFA>(config.stack.globalPlanner);
1184 if (spfaPlanner != nullptr)
1185 {
1186 const core::Pose start(srv.sceneProvider->scene().robot->getGlobalPose());
1187 // expensive spfa planning only once for all alternatives
1188 const auto planningResult = spfaPlanner->executePlanner(start);
1189
1190 for (const auto& target : targetAlternatives)
1191 {
1192 // cheap path reconstruction for every alternative
1193 const auto path = spfaPlanner->calculatePath(planningResult, target.target);
1194 if (path.has_value() and verifyGlobalPathPossible(path->trajectory))
1195 {
1196 globalPlan = path;
1197 srv.introspector->onGoal(target.target);
1198 goalReachedMonitor = std::nullopt;
1199 goalReachedMonitor = GoalReachedMonitor(
1200 target.target, srv.sceneProvider->scene(), config.goalReachedConfig);
1201
1202 ARMARX_INFO << "Valid path found for target " << target.target.matrix()
1203 << " with priority " << target.priority;
1204 break;
1205 }
1206 else
1207 {
1208 ARMARX_VERBOSE << "Target with priority " << target.priority
1209 << " not reachable";
1210 }
1211 }
1212 }
1213 else
1214 {
1215 const auto goal = targetAlternatives.front().target;
1217 << "Current global planner is not an SPFA planner, discarding alternatives!";
1218
1219 globalPlan = config.stack.globalPlanner->plan(goal);
1220 srv.introspector->onGoal(goal);
1221 goalReachedMonitor = std::nullopt;
1222 goalReachedMonitor =
1223 GoalReachedMonitor(goal, srv.sceneProvider->scene(), config.goalReachedConfig);
1224
1225 // we only use target alternatives with the spfa planner
1226 targetAlternatives.clear();
1227 }
1228
1229 if (srv.drawer != nullptr)
1230 {
1231 srv.drawer->callGenericDrawFunction(
1232 [&](viz::Client& client)
1233 { config.stack.globalPlanner->visualizeDebugInfo(client); });
1234 }
1235 else
1236 {
1237 ARMARX_WARNING << "No drawer available. Cannot visualize global planner debug info.";
1238 }
1239
1240
1242
1243 if (not globalPlan.has_value())
1244 {
1245 ARMARX_WARNING << "No global trajectory. Cannot move.";
1246 srv.publisher->globalPlanningFailed(core::GlobalPlanningFailedEvent{
1247 {.timestamp = armarx::Clock::Now()}, {"No global trajectory. Cannot move."}});
1248
1249 srv.introspector->failure();
1250 return false;
1251 }
1252
1253 return true;
1254 }
1255
1256 void
1257 Navigator::setupGlobalPlanSubvidision()
1258 {
1259 ARMARX_CHECK(globalPlan.has_value());
1260 ARMARX_CHECK(globalPlan->subdivision.empty());
1261 const auto& globalPlanPoses = globalPlan->plan.trajectory.poses();
1262
1264 if (not globalPlan->plan.trajectory.points().empty())
1265 {
1266 ARMARX_INFO << "Trajectory final pose " << globalPlanPoses.back().matrix();
1267
1268 goalReachedMonitor->updateGoal(globalPlanPoses.back());
1269 }
1270 srv.publisher->globalTrajectoryUpdated(core::GlobalTrajectoryUpdatedEvent{
1271 {.timestamp = armarx::Clock::Now()}, globalPlan->plan.trajectory});
1272
1273 reportPathGeometry(globalPlan->plan.trajectory);
1274
1275 ARMARX_INFO << "Global planning completed.";
1277 // only enable subdivision if local planner is actually enabled
1278 if ((not config.general.subdivision.enable) or (not hasLocalPlanner()) or
1279 globalPlan->plan.trajectory.points().empty())
1280 {
1281 // startGlobalPathSegment will hand trajectory over to introspection
1282 // after ramping has been performed; when subdivision is enabled
1283 // the introspection will receive the full subdivision (see below)
1284
1285 return;
1286 }
1287
1288 ARMARX_CHECK(hasLocalPlanner());
1289 ARMARX_INFO << "Starting subdividing global path.";
1290
1291 ARMARX_CHECK(srv.sceneProvider->scene().staticScene.has_value());
1293 srv.sceneProvider->scene().staticScene->distanceToObstaclesCostmap.has_value());
1294 const auto& costmap = srv.sceneProvider->scene().staticScene->distanceToObstaclesCostmap;
1295
1296 std::vector<bool> localPlanEligibility;
1297 localPlanEligibility.reserve(globalPlan->plan.trajectory.points().size());
1298 for (const auto& pose : globalPlanPoses)
1299 {
1300 Eigen::Vector2f pt = conv::to2D(pose.translation());
1301 localPlanEligibility.push_back(costmap->value(pt).value_or(0) >
1302 config.general.subdivision.localPlannerCostmapThreshold);
1303 }
1304
1305 // expand every sequence of false values in localPlanEligibility
1306 bool falseSequence = false;
1307 const int n = localPlanEligibility.size();
1308 const float expansion = config.general.subdivision.globalPlanExpansionDistance;
1309 if (expansion > 0)
1310 {
1311 for (int i = 0; i < n;)
1312 {
1313 if (falseSequence)
1314 {
1315 if (localPlanEligibility[i])
1316 {
1317 // false sequence ended, expand it appropriately to the back
1318 falseSequence = false;
1319
1320 // we expand until we have a distance >= expansion
1321 float totalDistance = 0;
1322 // false sequence ended -> there has to be an element before the current one
1324 for (; (totalDistance < expansion) and (i < n); i++)
1325 {
1326 localPlanEligibility[i] = false;
1327 totalDistance += (globalPlanPoses[i - 1].translation() -
1328 globalPlanPoses[i].translation())
1329 .norm();
1330 }
1331 continue; // we already incremented i
1332 }
1333
1334 // otherwise continue current false sequence
1335 }
1336 else
1337 {
1338 if (not localPlanEligibility[i])
1339 {
1340 // false sequence started, expand it appropriately to the front
1341 falseSequence = true;
1342
1343 // we expand until we have a distance >= expansion
1344 float totalDistance = 0;
1345 for (int j = i - 1; (totalDistance < expansion) and (j >= 0); j--)
1346 {
1347 localPlanEligibility[j] = false;
1348 totalDistance += (globalPlanPoses[j].translation() -
1349 globalPlanPoses[j + 1].translation())
1350 .norm();
1351 }
1352 }
1353
1354 // otherwise continue current true sequence
1355 }
1356
1357 i++;
1358 }
1359 }
1360
1362 // identify each consecutive true/false sequence and create subdivision
1363 // also check that each localPlanner segment is long enough
1364
1365 GlobalPathSubdivision::Subdivision segment;
1366 segment.s = 0;
1367 segment.useLocalPlanner = localPlanEligibility[0];
1368
1369 // returns true, iff the segment should be terminated and appended to the list of subdivisions
1370 const auto checkSegmentLength = [&]()
1371 {
1372 // only check the length if the current segment uses the local planner
1373 if (not segment.useLocalPlanner)
1374 {
1375 return true;
1376 }
1377
1378 const auto& subTrajectory =
1379 globalPlan->plan.trajectory.getSubTrajectory(segment.s, segment.t);
1380 const float segmentLength = subTrajectory.length();
1381 if (segmentLength >= config.general.subdivision.minSegmentDistance)
1382 {
1383 return true; // segment is long enough
1384 }
1385
1386 if (globalPlan->subdivision.empty() and static_cast<int>(segment.t) >= n)
1387 {
1388 // segment is too short but the only segment -> valid
1389 ARMARX_VERBOSE << "Segment " << globalPlan->subdivision.size() << ": [" << segment.s
1390 << ", " << segment.t
1391 << "); is the only segment. length=" << segmentLength;
1392 return true;
1393 }
1394
1395 ARMARX_VERBOSE << "Segment " << globalPlan->subdivision.size() << ": [" << segment.s
1396 << ", " << segment.t
1397 << "); was too short for local planner: " << segmentLength;
1398
1399 if (globalPlan->subdivision.empty())
1400 {
1401 // this is the first (but not the only) segment, join it to the next one
1402 segment.useLocalPlanner = false;
1404 << "First segment, convert to global planner segment and grow it further.";
1405 }
1406 else
1407 {
1408 if (static_cast<int>(segment.t) < n)
1409 {
1410 // there is a preceding segment, remove it and continue to grow it
1411 segment = globalPlan->subdivision.back();
1412 globalPlan->subdivision.pop_back();
1413 ARMARX_VERBOSE << "Removing preceding segment and grow it further.";
1414 }
1415 else
1416 {
1417 // this is the last segment, expand the preceding segment to include this one
1418 globalPlan->subdivision.back().t = segment.t;
1419 ARMARX_VERBOSE << "Last segment; extent previous segment to the end.";
1420 }
1421 }
1422 return false;
1423 };
1424
1425 for (int i = 0; i < n; i++)
1426 {
1427 // continue the current sequence until its end
1428 while (i < n and localPlanEligibility[i] == segment.useLocalPlanner)
1429 {
1430 i++;
1431 }
1432
1433 segment.t = i;
1434 if (checkSegmentLength())
1435 {
1436 globalPlan->subdivision.emplace_back(segment);
1437 ARMARX_VERBOSE << "Segment " << globalPlan->subdivision.size() << ": [" << segment.s
1438 << ", " << segment.t
1439 << "); localPlanner=" << segment.useLocalPlanner;
1440
1441 // start the next sequence
1442 segment.s = i;
1443 segment.useLocalPlanner = localPlanEligibility[i];
1444 }
1445 }
1446
1448
1449 // check subdivision is valid
1450 std::size_t lastSegmentEnd = 0;
1451 for (const auto& segment : globalPlan->subdivision)
1452 {
1453 ARMARX_CHECK_EQUAL(lastSegmentEnd, segment.s);
1454 const std::ptrdiff_t segmentLength = segment.t - segment.s;
1455 ARMARX_CHECK_POSITIVE(segmentLength);
1456 lastSegmentEnd = segment.t;
1457 }
1458 ARMARX_CHECK_EQUAL(lastSegmentEnd, globalPlan->plan.trajectory.points().size());
1459
1460 globalPlan->currentSegment = 0;
1461 srv.introspector->onGlobalPlannerSubdivision(globalPlan.value());
1462
1463 ARMARX_INFO << "Divided global path into " << globalPlan->subdivision.size() << " segments";
1464 }
1465
1466 void
1467 Navigator::applyParametrization(core::GlobalTrajectory& trajectory, const float startVelocity)
1468 {
1469 // Copied *before* apply(): in the non-subdivision branch `currentGlobalSegment()` hands
1470 // back `plan.trajectory` by reference, so apply() overwrites the planner's velocities in
1471 // place. Those velocities are the obstacle-aware limit, and the record below needs them.
1472 const core::GlobalTrajectory planned = trajectory;
1473
1474 core::TrajectoryParametrization applied = parametrizationMode;
1475
1476 // TOPP-RA fits a spline through the path and needs at least four distinct waypoints for
1477 // it. A pure rotation or a very short point-to-point motion has fewer, and would fail on
1478 // every invocation. Silent by design: this is an expected shape of request, not a fault.
1479 constexpr std::size_t minimumWaypointsForToppra = 4;
1480
1482 trajectory.points().size() < minimumWaypointsForToppra)
1483 {
1485 rampingFallback->apply(trajectory, startVelocity);
1486 }
1487 else
1488 {
1489 try
1490 {
1491 parametrization->apply(trajectory, startVelocity);
1492 }
1493 catch (const std::exception& e)
1494 {
1495 // An unparametrizable path must not abort the navigation request: ramping is
1496 // always executable, just slower.
1497 ARMARX_WARNING << "Trajectory parametrization failed (" << e.what()
1498 << "). Falling back to ramping for this request.";
1499
1501 trajectory = planned;
1502 rampingFallback->apply(trajectory, startVelocity);
1503 }
1504 }
1505
1506 // Which parametrization a request actually got is not otherwise visible in the log: the
1507 // requested one is announced when the navigator is built, but every degradation path
1508 // above is per-request. The two durations say what it bought -- both are the same
1509 // Riemann sum over the same path, so they are directly comparable.
1510 ARMARX_INFO << "Parametrized " << trajectory.points().size() << " waypoints with "
1511 << QUOTED(core::ToString(applied)) << ": "
1513 << " s (the planner's obstacle-aware velocities alone would take "
1515 << " s).";
1516
1517 if (config.general.parametrizationDump)
1518 {
1520 {.requested = config.stack.generalConfig.parametrization,
1521 .applied = applied,
1522 .planned = planned,
1523 .parametrized = trajectory,
1524 .toppra = dynamic_cast<const algorithms::Toppra*>(parametrization.get()),
1525 .driveParams = driveParams,
1526 .maxVelocity = config.stack.generalConfig.maxVel.linear,
1527 .maxAngularVelocity = config.stack.generalConfig.maxVel.angular},
1528 config.general.parametrizationDumpPath);
1529 }
1530 }
1531
1532 void
1533 Navigator::resumeAfterEmergencyStop()
1534 {
1535 if (not globalPlan.has_value() or isPaused() or isStopped())
1536 {
1537 return;
1538 }
1539
1540 if (globalPlan->useLocalPlanner(hasLocalPlanner()))
1541 {
1542 // The local planner re-plans from the robot every cycle, so its trajectory is
1543 // already anchored where the robot stands. Nothing to re-anchor.
1544 return;
1545 }
1546
1547 const core::GlobalTrajectory& trajectory = globalPlan->currentGlobalSegment();
1548 if (trajectory.points().empty())
1549 {
1550 return;
1551 }
1552
1553 const core::Position robotPosition =
1554 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose()).translation();
1555
1556 // The whole remainder, not a lookahead window: this replaces what the executor runs
1557 // until the next segment starts, and cutting it short would end the motion early.
1558 auto [remaining, endsAtGoal] = trajectory.getSubTrajectory(
1559 robotPosition, std::numeric_limits<float>::infinity());
1560
1561 if (remaining.points().size() < 2)
1562 {
1563 ARMARX_WARNING << "Cannot re-anchor the trajectory after the emergency stop: only "
1564 << remaining.points().size()
1565 << " waypoints remain. Resuming on the trajectory as planned.";
1566 return;
1567 }
1568
1569 ARMARX_IMPORTANT << "Re-anchoring the trajectory at the robot's current pose after the "
1570 "emergency stop: "
1571 << remaining.points().size() << " of " << trajectory.points().size()
1572 << " waypoints remain.";
1573
1574 applyParametrization(remaining, config.stack.generalConfig.boundaryVelocity);
1575
1576 updateExecutor(remaining);
1577 }
1578
1579 bool
1580 Navigator::startGlobalPathSegment(bool incrementSegment, bool rampFromCurrentVelocity)
1581 {
1582 if (not globalPlan->subdivision.empty())
1583 {
1584 // subdivision enabled
1585
1586 if (incrementSegment)
1587 {
1588 globalPlan->currentSegment++;
1589 }
1590
1591 if (globalPlan->currentSegment >= globalPlan->subdivision.size())
1592 {
1593 // global path already finished
1594 return true;
1595 }
1596
1597 ARMARX_INFO << "Starting segment " << globalPlan->currentSegment;
1598 const auto& currentSegment = globalPlan->subdivision[globalPlan->currentSegment];
1599
1600 globalPlan->currentSegmentTrajectory =
1601 globalPlan->plan.trajectory.getSubTrajectory(currentSegment.s, currentSegment.t);
1602
1603 applyParametrization(globalPlan->currentGlobalSegment(),
1604 rampFromCurrentVelocity
1605 ? srv.sceneProvider->scene().platformVelocity.linear.norm()
1606 : config.stack.generalConfig.boundaryVelocity);
1607
1608 goalReachedMonitor->updateGoal(
1609 globalPlan->currentGlobalSegment().points().back().waypoint.pose);
1610 }
1611 else
1612 {
1613 if (incrementSegment)
1614 {
1615 return true;
1616 }
1617
1618 applyParametrization(globalPlan->currentGlobalSegment(),
1619 rampFromCurrentVelocity
1620 ? srv.sceneProvider->scene().platformVelocity.linear.norm()
1621 : config.stack.generalConfig.boundaryVelocity);
1622
1623 goalReachedMonitor->updateGoal(
1624 globalPlan->currentGlobalSegment().points().back().waypoint.pose);
1625
1626 // only visualize global trajectory once ramping has been performed
1627 srv.introspector->onGlobalPlannerResult(globalPlan->plan);
1628 }
1629
1630 if (globalPlan->useLocalPlanner(hasLocalPlanner()))
1631 {
1632 const auto localPlannerResult = updateLocalPlanner();
1633 updateExecutor(localPlannerResult);
1634 updateIntrospector(localPlannerResult);
1635 }
1636 else
1637 {
1638 updateExecutor(globalPlan->currentGlobalSegment());
1639 }
1640
1641 ARMARX_INFO << "Start executing global plan segment (local planner="
1642 << globalPlan->useLocalPlanner(hasLocalPlanner())
1643 << "). Will now start all required threads.";
1645
1646 startStack();
1647 ARMARX_INFO << "Movement started.";
1648
1649 return false;
1650 }
1651
1652 void
1654 {
1655 }
1656
1657 void
1659 const std::optional<std::string>& providerName)
1660 {
1661 // Serialized against the periodic run() loop, which reads the state these mutate, and
1662 // against the other navigators sharing the scene provider.
1663 const std::scoped_lock lock{stateMtx, srv.sceneProvider->sceneMutex()};
1664
1665 // update static scene including locations
1666 updateScene(true);
1667
1668 const auto resolveLocation =
1669 [&](const std::string& location,
1670 const std::optional<std::string>& providerName) -> std::vector<core::Location>
1671 {
1672 const auto locations = srv.sceneProvider->scene().staticScene->locations;
1673
1674 ARMARX_VERBOSE << "Available locations";
1675 for (const auto& location : locations)
1676 {
1677 ARMARX_VERBOSE << QUOTED(location.name) << " from provider "
1678 << QUOTED(location.provider);
1679 }
1680
1681 const auto matchingLocs =
1682 core::util::findMatchingLocations(locations, location, providerName);
1683
1684 ARMARX_CHECK_NOT_EMPTY(matchingLocs)
1685 << "Unknown location " << QUOTED(location) << " and provider "
1686 << QUOTED(providerName.value_or("~unset~")) << ".";
1687 return matchingLocs;
1688 };
1689
1690 const auto split = simox::alg::split(location, ":");
1691 ARMARX_CHECK(0 < split.size() && split.size() <= 2)
1692 << "The given location does not match the format <location>(:<instance-id>)? '"
1693 << location << "'.";
1694
1695 std::string instanceID;
1696 if (split.size() == 2)
1697 {
1698 // An instance-id was given in the location
1699 instanceID = split.back();
1700 }
1701
1702 const auto matchingLocations = resolveLocation(split.front(), providerName);
1703
1704
1705 if (matchingLocations.empty())
1706 {
1707 ARMARX_WARNING << "Failed to resolve location " << QUOTED(location) << " from provider "
1708 << QUOTED(providerName.value_or("~unset~"));
1709 }
1710
1711 if (matchingLocations.size() > 1)
1712 {
1713 ARMARX_WARNING << "Found more than one matching location for " << QUOTED(location)
1714 << " from provider " << QUOTED(providerName.value_or("~unset~"));
1715 for (const auto& location : matchingLocations)
1716 {
1717 ARMARX_INFO << "- " << QUOTED(location.provider) << ": " << QUOTED(location.name);
1718 }
1719 }
1720
1721
1722 const auto goal = core::resolveLocation(srv.sceneProvider->scene().staticScene->objectMap,
1723 srv.sceneProvider->scene().staticScene->objectInfo,
1724 matchingLocations.front().framedPose,
1725 instanceID);
1726
1727 if (goal.pose.has_value())
1728 {
1729 moveToAbsolute({goal.pose.value()}, false);
1730 }
1731 else
1732 {
1733 ARMARX_ERROR << goal.errorMsg;
1734 }
1735 }
1736
1737 void
1738 Navigator::moveTowardsAbsolute(const core::Direction& direction)
1739 {
1740 }
1741
1742 void
1743 Navigator::run()
1744 {
1745 // Ensure only one thread can call this method at a time (This is not ensured by the
1746 // PeriodicTask), and that no API request mutates the plan or the goal monitor while
1747 // this iteration reads them.
1748 //
1749 // try_lock, not lock: this runs on the Ice timer thread, and blocking it while a
1750 // request holds the lock would deadlock any stop() that request issues -- stopping the
1751 // periodic task joins this very thread. Skipping an iteration costs nothing; the next
1752 // one follows one period later, by which time the request has released the lock.
1753 const std::unique_lock lock{stateMtx, std::try_to_lock};
1754 if (not lock.owns_lock())
1755 {
1757 << "Navigation state is busy with a request. Skipping this iteration.";
1758 return;
1759 }
1760
1761 // Same for the scene, which is shared with the other navigators of this component: this
1762 // iteration re-synchronizes it and then reads it (down to the safety guard, which
1763 // dereferences `Scene::dynamicScene`), so no one else may re-`emplace` it meanwhile.
1764 const std::unique_lock sceneLock{srv.sceneProvider->sceneMutex(), std::try_to_lock};
1765 if (not sceneLock.owns_lock())
1766 {
1768 << "Scene is busy with another navigator. Skipping this iteration.";
1769 return;
1770 }
1771
1772 if (not shouldRun)
1773 {
1775 << "Called Navigator::run() although shouldRun is false. Directly returning.";
1776 return;
1777 }
1778
1779 // TODO(fabian.reister): add debug observer logging
1780
1781 // scene update
1782 {
1783 ARMARX_DEBUG << "Updating scene";
1784
1785 // TODO: remove full update and only update costmap when required!!!
1786 // this will slow down the navigation loop significantly (>1s)
1788 [&]() { updateScene(true /* false (only dynamic and costmap)*/); });
1789
1791 << "Scene update: " << duration.toMilliSecondsDouble() << "ms.";
1792
1793 srv.debugObserverHelper->setDebugObserverDatafield("scene update [ms]",
1794 duration.toMilliSecondsDouble());
1795 }
1796
1797 // verify global path is still possible
1798 {
1799 // if we have target alternatives and the path is no longer valid, attempt to
1800 // navigate to a valid alternative
1801 if (globalPlan.has_value() and not targetAlternatives.empty())
1802 {
1803 checkGlobalPathAlternatives();
1804 }
1805 }
1806
1807 // eventually, draw
1808 if ((srv.introspector != nullptr) and (srv.sceneProvider != nullptr) and
1809 srv.sceneProvider->scene().robot)
1810 {
1811 ARMARX_DEBUG << "Drawing robot pose";
1812 srv.introspector->onRobotPose(
1813 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose()));
1814 }
1815
1816 // global planner update if goal has changed
1817 // niklas: symbol globalPlanningRequest is not used, so this code is dead
1818 /*{
1819 std::lock_guard g{globalPlanningRequestMtx};
1820
1821 if (globalPlanningRequest.has_value())
1822 {
1823 const auto& waypoints = globalPlanningRequest.value();
1824
1825 // recreate goal monitor
1826 {
1827 // first we check if we are already at the goal position
1828 goalReachedMonitor = std::nullopt;
1829 goalReachedMonitor = GoalReachedMonitor(
1830 waypoints.back(), srv.sceneProvider->scene(), config.goalReachedConfig);
1831
1832 if (goalReachedMonitor->goalReached(false))
1833 {
1834 ARMARX_INFO << "Already at goal position. Robot won't move.";
1835
1836 srv.publisher->goalReached(core::GoalReachedEvent{
1837 {armarx::Clock::Now()},
1838 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
1839
1840 return;
1841 }
1842 }
1843
1844 // global planning
1845 {
1846 // global planner
1847 ARMARX_INFO << "Update/Planning global trajectory";
1848 ARMARX_CHECK_NOT_NULL(config.stack.globalPlanner);
1849 // TODO plan on multiple waypoints, ignoring waypoints for now
1850 // idea: compute multiple global trajectories, one for each segment between waypoints.
1851
1852 srv.introspector->onGoal(waypoints.back());
1853 globalPlan = config.stack.globalPlanner->plan(waypoints.back());
1854
1855 ARMARX_TRACE;
1856
1857 if (not globalPlan.has_value())
1858 {
1859 ARMARX_WARNING << "No global trajectory. Cannot move.";
1860 srv.publisher->globalPlanningFailed(core::GlobalPlanningFailedEvent{
1861 {.timestamp = armarx::Clock::Now()}, {""}});
1862
1863 srv.introspector->failure();
1864 return;
1865 }
1866
1867 ARMARX_TRACE;
1868 srv.publisher->globalTrajectoryUpdated(globalPlan.value());
1869 srv.introspector->onGlobalPlannerResult(globalPlan.value());
1870
1871 if (not hasLocalPlanner())
1872 {
1873 updateExecutor(globalPlan.value());
1874 }
1875 }
1876 }
1877 }*/
1878
1879 // Before anything is activated again: the executor withdrew its controller request when
1880 // the emergency stop engaged, so this is the one chance to replace the trajectory the
1881 // robot would otherwise resume on.
1882 if (srv.executor != nullptr and srv.executor->consumeEmergencyStopRelease())
1883 {
1884 resumeAfterEmergencyStop();
1885 }
1886
1887 // local planner update
1888 {
1889 ARMARX_VERBOSE << "Local planner update";
1890
1892 [&]()
1893 {
1894 if (not globalPlan.has_value())
1895 {
1896 // No plan to follow -- the global planner failed, or the last one was
1897 // cleared while this task kept ticking. Nothing to plan locally.
1898 return;
1899 }
1900
1901 if (globalPlan->useLocalPlanner(hasLocalPlanner()))
1902 {
1903 const auto localPlannerResult = updateLocalPlanner();
1904 updateExecutor(localPlannerResult);
1905 updateIntrospector(localPlannerResult);
1906
1907 if (srv.executor != nullptr && localPlannerResult.has_value() &&
1908 not isPaused() && not isStopped())
1909 {
1910 srv.executor->ensureIsActive(
1911 ExecutorInterface::ControllerType::LocalTrajectory);
1912 }
1913 }
1914 else if (srv.executor != nullptr && not isPaused() && not isStopped())
1915 {
1916 srv.executor->ensureIsActive(
1917 ExecutorInterface::ControllerType::GlobalTrajectory);
1918 }
1919 });
1921 << "Local planner update: " << duration.toMilliSecondsDouble() << "ms.";
1922
1923 srv.debugObserverHelper->setDebugObserverDatafield("local planner update [ms]",
1924 duration.toMilliSecondsDouble());
1925 }
1926
1927 // update velocity limits of underlying platform controller
1928 // - apply safety guard, if present
1929 // - ensure the limits of the currently active navigator are used
1930 {
1931 // `updateSafetyGuard()` projects the robot onto the current global segment, so it
1932 // needs a plan. Without one, fall through to restoring the configured limits.
1933 if (hasSafetyGuard() and globalPlan.has_value())
1934 {
1935 ARMARX_VERBOSE << "Updating safety guard";
1936 const auto result = updateSafetyGuard();
1937 srv.debugObserverHelper->setDebugObserverDatafield("safety_guard.limit_linear",
1938 result.twistLimits.linear);
1939 srv.debugObserverHelper->setDebugObserverDatafield("safety_guard.limit_angular",
1940 result.twistLimits.angular);
1941
1942 if (srv.executor != nullptr)
1943 {
1944 srv.executor->updateVelocityLimits(result.twistLimits);
1945 }
1946
1947 // if the safety guard keeps the robot blocked for too long, re-run the
1948 // global planner
1949 checkRobotBlocked(result);
1950 }
1951 else
1952 // apply the maximum velocity limit from the general config, in case a different navigator applied other limits in between
1953 {
1954 if (srv.executor != nullptr)
1955 {
1956 srv.executor->updateVelocityLimits(config.stack.generalConfig.maxVel);
1957 }
1958 }
1959 }
1960
1961 // request to scale the calculated velocity by the velocity factor
1962 {
1964 srv.debugObserverHelper->setDebugObserverDatafield("velocity_factor",
1965 velocityFactor.load());
1966
1967 // scaling is done relatively; for typical absolute values: see SPFA, 500mm/s 0.4 rad/s
1968 if (srv.executor != nullptr)
1969 {
1970 srv.executor->updateVelocityFactor(velocityFactor);
1971 }
1972 }
1973
1974 // monitor update
1975 {
1977 ARMARX_DEBUG << "Monitor update";
1978
1979 const Duration duration =
1980 armarx::core::time::StopWatch::measure([&]() { updateMonitor(); });
1981
1983 << "Monitor update: " << duration.toMilliSecondsDouble() << "ms.";
1984
1985 srv.debugObserverHelper->setDebugObserverDatafield("monitor update [ms]",
1986 duration.toMilliSecondsDouble());
1987 }
1988 }
1989
1991 Navigator::updateSafetyGuard()
1992 {
1993 ARMARX_CHECK(hasSafetyGuard());
1994
1995 const core::Pose global_T_robot(srv.sceneProvider->scene().robot->getGlobalPose());
1996 const auto proj = globalPlan->currentGlobalSegment().getProjection(
1997 global_T_robot.translation(), core::VelocityInterpolation::LinearInterpolation);
1998 const Eigen::Vector3f global_V_movement = proj.wayPointAfter.waypoint.pose.translation() -
1999 proj.projection.waypoint.pose.translation();
2000
2001 return config.stack.safetyGuard->computeSafetyLimits(global_V_movement.head<2>());
2002 }
2003
2004 void
2005 Navigator::checkRobotBlocked(const safety_guard::SafetyGuardResult& result)
2006 {
2007 const auto& cfg = config.general.blockedReplanning;
2008
2009 if (not cfg.enabled)
2010 {
2011 return;
2012 }
2013
2014 // only relevant while actively navigating towards a goal
2015 if (isPaused() or isStopped() or not globalPlan.has_value() or
2016 not goalReachedMonitor.has_value())
2017 {
2018 blockedSince = std::nullopt;
2019 return;
2020 }
2021
2022 // the safety guard limits the platform velocity close to zero when the robot
2023 // cannot make progress (e.g. an obstacle blocks the planned path)
2024 const bool blocked = (result.twistLimits.linear < cfg.linearLimit) or
2025 (result.twistLimits.angular < cfg.angularLimit);
2026
2027 if (not blocked)
2028 {
2029 // robot is able to move again -> reset the timer
2030 blockedSince = std::nullopt;
2031 return;
2032 }
2033
2034 const armarx::DateTime now = armarx::Clock::Now();
2035 if (not blockedSince.has_value())
2036 {
2037 // start measuring how long the robot stays blocked
2038 blockedSince = now;
2039 return;
2040 }
2041
2042 const armarx::Duration blockedFor = now - blockedSince.value();
2043 srv.debugObserverHelper->setDebugObserverDatafield("safety_guard.blocked_for [ms]",
2044 blockedFor.toMilliSecondsDouble());
2045
2046 if (blockedFor < armarx::Duration::SecondsDouble(cfg.blockedTimeSeconds))
2047 {
2048 // not blocked long enough yet -> keep waiting (obstacle might clear)
2049 return;
2050 }
2051
2052 ARMARX_IMPORTANT << "Robot blocked for " << blockedFor.toMilliSecondsDouble()
2053 << "ms (safety guard limits near zero). Re-running the global planner.";
2054
2055 // reset so the freshly planned trajectory gets a chance before we re-check
2056 blockedSince = std::nullopt;
2057
2058 // re-run the global planner towards the current goal
2059 updateAbsolute({goalReachedMonitor->goal()});
2060 }
2061
2062 bool
2063 Navigator::hasSafetyGuard() const
2064 {
2065 return config.stack.safetyGuard != nullptr;
2066 }
2067
2068 bool
2069 Navigator::hasLocalPlanner() const noexcept
2070 {
2071 return config.stack.localPlanner != nullptr;
2072 }
2073
2074 void
2075 Navigator::updateScene(const bool fullUpdate)
2076 {
2077 ARMARX_CHECK_NOT_NULL(srv.sceneProvider);
2078 srv.sceneProvider->synchronize(armarx::Clock::Now(), fullUpdate);
2079 }
2080
2081 void
2082 Navigator::checkGlobalPathAlternatives()
2083 {
2084 ARMARX_CHECK_NOT_EMPTY(targetAlternatives);
2085
2086 const auto replanAlternatives = [&]()
2087 {
2088 lastAllAlternativesImpossible = std::nullopt;
2089
2090 // save current global plan to restore when no path is currently possible
2091 // i.e. if a person is standing in a doorway and no paths are possible anymore,
2092 // we restore the previous path and move along it until the safety guard stops the robot
2093 const auto previousGlobalPlan = globalPlan;
2094
2095 if (setupTargetAlternatives())
2096 {
2097 // an alternative path was found
2098 ARMARX_INFO << "An alternative path was found.";
2099 setupGlobalPlanSubvidision();
2100 }
2101 else
2102 {
2103 ARMARX_INFO << "No possible alternatives, restoring previous global path.";
2104 globalPlan = std::move(previousGlobalPlan);
2105 lastAllAlternativesImpossible = armarx::Clock::Now();
2106 }
2107
2108 // either start the new global path or continue on the old one
2109 startGlobalPathSegment(false, true);
2110 };
2111
2112 if (not verifyGlobalPathPossible(globalPlan->currentGlobalSegment()))
2113 {
2114 // global path is not possible -> either wait for timeout or check alternatives
2115
2116 if (lastAllAlternativesImpossible.has_value() and
2117 (armarx::Clock::Now() - lastAllAlternativesImpossible.value() <
2119 config.general.targetAlternativesFilterTimeSeconds)))
2120 {
2122 << "Current globalPlan invalid, waiting for timeout before replanning.";
2123 }
2124 else
2125 {
2126 ARMARX_INFO << "Global path no longer possible, trying other alternatives!";
2127
2128 replanAlternatives();
2129 }
2130 }
2131 else if (lastAllAlternativesImpossible.has_value())
2132 {
2133 // globalPlan was previously invalid but is valid now
2134 // -> recheck all possible alternatives and their priority
2136 << "Previously invalid global plan valid again -> replanning all alternatives";
2137
2138 replanAlternatives();
2139 }
2140 }
2141
2142 bool
2143 Navigator::verifyGlobalPathPossible(const core::GlobalTrajectory& plan)
2144 {
2145 const auto& costmap = srv.sceneProvider->scene().staticScene->distanceToObstaclesCostmap;
2146 ARMARX_CHECK(costmap.has_value());
2147
2148 for (const auto& pt : plan.points())
2149 {
2150 const auto& pose = pt.waypoint.pose;
2151 const Eigen::Vector2f position = conv::to2D(pose.translation());
2152 const auto vertex = costmap->toVertex(position);
2153 if (not costmap->isValid(vertex.index) or costmap->isInCollision(vertex.position))
2154 {
2155 return false;
2156 }
2157 }
2158
2159 return true;
2160 }
2161
2162 std::optional<local_planning::LocalPlannerResult>
2163 Navigator::updateLocalPlanner()
2164 {
2165 // We need to make sure the local planner is not simultaniously called from different threads
2166 const std::scoped_lock<std::mutex> lock{updateLocalPlannerMtx};
2167
2168 ARMARX_CHECK(hasLocalPlanner());
2169
2170 ARMARX_VERBOSE << "Updating local plan";
2171
2172 try
2173 {
2174 const auto& globalTrajectory = globalPlan->currentGlobalSegment();
2175 ARMARX_VERBOSE << globalTrajectory.points().size() << " points in global plan";
2176 localPlan = config.stack.localPlanner->plan(globalTrajectory);
2177 ARMARX_VERBOSE << "Local planning finished";
2178 if (localPlan.has_value())
2179 {
2180 srv.publisher->localTrajectoryUpdated(core::LocalTrajectoryUpdatedEvent{
2181 {.timestamp = armarx::Clock::Now()}, localPlan->trajectory});
2182 return localPlan;
2183 }
2184 // do not return here, so that localPlanningFailed event is published
2185 //return localPlan;
2186 }
2187 catch (...)
2188 {
2189 ARMARX_WARNING << "Failure in local planner: " << GetHandledExceptionString();
2190 srv.publisher->localPlanningFailed(core::LocalPlanningFailedEvent{
2191 {.timestamp = armarx::Clock::Now()}, {GetHandledExceptionString()}});
2192
2193 return std::nullopt;
2194 }
2195
2196 srv.publisher->localPlanningFailed(core::LocalPlanningFailedEvent{
2197 {.timestamp = armarx::Clock::Now()}, {"Unknown reason"}});
2198
2199 return std::nullopt;
2200 }
2201
2202 void
2203 Navigator::updateExecutor(const std::optional<local_planning::LocalPlannerResult>& localPlan)
2204 {
2205 if (srv.executor == nullptr)
2206 {
2207 return;
2208 }
2209
2210
2211 if (isPaused() or isStopped())
2212 {
2213 // [[unlikely]]
2214 ARMARX_VERBOSE << deactivateSpam(1) << "stopped or paused";
2215 return;
2216 }
2217
2218 if (not localPlan.has_value())
2219 {
2220 ARMARX_INFO << "Local plan is invalid!";
2221 ARMARX_CHECK_NOT_NULL(srv.executor);
2222 srv.executor->execute(core::LocalTrajectory{}, false);
2223 srv.executor->stop();
2224 return;
2225 }
2226
2227
2228 // const core::Rotation robot_R_world(srv.sceneProvider->scene().robot->getGlobalOrientation().inverse());
2229
2230 // ARMARX_VERBOSE
2231 // << deactivateSpam(100) << "Robot orientation "
2232 // << simox::math::mat3f_to_rpy(srv.sceneProvider->scene().robot->getGlobalOrientation()).z();
2233
2234 // core::Twist robotFrameVelocity;
2235
2236 // robotFrameVelocity.linear = robot_R_world * twist.linear;
2237 // // FIXME fix angular velocity
2238 // robotFrameVelocity.angular = twist.angular;
2239
2240 // ARMARX_VERBOSE << deactivateSpam(1) << "velocity in robot frame "
2241 // << robotFrameVelocity.linear;
2242
2243 ARMARX_CHECK_NOT_NULL(srv.executor);
2244 srv.executor->execute(localPlan->trajectory, true);
2245 }
2246
2247 void
2248 Navigator::updateExecutor(const core::GlobalTrajectory& globalTrajectory)
2249 {
2250 if (srv.executor == nullptr)
2251 {
2252 return;
2253 }
2254
2255 ARMARX_IMPORTANT << "Requested to execute global plan with "
2256 << globalTrajectory.points().size() << " points.";
2257
2258
2259 // if (isPaused() or isStopped())
2260 // {
2261 // // [[unlikely]]
2262 // ARMARX_VERBOSE << deactivateSpam(1) << "stopped or paused";
2263 // return;
2264 // }
2265
2266 if (globalTrajectory.points().empty())
2267 {
2268 ARMARX_INFO << "Global plan is invalid!";
2269 ARMARX_CHECK_NOT_NULL(srv.executor);
2270 srv.executor->stop();
2271 return;
2272 }
2273
2274 ARMARX_IMPORTANT << "Executing global plan with " << globalTrajectory.points().size()
2275 << " points.";
2276 ARMARX_CHECK_NOT_NULL(srv.executor);
2277 srv.executor->execute(globalTrajectory, false);
2278 }
2279
2280 void
2281 Navigator::updateIntrospector(
2282 const std::optional<local_planning::LocalPlannerResult>& localPlan)
2283 {
2284 ARMARX_CHECK_NOT_NULL(srv.introspector);
2285
2286 srv.introspector->onLocalPlannerResult(localPlan);
2287 }
2288
2289 void
2290 Navigator::updateMonitor()
2291 {
2293 //ARMARX_CHECK(goalReachedMonitor.has_value());
2294 if (not goalReachedMonitor.has_value())
2295 {
2296 // Note: I guess this can happen because of a race condition between the moveTo methods and the run method.
2297 // But I am not sure if a common mutex would work correctly.
2298 ARMARX_WARNING << "GoalReachedMonitor is not initialized.";
2299 return;
2300 }
2301
2302 const auto status = goalReachedMonitor->status();
2303 armarx::DebugObserverHelper* debugObserver = srv.debugObserverHelper;
2304
2305 if (debugObserver != nullptr)
2306 {
2307 debugObserver->setDebugObserverDatafield("goalPositionError", status.posError);
2308 debugObserver->setDebugObserverDatafield("goalOrientationError", status.oriError);
2309 debugObserver->setDebugObserverDatafield("goalPositionThreshold",
2310 goalReachedMonitor->getConfig().posTh);
2311 debugObserver->setDebugObserverDatafield("goalOrientationThreshold",
2312 goalReachedMonitor->getConfig().oriTh);
2313 }
2314
2315 if (not isPaused() and goalReachedMonitor->goalReached())
2316 {
2317 // [[unlikely]]
2318 ARMARX_INFO << "Current global path segment finished!";
2319
2320 // we finished the current segment -> start the new one
2321 if (startGlobalPathSegment(true, true))
2322 {
2323 // this segment was the last one, we are finished
2324 ARMARX_INFO << "Goal " << goalReachedMonitor->goal().translation().head<2>()
2325 << " reached!";
2326
2327 // Before `stop()`, and not relying on it: `stop()` deactivates the trajectory
2328 // controller only if it reports active, so the controller that holds this
2329 // request's execution record is not guaranteed to be told the request is over.
2330 // Asking here is what makes the record independent of that.
2331 if (srv.executor != nullptr)
2332 {
2333 srv.executor->dumpDiagnostics();
2334 }
2335
2336 stop();
2337
2338 srv.publisher->goalReached(core::GoalReachedEvent{
2340 {core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())}});
2341
2342 srv.introspector->success();
2343 }
2344 }
2345 }
2346
2347 void
2348 Navigator::stopAllThreads()
2349 {
2350 const std::scoped_lock<std::mutex> lock{runningTaskMtx};
2351
2352 if (runningTask)
2353 {
2354 runningTask->stop();
2355 }
2356
2357 shouldRun = false;
2358 }
2359
2360 // bool
2361 // Navigator::isStackResultValid() const noexcept
2362 // {
2363
2364 // // global planner
2365 // if (config.stack.globalPlanner != nullptr)
2366 // {
2367 // if (not globalPlan.has_value())
2368 // {
2369 // ARMARX_VERBOSE << deactivateSpam(1) << "Global trajectory not yet set.";
2370 // return false;
2371 // }
2372 // }
2373
2374 // // local planner
2375 // if (config.stack.localPlanner != nullptr)
2376 // {
2377 // if (not localPlan.has_value())
2378 // {
2379 // ARMARX_VERBOSE << deactivateSpam(1) << "Local trajectory not yet set.";
2380 // return false;
2381 // }
2382 // }
2383
2384 // // [[likely]]
2385 // return true;
2386 // }
2387
2388 // const core::Trajectory& Navigator::currentTrajectory() const
2389 // {
2390 // ARMARX_CHECK(isStackResultValid());
2391
2392 // if(localPlan.has_value())
2393 // {
2394 // return localPlan->trajectory;
2395 // }
2396
2397 // return globalPlan->trajectory;
2398 // }
2399
2400 void
2401 Navigator::setVelocityFactor(const float velocityFactor)
2402 {
2403 ARMARX_CHECK_POSITIVE(velocityFactor)
2404 << "Scaling factor for velocity may not be negative, but is " << velocityFactor;
2405 ARMARX_CHECK_LESS_EQUAL(velocityFactor, 1)
2406 << "Scaling factor for velocity may not be > 1, but is " << velocityFactor;
2407
2408 this->velocityFactor = velocityFactor;
2409 }
2410
2411 void
2413 {
2414 ARMARX_INFO << "Paused.";
2415
2416 executorEnabled.store(false);
2417
2418 if (srv.executor != nullptr)
2419 {
2420 ARMARX_INFO << "Stopping executor.";
2421 srv.executor->stop();
2422 }
2423 }
2424
2425 void
2427 {
2428 ARMARX_INFO << "Resume.";
2429
2430 executorEnabled.store(true);
2431
2432 if (srv.executor != nullptr)
2433 {
2434 if (hasLocalPlanner())
2435 {
2437 }
2438 else
2439 {
2441 }
2442 }
2443 }
2444
2445 void
2447 {
2448 ARMARX_INFO << "Stopping.";
2449
2450 pause();
2451
2452 // stop all threads, including this one.
2453 // Deliberately not under `stateMtx`: this joins the Ice timer thread, and holding the
2454 // lock across the join would deadlock against a run() iteration waiting for it.
2455 stopAllThreads();
2456
2457 const std::scoped_lock lock{stateMtx};
2458 goalReachedMonitor.reset();
2459 }
2460
2461 bool
2462 Navigator::isPaused() const noexcept
2463 {
2464 return not executorEnabled.load();
2465 }
2466
2467 bool
2468 Navigator::isStopped() const noexcept
2469 {
2470 return (not shouldRun) or (not runningTask) or (not runningTask->isRunning());
2471 }
2472
2474 config{std::move(other.config)},
2475 srv{std::move(other.srv)},
2476 driveParams{std::move(other.driveParams)},
2477 parametrizationMode{other.parametrizationMode},
2478 parametrization{std::move(other.parametrization)},
2479 rampingFallback{std::move(other.rampingFallback)},
2480 executorEnabled{other.executorEnabled.load()}
2481 {
2482 }
2483
2484
2485} // namespace armarx::navigation::server
#define ARMARX_CHECK_NOT_EMPTY(c)
SpamFilterDataPtr deactivateSpam(SpamFilterDataPtr const &spamFilter, float deactivationDurationSec, const std::string &identifier, bool deactivate)
Definition Logging.cpp:75
std::size_t degenerate
How many segments were excluded as too short to carry a direction.
#define QUOTED(x)
static DateTime Now()
Current time on the virtual clock.
Definition Clock.cpp:93
void setDebugObserverDatafield(const std::string &channelName, const std::string &datafieldName, const TimedVariantPtr &value) const
static Duration SecondsDouble(double seconds)
Constructs a duration in seconds.
Definition Duration.cpp:78
The periodic task executes one thread method repeatedly using the time period specified in the constr...
double toMilliSecondsDouble() const
Returns the amount of milliseconds.
Definition Duration.cpp:66
static Duration measure(std::function< void(void)> subjectToMeasure, ClockType clockType=ClockType::Virtual)
Measures the duration needed to execute the given lambda and returns it.
Definition StopWatch.cpp:31
Time-optimal reparametrization under per-motor torque and command-ramp limits.
Definition Toppra.h:52
static bool available()
Whether the python side was found at configure time. False means apply() throws.
Definition Toppra.cpp:226
static std::string unavailableReason()
Why it is unavailable, as recorded at configure time. Empty when available.
Definition Toppra.cpp:236
void moveToAlternatives(const std::vector< core::TargetAlternative > &targets, core::NavigationFrame navigationFrame) override
void moveTowards(const core::Direction &direction, core::NavigationFrame navigationFrame) override
void update(const std::vector< core::Pose > &waypoints, core::NavigationFrame navigationFrame) override
Navigator(const Config &config, const InjectedServices &services)
void setVelocityFactor(float velocityFactor) override
void moveToLocation(const std::string &location, const std::optional< std::string > &providerName) override
bool isPaused() const noexcept override
void moveTo(const std::vector< core::Pose > &waypoints, core::NavigationFrame navigationFrame) override
bool isStopped() const noexcept override
Brief description of class targets.
Definition targets.h:39
#define ARMARX_CHECK_GREATER(lhs, rhs)
This macro evaluates whether lhs is greater (>) than rhs and if it turns out to be false it will thro...
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_POSITIVE(number)
This macro evaluates whether number is positive (> 0) and if it turns out to be false it will throw a...
#define ARMARX_CHECK_LESS_EQUAL(lhs, rhs)
This macro evaluates whether lhs is less or equal (<=) rhs and if it turns out to be false it will th...
#define ARMARX_CHECK_GREATER_EQUAL(lhs, rhs)
This macro evaluates whether lhs is greater or equal (>=) rhs and if it turns out to be false it will...
#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_CHECK_EQUAL(lhs, rhs)
This macro evaluates whether lhs is equal (==) rhs and if it turns out to be false it will throw an E...
#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_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:182
#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
armarx::core::time::Duration Duration
Toppra::DriveParams LoadDriveParams(const std::filesystem::path &configFile)
Read Toppra::DriveParams from a PlatformDynamics<Robot>.json.
Definition Toppra.cpp:127
std::shared_ptr< TrajectoryParametrization > TrajectoryParametrizationPtr
std::filesystem::path DriveParamsPath(const std::string &robot)
Resolve config/platform/PlatformDynamics<robot>.json inside the armarx_navigation package.
Definition Toppra.cpp:119
This file is part of ArmarX.
std::vector< Eigen::Vector2f > to2D(const std::vector< Eigen::Vector3f > &v)
Definition eigen.cpp:29
std::vector< core::Location > findMatchingLocations(const std::vector< core::Location > &locations, const std::string &locationName, const std::optional< std::string > &provider)
Definition location.cpp:46
Graph::ConstVertex getVertexByName(const std::string &vertexName, const Graph &graph)
Definition Graph.cpp:209
Eigen::Vector3f Direction
Definition basic_types.h:39
TrajectoryParametrization
How the velocities along a planned path are assigned.
@ Ramping
Ramp down at the start, the goal and every corner. The stack's historical behaviour.
@ Toppra
Time-optimal under per-motor torque and command-ramp limits.
std::string ToString(const TrajectoryParametrization parametrization)
The lowercase name used in configs, logs and the analysis application's results.
std::vector< GraphPath > findPathsTo(Graph::ConstVertex vertex, const Graph &graph)
Definition Graph.cpp:182
void resolveLocation(Graph::Vertex &vertex, const aron::data::DictPtr &locationData)
Definition Graph.cpp:267
Eigen::Isometry3f Pose
Definition basic_types.h:31
Eigen::Vector3f Position
Definition basic_types.h:36
const core::Graph & getSubgraph(const std::string &vertexName, const Graphs &graphs)
Definition Graph.cpp:234
This file is part of ArmarX.
This file is part of ArmarX.
Definition Visu.h:48
This file is part of ArmarX.
Definition constants.cpp:4
This file is part of ArmarX.
std::vector< semrel::ShapeID > GraphPath
GraphPath findShortestPath(core::Graph graph, const core::Graph::ConstVertex &startVertex, const core::Graph::ConstVertex &goalVertex)
void writeParametrizationDump(const ParametrizationRecord &record, const std::string &path)
Write record as JSON, for offline rendering with plot-navigation-request.
std::vector< std::string > split(const std::string &source, const std::string &splitBy, bool trimElements=false, bool removeEmptyElements=false)
std::string GetHandledExceptionString()
Vertex target(const detail::edge_base< Directed, Vertex > &e, const PCG &)
constexpr auto n() noexcept
TrajectoryParametrization parametrization
How the planned velocity profile is (re-)assigned before execution.
Event describing that the global trajectory was updated.
Definition events.h:114
Event describing that the targeted goal was successfully reached.
Definition events.h:53
Event describing that the local trajectory was updated.
Definition events.h:122
GlobalPathSubdivision(const global_planning::GlobalPlannerResult &globalPlan)
Definition Navigator.cpp:73
global_planning::GlobalPlannerResult plan
Definition Navigator.h:79
const core::GlobalTrajectory & currentGlobalSegment() const
Definition Navigator.cpp:95
#define ARMARX_TRACE
Definition trace.h:75