Skip to content

A browser can fly the simulator, and it is a ground station - #67

Merged
aldwinhermanudin merged 2 commits into
mainfrom
sim45-web-interface
Sep 3, 2026
Merged

aldwinhermanudin merged 2 commits into
mainfrom
sim45-web-interface

Conversation

@aldwinhermanudin

Copy link
Copy Markdown
Contributor

Three site-survey failures in a row — a tree over the takeoff point, a parked car under the
landing point, a takeoff corridor — came down to where the aircraft was put, and each was
diagnosed by teleporting a disarmed vehicle and photographing it through a throwaway script.
This replaces that with a page you can fly.

Take off, land, translate, yaw. Chase, onboard and depth cameras, one large and two beside
it. GPS, local position, speed, attitude, IMU and battery — read from the same /fmu/out topics
a real Pixhawk publishes.

Making a general bridge obey hard stop 1

rosbridge_suite is wrapped rather than rewritten, per the reuse rule. It exists to expose a
whole ROS graph to a browser, which is exactly the wrong default for a page with a button that
arms an aircraft — so most of the work here is narrowing it:

measure effect
topics_pub_glob = ['/mission/command'] one writable topic. /fmu/in/vehicle_command unreachable — measured: 0 bytes on that topic from a browser that tried
services_glob / actions_glob = [] no node's set_parameters is reachable
proof-of-sim interlock in the node that arms ros2 topic pub walks straight past a transport, so the check cannot live in one
loopback by default; WEB_BIND names addresses publishing beyond loopback is a deliberate act with a loud warning

Three properties of rosbridge 2.7.0 that this depends on were read out of its source, not its
README — notably that an unset glob means no checking at all, so security is opt-in and a
missing line opens the bridge.

It is a ground station, in its own container

On real hardware the ROS 2 graph runs on the companion computer and a web page that flies the
aircraft runs on someone's laptop. sim-qgc has modelled that boundary since the Gazebo era;
sim-webui is its sibling. It carries no AirSim client and none of our own nodes.

The chase camera pointedly did not follow it — a real aircraft has no chase camera, so it
stays on the simulator side. Same for depth: 32FC1 metres is 1.2 MB/frame, so it is colourised
beside the source and shipped as JPEG.

sim_up.sh now holds its container list once, and a test asserts nothing else names containers
— the two hand-written copies were one edit away from a container left running under a teardown
that printed "verified".

Review

/review at high effort: 11 findings, all legitimate, all fixed, with 10 new regression
guards. Two were in the safety machinery itself and both were mine:

  • a blocking RPC on the thread that keeps PX4 in offboard — a stalled AirSim could starve the
    setpoint stream past COM_OF_LOSS_T and drop the aircraft into failsafe
  • an altitude fence that commanded a 40 m descent on a yaw keypress — it clamped the absolute
    target, so a delta of (0,0,0) at 100 m snapped the hold point to the 60 m ceiling

Both flown afterwards at 80 m, deliberately above the ceiling — the configuration that made
them reachable. The sortie flown when the feature was built was at 12 m and could never have
exposed either. A flight test that passes is not coverage.

Verification

  • 332 off-target tests, local CI tier 1 green
  • hand sorties over CitySample: 12.0 m → 11.98 m, 6.0 m → 6.14 m, 80 m with yaw and translate
  • movement proved body-frame in the air: 4 × forward went east; after yawing 90° left, forward
    went north
  • the leash probed deliberately — step 500.0 m -> 5.0 m, climb -500.0 m -> -5.0 m,
    altitude -2.0 m -> floor 2.0 m, every clamp logged
  • gate unchanged at 2/3. Seed 2's parked-car landing fails identically on the code from
    before this branch — established by reverting offboard_control.py to HEAD and reflying it
    (z range 0.026 vs 0.023 m, descent +1.73 vs +1.76 m/s)

Also here

