Skip to content

feat(px4): agent skills and agentic blueprints (PX4 stack 4/5) - #4292

Open
Ez4ezka wants to merge 4 commits into
dimensionalOS:mainfrom
Ez4ezka:ezen/feat/px4-4-skills
Open

Ez4ezka wants to merge 4 commits into
dimensionalOS:mainfrom
Ez4ezka:ezen/feat/px4-4-skills

Conversation

@Ez4ezka

@Ez4ezka Ez4ezka commented Sep 25, 2026 •

Copy link
Copy Markdown

What is this feature?

Natural-language control of the drone.

  • Px4SkillContainer (skill_container.py): takeoff, go_to, land, set_guidance_mode, flight_status. Each skill calls the connection RPC and waits for the outcome, so the supervisor refuses a skill exactly as it refuses the RPC. A pilot takeover, abort or E-STOP is reported to the agent as such. Follows UnitreeSkillContainer.
  • connection_spec.py: the connection RPCs the skills use.
  • blueprints_agentic.py: px4-agentic, px4-sitl-agentic. Separate file because it needs the agents extra.

Why do we need this?

Lets the dimOS agent fly the drone like the other robots.

How to Test

uv sync --extra px4 --extra agents
uv run pytest dimos/robot/px4/test_skill_container.py
# PX4 SITL running, a GCS heartbeat present, OPENAI_API_KEY set:
dimos run px4-sitl-agentic
dimos agent-send "take off to 3 meters"

In SITL the agent flew "take off to 3 meters", "fly 3 meters north" (3.03 m), "what is your flight status?" and "land".

Stack

Five PRs, all against main; each contains the ones above it. Merge in order. Review only this PR's own commit: 5ee3057

  1. feat(hardware): SIYI A8 gimbal and RTSP H.265 camera (PX4 stack 1/5) #4289 SIYI A8 gimbal and RTSP camera
  2. feat(msgs): NavSatFix, BatteryState and PX4 VehicleStatus (PX4 stack 2/5) #4290 NavSatFix, BatteryState, PX4 VehicleStatus
  3. feat(px4): Px4DroneConnection, flight supervisor and SITL gate (PX4 stack 3/5) #4291 PX4 connection, flight supervisor, SITL gate
  4. feat(px4): agent skills and agentic blueprints (PX4 stack 4/5) #4292 agent skills (this PR)
  5. feat(px4): target following on dimos/perception detections (PX4 stack 5/5) #4293 target following on dimos/perception detections

Which issue(s) does this PR close?

None. New platform: PX4 multicopters with a SIYI A8 gimbal camera.

Checklist

  • I have read and approved the CLA.

🤖 Generated with Claude Code

RtspCamera (dimos/hardware/sensors/camera/rtsp): RTSP URL, file or a
generated clip in; video (encoded, untouched), color_image and color_jpeg
out. PyAV only, url is required, set_video_enabled() and set_jpeg_rate()
are RPCs. Blueprint rtsp-camera-vis.

SiyiA8Gimbal (dimos/hardware/gimbal/siyi): gimbal tf chain, camera_info
(a8_camera_info() is the one source of the A8 intrinsics), aim requests
on gimbal_target. ip is a config field with no default; with it set the
SIYI SDK client polls the zoom and camera_info is withheld off 1x.

New px4 extra: av.

Tests: pytest dimos/hardware/gimbal/siyi dimos/hardware/sensors/camera/rtsp
-> 37 passed (gimbal 22, camera 15).
sensor_msgs.NavSatFix and sensor_msgs.BatteryState wrap the dimos_lcm
types and take their enum values from them. px4_msgs.VehicleStatus is a
hand-written LCM type with the wire layout of sensor_msgs/ImuInfo
(fingerprint, Header, big-endian fields); its base hash is an arbitrary
constant, there is no .lcm schema. The wire helpers are in
dimos/msgs/lcm_wire.py.

No registry, pyproject or lock change. The producer is Px4DroneConnection
in the next commit: its gps, battery and vehicle_status outputs.

