Skip to content

3.0.0

Choose a tag to compare

@vlk-jan vlk-jan released this 21 Sep 14:48
· 26 commits to master since this release

A major version because public helpers and a config file are removed.

Added

  • .mapdata files carry a format_version marker (FORMAT_VERSION in map_data/utils/serialization.py, currently 1). 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 (the Way defaults fill in), so a key added later does not break older files
  • plan_route() and route_planner reject a start or goal more than outside_map_tolerance (100 m) outside the map's area with start_outside_map / goal_outside_map (any algorithm, checked before the snap distances; route_planner names 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 a map_data_tracker section. The default is the new config/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, default true): frames are read from /tf_static alone. Set it to false when 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_cloud takes a highway_types parameter (and highway_types:= launch argument), as route_planner does: 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=yes or 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 and osm_cloud) are in config/helhest_jr.yaml, now the default config_file of osm_cloud.launch.py
  • Migrations for annotation stores written before v2 (migrate_change_log() and the legacy store normalization in map_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) and save_mapdata() (map_data/utils/serialization.py), each with a single caller; use MapData.save()
  • The requests dependency (python3-requests). map_data/utils/overpass.py posts through urllib.request and identifies itself as map_data/<version>

Changed

  • smooth_path runs 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/mapdata serves the merged GeoJSON from a cache keyed on the .mapdata file'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 an ETag and honors If-None-Match with 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_annotations copies 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, and math replaces np.sqrt for 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.hypot rather than np.linalg.norm (the A* heuristic, the snap costs and the spur collapse), and latlon_to_utm_path / utm_path_to_latlon_pairs convert whole arrays in one utm call instead of point by point
  • osm_cloud queries 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++ EventsExecutor when the distro has it (measured 6 % vs 60 % CPU against a 250 Hz robot), falling back to rclpy.spin; TF comes from /tf_static only (see tf_static_only), and telemetry updates less often
  • route_planner no longer blocks 0.5 s per goal waiting for the earth_frame -> local_frame transform: 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.toml and .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. Every RRTStar gets its own random.Random (injectable as RRTStar(..., rng=...) / ReplanPath(..., rng=...), or via a seed under rrt: in config/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:1 style 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
  • 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_cloud published an empty grid that only a restart cured. A Fixposition unit without a fusion fix publishes FP_ECEF -> FP_ENU0 as all zeros, and the node latched it at start-up: utm_to_local_via_ecef then worked at an altitude of -6378 km and collapsed the whole map into a metre-wide box, so no grid cell landed within max_path_dist of a way (7 of the last 25 field runs, (0, 4) or (30, 4) points, FP_ENU0 origin at lat=180.0 in the log). Such a transform is now rejected and waited out; the /tf_static subscription 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.yaml pointed sequence_path_topic at /crl_commander/goal_sequence, which the commander only publishes in SEQUENCE mode, while road_follower drives 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 setter on 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 multipolygon and boundary relations are merged now. Existing .mapdata files keep the merged ways until they are parsed again

Full Changelog: v2.0.0...v3.0.0 · changelog