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 ARMARX_INFO << "Received moveTo() request.";
369
370 std::vector<core::Pose> globalWaypoints;
371 switch (navigationFrame)
372 {
374 globalWaypoints = waypoints;
375 break;
377 globalWaypoints.reserve(waypoints.size());
378
379 ARMARX_CHECK(srv.sceneProvider->synchronize(Clock::Now(), true));
380
381 const core::Pose global_T_robot(srv.sceneProvider->scene().robot->getGlobalPose());
382 ARMARX_VERBOSE << "Initial robot pose: " << global_T_robot.matrix();
383
384 std::transform(std::begin(waypoints),
385 std::end(waypoints),
386 std::back_inserter(globalWaypoints),
387 [&](const core::Pose& p) { return global_T_robot * p; });
388 break;
389 }
390
392 moveToAbsolute(globalWaypoints);
393 }
394
395 void
396 Navigator::moveToAlternatives(const std::vector<core::TargetAlternative>& targets,
397 core::NavigationFrame navigationFrame)
398 {
399 ARMARX_INFO << "Received moveToAlternatives() request.";
400
401 std::vector<core::TargetAlternative> globalTargets;
402 switch (navigationFrame)
403 {
405 globalTargets = targets;
406 break;
408 globalTargets.reserve(targets.size());
409
410 ARMARX_CHECK(srv.sceneProvider->synchronize(Clock::Now(), true));
411
412 const core::Pose global_T_robot(srv.sceneProvider->scene().robot->getGlobalPose());
413 ARMARX_VERBOSE << "Initial robot pose: " << global_T_robot.matrix();
414
415 std::transform(std::begin(targets),
416 std::end(targets),
417 std::back_inserter(globalTargets),
418 [&](const core::TargetAlternative& p) {
420 .target = global_T_robot * p.target, .priority = p.priority};
421 });
422 break;
423 }
424
426 moveToAbsoluteAlternatives(globalTargets);
427 }
428
429 void
430 Navigator::update(const std::vector<core::Pose>& waypoints,
431 core::NavigationFrame navigationFrame)
432 {
433 ARMARX_INFO << "Received update() request.";
434
435 std::vector<core::Pose> globalWaypoints;
436 switch (navigationFrame)
437 {
439 globalWaypoints = waypoints;
440 break;
442 globalWaypoints.reserve(waypoints.size());
443 std::transform(
444 std::begin(waypoints),
445 std::end(waypoints),
446 std::back_inserter(globalWaypoints),
447 [&](const core::Pose& p)
448 { return core::Pose(srv.sceneProvider->scene().robot->getGlobalPose()) * p; });
449 break;
450 }
451
453 updateAbsolute(globalWaypoints);
454 }
455
456 using GraphPath = std::vector<semrel::ShapeID>;
457
460 const core::Graph::ConstVertex& startVertex,
461 const core::Graph::ConstVertex& goalVertex)
462 {
463 ARMARX_VERBOSE << "Graph consists of " << graph.numVertices() << " vertices and "
464 << graph.numEdges() << " edges.";
465
466 std::vector<core::Graph::VertexDescriptor> predecessors(graph.numVertices());
467 std::vector<int> d(num_vertices(graph));
468
469 // FIXME ARMARX_CHECK(graphBuilder.startVertex.has_value());
470
471 auto weightMap = boost::get(&core::EdgeAttribs::m_value, graph);
472
473 core::Graph::VertexDescriptor start = startVertex.descriptor();
474
475 auto predecessorMap = boost::make_iterator_property_map(
476 predecessors.begin(), boost::get(boost::vertex_index, graph));
477
478 auto params = boost::predecessor_map(predecessorMap)
479 .distance_map(boost::make_iterator_property_map(
480 d.begin(), boost::get(boost::vertex_index, graph)))
481 .weight_map(weightMap);
482
486
487 ARMARX_VERBOSE << "Edge weights:";
488 for (const auto& edge : graph.edges())
489 {
490 ARMARX_VERBOSE << edge.sourceObjectID() << " -> " << edge.targetObjectID() << ": "
491 << edge.attrib().m_value;
492 }
493
494 std::vector<core::Graph::EdgeDescriptor> edgesToBeRemoved;
495 for (const auto edge : graph.edges())
496 {
497 if (edge.attrib().m_value == std::numeric_limits<float>::max())
498 {
499 edgesToBeRemoved.push_back(edge.descriptor());
500 }
501 }
502
503 for (const auto edge : edgesToBeRemoved)
504 {
505 boost::remove_edge(edge, graph);
506 }
507 ARMARX_VERBOSE << "Edge weights after removal of inf edges:";
508
509 for (const auto& edge : graph.edges())
510 {
511 ARMARX_VERBOSE << edge.sourceObjectID() << " -> " << edge.targetObjectID() << ": "
512 << edge.attrib().m_value;
513 ARMARX_VERBOSE << "Edge: " << edge << "cost: " << edge.attrib().cost()
514 << ", type: " << static_cast<int>(edge.attrib().strategy)
515 << " , has traj " << edge.attrib().trajectory.has_value();
516 }
517
518 ARMARX_VERBOSE << "Searching shortest path from vertex with id `"
519 << graph.vertex(start).objectID() << "` to vertex `"
520 << graph.vertex(goalVertex.descriptor()).objectID() << "`";
521
522 boost::dijkstra_shortest_paths(graph, start, params);
523
524 // find shortest path
526
527 // WARNING: shortest path will be from goal to start first
528 GraphPath shortestPath;
529
530 core::Graph::VertexDescriptor currentVertex = goalVertex.descriptor();
531 while (currentVertex != startVertex.descriptor())
532 {
533 shortestPath.push_back(graph.vertex(currentVertex).objectID());
534
536
537 auto parent = predecessorMap[currentVertex];
538 ARMARX_VERBOSE << "Parent id: " << parent;
539
541
542 // find edge between parent and currentVertex
543 auto outEdges = graph.vertex(parent).outEdges();
544 ARMARX_VERBOSE << "Parent has " << std::distance(outEdges.begin(), outEdges.end())
545 << " out edges";
546
547 ARMARX_CHECK_GREATER(std::distance(outEdges.begin(), outEdges.end()), 0)
548 << "Cannot reach another vertex from vertex `"
549 << graph.vertex(parent).objectID(); // not empty
550
551 auto edgeIt = std::find_if(outEdges.begin(),
552 outEdges.end(),
553 [&currentVertex](const auto& edge) -> bool
554 { return edge.target().descriptor() == currentVertex; });
555
556 ARMARX_CHECK(edgeIt != outEdges.end());
557 // shortestPath.edges.push_back(edgeIt->descriptor());
558
559 currentVertex = parent;
560 }
561
562 shortestPath.push_back(startVertex.objectID());
563
564 // ARMARX_CHECK_EQUAL(shortestPath.vertices.size() - 1, shortestPath.edges.size());
565
567 // reverse the range => now it is from start to goal again
568 shortestPath = shortestPath | ranges::views::reverse | ranges::to_vector;
569 // shortestPath.edges = shortestPath.edges | ranges::views::reverse | ranges::to_vector;
570
572 return shortestPath;
573 }
574
576 Navigator::convertToTrajectory(const GraphPath& shortestPath, const core::Graph& graph) const
577 {
579
580 ARMARX_CHECK_GREATER_EQUAL(shortestPath.size(), 2)
581 << "At least start and goal vertices must be available";
582
583 // ARMARX_CHECK_EQUAL(shortestPath.size() - 1, shortestPath.edges.size());
584
585 std::vector<core::GlobalTrajectoryPoint> trajectoryPoints;
586
587 // TODO add the start
588 // trajectoryPoints.push_back(core::TrajectoryPoint{
589 // .waypoint = {.pose = graph.vertex(shortestPath.front()).attrib().getPose()},
590 // .velocity = 0.F});
591
592 ARMARX_VERBOSE << "Shortest path with " << shortestPath.size() << " vertices";
594
595 for (size_t i = 0; i < shortestPath.size() - 1; i++)
596 {
597 // ARMARX_CHECK(graph.hasEdge(shortestPath.edges.at(i).m_source,
598 // shortestPath.edges.at(i).m_target));
599
600 // TODO add method edge(shapeId, shapeId)
601 const core::Graph::ConstEdge edge =
602 graph.edge(graph.vertex(shortestPath.at(i)), graph.vertex(shortestPath.at(i + 1)));
603
605 ARMARX_VERBOSE << "Index " << i;
606 ARMARX_VERBOSE << static_cast<int>(edge.attrib().strategy);
607 ARMARX_VERBOSE << edge;
608 switch (edge.attrib().strategy)
609 {
611 {
612 ARMARX_INFO << "Free navigation on edge";
613
615 ARMARX_CHECK(edge.attrib().trajectory.has_value());
616
618 ARMARX_INFO << "Length: " << edge.attrib().trajectory->length();
619
620
622 ARMARX_INFO << "Free navigation with "
623 << edge.attrib().trajectory->points().size() << " waypoints";
624
625 // we have a trajectory
626 // FIXME trajectory points can be invalid => more points than expected. Why?
628 const std::vector<core::GlobalTrajectoryPoint> edgeTrajectoryPoints =
629 edge.attrib().trajectory->points();
630
631 // if trajectory is being initialized, include the start, otherwise skip it
632 const int offset = trajectoryPoints.empty() ? 0 : 1;
633
634 if (edgeTrajectoryPoints.size() > 2) // not only start and goal position
635 {
637 // append `edge trajectory` to `trajectory` (without start and goal points)
638 trajectoryPoints.insert(trajectoryPoints.end(),
639 edgeTrajectoryPoints.begin() + offset,
640 edgeTrajectoryPoints.end());
641 }
642
643
645 // clang-format off
646 // trajectoryPoints.push_back(
647 // core::TrajectoryPoint
648 // {
649 // .waypoint =
650 // {
651 // .pose = graph.vertex(shortestPath.at(i+1)).attrib().getPose()
652 // },
653 // .velocity = std::numeric_limits<float>::max()
654 // });
655 // clang-format on
656
657 break;
658 }
659
661 {
662 ARMARX_INFO << "Point2Point navigation on edge";
663
664 // FIXME variable
665 const float point2pointVelocity = 400;
666
667 const core::GlobalTrajectoryPoint currentTrajPt = {
668 .waypoint = {.pose = resolveGraphVertex(graph.vertex(shortestPath.at(i)))},
669 .velocity = point2pointVelocity};
670
671 const core::GlobalTrajectoryPoint nextTrajPt{
672 .waypoint = {.pose =
673 resolveGraphVertex(graph.vertex(shortestPath.at(i + 1)))},
674 .velocity = 0};
675
676 // resample event straight lines
677 // ARMARX_CHECK_NOT_EMPTY(trajectoryPoints);
678
679 // const core::Trajectory segmentTraj({trajectoryPoints.back(), nextTrajPt});
680 // ARMARX_INFO << "Segment trajectory length: " << segmentTraj.length();
681
682 // const auto resampledTrajectory = segmentTraj.resample(500); // FIXME param
683
684 // ARMARX_INFO << "Resampled trajectory contains "
685 // << resampledTrajectory.points().size() << " points";
686
687 // this is the same pose as the goal of the previous segment but with a different velocity
688 // FIXME MUST set velocity here.
689 // trajectoryPoints.push_back(
690 // core::TrajectoryPoint{.waypoint = trajectoryPoints.back().waypoint,
691 // .velocity = std::numeric_limits<float>::max()});
692
694 // trajectoryPoints.insert(trajectoryPoints.end(),
695 // resampledTrajectory.points().begin(),
696 // resampledTrajectory.points().end());
697 trajectoryPoints.push_back(currentTrajPt);
698 trajectoryPoints.push_back(nextTrajPt);
699
700 break;
701 }
702 default:
703 {
704 ARMARX_ERROR << "Boom.";
705 }
706 }
707 }
708
709 ARMARX_INFO << "Trajectory consists of " << trajectoryPoints.size() << " points";
710
711 for (const auto& pt : trajectoryPoints)
712 {
713 ARMARX_INFO << pt.waypoint.pose.translation();
714 }
715
716 return {trajectoryPoints};
717 }
718
719 GraphBuilder
720 Navigator::convertToGraph(const std::vector<client::WaypointTarget>& targets) const
721 {
722 //
723 GraphBuilder graphBuilder;
724 graphBuilder.initialize(core::Pose(srv.sceneProvider->scene().robot->getGlobalPose()));
725
726 // std::optional<Graph*> activeSubgraph;
727 for (const auto& target : targets)
728 {
729 ARMARX_INFO << "Adding target " << QUOTED(target) << " to graph";
730 // if the last location was on a subgraph, that we are about to leave
731 // const bool leavingSubgraph = [&]() -> bool
732 // {
733 // if (not activeSubgraph.has_value())
734 // {
735 // return false;
736 // }
737
738 // // if a user specifies a pose, then this pose is not meant to be part of a graph
739 // // -> can be reached directly
740 // if (target.pose.has_value())
741 // {
742 // return true;
743 // }
744
745 // if (not target.locationId->empty())
746 // {
747 // const auto& subgraph = core::getSubgraph(target.locationId.value(),srv.sceneProvider->scene().graph->subgraphs);
748 // return subgraph.name() != activeSubgraph;
749 // }
750
751 // throw LocalException("this line should not be reachable");
752 // }();
753
754 // if(leavingSubgraph)
755 // {
756 // const auto activeNodes = graph.activeVertices();
757 // ARMARX_CHECK(activeNodes.size() == 1);
758 // graph.connect(activeSubgraph->getRoutesFrom(activeNodes.front()));
759 // }
760
761
762 // if a user specifies a pose, then this pose is not meant to be part of a graph
763 // -> can be reached directly
764 if (target.pose.has_value())
765 {
766 graphBuilder.connect(target.pose.value(), target.strategy);
767 continue;
768 }
769
770 // if a user specified a vertex of a subgraph (aka location), then this vertex
771 // might not be reachable directly. It might be needed to navigate on the graph
772 // instead. Thus, we collect all routes to reach the node.
773 if (not target.locationId->empty())
774 {
775 const auto& subgraph = core::getSubgraph(
776 target.locationId.value(), srv.sceneProvider->scene().graph->subgraphs);
777
778 const auto vertex = core::getVertexByName(target.locationId.value(), subgraph);
779
780 ARMARX_INFO << "Vertex " << QUOTED(target.locationId.value()) << " resolved to "
781 << vertex.attrib().getPose();
782
783 const std::vector<core::GraphPath> routes = core::findPathsTo(vertex, subgraph);
784 // const auto routes = subgraph->getRoutesTo(target.locationId);
785
786 ARMARX_INFO << "Found " << routes.size() << " routes to location `"
787 << target.locationId.value();
788
789 ARMARX_CHECK(not routes.empty()) << "The location `" << target.locationId.value()
790 << "` is not a reachable vertex on the graph!";
791
792 // we now add all routes to the graph that take us to the desired location
793 graphBuilder.connect(routes,
794 target.strategy); // TODO: all routes to the same target
795
796 continue;
797 }
798
799 ARMARX_ERROR << "Either `location_id` or `pose` has to be provided!";
800 }
801
802 const auto goalVertex = graphBuilder.getGraph().vertex(graphBuilder.goalVertex());
803 ARMARX_INFO << "Goal vertex is " << QUOTED(goalVertex.attrib().getLocationName());
804
805 return graphBuilder;
806 }
807
809 Navigator::resolveGraphVertex(const core::Graph::ConstVertex& vertex) const
810 {
811 const auto goal = core::resolveLocation(srv.sceneProvider->scene().staticScene->objectMap,
812 srv.sceneProvider->scene().staticScene->objectInfo,
813 vertex.attrib().getPose());
814 ARMARX_CHECK(goal.pose.has_value())
815 << "The location of vertex " << vertex.attrib().getLocationName()
816 << " couldn't be resolved (" << goal.errorMsg << ")";
817 return goal.pose.value();
818 }
819
820 void
821 Navigator::moveTo(const std::vector<client::WaypointTarget>& targets,
822 core::NavigationFrame navigationFrame)
823 {
824 // arlt: Is this function deprecated?
826 << "only absolute movement implemented atm.";
827
829
830 validate(targets);
831
832 ARMARX_CHECK_NOT_EMPTY(targets) << "At least the goal has to be provided!";
833 ARMARX_INFO << "Navigating to " << targets.back();
834
835 // update static scene including locations
836 updateScene(true);
837
838 auto graphBuilder = convertToGraph(targets);
839
841
842 core::Graph graph = graphBuilder.getGraph();
843 auto startVertex = graphBuilder.startVertex;
844 auto goalVertex = graphBuilder.getGraph().vertex(graphBuilder.goalVertex());
845
846 ARMARX_INFO << "Goal pose according to graph is " << graphBuilder.goalPose().matrix();
847
848 ARMARX_CHECK(startVertex.has_value());
849
851
852 goalReachedMonitor = std::nullopt;
853 goalReachedMonitor = GoalReachedMonitor(
854 graphBuilder.goalPose(), srv.sceneProvider->scene(), config.goalReachedConfig);
855
856 if (goalReachedMonitor->goalReached(false))
857 {
858 ARMARX_IMPORTANT << "Already at goal position "
859 << goalReachedMonitor->goal().translation().head<2>()
860 << ". Robot won't move.";
861
862 srv.publisher->goalReached(core::GoalReachedEvent{
864 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
865
866 return;
867 }
868
870 setGraphEdgeCosts(graph);
871
873
874 srv.introspector->onGlobalGraph(graph);
875
877 const auto shortestPath = findShortestPath(graph, startVertex.value(), goalVertex);
878
879 // print
881
882 std::vector<core::Pose> vertexPoses;
883 vertexPoses.emplace_back(srv.sceneProvider->scene().robot->getGlobalPose());
884
885 ARMARX_INFO << "Navigating along the following nodes:";
886 for (const semrel::ShapeID& vertex : shortestPath)
887 {
888 ARMARX_INFO << " - " << graph.vertex(vertex).attrib().getLocationName();
889 vertexPoses.push_back(resolveGraphVertex(graph.vertex(vertex)));
890 }
891
892 srv.introspector->onGlobalShortestPath(vertexPoses);
893
894
895 // convert graph / vertex chain to trajectory
897 core::GlobalTrajectory globalPlanTrajectory = convertToTrajectory(shortestPath, graph);
898
899 // globalPlanTrajectory.setMaxVelocity(1000); // FIXME
900
901 // move ...
902
903 // this is our `global plan`
905
906 globalPlan = global_planning::GlobalPlannerResult{.trajectory = globalPlanTrajectory,
907 .helperTrajectory = std::nullopt};
908
909 // the following is similar to moveToAbsolute
910 // TODO(fabian.reister): remove code duplication
911
912 srv.executor->execute(globalPlan->currentGlobalSegment());
913
915 srv.publisher->globalTrajectoryUpdated(core::GlobalTrajectoryUpdatedEvent{
916 {.timestamp = armarx::Clock::Now()}, globalPlan->currentGlobalSegment()});
917
919 srv.introspector->onGlobalPlannerResult(globalPlan->plan);
920
921 ARMARX_INFO << "Global planning completed. Will now start all required threads";
923
924 startStack();
925 }
926
927 void
928 Navigator::startStack()
929 {
930
932
933 ARMARX_INFO << "Starting stack.";
934
935 {
936 // ensure running task is never started and stopped simultaneously
937 const std::scoped_lock<std::mutex> lock{runningTaskMtx};
938
939 shouldRun = true;
940
941 // FIXME instead of PeriodicTask, use RunningTask.
942 if (not runningTask)
943 {
944 runningTask =
946 &Navigator::run,
947 config.general.tasks.replanningUpdatePeriod,
948 false,
949 "PeriodicTask");
950 runningTask->start();
951 }
952 else if (not runningTask->isRunning())
953 {
954 runningTask->start();
955 }
956 }
957
958 // FIXME create separate function for this.
959 if (globalPlan->useLocalPlanner(hasLocalPlanner()))
960 {
961 if (srv.executor != nullptr)
962 {
963 srv.executor->start(ExecutorInterface::ControllerType::LocalTrajectory);
964 }
965 }
966 else
967 {
968 if (srv.executor != nullptr)
969 {
970 srv.executor->start(ExecutorInterface::ControllerType::GlobalTrajectory);
971 }
972 }
973
974 // Could be required if pauseMovement() has been called in the past.
975 resume();
976 srv.publisher->movementStarted(core::MovementStartedEvent{
977 {.timestamp = armarx::Clock::Now()},
978 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
979 }
980
981 void
982 Navigator::moveToAbsolute(const std::vector<core::Pose>& waypoints, bool sceneUpdate)
983 {
985
986 // if this navigator is in use, stop the movement ...
987 pause();
988 // ... and all threads
989 stopAllThreads();
990
991 // FIXME remove
992 //std::this_thread::sleep_for(std::chrono::seconds(1));
993
995 ARMARX_CHECK_NOT_EMPTY(waypoints);
996
997 if (sceneUpdate)
998 {
999 updateScene(true);
1000 }
1001
1002 ARMARX_INFO << "Request to move from " << srv.sceneProvider->scene().robot->getGlobalPose()
1003 << " to " << waypoints.back().matrix();
1004
1005 // first we check if we are already at the goal position
1006 goalReachedMonitor = std::nullopt;
1007 goalReachedMonitor = GoalReachedMonitor(
1008 waypoints.back(), srv.sceneProvider->scene(), config.goalReachedConfig);
1009
1010 if (goalReachedMonitor->goalReached(false))
1011 {
1012 ARMARX_INFO << "Already at goal position. Robot won't move.";
1013 ARMARX_CHECK_NOT_NULL(srv.publisher);
1014
1015 ARMARX_VERBOSE << "Finalizing";
1016 srv.publisher->goalReached(core::GoalReachedEvent{
1018 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
1019 ARMARX_VERBOSE << "Finalized";
1020
1021 return;
1022 }
1023
1024 // global planner
1025 ARMARX_INFO << "Planning global trajectory";
1026 ARMARX_CHECK_NOT_NULL(config.stack.globalPlanner);
1027 // TODO plan on multiple waypoints, ignoring waypoints for now
1028 // idea: compute multiple global trajectories, one for each segment between waypoints.
1029
1030 srv.introspector->onGoal(waypoints.back());
1031 globalPlan = config.stack.globalPlanner->plan(waypoints.back());
1032
1033 if (srv.drawer != nullptr)
1034 {
1035 srv.drawer->callGenericDrawFunction(
1036 [&](viz::Client& client)
1037 { config.stack.globalPlanner->visualizeDebugInfo(client); });
1038 }
1039 else
1040 {
1041 ARMARX_WARNING << "No drawer available. Cannot visualize global planner debug info.";
1042 }
1043
1044
1046
1047 if (not globalPlan.has_value())
1048 {
1049 ARMARX_WARNING << "No global trajectory. Cannot move.";
1050 srv.publisher->globalPlanningFailed(core::GlobalPlanningFailedEvent{
1051 {.timestamp = armarx::Clock::Now()}, {"No global trajectory. Cannot move."}});
1052
1053 srv.introspector->failure();
1054 return;
1055 }
1056
1057 // create subdivision and start first segment
1058 setupGlobalPlanSubvidision();
1059
1060 startGlobalPathSegment(false, false);
1061 }
1062
1063 void
1064 Navigator::updateAbsolute(const std::vector<core::Pose>& waypoints)
1065 {
1067 ARMARX_CHECK_NOT_EMPTY(waypoints);
1068
1069 // Assume nothing in static obstacles changed -> also don't update costmap
1070 updateScene(false);
1071
1072 // global planner
1073 ARMARX_INFO << "Planning global trajectory";
1074 ARMARX_CHECK_NOT_NULL(config.stack.globalPlanner);
1075 // TODO plan on multiple waypoints, ignoring waypoints for now
1076 // idea: compute multiple global trajectories, one for each segment between waypoints.
1077
1078 srv.introspector->onGoal(waypoints.back());
1079 globalPlan = config.stack.globalPlanner->plan(waypoints.back());
1080
1082
1083 if (not globalPlan.has_value())
1084 {
1085 ARMARX_WARNING << "No global trajectory. Cannot move.";
1086 srv.publisher->globalPlanningFailed(core::GlobalPlanningFailedEvent{
1087 {.timestamp = armarx::Clock::Now()}, {"No global trajectory. Cannot move."}});
1088
1089 srv.introspector->failure();
1090 return;
1091 }
1092
1093 // create subdivision and start first segment
1094 setupGlobalPlanSubvidision();
1095
1096 startGlobalPathSegment(false, true);
1097 }
1098
1099 void
1100 Navigator::moveToAbsoluteAlternatives(const std::vector<core::TargetAlternative>& targets)
1101 {
1103
1104 // if this navigator is in use, stop the movement ...
1105 pause();
1106 // ... and all threads
1107 stopAllThreads();
1108
1110 ARMARX_CHECK_NOT_EMPTY(targets);
1111
1112 updateScene(true);
1113
1114 ARMARX_INFO << "Request to move from " << srv.sceneProvider->scene().robot->getGlobalPose()
1115 << " to " << targets.size() << " alternatives";
1116
1117 lastAllAlternativesImpossible = std::nullopt;
1118
1119 // first, check whether we are already at any target
1120 for (const auto& t : targets)
1121 {
1122 GoalReachedMonitor monitor(
1123 t.target, srv.sceneProvider->scene(), config.goalReachedConfig);
1124
1125 if (monitor.goalReached(false))
1126 {
1127 ARMARX_INFO << "Already at a possible goal position. Robot won't move.";
1128
1129 srv.publisher->goalReached(core::GoalReachedEvent{
1131 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
1132
1133 return;
1134 }
1135 }
1136
1137 // sort alternatives by their priority (in descending order)
1138 targetAlternatives = targets;
1139 ranges::sort(targetAlternatives, std::greater{}, &core::TargetAlternative::priority);
1140
1141 if (setupTargetAlternatives())
1142 {
1143 // create subdivision and start first segment
1144 setupGlobalPlanSubvidision();
1145
1146 startGlobalPathSegment(false, false);
1147 }
1148 }
1149
1150 bool
1151 Navigator::setupTargetAlternatives()
1152 {
1153 ARMARX_CHECK_NOT_EMPTY(targetAlternatives);
1154
1155 globalPlan = std::nullopt;
1156
1157 // Spfa planner plans into the "whole room", i.e. by executing the algorithm with
1158 // a single start position, a path to any goal can be constructed.
1159 // Thus, we only use alternatives with the spfa planner to avoid repeatedly replanning the
1160 // global path.
1161 const auto spfaPlanner =
1162 std::dynamic_pointer_cast<global_planning::SPFA>(config.stack.globalPlanner);
1163 if (spfaPlanner != nullptr)
1164 {
1165 const core::Pose start(srv.sceneProvider->scene().robot->getGlobalPose());
1166 // expensive spfa planning only once for all alternatives
1167 const auto planningResult = spfaPlanner->executePlanner(start);
1168
1169 for (const auto& target : targetAlternatives)
1170 {
1171 // cheap path reconstruction for every alternative
1172 const auto path = spfaPlanner->calculatePath(planningResult, target.target);
1173 if (path.has_value() and verifyGlobalPathPossible(path->trajectory))
1174 {
1175 globalPlan = path;
1176 srv.introspector->onGoal(target.target);
1177 goalReachedMonitor = std::nullopt;
1178 goalReachedMonitor = GoalReachedMonitor(
1179 target.target, srv.sceneProvider->scene(), config.goalReachedConfig);
1180
1181 ARMARX_INFO << "Valid path found for target " << target.target.matrix()
1182 << " with priority " << target.priority;
1183 break;
1184 }
1185 else
1186 {
1187 ARMARX_VERBOSE << "Target with priority " << target.priority
1188 << " not reachable";
1189 }
1190 }
1191 }
1192 else
1193 {
1194 const auto goal = targetAlternatives.front().target;
1196 << "Current global planner is not an SPFA planner, discarding alternatives!";
1197
1198 globalPlan = config.stack.globalPlanner->plan(goal);
1199 srv.introspector->onGoal(goal);
1200 goalReachedMonitor = std::nullopt;
1201 goalReachedMonitor =
1202 GoalReachedMonitor(goal, srv.sceneProvider->scene(), config.goalReachedConfig);
1203
1204 // we only use target alternatives with the spfa planner
1205 targetAlternatives.clear();
1206 }
1207
1208 if (srv.drawer != nullptr)
1209 {
1210 srv.drawer->callGenericDrawFunction(
1211 [&](viz::Client& client)
1212 { config.stack.globalPlanner->visualizeDebugInfo(client); });
1213 }
1214 else
1215 {
1216 ARMARX_WARNING << "No drawer available. Cannot visualize global planner debug info.";
1217 }
1218
1219
1221
1222 if (not globalPlan.has_value())
1223 {
1224 ARMARX_WARNING << "No global trajectory. Cannot move.";
1225 srv.publisher->globalPlanningFailed(core::GlobalPlanningFailedEvent{
1226 {.timestamp = armarx::Clock::Now()}, {"No global trajectory. Cannot move."}});
1227
1228 srv.introspector->failure();
1229 return false;
1230 }
1231
1232 return true;
1233 }
1234
1235 void
1236 Navigator::setupGlobalPlanSubvidision()
1237 {
1238 ARMARX_CHECK(globalPlan.has_value());
1239 ARMARX_CHECK(globalPlan->subdivision.empty());
1240 const auto& globalPlanPoses = globalPlan->plan.trajectory.poses();
1241
1243 if (not globalPlan->plan.trajectory.points().empty())
1244 {
1245 ARMARX_INFO << "Trajectory final pose " << globalPlanPoses.back().matrix();
1246
1247 goalReachedMonitor->updateGoal(globalPlanPoses.back());
1248 }
1249 srv.publisher->globalTrajectoryUpdated(core::GlobalTrajectoryUpdatedEvent{
1250 {.timestamp = armarx::Clock::Now()}, globalPlan->plan.trajectory});
1251
1252 reportPathGeometry(globalPlan->plan.trajectory);
1253
1254 ARMARX_INFO << "Global planning completed.";
1256 // only enable subdivision if local planner is actually enabled
1257 if ((not config.general.subdivision.enable) or (not hasLocalPlanner()) or
1258 globalPlan->plan.trajectory.points().empty())
1259 {
1260 // startGlobalPathSegment will hand trajectory over to introspection
1261 // after ramping has been performed; when subdivision is enabled
1262 // the introspection will receive the full subdivision (see below)
1263
1264 return;
1265 }
1266
1267 ARMARX_CHECK(hasLocalPlanner());
1268 ARMARX_INFO << "Starting subdividing global path.";
1269
1270 ARMARX_CHECK(srv.sceneProvider->scene().staticScene.has_value());
1272 srv.sceneProvider->scene().staticScene->distanceToObstaclesCostmap.has_value());
1273 const auto& costmap = srv.sceneProvider->scene().staticScene->distanceToObstaclesCostmap;
1274
1275 std::vector<bool> localPlanEligibility;
1276 localPlanEligibility.reserve(globalPlan->plan.trajectory.points().size());
1277 for (const auto& pose : globalPlanPoses)
1278 {
1279 Eigen::Vector2f pt = conv::to2D(pose.translation());
1280 localPlanEligibility.push_back(costmap->value(pt).value_or(0) >
1281 config.general.subdivision.localPlannerCostmapThreshold);
1282 }
1283
1284 // expand every sequence of false values in localPlanEligibility
1285 bool falseSequence = false;
1286 const int n = localPlanEligibility.size();
1287 const float expansion = config.general.subdivision.globalPlanExpansionDistance;
1288 if (expansion > 0)
1289 {
1290 for (int i = 0; i < n;)
1291 {
1292 if (falseSequence)
1293 {
1294 if (localPlanEligibility[i])
1295 {
1296 // false sequence ended, expand it appropriately to the back
1297 falseSequence = false;
1298
1299 // we expand until we have a distance >= expansion
1300 float totalDistance = 0;
1301 // false sequence ended -> there has to be an element before the current one
1303 for (; (totalDistance < expansion) and (i < n); i++)
1304 {
1305 localPlanEligibility[i] = false;
1306 totalDistance += (globalPlanPoses[i - 1].translation() -
1307 globalPlanPoses[i].translation())
1308 .norm();
1309 }
1310 continue; // we already incremented i
1311 }
1312
1313 // otherwise continue current false sequence
1314 }
1315 else
1316 {
1317 if (not localPlanEligibility[i])
1318 {
1319 // false sequence started, expand it appropriately to the front
1320 falseSequence = true;
1321
1322 // we expand until we have a distance >= expansion
1323 float totalDistance = 0;
1324 for (int j = i - 1; (totalDistance < expansion) and (j >= 0); j--)
1325 {
1326 localPlanEligibility[j] = false;
1327 totalDistance += (globalPlanPoses[j].translation() -
1328 globalPlanPoses[j + 1].translation())
1329 .norm();
1330 }
1331 }
1332
1333 // otherwise continue current true sequence
1334 }
1335
1336 i++;
1337 }
1338 }
1339
1341 // identify each consecutive true/false sequence and create subdivision
1342 // also check that each localPlanner segment is long enough
1343
1344 GlobalPathSubdivision::Subdivision segment;
1345 segment.s = 0;
1346 segment.useLocalPlanner = localPlanEligibility[0];
1347
1348 // returns true, iff the segment should be terminated and appended to the list of subdivisions
1349 const auto checkSegmentLength = [&]()
1350 {
1351 // only check the length if the current segment uses the local planner
1352 if (not segment.useLocalPlanner)
1353 {
1354 return true;
1355 }
1356
1357 const auto& subTrajectory =
1358 globalPlan->plan.trajectory.getSubTrajectory(segment.s, segment.t);
1359 const float segmentLength = subTrajectory.length();
1360 if (segmentLength >= config.general.subdivision.minSegmentDistance)
1361 {
1362 return true; // segment is long enough
1363 }
1364
1365 if (globalPlan->subdivision.empty() and static_cast<int>(segment.t) >= n)
1366 {
1367 // segment is too short but the only segment -> valid
1368 ARMARX_VERBOSE << "Segment " << globalPlan->subdivision.size() << ": [" << segment.s
1369 << ", " << segment.t
1370 << "); is the only segment. length=" << segmentLength;
1371 return true;
1372 }
1373
1374 ARMARX_VERBOSE << "Segment " << globalPlan->subdivision.size() << ": [" << segment.s
1375 << ", " << segment.t
1376 << "); was too short for local planner: " << segmentLength;
1377
1378 if (globalPlan->subdivision.empty())
1379 {
1380 // this is the first (but not the only) segment, join it to the next one
1381 segment.useLocalPlanner = false;
1383 << "First segment, convert to global planner segment and grow it further.";
1384 }
1385 else
1386 {
1387 if (static_cast<int>(segment.t) < n)
1388 {
1389 // there is a preceding segment, remove it and continue to grow it
1390 segment = globalPlan->subdivision.back();
1391 globalPlan->subdivision.pop_back();
1392 ARMARX_VERBOSE << "Removing preceding segment and grow it further.";
1393 }
1394 else
1395 {
1396 // this is the last segment, expand the preceding segment to include this one
1397 globalPlan->subdivision.back().t = segment.t;
1398 ARMARX_VERBOSE << "Last segment; extent previous segment to the end.";
1399 }
1400 }
1401 return false;
1402 };
1403
1404 for (int i = 0; i < n; i++)
1405 {
1406 // continue the current sequence until its end
1407 while (i < n and localPlanEligibility[i] == segment.useLocalPlanner)
1408 {
1409 i++;
1410 }
1411
1412 segment.t = i;
1413 if (checkSegmentLength())
1414 {
1415 globalPlan->subdivision.emplace_back(segment);
1416 ARMARX_VERBOSE << "Segment " << globalPlan->subdivision.size() << ": [" << segment.s
1417 << ", " << segment.t
1418 << "); localPlanner=" << segment.useLocalPlanner;
1419
1420 // start the next sequence
1421 segment.s = i;
1422 segment.useLocalPlanner = localPlanEligibility[i];
1423 }
1424 }
1425
1427
1428 // check subdivision is valid
1429 std::size_t lastSegmentEnd = 0;
1430 for (const auto& segment : globalPlan->subdivision)
1431 {
1432 ARMARX_CHECK_EQUAL(lastSegmentEnd, segment.s);
1433 const std::ptrdiff_t segmentLength = segment.t - segment.s;
1434 ARMARX_CHECK_POSITIVE(segmentLength);
1435 lastSegmentEnd = segment.t;
1436 }
1437 ARMARX_CHECK_EQUAL(lastSegmentEnd, globalPlan->plan.trajectory.points().size());
1438
1439 globalPlan->currentSegment = 0;
1440 srv.introspector->onGlobalPlannerSubdivision(globalPlan.value());
1441
1442 ARMARX_INFO << "Divided global path into " << globalPlan->subdivision.size() << " segments";
1443 }
1444
1445 void
1446 Navigator::applyParametrization(core::GlobalTrajectory& trajectory, const float startVelocity)
1447 {
1448 // Copied *before* apply(): in the non-subdivision branch `currentGlobalSegment()` hands
1449 // back `plan.trajectory` by reference, so apply() overwrites the planner's velocities in
1450 // place. Those velocities are the obstacle-aware limit, and the record below needs them.
1451 const core::GlobalTrajectory planned = trajectory;
1452
1453 core::TrajectoryParametrization applied = parametrizationMode;
1454
1455 // TOPP-RA fits a spline through the path and needs at least four distinct waypoints for
1456 // it. A pure rotation or a very short point-to-point motion has fewer, and would fail on
1457 // every invocation. Silent by design: this is an expected shape of request, not a fault.
1458 constexpr std::size_t minimumWaypointsForToppra = 4;
1459
1461 trajectory.points().size() < minimumWaypointsForToppra)
1462 {
1464 rampingFallback->apply(trajectory, startVelocity);
1465 }
1466 else
1467 {
1468 try
1469 {
1470 parametrization->apply(trajectory, startVelocity);
1471 }
1472 catch (const std::exception& e)
1473 {
1474 // An unparametrizable path must not abort the navigation request: ramping is
1475 // always executable, just slower.
1476 ARMARX_WARNING << "Trajectory parametrization failed (" << e.what()
1477 << "). Falling back to ramping for this request.";
1478
1480 trajectory = planned;
1481 rampingFallback->apply(trajectory, startVelocity);
1482 }
1483 }
1484
1485 // Which parametrization a request actually got is not otherwise visible in the log: the
1486 // requested one is announced when the navigator is built, but every degradation path
1487 // above is per-request. The two durations say what it bought -- both are the same
1488 // Riemann sum over the same path, so they are directly comparable.
1489 ARMARX_INFO << "Parametrized " << trajectory.points().size() << " waypoints with "
1490 << QUOTED(core::ToString(applied)) << ": "
1492 << " s (the planner's obstacle-aware velocities alone would take "
1494 << " s).";
1495
1496 if (config.general.parametrizationDump)
1497 {
1499 {.requested = config.stack.generalConfig.parametrization,
1500 .applied = applied,
1501 .planned = planned,
1502 .parametrized = trajectory,
1503 .toppra = dynamic_cast<const algorithms::Toppra*>(parametrization.get()),
1504 .driveParams = driveParams,
1505 .maxVelocity = config.stack.generalConfig.maxVel.linear,
1506 .maxAngularVelocity = config.stack.generalConfig.maxVel.angular},
1507 config.general.parametrizationDumpPath);
1508 }
1509 }
1510
1511 void
1512 Navigator::resumeAfterEmergencyStop()
1513 {
1514 if (not globalPlan.has_value() or isPaused() or isStopped())
1515 {
1516 return;
1517 }
1518
1519 if (globalPlan->useLocalPlanner(hasLocalPlanner()))
1520 {
1521 // The local planner re-plans from the robot every cycle, so its trajectory is
1522 // already anchored where the robot stands. Nothing to re-anchor.
1523 return;
1524 }
1525
1526 const core::GlobalTrajectory& trajectory = globalPlan->currentGlobalSegment();
1527 if (trajectory.points().empty())
1528 {
1529 return;
1530 }
1531
1532 const core::Position robotPosition =
1533 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose()).translation();
1534
1535 // The whole remainder, not a lookahead window: this replaces what the executor runs
1536 // until the next segment starts, and cutting it short would end the motion early.
1537 auto [remaining, endsAtGoal] = trajectory.getSubTrajectory(
1538 robotPosition, std::numeric_limits<float>::infinity());
1539
1540 if (remaining.points().size() < 2)
1541 {
1542 ARMARX_WARNING << "Cannot re-anchor the trajectory after the emergency stop: only "
1543 << remaining.points().size()
1544 << " waypoints remain. Resuming on the trajectory as planned.";
1545 return;
1546 }
1547
1548 ARMARX_IMPORTANT << "Re-anchoring the trajectory at the robot's current pose after the "
1549 "emergency stop: "
1550 << remaining.points().size() << " of " << trajectory.points().size()
1551 << " waypoints remain.";
1552
1553 applyParametrization(remaining, config.stack.generalConfig.boundaryVelocity);
1554
1555 updateExecutor(remaining);
1556 }
1557
1558 bool
1559 Navigator::startGlobalPathSegment(bool incrementSegment, bool rampFromCurrentVelocity)
1560 {
1561 if (not globalPlan->subdivision.empty())
1562 {
1563 // subdivision enabled
1564
1565 if (incrementSegment)
1566 {
1567 globalPlan->currentSegment++;
1568 }
1569
1570 if (globalPlan->currentSegment >= globalPlan->subdivision.size())
1571 {
1572 // global path already finished
1573 return true;
1574 }
1575
1576 ARMARX_INFO << "Starting segment " << globalPlan->currentSegment;
1577 const auto& currentSegment = globalPlan->subdivision[globalPlan->currentSegment];
1578
1579 globalPlan->currentSegmentTrajectory =
1580 globalPlan->plan.trajectory.getSubTrajectory(currentSegment.s, currentSegment.t);
1581
1582 applyParametrization(globalPlan->currentGlobalSegment(),
1583 rampFromCurrentVelocity
1584 ? srv.sceneProvider->scene().platformVelocity.linear.norm()
1585 : config.stack.generalConfig.boundaryVelocity);
1586
1587 goalReachedMonitor->updateGoal(
1588 globalPlan->currentGlobalSegment().points().back().waypoint.pose);
1589 }
1590 else
1591 {
1592 if (incrementSegment)
1593 {
1594 return true;
1595 }
1596
1597 applyParametrization(globalPlan->currentGlobalSegment(),
1598 rampFromCurrentVelocity
1599 ? srv.sceneProvider->scene().platformVelocity.linear.norm()
1600 : config.stack.generalConfig.boundaryVelocity);
1601
1602 goalReachedMonitor->updateGoal(
1603 globalPlan->currentGlobalSegment().points().back().waypoint.pose);
1604
1605 // only visualize global trajectory once ramping has been performed
1606 srv.introspector->onGlobalPlannerResult(globalPlan->plan);
1607 }
1608
1609 if (globalPlan->useLocalPlanner(hasLocalPlanner()))
1610 {
1611 const auto localPlannerResult = updateLocalPlanner();
1612 updateExecutor(localPlannerResult);
1613 updateIntrospector(localPlannerResult);
1614 }
1615 else
1616 {
1617 updateExecutor(globalPlan->currentGlobalSegment());
1618 }
1619
1620 ARMARX_INFO << "Start executing global plan segment (local planner="
1621 << globalPlan->useLocalPlanner(hasLocalPlanner())
1622 << "). Will now start all required threads.";
1624
1625 startStack();
1626 ARMARX_INFO << "Movement started.";
1627
1628 return false;
1629 }
1630
1631 void
1633 {
1634 }
1635
1636 void
1638 const std::optional<std::string>& providerName)
1639 {
1640 // update static scene including locations
1641 updateScene(true);
1642
1643 const auto resolveLocation =
1644 [&](const std::string& location,
1645 const std::optional<std::string>& providerName) -> std::vector<core::Location>
1646 {
1647 const auto locations = srv.sceneProvider->scene().staticScene->locations;
1648
1649 ARMARX_VERBOSE << "Available locations";
1650 for (const auto& location : locations)
1651 {
1652 ARMARX_VERBOSE << QUOTED(location.name) << " from provider "
1653 << QUOTED(location.provider);
1654 }
1655
1656 const auto matchingLocs =
1657 core::util::findMatchingLocations(locations, location, providerName);
1658
1659 ARMARX_CHECK_NOT_EMPTY(matchingLocs)
1660 << "Unknown location " << QUOTED(location) << " and provider "
1661 << QUOTED(providerName.value_or("~unset~")) << ".";
1662 return matchingLocs;
1663 };
1664
1665 const auto split = simox::alg::split(location, ":");
1666 ARMARX_CHECK(0 < split.size() && split.size() <= 2)
1667 << "The given location does not match the format <location>(:<instance-id>)? '"
1668 << location << "'.";
1669
1670 std::string instanceID;
1671 if (split.size() == 2)
1672 {
1673 // An instance-id was given in the location
1674 instanceID = split.back();
1675 }
1676
1677 const auto matchingLocations = resolveLocation(split.front(), providerName);
1678
1679
1680 if (matchingLocations.empty())
1681 {
1682 ARMARX_WARNING << "Failed to resolve location " << QUOTED(location) << " from provider "
1683 << QUOTED(providerName.value_or("~unset~"));
1684 }
1685
1686 if (matchingLocations.size() > 1)
1687 {
1688 ARMARX_WARNING << "Found more than one matching location for " << QUOTED(location)
1689 << " from provider " << QUOTED(providerName.value_or("~unset~"));
1690 for (const auto& location : matchingLocations)
1691 {
1692 ARMARX_INFO << "- " << QUOTED(location.provider) << ": " << QUOTED(location.name);
1693 }
1694 }
1695
1696
1697 const auto goal = core::resolveLocation(srv.sceneProvider->scene().staticScene->objectMap,
1698 srv.sceneProvider->scene().staticScene->objectInfo,
1699 matchingLocations.front().framedPose,
1700 instanceID);
1701
1702 if (goal.pose.has_value())
1703 {
1704 moveToAbsolute({goal.pose.value()}, false);
1705 }
1706 else
1707 {
1708 ARMARX_ERROR << goal.errorMsg;
1709 }
1710 }
1711
1712 void
1713 Navigator::moveTowardsAbsolute(const core::Direction& direction)
1714 {
1715 }
1716
1717 void
1718 Navigator::run()
1719 {
1720 // Ensure only one thread can call this method at a time (This is not ensured by the PeriodicTask).
1721 // Given the lock in updateLocalPlanner, this is currently not fixing any bugs on its own,
1722 // however, running this function from different threads simultaneously does not provide any benefits
1723 // and could lead to further bugs.
1724 const std::scoped_lock<std::mutex> lock{runMtx};
1725
1726 if (not shouldRun)
1727 {
1729 << "Called Navigator::run() although shouldRun is false. Directly returning.";
1730 return;
1731 }
1732
1733 // TODO(fabian.reister): add debug observer logging
1734
1735 // scene update
1736 {
1737 ARMARX_DEBUG << "Updating scene";
1738
1739 // TODO: remove full update and only update costmap when required!!!
1740 // this will slow down the navigation loop significantly (>1s)
1741 const Duration duration = armarx::core::time::StopWatch::measure(
1742 [&]() { updateScene(true /* false (only dynamic and costmap)*/); });
1743
1745 << "Scene update: " << duration.toMilliSecondsDouble() << "ms.";
1746
1747 srv.debugObserverHelper->setDebugObserverDatafield("scene update [ms]",
1748 duration.toMilliSecondsDouble());
1749 }
1750
1751 // verify global path is still possible
1752 {
1753 // if we have target alternatives and the path is no longer valid, attempt to
1754 // navigate to a valid alternative
1755 if (globalPlan.has_value() and not targetAlternatives.empty())
1756 {
1757 checkGlobalPathAlternatives();
1758 }
1759 }
1760
1761 // eventually, draw
1762 if ((srv.introspector != nullptr) and (srv.sceneProvider != nullptr) and
1763 srv.sceneProvider->scene().robot)
1764 {
1765 ARMARX_DEBUG << "Drawing robot pose";
1766 srv.introspector->onRobotPose(
1767 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose()));
1768 }
1769
1770 // global planner update if goal has changed
1771 // niklas: symbol globalPlanningRequest is not used, so this code is dead
1772 /*{
1773 std::lock_guard g{globalPlanningRequestMtx};
1774
1775 if (globalPlanningRequest.has_value())
1776 {
1777 const auto& waypoints = globalPlanningRequest.value();
1778
1779 // recreate goal monitor
1780 {
1781 // first we check if we are already at the goal position
1782 goalReachedMonitor = std::nullopt;
1783 goalReachedMonitor = GoalReachedMonitor(
1784 waypoints.back(), srv.sceneProvider->scene(), config.goalReachedConfig);
1785
1786 if (goalReachedMonitor->goalReached(false))
1787 {
1788 ARMARX_INFO << "Already at goal position. Robot won't move.";
1789
1790 srv.publisher->goalReached(core::GoalReachedEvent{
1791 {armarx::Clock::Now()},
1792 core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())});
1793
1794 return;
1795 }
1796 }
1797
1798 // global planning
1799 {
1800 // global planner
1801 ARMARX_INFO << "Update/Planning global trajectory";
1802 ARMARX_CHECK_NOT_NULL(config.stack.globalPlanner);
1803 // TODO plan on multiple waypoints, ignoring waypoints for now
1804 // idea: compute multiple global trajectories, one for each segment between waypoints.
1805
1806 srv.introspector->onGoal(waypoints.back());
1807 globalPlan = config.stack.globalPlanner->plan(waypoints.back());
1808
1809 ARMARX_TRACE;
1810
1811 if (not globalPlan.has_value())
1812 {
1813 ARMARX_WARNING << "No global trajectory. Cannot move.";
1814 srv.publisher->globalPlanningFailed(core::GlobalPlanningFailedEvent{
1815 {.timestamp = armarx::Clock::Now()}, {""}});
1816
1817 srv.introspector->failure();
1818 return;
1819 }
1820
1821 ARMARX_TRACE;
1822 srv.publisher->globalTrajectoryUpdated(globalPlan.value());
1823 srv.introspector->onGlobalPlannerResult(globalPlan.value());
1824
1825 if (not hasLocalPlanner())
1826 {
1827 updateExecutor(globalPlan.value());
1828 }
1829 }
1830 }
1831 }*/
1832
1833 // Before anything is activated again: the executor withdrew its controller request when
1834 // the emergency stop engaged, so this is the one chance to replace the trajectory the
1835 // robot would otherwise resume on.
1836 if (srv.executor != nullptr and srv.executor->consumeEmergencyStopRelease())
1837 {
1838 resumeAfterEmergencyStop();
1839 }
1840
1841 // local planner update
1842 {
1843 ARMARX_VERBOSE << "Local planner update";
1844
1846 [&]()
1847 {
1848 if (globalPlan->useLocalPlanner(hasLocalPlanner()))
1849 {
1850 const auto localPlannerResult = updateLocalPlanner();
1851 updateExecutor(localPlannerResult);
1852 updateIntrospector(localPlannerResult);
1853
1854 if (srv.executor != nullptr && localPlannerResult.has_value() &&
1855 not isPaused() && not isStopped())
1856 {
1857 srv.executor->ensureIsActive(
1858 ExecutorInterface::ControllerType::LocalTrajectory);
1859 }
1860 }
1861 else if (srv.executor != nullptr && not isPaused() && not isStopped())
1862 {
1863 srv.executor->ensureIsActive(
1864 ExecutorInterface::ControllerType::GlobalTrajectory);
1865 }
1866 });
1868 << "Local planner update: " << duration.toMilliSecondsDouble() << "ms.";
1869
1870 srv.debugObserverHelper->setDebugObserverDatafield("local planner update [ms]",
1871 duration.toMilliSecondsDouble());
1872 }
1873
1874 // update velocity limits of underlying platform controller
1875 // - apply safety guard, if present
1876 // - ensure the limits of the currently active navigator are used
1877 {
1878 if (hasSafetyGuard())
1879 {
1880 ARMARX_VERBOSE << "Updating safety guard";
1881 const auto result = updateSafetyGuard();
1882 srv.debugObserverHelper->setDebugObserverDatafield("safety_guard.limit_linear",
1883 result.twistLimits.linear);
1884 srv.debugObserverHelper->setDebugObserverDatafield("safety_guard.limit_angular",
1885 result.twistLimits.angular);
1886
1887 if (srv.executor != nullptr)
1888 {
1889 srv.executor->updateVelocityLimits(result.twistLimits);
1890 }
1891
1892 // if the safety guard keeps the robot blocked for too long, re-run the
1893 // global planner
1894 checkRobotBlocked(result);
1895 }
1896 else
1897 // apply the maximum velocity limit from the general config, in case a different navigator applied other limits in between
1898 {
1899 if (srv.executor != nullptr)
1900 {
1901 srv.executor->updateVelocityLimits(config.stack.generalConfig.maxVel);
1902 }
1903 }
1904 }
1905
1906 // request to scale the calculated velocity by the velocity factor
1907 {
1909 srv.debugObserverHelper->setDebugObserverDatafield("velocity_factor",
1910 velocityFactor.load());
1911
1912 // scaling is done relatively; for typical absolute values: see SPFA, 500mm/s 0.4 rad/s
1913 if (srv.executor != nullptr)
1914 {
1915 srv.executor->updateVelocityFactor(velocityFactor);
1916 }
1917 }
1918
1919 // monitor update
1920 {
1922 ARMARX_DEBUG << "Monitor update";
1923
1924 const Duration duration =
1925 armarx::core::time::StopWatch::measure([&]() { updateMonitor(); });
1926
1928 << "Monitor update: " << duration.toMilliSecondsDouble() << "ms.";
1929
1930 srv.debugObserverHelper->setDebugObserverDatafield("monitor update [ms]",
1931 duration.toMilliSecondsDouble());
1932 }
1933 }
1934
1936 Navigator::updateSafetyGuard()
1937 {
1938 ARMARX_CHECK(hasSafetyGuard());
1939
1940 const core::Pose global_T_robot(srv.sceneProvider->scene().robot->getGlobalPose());
1941 const auto proj = globalPlan->currentGlobalSegment().getProjection(
1942 global_T_robot.translation(), core::VelocityInterpolation::LinearInterpolation);
1943 const Eigen::Vector3f global_V_movement = proj.wayPointAfter.waypoint.pose.translation() -
1944 proj.projection.waypoint.pose.translation();
1945
1946 return config.stack.safetyGuard->computeSafetyLimits(global_V_movement.head<2>());
1947 }
1948
1949 void
1950 Navigator::checkRobotBlocked(const safety_guard::SafetyGuardResult& result)
1951 {
1952 const auto& cfg = config.general.blockedReplanning;
1953
1954 if (not cfg.enabled)
1955 {
1956 return;
1957 }
1958
1959 // only relevant while actively navigating towards a goal
1960 if (isPaused() or isStopped() or not globalPlan.has_value() or
1961 not goalReachedMonitor.has_value())
1962 {
1963 blockedSince = std::nullopt;
1964 return;
1965 }
1966
1967 // the safety guard limits the platform velocity close to zero when the robot
1968 // cannot make progress (e.g. an obstacle blocks the planned path)
1969 const bool blocked = (result.twistLimits.linear < cfg.linearLimit) or
1970 (result.twistLimits.angular < cfg.angularLimit);
1971
1972 if (not blocked)
1973 {
1974 // robot is able to move again -> reset the timer
1975 blockedSince = std::nullopt;
1976 return;
1977 }
1978
1979 const armarx::DateTime now = armarx::Clock::Now();
1980 if (not blockedSince.has_value())
1981 {
1982 // start measuring how long the robot stays blocked
1983 blockedSince = now;
1984 return;
1985 }
1986
1987 const armarx::Duration blockedFor = now - blockedSince.value();
1988 srv.debugObserverHelper->setDebugObserverDatafield("safety_guard.blocked_for [ms]",
1989 blockedFor.toMilliSecondsDouble());
1990
1991 if (blockedFor < armarx::Duration::SecondsDouble(cfg.blockedTimeSeconds))
1992 {
1993 // not blocked long enough yet -> keep waiting (obstacle might clear)
1994 return;
1995 }
1996
1997 ARMARX_IMPORTANT << "Robot blocked for " << blockedFor.toMilliSecondsDouble()
1998 << "ms (safety guard limits near zero). Re-running the global planner.";
1999
2000 // reset so the freshly planned trajectory gets a chance before we re-check
2001 blockedSince = std::nullopt;
2002
2003 // re-run the global planner towards the current goal
2004 updateAbsolute({goalReachedMonitor->goal()});
2005 }
2006
2007 bool
2008 Navigator::hasSafetyGuard() const
2009 {
2010 return config.stack.safetyGuard != nullptr;
2011 }
2012
2013 bool
2014 Navigator::hasLocalPlanner() const noexcept
2015 {
2016 return config.stack.localPlanner != nullptr;
2017 }
2018
2019 void
2020 Navigator::updateScene(const bool fullUpdate)
2021 {
2022 ARMARX_CHECK_NOT_NULL(srv.sceneProvider);
2023 srv.sceneProvider->synchronize(armarx::Clock::Now(), fullUpdate);
2024 }
2025
2026 void
2027 Navigator::checkGlobalPathAlternatives()
2028 {
2029 ARMARX_CHECK_NOT_EMPTY(targetAlternatives);
2030
2031 const auto replanAlternatives = [&]()
2032 {
2033 lastAllAlternativesImpossible = std::nullopt;
2034
2035 // save current global plan to restore when no path is currently possible
2036 // i.e. if a person is standing in a doorway and no paths are possible anymore,
2037 // we restore the previous path and move along it until the safety guard stops the robot
2038 const auto previousGlobalPlan = globalPlan;
2039
2040 if (setupTargetAlternatives())
2041 {
2042 // an alternative path was found
2043 ARMARX_INFO << "An alternative path was found.";
2044 setupGlobalPlanSubvidision();
2045 }
2046 else
2047 {
2048 ARMARX_INFO << "No possible alternatives, restoring previous global path.";
2049 globalPlan = std::move(previousGlobalPlan);
2050 lastAllAlternativesImpossible = armarx::Clock::Now();
2051 }
2052
2053 // either start the new global path or continue on the old one
2054 startGlobalPathSegment(false, true);
2055 };
2056
2057 if (not verifyGlobalPathPossible(globalPlan->currentGlobalSegment()))
2058 {
2059 // global path is not possible -> either wait for timeout or check alternatives
2060
2061 if (lastAllAlternativesImpossible.has_value() and
2062 (armarx::Clock::Now() - lastAllAlternativesImpossible.value() <
2064 config.general.targetAlternativesFilterTimeSeconds)))
2065 {
2067 << "Current globalPlan invalid, waiting for timeout before replanning.";
2068 }
2069 else
2070 {
2071 ARMARX_INFO << "Global path no longer possible, trying other alternatives!";
2072
2073 replanAlternatives();
2074 }
2075 }
2076 else if (lastAllAlternativesImpossible.has_value())
2077 {
2078 // globalPlan was previously invalid but is valid now
2079 // -> recheck all possible alternatives and their priority
2081 << "Previously invalid global plan valid again -> replanning all alternatives";
2082
2083 replanAlternatives();
2084 }
2085 }
2086
2087 bool
2088 Navigator::verifyGlobalPathPossible(const core::GlobalTrajectory& plan)
2089 {
2090 const auto& costmap = srv.sceneProvider->scene().staticScene->distanceToObstaclesCostmap;
2091 ARMARX_CHECK(costmap.has_value());
2092
2093 for (const auto& pt : plan.points())
2094 {
2095 const auto& pose = pt.waypoint.pose;
2096 const Eigen::Vector2f position = conv::to2D(pose.translation());
2097 const auto vertex = costmap->toVertex(position);
2098 if (not costmap->isValid(vertex.index) or costmap->isInCollision(vertex.position))
2099 {
2100 return false;
2101 }
2102 }
2103
2104 return true;
2105 }
2106
2107 std::optional<local_planning::LocalPlannerResult>
2108 Navigator::updateLocalPlanner()
2109 {
2110 // We need to make sure the local planner is not simultaniously called from different threads
2111 const std::scoped_lock<std::mutex> lock{updateLocalPlannerMtx};
2112
2113 ARMARX_CHECK(hasLocalPlanner());
2114
2115 ARMARX_VERBOSE << "Updating local plan";
2116
2117 try
2118 {
2119 const auto& globalTrajectory = globalPlan->currentGlobalSegment();
2120 ARMARX_VERBOSE << globalTrajectory.points().size() << " points in global plan";
2121 localPlan = config.stack.localPlanner->plan(globalTrajectory);
2122 ARMARX_VERBOSE << "Local planning finished";
2123 if (localPlan.has_value())
2124 {
2125 srv.publisher->localTrajectoryUpdated(core::LocalTrajectoryUpdatedEvent{
2126 {.timestamp = armarx::Clock::Now()}, localPlan->trajectory});
2127 return localPlan;
2128 }
2129 // do not return here, so that localPlanningFailed event is published
2130 //return localPlan;
2131 }
2132 catch (...)
2133 {
2134 ARMARX_WARNING << "Failure in local planner: " << GetHandledExceptionString();
2135 srv.publisher->localPlanningFailed(core::LocalPlanningFailedEvent{
2136 {.timestamp = armarx::Clock::Now()}, {GetHandledExceptionString()}});
2137
2138 return std::nullopt;
2139 }
2140
2141 srv.publisher->localPlanningFailed(core::LocalPlanningFailedEvent{
2142 {.timestamp = armarx::Clock::Now()}, {"Unknown reason"}});
2143
2144 return std::nullopt;
2145 }
2146
2147 void
2148 Navigator::updateExecutor(const std::optional<local_planning::LocalPlannerResult>& localPlan)
2149 {
2150 if (srv.executor == nullptr)
2151 {
2152 return;
2153 }
2154
2155
2156 if (isPaused() or isStopped())
2157 {
2158 // [[unlikely]]
2159 ARMARX_VERBOSE << deactivateSpam(1) << "stopped or paused";
2160 return;
2161 }
2162
2163 if (not localPlan.has_value())
2164 {
2165 ARMARX_INFO << "Local plan is invalid!";
2166 ARMARX_CHECK_NOT_NULL(srv.executor);
2167 srv.executor->execute(core::LocalTrajectory{}, false);
2168 srv.executor->stop();
2169 return;
2170 }
2171
2172
2173 // const core::Rotation robot_R_world(srv.sceneProvider->scene().robot->getGlobalOrientation().inverse());
2174
2175 // ARMARX_VERBOSE
2176 // << deactivateSpam(100) << "Robot orientation "
2177 // << simox::math::mat3f_to_rpy(srv.sceneProvider->scene().robot->getGlobalOrientation()).z();
2178
2179 // core::Twist robotFrameVelocity;
2180
2181 // robotFrameVelocity.linear = robot_R_world * twist.linear;
2182 // // FIXME fix angular velocity
2183 // robotFrameVelocity.angular = twist.angular;
2184
2185 // ARMARX_VERBOSE << deactivateSpam(1) << "velocity in robot frame "
2186 // << robotFrameVelocity.linear;
2187
2188 ARMARX_CHECK_NOT_NULL(srv.executor);
2189 srv.executor->execute(localPlan->trajectory, true);
2190 }
2191
2192 void
2193 Navigator::updateExecutor(const core::GlobalTrajectory& globalTrajectory)
2194 {
2195 if (srv.executor == nullptr)
2196 {
2197 return;
2198 }
2199
2200 ARMARX_IMPORTANT << "Requested to execute global plan with "
2201 << globalTrajectory.points().size() << " points.";
2202
2203
2204 // if (isPaused() or isStopped())
2205 // {
2206 // // [[unlikely]]
2207 // ARMARX_VERBOSE << deactivateSpam(1) << "stopped or paused";
2208 // return;
2209 // }
2210
2211 if (globalTrajectory.points().empty())
2212 {
2213 ARMARX_INFO << "Global plan is invalid!";
2214 ARMARX_CHECK_NOT_NULL(srv.executor);
2215 srv.executor->stop();
2216 return;
2217 }
2218
2219 ARMARX_IMPORTANT << "Executing global plan with " << globalTrajectory.points().size()
2220 << " points.";
2221 ARMARX_CHECK_NOT_NULL(srv.executor);
2222 srv.executor->execute(globalTrajectory, false);
2223 }
2224
2225 void
2226 Navigator::updateIntrospector(
2227 const std::optional<local_planning::LocalPlannerResult>& localPlan)
2228 {
2229 ARMARX_CHECK_NOT_NULL(srv.introspector);
2230
2231 srv.introspector->onLocalPlannerResult(localPlan);
2232 }
2233
2234 void
2235 Navigator::updateMonitor()
2236 {
2238 //ARMARX_CHECK(goalReachedMonitor.has_value());
2239 if (not goalReachedMonitor.has_value())
2240 {
2241 // Note: I guess this can happen because of a race condition between the moveTo methods and the run method.
2242 // But I am not sure if a common mutex would work correctly.
2243 ARMARX_WARNING << "GoalReachedMonitor is not initialized.";
2244 return;
2245 }
2246
2247 const auto status = goalReachedMonitor->status();
2248 armarx::DebugObserverHelper* debugObserver = srv.debugObserverHelper;
2249
2250 if (debugObserver != nullptr)
2251 {
2252 debugObserver->setDebugObserverDatafield("goalPositionError", status.posError);
2253 debugObserver->setDebugObserverDatafield("goalOrientationError", status.oriError);
2254 debugObserver->setDebugObserverDatafield("goalPositionThreshold",
2255 goalReachedMonitor->getConfig().posTh);
2256 debugObserver->setDebugObserverDatafield("goalOrientationThreshold",
2257 goalReachedMonitor->getConfig().oriTh);
2258 }
2259
2260 if (not isPaused() and goalReachedMonitor->goalReached())
2261 {
2262 // [[unlikely]]
2263 ARMARX_INFO << "Current global path segment finished!";
2264
2265 // we finished the current segment -> start the new one
2266 if (startGlobalPathSegment(true, true))
2267 {
2268 // this segment was the last one, we are finished
2269 ARMARX_INFO << "Goal " << goalReachedMonitor->goal().translation().head<2>()
2270 << " reached!";
2271
2272 // Before `stop()`, and not relying on it: `stop()` deactivates the trajectory
2273 // controller only if it reports active, so the controller that holds this
2274 // request's execution record is not guaranteed to be told the request is over.
2275 // Asking here is what makes the record independent of that.
2276 if (srv.executor != nullptr)
2277 {
2278 srv.executor->dumpDiagnostics();
2279 }
2280
2281 stop();
2282
2283 srv.publisher->goalReached(core::GoalReachedEvent{
2285 {core::Pose(srv.sceneProvider->scene().robot->getGlobalPose())}});
2286
2287 srv.introspector->success();
2288 }
2289 }
2290 }
2291
2292 void
2293 Navigator::stopAllThreads()
2294 {
2295 const std::scoped_lock<std::mutex> lock{runningTaskMtx};
2296
2297 if (runningTask)
2298 {
2299 runningTask->stop();
2300 }
2301
2302 shouldRun = false;
2303 }
2304
2305 // bool
2306 // Navigator::isStackResultValid() const noexcept
2307 // {
2308
2309 // // global planner
2310 // if (config.stack.globalPlanner != nullptr)
2311 // {
2312 // if (not globalPlan.has_value())
2313 // {
2314 // ARMARX_VERBOSE << deactivateSpam(1) << "Global trajectory not yet set.";
2315 // return false;
2316 // }
2317 // }
2318
2319 // // local planner
2320 // if (config.stack.localPlanner != nullptr)
2321 // {
2322 // if (not localPlan.has_value())
2323 // {
2324 // ARMARX_VERBOSE << deactivateSpam(1) << "Local trajectory not yet set.";
2325 // return false;
2326 // }
2327 // }
2328
2329 // // [[likely]]
2330 // return true;
2331 // }
2332
2333 // const core::Trajectory& Navigator::currentTrajectory() const
2334 // {
2335 // ARMARX_CHECK(isStackResultValid());
2336
2337 // if(localPlan.has_value())
2338 // {
2339 // return localPlan->trajectory;
2340 // }
2341
2342 // return globalPlan->trajectory;
2343 // }
2344
2345 void
2346 Navigator::setVelocityFactor(const float velocityFactor)
2347 {
2348 ARMARX_CHECK_POSITIVE(velocityFactor)
2349 << "Scaling factor for velocity may not be negative, but is " << velocityFactor;
2350 ARMARX_CHECK_LESS_EQUAL(velocityFactor, 1)
2351 << "Scaling factor for velocity may not be > 1, but is " << velocityFactor;
2352
2353 this->velocityFactor = velocityFactor;
2354 }
2355
2356 void
2358 {
2359 ARMARX_INFO << "Paused.";
2360
2361 executorEnabled.store(false);
2362
2363 if (srv.executor != nullptr)
2364 {
2365 ARMARX_INFO << "Stopping executor.";
2366 srv.executor->stop();
2367 }
2368 }
2369
2370 void
2372 {
2373 ARMARX_INFO << "Resume.";
2374
2375 executorEnabled.store(true);
2376
2377 if (srv.executor != nullptr)
2378 {
2379 if (hasLocalPlanner())
2380 {
2382 }
2383 else
2384 {
2386 }
2387 }
2388 }
2389
2390 void
2392 {
2393 ARMARX_INFO << "Stopping.";
2394
2395 pause();
2396
2397 // stop all threads, including this one
2398 stopAllThreads();
2399
2400 goalReachedMonitor.reset();
2401 goalReachedMonitor = std::nullopt;
2402 }
2403
2404 bool
2405 Navigator::isPaused() const noexcept
2406 {
2407 return not executorEnabled.load();
2408 }
2409
2410 bool
2411 Navigator::isStopped() const noexcept
2412 {
2413 return (not shouldRun) or (not runningTask) or (not runningTask->isRunning());
2414 }
2415
2417 config{std::move(other.config)},
2418 srv{std::move(other.srv)},
2419 parametrization{std::move(other.parametrization)},
2420 executorEnabled{other.executorEnabled.load()}
2421 {
2422 }
2423
2424
2425} // 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:217
static std::string unavailableReason()
Why it is unavailable, as recorded at configure time. Empty when available.
Definition Toppra.cpp:227
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