Tests: pytest dimos/msgs/px4_msgs dimos/msgs/sensor_msgs/test_NavSatFix.py
dimos/msgs/sensor_msgs/test_BatteryState.py -> 6 passed (43 with the
payload tests below).
dimos/robot/px4:
- connection.py: Px4DroneConnection, the one MAVLink link. The vehicle as
  streams (odometry, imu, gps, battery, gimbal_attitude, vehicle_status)
  and the operator RPCs takeoff, go_to, land, hold, set_guidance_mode,
  estop*. No arm, mode or raw-setpoint RPC.
- supervisor_core.py: 20 Hz state machine, no I/O. Preflight, RC enable
  switch, fence and ceiling, pilot override, E-STOP latch, go-to, TELEOP.
  When the tick stops the setpoints stop and PX4's Offboard-loss failsafe
  takes over.
- mavlink.py: pymavlink socket, vehicle state, PX4 modes, NED/FLU frames.
- blueprints.py: px4-basic, px4-drone, px4-sitl, px4-teleop,
  px4-sitl-teleop. tool_sitl_gate.py runs px4-sitl against PX4 SITL.

px4 extra += pymavlink; stubs/pymavlink/mavutil.pyi is extended for mypy.

Tests: pytest dimos/robot/px4 -> 103 passed (connection 10, mavlink 21,
supervisor_core 4, supervisor_operator 68); 146 with the payload and
message tests below.
Px4SkillContainer: takeoff, go_to, land, set_guidance_mode, flight_status.
Each skill is the connection RPC plus a wait for the outcome, so the
supervisor refuses a skill exactly as it refuses the RPC; a takeover while
the hover settles is reported, not read as an arrival.

connection_spec.py holds the RPCs the skills call. px4-agentic and
px4-sitl-agentic are in blueprints_agentic.py because they need the agents
extra. supervisor_core.py gains TAKEOFF_STATES, which the takeoff skill
waits on.

Tests: pytest dimos/robot/px4/test_skill_container.py -> 13 passed, against
a scripted connection plus one contract test against the real
Px4DroneConnection and SupervisorCore.status(); pytest dimos/robot/px4 ->
116 passed; 159 with the payload and message tests below.
@greptile-apps

greptile-apps Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 0/5

[Medium risk] Adds gimbal, camera, and PX4 agent modules with new message types.

Not safe to merge: flight safety gates, operator feedback, video and gimbal behavior, and credential-safe logging need fixes.

Findings

  1. P1 Security RTSP passwords enter logs ▶
  2. P1 Video drops can freeze playback ▶
  3. P1 Aim requests lack a consumer ▶
  4. P1 Preflight accepts unusable telemetry ▶
  5. P1 Other traffic masks lost heartbeats ▶
  6. P1 Unsettled maneuvers report success ▶
  7. P1 SITL mode bypasses RC gating ▶
  8. P1 Lost zoom replies retain calibration ▶

Summary

This PR adds PX4 flight control and skills, RTSP video, SIYI gimbal support, messages, blueprints, and tests. Flight can proceed with missing or stale safety data, or without the physical RC enable condition; skills can report success before settling. Gimbal aim requests have no PX4 consumer, dropped video can freeze decoding, stale zoom can produce incorrect calibration, and RTSP credentials can enter logs. These issues must be fixed before merging.

Reviews (1) · Last reviewed commit: "feat(px4): agent skills and agentic blue..."

self.relay_once()
except (av.FFmpegError, OSError, IndexError, ValueError) as exc:
self._count("video", errors=1)
logger.warning("camera stream unavailable", url=self._source, error=str(exc))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security RTSP passwords enter logs

If the configured RTSP URL contains a password, stream failure logs the URL and potentially unredacted error text; a disconnect also logs the URL. Anyone with access to console or persistent logs can read the password. Redact credentials from both logged fields before merging.

How this was verified: A synthetic password appeared in console and JSON logs when the failure and disconnect paths ran.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Artifacts

Synthetic RTSP logging check script

  • This authored script drives the real relay loop through simulated failure and disconnect branches without real credentials or a camera connection.

Stream logs before redaction

  • Running the unchanged logging path returned exit code 0 and exposed the synthetic password in both branches.

Stream logs with illustrative runtime redaction

  • Running the same branches with a runtime-only redacting logger returned exit code 0 and kept the synthetic password out of both logs.

