Repository navigation
3.0.0
A major version because public helpers and a config file are removed.
Added
.mapdatafiles carry aformat_versionmarker (FORMAT_VERSIONinmap_data/utils/serialization.py, currently1). Files written before the marker load as before, a file from a newer version is rejected with a message naming both versions instead of half-loading, and every key of a stored way is optional on load (theWaydefaults fill in), so a key added later does not break older filesplan_route()androute_plannerreject a start or goal more thanoutside_map_tolerance(100 m) outside the map's area withstart_outside_map/goal_outside_map(any algorithm, checked before the snap distances;route_plannernames the loaded file), so a wrong map file or a QR code for another map fails at once instead of planning towards the map's edge- The viewer's tracker topics can be changed without a restart: Topics… in the Tracker sidebar resubscribes the running tracker and can save the topics back to its config file, comments kept (
GET/PUT /api/tracker/settings).map_data_viewer --config <file>names that file, a ROS 2 parameter file with amap_data_trackersection. The default is the newconfig/tracker.yaml, which holds the built-in defaults, so nothing changes for existing setups config/helhest_jr.yaml: tracker topics of helhest-jr (map_data_viewer --config config/helhest_jr.yaml)- Tracker
tf_static_only(config file only, defaulttrue): frames are read from/tf_staticalone. Set it tofalsewhen a frame above the robot is published on/tf, which costs a lot of CPU - The Tracker sidebar keeps the last five speech messages instead of only the newest, so a message that scrolls by while you look elsewhere is still readable
- Tracker Clear Trail: drops the recorded trail on the node (
DELETE /api/tracker/trail), so it stays gone instead of coming back with the next fix - Planner Start at robot: with a robot position on the tracker's telemetry, the toggle pins waypoint 0 to the robot and refreshes it when Replan Path runs, so a route is planned from where the robot actually is instead of a clicked start
osm_cloudtakes ahighway_typesparameter (andhighway_types:=launch argument), asroute_plannerdoes: the grid can be drawn from roads as well as footways, so it shows the network the graph planner routes on when roads are allowed. The default stays["footway"]- The graph planner crosses walkable areas (closed
area=yesor multipolygon ways, e.g. pedestrian squares) along the shortest path inside them, holes and concave corners respected, instead of walking their rim. Any network node on, inside or within 1 m of the area enters it; a waypoint inside an area is no longer snapped to its rim, and one snapped onto (or within 1 m of) an area crosses it from there. The crossings live with the area (map_data.pathsolver.walkable_area) and add no graph edges
Removed
config/helhest.yaml: helhest-jr is the only Fixposition robot, and its topics (tracker andosm_cloud) are inconfig/helhest_jr.yaml, now the defaultconfig_fileofosm_cloud.launch.py- Migrations for annotation stores written before v2 (
migrate_change_log()and the legacy store normalization inmap_data/viewer/helpers.py): a store from that era now loads as whatever it literally contains. Re-export such a map from a v2 viewer before upgrading find_config()(map_data/utils/config.py) andsave_mapdata()(map_data/utils/serialization.py), each with a single caller; useMapData.save()- The
requestsdependency (python3-requests).map_data/utils/overpass.pyposts throughurllib.requestand identifies itself asmap_data/<version>
Changed
smooth_pathruns as a vectorized Jacobi sweep over the whole path instead of a scalar Gauss-Seidel triple loop: ~100x faster on a 1000-waypoint path (40 ms to 0.3 ms). Both schemes share the same fixed point, so the smoothed path is the same up to the stopping tolerance/api/mapdataserves the merged GeoJSON from a cache keyed on the.mapdatafile's identity (mtime, size, inode) and a digest of the annotation store, so repeat calls no longer copy the map, re-apply every edit and rebuild the FeatureCollection. The response carries anETagand honorsIf-None-Matchwith a 304. Any edit or a change to the file on disk invalidates the entry- Loading a map for planning no longer
deepcopys it.apply_annotationscopies the map shallowly and only the ways an edit touches, so an annotated map costs a copy of the edited ways instead of the whole node table - The grid A* inner loop runs on plain Python floats and an
array("f")of g scores instead of numpy scalars, andmathreplacesnp.sqrtfor the heuristic. Every g score is rounded back to float32, so the expansion order and the resulting path are bit-identical to before - Obstacles are marked in the point grid by index range: each obstacle's bounds become a sub-block of the lattice and only that block is tested, instead of masking the whole grid once per obstacle
- The graph planner measures distances with
math.hypotrather thannp.linalg.norm(the A* heuristic, the snap costs and the spur collapse), andlatlon_to_utm_path/utm_path_to_latlon_pairsconvert whole arrays in oneutmcall instead of point by point osm_cloudqueries its kd-tree in 2-D over all cores at once and splits ways with numpy instead of a per-node loop- The viewer's tracker spins on the C++
EventsExecutorwhen the distro has it (measured 6 % vs 60 % CPU against a 250 Hz robot), falling back torclpy.spin; TF comes from/tf_staticonly (seetf_static_only), and telemetry updates less often route_plannerno longer blocks 0.5 s per goal waiting for theearth_frame -> local_frametransform: the lookup does not wait, the warning is throttled to 30 s, and the one-shot preload timer is destroyed instead of only cancelled- CI pins ruff to 0.16.6 (matching
pyproject.tomland.pre-commit-config.yaml), builds the docs with--strict, smoke-tests the demo on pull requests, and fails under 72 % coverage
Fixed
- RRT* no longer samples from the module-global
random. EveryRRTStargets its ownrandom.Random(injectable asRRTStar(..., rng=...)/ReplanPath(..., rng=...), or via aseedunderrrt:inconfig/planner_defaults.yaml), so concurrent planners in different request threads cannot interleave each other's draws, a failing run can be replayed, and tests no longer have to seed the global stream - Moving a node that several ways share now moves it for the planner the way the viewer draws it. Moves are stored per way, but the merged map resolved them per node id: the last override won, the other ways were dragged along in the graph and the junction survived. The merge (
apply_way_edits) now gives the moved way its own copy of the node (a fresh negative id) and leaves the original where it was, so the junction is broken exactly where the viewer shows a gap; undoing the move joins it again. Nothing changes in the store - Ways that meet on the map now meet in the graph. The graph planner connects ways by node id only, and an edit never produced a shared id by itself. The annotation merge now ends with
join_ways(), which gives such ways a real node to meet at:- an end node dragged onto another way (within 5 m,
JOIN_DISTANCE_M) joins it there: at a node of that way when one is within 1 m, else at a new node on the edge. Before, the end kept the junctions it had and gained none; since the shared-node fix above it was left loose - a drawn path joins every way it crosses, and a way that ends on it. Only its two ends were connected before, and only by the planner, so a path drawn across a footway showed a crossroad the planner could not turn at
- splitting a drawn path that an export had baked into the map no longer cuts it off the network (the planner's own stitching skipped the
-18:1style ids of its segments), and a path drawn on such a map no longer reuses the baked path's way id The junction is only added to the node lists, so no geometry moves; being real nodes, the junctions survive an export. The planner's own stitching of drawn-path ends stays for maps merged elsewhere and skips an end that is already a node of another way
- an end node dragged onto another way (within 5 m,
- A junction node moved to the same spot (0.5 m) in every way that uses it stays one junction, instead of one copy per way lying on top of each other, unconnected
- A move in one way no longer leaks into the others: a way rebuilt for a deleted node or a split took a shared node's position from the way that had moved it, and a move recorded on a way deleted afterwards still moved the node for everyone
osm_cloudpublished an empty grid that only a restart cured. A Fixposition unit without a fusion fix publishesFP_ECEF -> FP_ENU0as all zeros, and the node latched it at start-up:utm_to_local_via_ecefthen worked at an altitude of -6378 km and collapsed the whole map into a metre-wide box, so no grid cell landed withinmax_path_distof a way (7 of the last 25 field runs,(0, 4)or(30, 4)points,FP_ENU0 origin at lat=180.0in the log). Such a transform is now rejected and waited out; the/tf_staticsubscription is kept for the node's life, so a placement transform that arrives late or moves (the ENU0 origin changes whenever the GNSS/INS driver restarts) rebuilds and re-publishes the grid and the intersections instead of needing the node restarted; and a grid that comes out empty over a map that has ways is logged as an error rather than latched onto every late subscriber- The tracker kept showing a plan and goal that no longer existed. The plan and the goal are dropped once nothing publishes their topics (a planner that stopped or restarted), or once the planner has been sending empty plans for 3 s (
PLAN_STALE_TIMEOUT) — crl_commander republishes at 20 Hz and publishes an empty plan while it has no goal, so STOP, goal reached and goal aborted all clear the display. Silence alone never clears anything, so a publish-once planner (nav2) keeps its plan - The tracker drew no high-level route on helhest-jr:
config/helhest_jr.yamlpointedsequence_path_topicat/crl_commander/goal_sequence, which the commander only publishes in SEQUENCE mode, whileroad_followerdrives it goal by goal. It now reads/road_follower/route_path, the route actually being followed whatever its source. A route is also no longer decimated to every 10th pose (which cut every corner of a waypoint route); it is thinned only above 200 poses - The viewer's telemetry Socket.IO connection failed with
property 'session' of 'RequestContext' object has no setteron Flask 3.0.2 with Flask-SocketIO 5.3 (Ubuntu 24.04 packages), so the tracker received nothing. The Socket.IO server no longer manages a Flask session - Route relations (hiking, bike, bus) are no longer merged like multipolygons. Their member ways were chained into one negative-id way carrying every member's tags, so a single bridge or stairs member made the whole route non-traversable and the planner avoided kilometres of ordinary footway. Only
multipolygonandboundaryrelations are merged now. Existing.mapdatafiles keep the merged ways until they are parsed again
Full Changelog: v2.0.0...v3.0.0 · changelog