Skip to content

Adding a New Exploration Strategy

The exploration system is designed around a simple three-method protocol. Swapping in a new algorithm requires implementing the protocol, registering one elif branch, and selecting it with an env var — no other files need to change.

Step 1: Implement the protocol

Create a class that implements update, is_complete, and reset:

# src/xiao_hei_vln/exploration/my_strategy.py
from xiao_hei_vln.messages.inputs import VLMInput
from xiao_hei_vln.messages.outputs import Waypoint


class MyStrategy:
    def __init__(self, max_waypoints: int = 100) -> None:
        self._max_waypoints = max_waypoints
        self._visited: list[Waypoint] = []
        self._done = False

    def update(self, snapshot: VLMInput) -> Waypoint | None:
        """Called once per tick. Return the next waypoint or None."""
        if self._done:
            return None

        pose = snapshot.pose
        if pose is None:
            return None  # wait for odometry

        # Your navigation logic here.
        # Use snapshot.terrain_ext for terrain data,
        # snapshot.pose for robot position.
        wp = self._pick_next_waypoint(pose)

        if len(self._visited) >= self._max_waypoints:
            self._done = True
            return None

        return wp

    def is_complete(self) -> bool:
        return self._done

    def reset(self) -> None:
        self._visited = []
        self._done = False

    def _pick_next_waypoint(self, pose) -> Waypoint | None:
        ...

Optional: plot support

If your strategy maintains a grid and a list of visited waypoints, expose them so _maybe_save_png() can save a debug PNG after exploration ends:

def get_visited_waypoints(self) -> list[Waypoint]:
    return list(self._visited)

def get_grid(self) -> OccupancyGrid:
    return self._grid

Without these methods the PNG step is silently skipped — nothing breaks.

Step 2: Register in the factory

Edit _build_explorer() in src/xiao_hei_vln/app/main.py. Add an elif branch for your strategy name:

def _build_explorer(node):
    if _EXPLORATION_MAX_WAYPOINTS <= 0:
        return None

    if _EXPLORATION_STRATEGY == "frontier":
        from xiao_hei_vln.exploration import FrontierExplorer
        explorer = FrontierExplorer(
            max_waypoints=_EXPLORATION_MAX_WAYPOINTS,
            ...
        )
    elif _EXPLORATION_STRATEGY == "my_strategy":          # ← add this
        from xiao_hei_vln.exploration.my_strategy import MyStrategy
        explorer = MyStrategy(max_waypoints=_EXPLORATION_MAX_WAYPOINTS)
    else:
        node.get_logger().error(...)
        return None

    node.get_logger().info(f"Exploration enabled: {type(explorer).__name__} ...")
    return explorer

Step 3: Select at runtime

# Docker Compose
XIAO_HEI_EXPLORATION_STRATEGY=my_strategy docker/run perception up -d --build

# Or export before compose
export XIAO_HEI_EXPLORATION_STRATEGY=my_strategy

The default is frontier. An unrecognised name logs an error and disables exploration rather than crashing.

Step 4: Test without ROS

The protocol is pure Python — no ROS install needed for unit tests. Build a VLMInput by hand and drive update() directly:

from xiao_hei_vln.messages.inputs import VLMInput
from xiao_hei_vln.messages.common import Stamp
from xiao_hei_vln.messages.sensors import OdomPose
from xiao_hei_vln.exploration.my_strategy import MyStrategy


def test_my_strategy_completes():
    strategy = MyStrategy(max_waypoints=3)

    pose = OdomPose(...)          # build a minimal pose
    snapshot = VLMInput(
        tick_id=0,
        tick_time=Stamp.from_seconds(0.0),
        pose=pose,
    )

    for _ in range(100):
        strategy.update(snapshot)
        if strategy.is_complete():
            break

    assert strategy.is_complete()

Run: uv run pytest tests/ -q

Step 5: Run end-to-end

XIAO_HEI_EXPLORATION_STRATEGY=my_strategy \
XIAO_HEI_EXPLORATION_MAX_WAYPOINTS=50 \
docker/run perception up -d --build

docker logs -f xiao_hei_ai_module
# look for: Exploration enabled: MyStrategy (strategy=my_strategy, ...)

Check exploration_logs/exploration.log for WP_SET, WP_ADVANCE, WP_SKIP, and DONE events as the run progresses.