What this is: A working session where we calibrated the drone’s fan scan — the sideways sweep of its distance sensor — so the resulting map of points comes out straight instead of curved on one side.
Why it’s here: It is a clean example of how a “visual glitch” in a simulator is almost never one bug. The curved fan turned out to be three separate problems stacked on top of each other, and the most important one had nothing to do with where the drone was — it was about what angle the sensor was actually pointing versus what we told it to point.
Date: 2026-06-15 Ticket: Interactive calibration session with Aleks, before training.
Glossary
- Fan scan (sweep): The drone holds still and rotates a small distance sensor side to side, like a hand fan opening. Each step it records one distance reading. Stitch ~181 of those readings together and you get a fan-shaped slice of the world in front of the drone.
- Conscious scan / scan-on-button: Instead of the drone constantly sweeping on its own (an auto-cycle), it only scans when you explicitly press the button. Think of it as raising your hand to look around when you decide to, rather than nervously glancing everywhere all the time.
- Veer / drift / “curve”: The defect we were chasing. One side of the fan bends away from where it should be, so a flat wall looks like a banana on screen.
- Servo (SG90): The tiny motor that turns the sensor. It does not teleport to a new angle — it drives there over time, the way a clock hand sweeps rather than jumps.
- Pose: The drone’s position and heading at a given instant.
- Commanded angle vs. actual angle: The angle you order the servo to reach, versus the angle the servo is truly at right now. These are not the same thing while it is still moving.
- TF-Luna: A real, cheap, single-beam distance sensor — the kind of hardware this whole scan is modeled on.
- mavros / SITL: The bridge and the software-in-the-loop flight stack that let us fly the simulated drone exactly as we would fly the real one.
1. The two things that kicked off the session
We started with two complaints from a live flying session:
- The drone kept scanning by itself. During manual flight it would auto-cycle the sweep and quietly take over, which is annoying when you are the one flying.
- The fan came out crooked. Aleks sent a fan-calibration screenshot showing the fan visibly curved on one side — a wall that should be flat looked bent.
The first one was a quick policy change. The second one ate most of the session.
2. Auto-scan off by default
The sweep node used to run its own loop and elbow its way into manual mode. We flipped it to opt-in:
drone.launch.py autoscandefault changedtrue → false.launch.shgained--autoscan(turn it on) and--no-autoscan(the new default).- Scanning now happens only when you publish to
/drone/sweep/start.
Training runs were unaffected — they already launched with auto-scan off.
The mental model: the drone no longer scans nervously on its own. It scans when told. A conscious scan.
3. Problem one — every reading shared one stale pose
This one only bites while the drone is flying.
The fan is built from ~181 individual readings, one per angle. The old code registered all of them using a single pose — the pose captured at the end of the pass. But a full pass takes about 21.7 seconds. Over that time the drone drifts a little and its heading reacts to wind and thrust. So the readings taken early in the pass were being placed on the map as if the drone had been sitting where it finally ended up. The far side of the fan came out smeared.
The fix: keep a small buffer of the drone’s pose for every angle during the pass, then place each point using the pose from the exact moment that reading was taken.
A quick numerical check made the win obvious:
| Pose handling | Average error (RMS) | Worst-edge error |
|---|---|---|
| Single end-of-pass pose (8° of drift) | 35 cm | 94 cm |
| Per-reading pose | ~0 | ~0 |
So with a realistic 8° of heading drift, the old method smeared points by a third of a metre on average and nearly a metre at the edge. Per-reading poses brought that to basically nothing.
4. Problem two — the curve that survived even when the drone sat perfectly still
Here is the twist. We fixed the pose problem and the fan was still curved when the drone was completely stationary. If the drone is not moving, pose cannot be the cause. So the curve had to be coming from the scan itself. It turned out to be two more issues hiding underneath.
4.1 The servo hadn’t finished going home
Each sweep starts by commanding the sensor to angle θ=0 and waiting 120 ms before the first reading. The trouble: at the end of the previous pass the servo was parked at the far end (θ=π). Driving from one end back to home takes around 520 ms — far longer than the 120 ms wait. So the very first readings of the new fan were taken while the servo was still moving into position. That bent the start side of the fan.
The fix: give the servo a proper home_settle_ms = 900 on step zero — wait until it has actually arrived before reading.
4.2 The real root cause — we were drawing commanded angles, not actual ones
This was the main thing Aleks had been seeing.
The fan in the GUI was being drawn from the commanded angles — the angles we asked the servo for. But a bench measurement showed the servo doesn’t sit exactly where you tell it:
Command the servo to
1.0rad and the joint actually settles at 1.06–1.075 rad — an offset of roughly +4°, and it converges slowly.
So the beam was pointing about four degrees off from where the drawing assumed it was, and the size of that gap changed across the sweep. Commanded angle ≠ true beam direction → a curved fan.
The fix: subscribe the point builder to the simulator’s joint_state topic and compute each point’s bearing from the actual angle of the servo joint, not the commanded one. Every recorded point now carries bearing_source: actual_joint.
4.3 The point-builder node wasn’t even running
Last piece: the node that publishes the corrected points (/scan/record) was not in the launch stack, so its topic was empty — which is why the interface had fallen back to drawing from the commanded-angle topic in the first place. We added it to the stack (scan_points, default on; training can switch it off).
5. Validation on the test stand
Live, on the base_stand_12x12 stand:
- Auto-scan off:
autoscan_nodeno longer appears inros2 node list. - Per-reading pose + actual angle: the fan registered 181 points per-ray, 181 poses in the buffer, ground truth present;
/scan/recordreportsregistration: per_rayandbearing_source: actual_joint. - Home-settle: the fine sweep now runs cleanly from the first reading.
- The final “is the fan actually straight now?” verdict is a visual one, left to Aleks looking at the GUI.
6. The contract for whoever draws the fan
To keep this fixed, the rule for the interface is simple:
- Draw the fan and its points from
/scan/record. Each point’sx, y, zis already the final world coordinate computed from the true servo angle — do not recompute it. - Do not draw from
/drone/sweep/result. That topic is for progress and status, not geometry. It carries commanded angles, which is exactly the trap we just climbed out of.
7. A dead end worth recording — CPU lidar
While digging, we checked whether a CPU-based lidar would dodge the angle problems. It is a dead end for now:
- The GPU lidar in this Gazebo release is inaccurate at shallow, glancing angles (a known, still-open simulator issue).
- A pure CPU lidar that skips rendering is simply not implemented in this version — the feature request has sat open since 2020 with no patch — so
gpu_lidaris the only option.
So shallow-angle noise is something we live with. That is actually fine: the mapper and the training are built to tolerate a noisy fan, and a noisy fan is realistic for the real TF-Luna hardware anyway.
8. Where this leaves us
- Awaiting Aleks’s eyeball verdict on fan straightness from
/scan/record. - If we ever want a partial-sector fan, we can add a
/drone/sweep/start_arctrigger. - The +4° servo offset could be tuned out at the controller level someday, but reading the actual angle already sidesteps it, so it is not urgent.
The lasting lesson: a single visual symptom — “the fan is curved” — was three independent bugs (a stale pose, an unsettled servo, and commanded-vs-actual angles) wearing one coat. Fixing only one of them would have left the fan looking less wrong but still wrong, which is the most confusing state of all.