The SIM-27 witness now brackets a flight instead of watching it, with no vendor patch — the
direct fix worked, was measured working, and was deleted because patching an upstream so our
tool stops breaking it inverts the primary rule. Its worklog is rewritten, at the instruction in
docs/handoff-2026-09-01.md, because the original described the fix that was thrown away.

Known limits

  • No authentication. Anything that can reach the page can fly the simulator. SITL-only, and
    the interlock refuses take-off unless AirSim's RPC answers.
  • The downward site survey — SIM-44's open half, and the reason seed 2 still fails — is now
    possible from a browser but has not been flown.

SIM-45 through SIM-49. Three site-survey failures in a row came down to where
the aircraft was put, and each was diagnosed by teleporting a disarmed vehicle
and photographing it through a throwaway script. This replaces that with a page.

Take off, land, translate, yaw. Chase, onboard and depth cameras, one large and
two beside it. GPS, local position, speed, attitude, IMU, battery -- read from
the same /fmu/out topics a real Pixhawk publishes.

The transport is rosbridge, wrapped rather than rewritten, and the work was
making a deliberately general bridge obey hard stop 1:

  * topics_pub_glob allows exactly /mission/command. Enforced in publish.py
    before the registration is created, so /fmu/in/vehicle_command is
    unreachable. Measured: 0 bytes on that topic from a browser that tried.
  * services and actions are closed outright, so no node's set_parameters is
    reachable.
  * a proof-of-sim interlock lives in the node that arms, not in the transport,
    because ros2 topic pub walks straight past a transport.
  * loopback by default; WEB_BIND names addresses deliberately.

The ground station is its own container. On real hardware the ROS 2 graph runs
on the companion computer and a web page that flies the aircraft runs on someone's
laptop, and sim-qgc has modelled that boundary since the Gazebo era. sim-webui
carries no AirSim client and none of our nodes. The chase camera pointedly did
not follow it: a real aircraft has no chase camera, so it stays with the simulator.

Movement is bounded by a leash in the node, never in the page: a per-step cap, a
radius around the take-off point, and an altitude band, each clamped and logged
rather than silently refused.

The depth camera is 32FC1 metres, which web_video_server cannot render, so it is
colourised beside the source and published as a JPEG -- with a fixed range, so a
colour means the same distance in every frame, and with no-return given its own
colour rather than the far end of the ramp.

Also here: the SIM-27 witness now brackets a flight instead of watching it, with
no vendor patch. Its worklog is rewritten, at the instruction in the handoff,
because the original described the fix that was thrown away.

Eleven review findings fixed, two of which were in the safety machinery itself: a
blocking RPC on the thread that keeps PX4 in offboard, and an altitude fence that
commanded a 40 m descent on a yaw keypress. Both flown at 80 m afterwards, which
is the configuration that made them reachable -- the sortie flown when the feature
was built was at 12 m and could never have exposed either.

332 off-target tests. Gate unchanged at 2/3; seed 2's parked-car landing fails
identically on the code from before this branch.
`tests/test_depth_view.py` imports numpy -- the depth colouriser's own dependency,
where the fixed-range and no-return decisions live -- and the CI workflow installed
only pytest and pyyaml. The full suite passed locally, because this machine has
numpy, and the required check went red on the pull request with a collection error.

That is the same shape as the rule already learned about vendor/: a test that needs
something the runner does not have can only ever pass on the developer's machine.
One was reading a vendored file, this one is importing a library, and both look
green in exactly the place that does not matter.

So the workflow installs numpy, and tests/test_ci_dependencies.py parses every
top-level import in tests/ and fails if one is missing from the pip line.
Guarded imports are exempt: pytest.importorskip degrades to a skip rather than
taking collection down with it. Verified by removing numpy from the workflow and
watching the new test go red.
@aldwinhermanudin
aldwinhermanudin merged commit 4b19478 into main Sep 3, 2026
1 check passed
@aldwinhermanudin
aldwinhermanudin deleted the sim45-web-interface branch September 3, 2026 00:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant