Skip to content

Grass is not an obstacle (it took nine stops to get there)

Until tonight the sim lawn was a flat green plane. Real grass is 5 to 10 cm tall, and our 3D-lidar safety gate stopped for anything 3 cm above the ground. So we grew grass the lidar can see, and the robot never left the dock. Seventeen runs later it mows a whole zone in 7 cm grass, over bumps and a 25 cm mound, with zero false stops. Every fix in between came from a stop the robot explained itself.

Growing grass a lidar can see

Gazebo's gpu_lidar renders the scene, so it sees visual geometry. A green texture is invisible to it. Grass has to be geometry: 58,000 tufts, each a thin four-sided pyramid, on a jittered 7 cm grid with heights of nominal ±25%. They're all one OBJ mesh with no collision (the robot drives through grass), so the GPU draws one thing, not 58,000 models. It takes 0.2 s to generate and is 12 MB. make mow-run GRASS=0.07.

For uneven ground, TERRAIN=bumpy adds a collision mesh: twelve 3–6 cm bumps and one broad 25 cm mound, with slopes up to 12°. Heights are never below zero, so the old plane still holds up the rest of the yard, and the dock stays level. (First try: Gazebo segfaulted. DART needs a normal per vertex in a collision mesh. Without them it drops the mesh and the server crashes. We now compute them from the terrain's gradient.)

The baseline: stuck at the dock

Run What Result
grass-3d-1 7 cm grass, the gate as it was STOP at 0.35 m before leaving the dock, latched forever. 0 m² mowed

That's the honest starting point. "Obstacle = anything 3 cm above z = 0" makes every blade an obstacle.

The stops, one at a time

The 3D point cloud is too heavy to record, so after the first failure the gate started keeping evidence: every STOP event now carries the nearest offending points (position and height above the ground) and the fitted ground plane. scripts/stop_probe.py <run> turns that into world coordinates. Each row below is one stop it explained:

Run The stop What it really was Fix
grass-3d-2/3 0.56 m ahead while turning The flowerbed (15 cm), real, but 1 m away during a pivot Protect what the motion sweeps: a pivot checks a circle (0.575 m + 0.10 m), not a corridor
grass-3d-4 0.21 m from the axle The robot itself: the lidar sees its own deck Ignore points inside the footprint
grass-3d-6 one point, 10.1 cm, behind A tall tuft against a plane fitted ahead and extrapolated behind An object needs 3 returns within 10 cm; clearance = tallest grass + 3 cm = 12 cm
grass-3d-7 flowerbed edge, twice Real: 13–15 cm, exactly the bed's height None in software (a decision for Chris, below)
grass-bumpy-2 grass behind, 12–14 cm The forward plane was 4 cm low behind the robot A second plane fitted around the robot for pivots
grass-bumpy-3 grass on a bump, 12–14 cm A plane can't follow a 6 cm bump 0.5 m wide Local ground: per-cell lowest return, capped at 4 cm above its neighbours

The cap in the last row is the important design choice. Taking each cell's lowest return as "ground" follows bumps, but a box that fills a cell completely shows only its top, and its top would become the ground. Capping a cell at its neighbours' ground + 4 cm means an object loses at most 4 cm of apparent height. A 20 cm box still reads ≥ 16 cm (a unit test checks it), and the face nearest the robot always has real ground beside it.

Two passes

Run Ground Mowed False stops Coverage Cross-track max
grass-open-1 flat, 7 cm grass 37.1 m² 0 98.3% 9.7 cm
grass-bumpy-4 bumps + 25 cm mound, 7 cm grass 37.0 m² 0 96.3% 11.1 cm

Coverage map, bumpy lawn

And the 2D lidar on the same bumpy lawn (grass-bumpy-2d-1)? We expected the mound to look like a wall to a horizontal beam 15 cm up. It didn't stop the robot, but it slowed it for 124 s of a 397 s mission (the 3D gate: 5 s of 342 s). The beam meets the rising slope 1 to 1.5 m ahead, inside the slow field but outside the stop field, and once the robot is on the slope it tilts with it and the beam clears. A steeper or shorter rise would stop it. The 2D lidar was never going to see a hedgehog anyway; this is one more reason the 3D lidar is the safety sensor.

Both ran in a new lawn zone, the Open patch, between the house and the tree, with nothing raised in it. The test patch has the flowerbed, and that turned out to be its own problem.

Still stopping for every real thing?

Ignoring grass is only worth something if the gate still stops for everything else. So we ran the obstacle course twice with the new gate, 3D lidar. First as a regression on the flat lawn (default clearance 3 cm), then in 7 cm grass with the 12 cm clearance:

Obstacle Height Flat, new gate 7 cm grass, clearance 12 cm
pole 1 m PASS, seen at 1.47 m, 0.57 m to spare PASS, seen at 1.46 m, 0.57 m to spare
box 20 cm PASS, 1.49 m, 0.58 m PASS, 1.47 m, 0.57 m
child (lying) 25 cm PASS, 1.48 m, 0.57 m PASS, 1.49 m, 0.57 m
ball 20 cm PASS, 1.48 m, 0.57 m PASS, 1.43 m, 0.56 m
hedgehog 10 cm PASS, 1.48 m, 0.57 m HIT, never seen
hose 4 cm PASS, 1.27 m, 0.56 m HIT, never seen
nothing no false stops no false stops

The two hits are the result Phase 5's plan predicted, and they're why the plan has more steps. Height is the only thing the lidar measures. If the grass is 9 cm tall and a hedgehog is 10 cm tall, no threshold separates them: a clearance low enough to see the hedgehog stops for the grass. The hose lies under the grass. Two things follow. D2, the long-grass rule: keep the lawn short enough (≤ ~8 cm) that the clearance stays low. And the camera (5.3), which can tell an animal from a tuft by what it looks like, not how tall it is.

So how short is short enough? With our clearance rule (tallest grass, nominal × 1.25, plus 3 cm), a 10 cm hedgehog stays above the clearance only while the grass is under about 5.6 cm. We tested the edge: 5 cm grass, clearance 9.25 cm.

Run Result
course-grass5-hedgehog-3d PASS: hedgehog seen at 1.22 m, stopped with 0.56 m to spare
course-grass5-none-3d no false stops
grass5-bumpy-1 (full mow, bumpy) 37.1 m², 0 false stops, coverage 96.6%

That gives D2 its number. We had pencilled in "about 8 cm", which is too tall. For the lidar alone to protect a hedgehog, the long-grass rule has to trip at about 5.5 cm of uncut grass. Our sim lawn's cut height is 6 cm, so even a fresh cut sits at the edge. That's a decision for Chris (cut lower, or accept that the camera does this job in grass over 5.5 cm).

(The flat child run needed a rerun: Gazebo started but never published /clock, a flaky start we've seen once before, unrelated to the gate.)

A decision for Chris: raised flowerbeds

The 3D lidar sees a 15 cm raised bed as what it is, an obstacle. The 2D lidar never did, because it sits at 15 cm. The planner ends stripes with the nose 0.26 m from a keep-out. But since course-b the gate keeps a floor under its stop field (the blind range of the downward beams, so a hedgehog can't vanish under them), which stops the robot about 0.6 m from anything taller than the clearance. Head-on to a raised bed, those two can't both be satisfied.

  • A. The planner keeps ~0.7 m from raised keep-outs. Safe, and leaves a strip for the trimmer.
  • B. The gate ignores returns inside a mapped keep-out. Full coverage, but less caution (a child in the bed).
  • C. A by default, B as an app-only owner switch.

Claude's recommendation is A now. Nothing is built until Chris decides.

What we learned

  • Make the robot explain its stops. Every fix tonight came from evidence the gate logged itself. Without it we would have tuned the clearance up until the stops went away, and hidden the flowerbed problem.
  • Protect the space the motion sweeps. A stop field is about where the robot is going: a corridor when driving, a circle when turning. The old gate checked a corridor during pivots.
  • Fit the ground where you're about to measure. A plane fitted ahead was wrong behind; a plane was wrong on a bump. Each is fine for its own job.
  • One return is not an object. Real things return many points; noise and grass tips return one.

Next: 5.1a, the long-grass rule. The mowed stripe beside the robot is a free measurement of cut height, and the uncut grass ahead tells us how tall the lawn is. The clearance should come from that measurement, not from a number we typed.