Skip to content
HomeProjectsHighlightsBlogPlaygroundRésuméAbout

Guido

by Michael Rusu, Jason Sacerio, Carlos Catala, & Adrian Osorio

We built a small-scale, voice-controlled mobility prototype that maps unfamiliar rooms and navigates to destinations. Guido connects LiDAR mapping, frontier exploration, and ROS 2 motion control so a spoken command can become a route through the room.

Code Devpost Video

HackUSF 2026
Most Innovative Hack · 280+ projects

Explore the project ↓
The problemArchitectureMapping and explorationVoice and motionResultsDemo

The problem

Moving through a room involves a sequence of decisions: choosing a destination, finding a clear route, avoiding obstacles, and adjusting as the surroundings change. For a wheelchair user, managing that movement can require sustained physical effort or help from another person.

Guido explores how voice control and autonomous navigation can support that process. We built a small-scale platform that accepts spoken movement commands, maps its surroundings with LiDAR, and navigates to waypoints. Exploring an unfamiliar room also lets it build the map it needs for later journeys.

My contribution

I developed frontier-based exploration and mapping, designed the modular SLAM, Nav2, and supervisor architecture, and helped integrate the Jetson, LiDAR, Arduino, and motion platform. The working prototype brought together our team’s hardware, voice, and navigation work.

360°LiDAR sensing
ROS 2modular autonomy
2driven wheels with encoders

Architecture

Guido architecture: speech and sensor inputs feed supervision, SLAM and Nav2, followed by the serial bridge and motor control.
Figure 1. Commands, autonomy, and motor control across the Guido stack. Editable Excalidraw Download figure

The architecture separates command interpretation, supervision, navigation, and motor control into ROS 2 nodes. A command describes the intended action. The supervisor checks the operating mode, available map, robot pose, and sensor health before issuing a navigation request. Nav2 handles route execution, while the serial bridge translates velocity commands for the motor controller.

Mapping supplies the spatial context for these decisions. SLAM Toolbox combines LiDAR observations and the robot’s estimated motion to maintain the occupancy map and pose. The exploration planner consumes that map, chooses a reachable frontier, and passes the resulting goal through the navigation layer.

ComponentRole
NVIDIA Jetson Orin NanoOnboard compute for perception, commands, and autonomy.
LD19 LiDARDistance measurements around the platform for mapping and obstacle detection.
Arduino and motor driverLow-level motor commands over the serial connection.
Encoders and MPU6050Wheel-motion and inertial feedback for the prototype.

Mapping and exploration

LiDAR measures the distance to surfaces around the robot. The scan appears as a set of returns in the visualization below. As Guido moves, SLAM Toolbox aligns successive scans with the robot’s estimated motion to build an occupancy map.

Enhanced, straight-on Guido LiDAR visualization, showing scan returns around the orange robot marker on a gridded map.
Figure 2. Enhanced LiDAR view from Guido’s prototype demonstration. Download figure

An occupancy grid represents the room as free, occupied, and unknown cells. Guido finds unknown cells next to free space and groups connected boundary cells into frontier clusters. The minimum cluster size removes small fragments that would otherwise create many weak exploration candidates.

The code below detects that boundary in the occupancy map. A value of −1 marks an unknown cell; 0 marks free space. The helper checks all eight neighboring cells, and the planner groups the detected boundary cells into connected clusters.

Python · frontier detectionSource ↗
def _find_frontier_cells(grid: OccupancyGridSnapshot) -> set[Tuple[int, int]]:
    cells = set()
    for row in range(grid.height):
        for col in range(grid.width):
            if grid.value(row, col) != -1:
                continue
            if _has_adjacent_free_cell(grid, row, col):
                cells.add((row, col))
    return cells


def _has_adjacent_free_cell(grid: OccupancyGridSnapshot, row: int, col: int) -> bool:
    for row_offset, col_offset in _NEIGHBORS_8:
        neighbor_row = row + row_offset
        neighbor_col = col + col_offset
        if not grid.in_bounds(neighbor_row, neighbor_col):
            continue
        if grid.value(neighbor_row, neighbor_col) == 0:
            return True
    return False

A frontier identifies an area worth observing. The actual navigation goal is a reachable free cell alongside that cluster. The planner searches the free space connected to the robot, filters candidates near blacklisted frontiers, and selects a suitable approach point.

Candidate ranking combines travel cost and information gain. The implementation estimates travel cost with A*, normalizes distance and gain across the available candidates, and applies a configurable weight. Lower scores are preferred. The default information-gain weight is 0.3.

score = (1 − weight) × normalized distance
        − weight × normalized information gain

During exploration, a rejected or timed-out goal is added to a blacklist so the next planning cycle can choose another frontier. The supervisor also tracks empty planning cycles and supports a coverage threshold for ending exploration. Goal timeouts scale with estimated travel distance, expected speed, and a safety factor.

Voice and motion control

The voice pipeline turns speech into text using Vosk or ElevenLabs. A transcript bridge filters partial results and normalizes wake words and destination aliases. The agent and command layers interpret that request, while the supervisor maintains navigation state and checks whether the action can proceed.

Fresh sensor data is part of that decision. The health monitor tracks the most recent LiDAR scan, odometry, and transform updates. Its defaults allow 0.5 seconds for scan freshness, 2 seconds for odometry, and 1 second for TF. In exploration mode, a failed health check clears the active exploration state and emits a stop action.

A simulated supervisor sequence: accept exploration, request navigation, refresh sensors, detect a stale scan and emit STOP.
Figure 3. A simulated-clock run of the exploration supervisor. Scan age reaches 0.55 seconds while odometry and TF remain fresh. Editable Excalidraw Download figure
Python · exploration health guardSource ↗
if self._exploration_active:
    health_status = self._health_monitor.status(now=current_time)
    if not health_status.healthy:
        self._exploration_active = False
        self._empty_frontier_cycles = 0
        self._pending_map_save = None
        self._map_save_in_progress = None
        self._active_goal = None
        return CommandResponse(
            accepted=False,
            message="Exploration aborted: {}.".format(health_status.details),
            actions=(
                SupervisorAction(
                    action_type=ActionType.STOP,
                    reason="exploration aborted due to stale health",
                ),
            ),
        )

The serial bridge provides another timeout at the motion boundary. It tracks the age of the latest velocity command and writes STOP when that age exceeds its configured limit, which defaults to 0.5 seconds. This gives the motor interface a direct response to interrupted command delivery.

These layers address different points in the command path: sensor freshness before and during exploration, goal timeouts during navigation, and command freshness at the serial bridge. Keeping those responsibilities explicit made integration and debugging easier as the hardware came together.

Results

Guido won Most Innovative Hack at HackUSF 2026 among more than 280 projects. The team demonstrated a working small-scale platform with voice-driven motion, LiDAR mapping, waypoint navigation, and obstacle avoidance.

For this case study, I ran 27 existing frontier and supervisor checks against the pinned source. They cover cluster extraction, reachable goals, blacklisting, command prerequisites, timeouts, and stale-sensor handling. Figure 3 follows a deterministic supervisor fixture.

CheckObserved behavior
Reachable goal selectionThe selected approach point lies in connected free space.
Failed frontier handlingRejected and timed-out frontiers enter the blacklist before replanning.
Stale exploration dataThe supervisor clears exploration state and emits STOP.
Navigation timeoutAn expired navigation goal produces a stop action.

The opening scene is a separate illustrative room with a fixed route and simulated LiDAR rays. It provides a direct way to explore the spatial ideas behind Guido. The demonstration below shows the physical prototype.

Demo

Watch the Guido demo on YouTube.

with by michael