View artifacts

T-Rex Ran code and verified through T-Rex

# Out of the connection: the supervisor.
("supervisor_state", String): _zenoh_transport("supervisor_state", String),
# Camera and gimbal.
("video", CompressedVideo): _zenoh_transport("video", CompressedVideo, latest_wins=True),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Video drops can freeze playback

If congestion drops an H.265 access unit, later units can depend on the missing data. This stream uses droppable delivery, so the operator's video can freeze instead of merely skipping a frame. Preserve the access-unit sequence or provide tested loss recovery before merging.

Artifacts

Exact Python reproduction script

  • The authored script generates H.265 camera output, reads the PX4 transport QoS, and decodes intact or loss-injected access units; it is the execution source for both captures.

Decoder output with every access unit delivered

  • Running the reproduction script with `--condition intact` decoded all 50 frames without errors; the complete stream remains decodable.

Decoder output after one access unit was discarded

  • Running the same script with `--condition loss` discarded access unit 10 and decoded only 10 frames; one loss can freeze the remaining clip.

View artifacts

T-Rex Ran code and verified through T-Rex

if now - self._last_aim_mono < 1.0 / self.config.aim_hz:
return False
self._last_aim_mono = now
self.gimbal_target.publish(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Aim requests lack a consumer

aim() and the line-of-sight path publish gimbal_target requests, but the PX4 connection has no matching input or MAVLink command path. An aim request can be reported as published while the physical gimbal does not move. Wire a consumer and command path before merging.

Artifacts

Synthetic gimbal service-contract probe

  • The authored script runs the real aim and line-of-sight code against a fake consumer and the checked-out PX4 contract, without aircraft hardware.

Aim requests with a synthetic wired consumer

  • The executed control run published two aim requests and delivered both to a fake consumer, showing what a wired path would receive.

Aim requests with the checked-out PX4 connection

  • The executed checked-out-contract run published two aim requests but delivered zero commands because PX4 exposes no matching input.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +264 to +270
if st.gps is None or st.gps.fix < c.min_fix_type:
f.append("GPS fix")
elif not math.isnan(st.gps.eph) and st.gps.eph > c.max_eph_m:
f.append(f"eph {st.gps.eph:.1f}")
batt = st.batt_pct
if batt >= 0 and batt < c.min_batt_pct:
f.append(f"battery {batt}%")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Preflight accepts unusable telemetry

Preflight accepts an unknown battery percentage, including when the battery measurement is unavailable, and does not check the age of GPS or battery data. It can proceed toward takeoff without usable current safety measurements. Require valid, fresh measurements before merging.

Artifacts

Synthetic preflight reproduction script

  • The authored Python harness runs the real supervisor preflight path against synthetic telemetry and a fake actuator, without contacting hardware.

Current preflight behavior with missing and aged telemetry

  • The captured command output shows all four defective telemetry cases entering STREAMING, confirming that current preflight accepts them.

Preflight behavior with an in-memory freshness guard

  • The captured command output shows an uncommitted subclass guard retaining all four defective cases in PREFLIGHT while accepting healthy telemetry, demonstrating the comparison without changing production files.

View artifacts

T-Rex Ran code and verified through T-Rex

# HEARTBEAT is 1 Hz and px4_stale_s is 1.0 s, so heartbeat age alone sits on the
# threshold and a few ms of jitter would abort a flight (seen in SITL). Any message
# from 1/1 proves the link.
return min(st.heartbeat_age, st.px4_msg_age)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Other traffic masks lost heartbeats

When heartbeats stop but other PX4 messages continue, taking the minimum message age treats the heartbeat as fresh. The supervisor can keep sending guidance setpoints using armed and mode information it can no longer verify instead of aborting. Check heartbeat freshness separately before merging.

Artifacts

Authored supervisor reproduction script

  • This script calls the real supervisor methods with fake telemetry and compares repository behavior with a process-local age-policy alternative, without editing tracked files.

Repository behavior with a stale heartbeat

  • The captured command exited 0 and shows GOTO sending a velocity setpoint with an eight-second-old heartbeat, confirming the defect.

Process-local alternative with a stale heartbeat

  • The captured command exited 0 and shows the same telemetry causing ABORT and no setpoint after an in-memory age-policy change, demonstrating the expected safety distinction.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +100 to +103
status = self._poll(_settled, self.config.settle_timeout_s)
if status["state"] not in GUIDANCE_STATES: # pilot, abort, E-STOP: no setpoint left
return f"{said}, then interrupted: {status['reason']}. {_where(status)}"
return f"{said}. {_where(status)}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unsettled maneuvers report success

When settling times out outside the 15 cm tolerance, _poll() returns its last status and _where_settled() still says “Arrived” or “Took off” if guidance remains active. The operator or agent is told the maneuver settled when it did not. Report the unsettled outcome before merging.

Artifacts

Synthetic skill service and timer harness

  • The authored executable harness compiles the relevant source and calls both skills against deterministic statuses without editing tracked files.

Current skill responses after settling times out

  • The captured before run shows both skills reporting success at 0.50 m error after the 2.00 s timeout.

Skill responses with an in-memory unsettled-status guard

  • The captured after run uses the same statuses and timers with an in-memory guard and shows both skills reporting that they did not settle.

View artifacts

T-Rex Ran code and verified through T-Rex

cfg = self.config
self._acquire_writer_lock()
self._state = VehicleState(gimbal_mount=MOUNT_PRESETS[cfg.gimbal_mount_preset])
self._core = SupervisorCore(cfg.limits, cfg.guidance, sitl=cfg.sitl)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 SITL mode bypasses RC gating

If a physical-aircraft connection is configured with sitl=True, it accepts sitl_enable(True) independently of the MAVLink destination. That software flag replaces the physical RC enable switch, allowing preflight and continued guidance without fresh RC input. Prevent this override on physical-aircraft connections before merging.

Artifacts

Safe PX4 service-contract reproduction script

  • The authored script invokes the connection and supervisor with fake vehicle state, I/O, and workers, allowing both constructor behaviors to be compared without aircraft contact.

Service responses with the RC safety gate retained

  • The captured command output shows the in-memory `sitl=False` baseline rejecting the enable override and waiting for fresh RC input.

Service responses with configured SITL on a physical target

  • The captured command output shows repository HEAD accepting the enable override, passing preflight without RC telemetry, and omitting the RC abort reason.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +240 to +242
if zoom is not None: # a lost reply keeps the last known zoom
with self._lock:
self._zoom = zoom

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Lost zoom replies retain calibration

If zoom replies stop after a 1× reading, this loop retains that reading indefinitely. Should the camera zoom change during the outage, publish_camera_info() continues sending 1× intrinsics, giving geometry consumers incorrect calibration. Treat a lost reply as unknown zoom before merging.

Artifacts

Fake SDK zoom-outage reproduction script

  • The authored script polls a fake SDK and invokes the real CameraInfo publication path, with the proposed fix applied only in memory for comparison.

CameraInfo publication before the fix

  • Running the repository code after a 1x reply and a lost reply shows CameraInfo still published after the camera changes to 2.5x.

CameraInfo withheld with the in-memory fix

  • Running the same SDK sequence with only the zoom-loop replacement in memory shows unknown zoom and no new CameraInfo publication.

View artifacts

T-Rex Ran code and verified through T-Rex

@greptile-apps

greptile-apps Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P1 RTSP password exposed in stream-failure and disconnect logs ▶

    • Bug
      • A URL containing credentials is emitted to console and JSON logs when opening the stream fails or a live stream ends. The failure log can also repeat it in the error field.
    • Cause
      • _relay_loop() passes self._source directly as url at lines 177 and 185 and passes the unredacted exception text as error at line 177.
    • Fix
      • Redact URL userinfo before logging and sanitize exception text that may contain the URL; retain the original source only for connection attempts.
  • P1 Droppable QoS can freeze the PX4 H.265 video stream ▶

    • Bug
      • A single discarded access unit stopped the synthetic decoder after frame 10, rather than merely losing one frame. An operator viewing this stream could see frozen video until a decodable refresh.
    • Cause
      • dimos/robot/px4/blueprints.py:97 assigns latest-wins to CompressedVideo, producing Zenoh best_effort reliability and drop congestion control for a dependent H.265 access-unit stream.
    • Fix
      • Use delivery that does not discard video access units under congestion, with appropriate backpressure or bitrate control; alternatively, provide a video-specific loss-recovery strategy and verify decoder recovery.
  • P1 Gimbal aim requests have no PX4 consumer ▶

    • Bug
      • aim() reports success and target_los produces aim requests, but neither can command the gimbal through Px4DroneConnection.
    • Cause
      • dimos/hardware/gimbal/siyi/gimbal.py:200 publishes gimbal_target, while dimos/robot/px4/connection.py:167-169 exposes only cmd_vel as an input; the connection has no matching gimbal command RPC, and dimos/robot/px4/blueprints.py:79-102 has no explicit gimbal_target transport.
    • Fix
      • Add a gimbal_target consumer and MAVLink command path to the connection, then wire the stream in the PX4 blueprint and test delivery end to end.
  • P1 Unavailable or aged GPS and battery measurements pass preflight ▶

    • Bug
      • Preflight accepts a missing SYS_STATUS or battery percentage of -1, and accepts GPS and battery measurements aged 120 seconds while the other synthetic telemetry is fresh. The supervisor proceeds to STREAMING rather than waiting for usable measurements.
    • Cause
      • preflight_failures() checks GPS fix and accuracy without checking the GPS measurement time. Its battery condition only rejects nonnegative percentages below the threshold, and it does not check SYS_STATUS presence or measurement time.
    • Fix
      • Fail preflight when SYS_STATUS is absent, battery percentage is unknown, or GPS or battery measurements exceed an appropriate freshness limit. Keep the supervisor in PREFLIGHT until valid measurements arrive.
  • P1 Fresh PX4 traffic masks a stale heartbeat during flight ▶

    • Bug
      • With heartbeat_age=8.0s, px4_msg_age=0.02s, and fresh local position, the supervisor remains in GOTO and sends a velocity setpoint instead of aborting. Preflight also reports no heartbeat failure.
    • Cause
      • _px4_age takes the minimum of heartbeat and message ages at dimos/robot/px4/supervisor_core.py:256, so unrelated fresh traffic hides an indefinitely stale heartbeat.
    • Fix
      • Check heartbeat freshness independently of link traffic, with a heartbeat-specific threshold that tolerates normal 1 Hz timing jitter; require both checks before preflight or continued guidance.
  • P1 Settling timeout still produces success responses ▶

    • Bug
      • Both go_to and takeoff tell the caller the vehicle arrived or took off even when it remains beyond the 15 cm settling threshold after the timeout.
    • Cause
      • _where_settled at dimos/robot/px4/skill_container.py:100-103 checks for interrupted guidance but never checks whether the final status satisfies _settled before returning the success message.
    • Fix
      • After polling, check _settled(status) and return an explicit failure or unsettled result when the deadline expires outside tolerance.
  • P1 SITL configuration can disable physical-aircraft RC safety gates ▶

    • Bug
      • With a physical target configured and RC telemetry absent, the public sitl_enable(True) RPC is accepted when sitl=True. Preflight then advances toward Offboard despite the RC switch being off, and the RC-loss abort check no longer stops flight.
    • Cause
      • connection.py:210 passes configurable cfg.sitl directly to SupervisorCore; its SITL path uses a caller-set fake enable value and skips RC freshness checks.
    • Fix
      • Prevent SITL mode from being used with physical-aircraft targets, and ensure a remotely callable enable override cannot replace physical RC enable and freshness checks.
  • P1 Stale 1x zoom allows incorrect CameraInfo during SDK reply outage ▶

    • Bug
      • When the camera changes from 1x to 2.5x while zoom replies are unavailable, CameraInfo continues to publish intrinsics calibrated for 1x.
    • Cause
      • gimbal.py:240-242 updates _zoom only for non-None replies, retaining the last 1x value when query_zoom() returns None.
    • Fix
      • Set _zoom to None when a poll receives no reply, so publish_camera_info() withholds intrinsics until zoom is known again.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

first-time-contributor PR opened by an author who had not previously committed to this repository

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant