Skip to content

Enable vehicle teleoperation#686

Open
jyue86 wants to merge 6 commits into
NVIDIA:mainfrom
jyue86:vehicle-teleop
Open

Enable vehicle teleoperation#686
jyue86 wants to merge 6 commits into
NVIDIA:mainfrom
jyue86:vehicle-teleop

Conversation

@jyue86

@jyue86 jyue86 commented Jun 15, 2026

Copy link
Copy Markdown

Description

The goal of this PR is to enable vehicle teleoperation via a drive-by-wire (DBW) solution. Input controls are from steering wheel/pedals or a keyboard setup for easier local testing, and sent to Comma.ai's Panda device. The Panda device takes care of sending the input control data as CAN messages to the car's CAN bus.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Testing

Testing done on physical vehicle.
TBD: Add demo videos and commands after I've added example code

Checklist

  • I have read and understood the contribution guidelines
  • I have run the linter and formatter with SKIP=check-copyright-year pre-commit run --all-files
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix/feature works (or explained why not)
  • I have signed off all my commits (git commit -s) per the DCO

Summary by CodeRabbit

  • New Features

    • Added vehicle teleoperation example with steering wheel, keyboard, and remote control interfaces
    • Added steering wheel plugin for Linux joystick device support
    • Added command recording and replay functionality for vehicle control logs
  • Tests

    • Added schema validation tests for steering wheel and vehicle control message types

@github-actions

Copy link
Copy Markdown
Contributor

📝 Docs preview is not auto-deployed for fork PRs.

A maintainer with write access to NVIDIA/IsaacTeleop can deploy a preview by
commenting /preview-docs on this PR. Once deployed, the preview
will live at:

https://nvidia.github.io/IsaacTeleop/preview/pr-686/

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a full vehicle teleoperation feature. It adds two FlatBuffers schemas (steering_wheel.fbs, vehicle_control.fbs) with corresponding C++ Catch2 tests and Python pybind11 bindings. A Linux-only steering_wheel_plugin C++ executable reads a joystick device via select()/ioctl, normalizes axes, and pushes SteeringWheelOutput flatbuffers at 90 Hz through an OpenXR SchemaPusher. Live and replay SteeringWheelTracker implementations and their factory wiring are added to the live_trackers and replay_trackers libraries. A SteeringWheelSource retargeting node and SteeringWheelInput tensor types are added to the retargeting engine. A VehicleControlRetargeter converts wheel samples to clamped VehicleControlCommands with deadzone/scaling configuration. The vehicle_teleop example provides three ZMQ-based Python workers (remote steering via OpenXR, keyboard, and Kia Panda via OpenDBC), MCAP command logging/replay, and four launcher shell scripts.

Sequence Diagram

sequenceDiagram
  participant SteeringWheelPlugin
  participant OpenXRSession
  participant IsaacRemoteSteeringWorker
  participant VehicleControlRetargeter
  participant ZMQ_PUB
  participant KiaPandaWorker
  participant PandaRunner

  SteeringWheelPlugin->>OpenXRSession: push SteeringWheelOutput FlatBuffer at 90 Hz
  IsaacRemoteSteeringWorker->>OpenXRSession: poll SteeringWheelTracker via DeviceIO session
  OpenXRSession-->>IsaacRemoteSteeringWorker: SteeringWheelOutputTrackedT
  IsaacRemoteSteeringWorker->>VehicleControlRetargeter: retarget(sample, sequence=N)
  VehicleControlRetargeter-->>IsaacRemoteSteeringWorker: VehicleControlCommand(steer, accel, throttle, brake)
  IsaacRemoteSteeringWorker->>ZMQ_PUB: JSON-encoded command at --rate-hz
  ZMQ_PUB->>KiaPandaWorker: topic + JSON payload
  KiaPandaWorker->>KiaPandaWorker: timeout check → neutral if stale
  KiaPandaWorker->>PandaRunner: apply CarControl actuator values
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Enable vehicle teleoperation' accurately and concisely captures the main feature introduced in this PR—adding vehicle teleoperation functionality as clearly demonstrated by the comprehensive changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

jyue86 added 6 commits June 22, 2026 22:50
Signed-off-by: Justin Yue <jyue009@ucr.edu>
Signed-off-by: Justin Yue <jyue009@ucr.edu>
Signed-off-by: Justin Yue <jyue009@ucr.edu>
Signed-off-by: Justin Yue <jyue009@ucr.edu>
Signed-off-by: Justin Yue <jyue009@ucr.edu>
Signed-off-by: Justin Yue <jyue009@ucr.edu>
@jyue86 jyue86 marked this pull request as ready for review June 23, 2026 09:42
Copilot AI review requested due to automatic review settings June 23, 2026 09:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds end-to-end support for vehicle teleoperation by introducing a steering-wheel DeviceIO data path, a normalized vehicle control command schema, and an example that publishes commands over ZMQ to a Panda (DBW) worker.

Changes:

  • Adds new FlatBuffer schemas + C++/Python bindings for SteeringWheelOutput and VehicleControlCommand, including schema tests.
  • Introduces a Linux steering wheel native plugin plus live/replay tracker support and retargeting-engine source/tensor types.
  • Adds a complete examples/vehicle_teleop workflow (remote steering worker, keyboard fallback, command logging/replay, Panda worker).

Reviewed changes

Copilot reviewed 60 out of 60 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
CMakeLists.txt Wires new example and plugin into top-level build options.
examples/vehicle_teleop/.gitignore Ignores logs, third-party clones, and venv for the example.
examples/vehicle_teleop/CMakeLists.txt Installs the vehicle teleop example (python + scripts + config).
examples/vehicle_teleop/README.md Documents setup and remote/vehicle/replay workflows.
examples/vehicle_teleop/config/steering_wheel_config.yaml Provides default axis mapping for a G920-style wheel.
examples/vehicle_teleop/python/pyproject.toml Declares example Python dependencies (mcap/zmq/panda deps, etc.).
examples/vehicle_teleop/python/vehicle_teleop/__init__.py Example package marker.
examples/vehicle_teleop/python/vehicle_teleop/command_log.py MCAP JSON-schema logging + reader for command/sample records.
examples/vehicle_teleop/python/vehicle_teleop/isaac_keyboard_control_worker.py Keyboard fallback publisher using the new vehicle retargeter.
examples/vehicle_teleop/python/vehicle_teleop/isaac_remote_steering_worker.py Remote worker that spawns the native plugin, reads DeviceIO, retargets, publishes ZMQ, and optionally logs MCAP.
examples/vehicle_teleop/python/vehicle_teleop/kia_panda_worker.py Vehicle-side ZMQ subscriber that writes commands to PandaRunner (or dry-run prints).
examples/vehicle_teleop/python/vehicle_teleop/replay_command_mcap.py Replays MCAP logs and prints retargeted command values (optional realtime pacing).
examples/vehicle_teleop/python/vehicle_teleop/vehicle_command.py Defines the example’s wire-format command dataclass and helpers.
examples/vehicle_teleop/scripts/replay_command_mcap.sh Convenience launcher for MCAP replay.
examples/vehicle_teleop/scripts/run_isaac_keyboard_control_worker.sh Convenience launcher for keyboard worker.
examples/vehicle_teleop/scripts/run_isaac_remote_steering_worker.sh Convenience launcher for remote steering worker.
examples/vehicle_teleop/scripts/run_kia_panda_worker.sh Convenience launcher for vehicle Panda worker (sets PYTHONPATH for thirdparty).
src/core/deviceio_base/cpp/inc/deviceio_base/steering_wheel_tracker_base.hpp Adds the tracker-impl interface for steering wheel data.
src/core/deviceio_trackers/cpp/CMakeLists.txt Adds steering wheel tracker sources/headers to the deviceio tracker library.
src/core/deviceio_trackers/cpp/inc/deviceio_trackers/steering_wheel_tracker.hpp New public tracker facade for steering wheel state.
src/core/deviceio_trackers/cpp/steering_wheel_tracker.cpp Implements the new tracker facade.
src/core/deviceio_trackers/python/deviceio_trackers_init.py Exposes SteeringWheelTracker in the Python tracker package.
src/core/deviceio_trackers/python/tracker_bindings.cpp Adds pybind bindings for SteeringWheelTracker.
src/core/live_trackers/cpp/CMakeLists.txt Adds live steering wheel tracker implementation to live trackers lib.
src/core/live_trackers/cpp/inc/live_trackers/live_deviceio_factory.hpp Adds factory support for live steering wheel tracker impl.
src/core/live_trackers/cpp/live_deviceio_factory.cpp Creates live steering wheel tracker impls and registers extension requirements.
src/core/live_trackers/cpp/live_steering_wheel_tracker_impl.cpp Implements live steering wheel schema tracker + optional MCAP recording channels.
src/core/live_trackers/cpp/live_steering_wheel_tracker_impl.hpp Declares live steering wheel tracker implementation.
src/core/mcap/cpp/inc/mcap/recording_traits.hpp Adds recording traits for steering wheel channels/schema naming.
src/core/python/deviceio_init.py Exposes SteeringWheelTracker + SteeringWheelOutput in isaacteleop.deviceio.
src/core/replay_trackers/cpp/CMakeLists.txt Adds replay steering wheel tracker implementation to replay trackers lib.
src/core/replay_trackers/cpp/inc/replay_trackers/replay_deviceio_factory.hpp Adds factory support for replay steering wheel tracker impl.
src/core/replay_trackers/cpp/replay_deviceio_factory.cpp Registers replay steering wheel tracker creation in the dispatch table.
src/core/replay_trackers/cpp/replay_steering_wheel_tracker_impl.cpp Implements replay steering wheel tracker from MCAP.
src/core/replay_trackers/cpp/replay_steering_wheel_tracker_impl.hpp Declares replay steering wheel tracker implementation.
src/core/retargeting_engine/python/deviceio_source_nodes/__init__.py Exposes SteeringWheelSource and its tensor types.
src/core/retargeting_engine/python/deviceio_source_nodes/deviceio_tensor_types.py Adds tensor wrapper types for tracked steering wheel data.
src/core/retargeting_engine/python/deviceio_source_nodes/steering_wheel_source.py New DeviceIO-to-retargeting-engine source node for steering wheel input.
src/core/retargeting_engine/python/tensor_types/__init__.py Exports SteeringWheelInput + index enum.
src/core/retargeting_engine/python/tensor_types/indices.py Adds SteeringWheelInputIndex.
src/core/retargeting_engine/python/tensor_types/standard_types.py Defines SteeringWheelInput tensor group type.
src/core/schema/fbs/steering_wheel.fbs New FlatBuffer schema for steering wheel output (+ tracked/record wrappers).
src/core/schema/fbs/vehicle_control.fbs New FlatBuffer schema for normalized vehicle control commands (+ record wrapper).
src/core/schema/python/CMakeLists.txt Adds new binding headers to the schema pybind module target.
src/core/schema/python/schema_init.py Exposes new schema types in the Python package API.
src/core/schema/python/schema_module.cpp Binds steering wheel and vehicle control types in _schema.
src/core/schema/python/steering_wheel_bindings.h Implements Python bindings for steering wheel schema types.
src/core/schema/python/vehicle_control_bindings.h Implements Python bindings for vehicle control schema types.
src/core/schema_tests/cpp/CMakeLists.txt Adds C++ schema tests for the new generated types.
src/core/schema_tests/cpp/test_steering_wheel.cpp C++ unit tests for generated steering wheel FlatBuffer types.
src/core/schema_tests/cpp/test_vehicle_control.cpp C++ unit tests for generated vehicle control FlatBuffer types.
src/core/schema_tests/python/test_vehicle_io.py Python unit tests for new schema bindings.
src/plugins/steering_wheel/CMakeLists.txt Adds Linux-only steering wheel plugin build/install rules.
src/plugins/steering_wheel/README.md Documents steering wheel plugin usage and axis mapping.
src/plugins/steering_wheel/main.cpp Entry point for the steering wheel plugin binary.
src/plugins/steering_wheel/plugin.yaml Plugin manifest metadata.
src/plugins/steering_wheel/steering_wheel_plugin.cpp Implements joystick reading + schema pushing for steering wheel output.
src/plugins/steering_wheel/steering_wheel_plugin.hpp Declares steering wheel plugin class and axis mapping.
src/retargeters/__init__.py Exposes the new vehicle control retargeter in the retargeters package API.
src/retargeters/vehicle_control_retargeter.py Adds steering wheel -> normalized vehicle control command retargeter.

Comment on lines +145 to +152
void SteeringWheelPlugin::close_device()
{
if (device_fd_ < 0)
return;

close(device_fd_);
device_fd_ = -1;
}
Comment on lines +52 to +56
def retarget(
self, sample: SteeringWheelLike | SteeringWheelOutput, *, sequence: int
) -> VehicleControlCommand:
steer = self._apply_deadzone(
(sample.steering - self._steering_neutral) * self._config.steer_scale,

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/vehicle_teleop/python/vehicle_teleop/command_log.py`:
- Around line 106-133: The context manager's resource cleanup is not
exception-safe because if an exception occurs during writer setup in the
__enter__ method or during finish() in the __exit__ method, the file descriptor
may never be closed, leaving resources open and potentially masking the original
error. Wrap the writer registration logic in __enter__ with a try/finally block
to ensure the file is closed if any exception occurs during setup, and wrap the
self._writer.finish() call in __exit__ with a try/finally block to ensure the
file close operation in the finally clause always executes even if finish()
throws an exception.

In
`@examples/vehicle_teleop/python/vehicle_teleop/isaac_keyboard_control_worker.py`:
- Line 119: Add validation for the self._rate_hz parameter to reject
non-positive values (zero or negative). Before the division operation at line
119 where period_s is calculated from 1.0 / self._rate_hz, ensure that
self._rate_hz is greater than zero by either validating it during initialization
of the class or immediately before it is used. This will prevent division by
zero crashes and ensure that sleep duration calculations at lines 149 and 198
receive valid positive rate values.

In
`@examples/vehicle_teleop/python/vehicle_teleop/isaac_remote_steering_worker.py`:
- Around line 192-193: The `--rate-hz` parameter stored in `self._rate_hz` is
not validated to be strictly positive, causing a ZeroDivisionError when
computing period_s at line 192 (division by zero) and potentially creating a
busy loop when negative values are used in operations at lines 230 and 339. Add
validation when the rate_hz parameter is parsed or set to ensure it is strictly
greater than 0, raising an appropriate error with a clear message if the
validation fails.

In `@examples/vehicle_teleop/python/vehicle_teleop/kia_panda_worker.py`:
- Around line 72-73: The ZMQ subscription at the setsockopt_string call is using
prefix-based matching which allows similarly-prefixed topics to pass through
unintended, and the message parsing logic (also referenced in the comment at
lines 92-95) lacks error handling for malformed frames or JSON payloads, causing
worker termination. Implement exact topic matching by filtering received
messages against the exact self._topic value instead of relying on ZMQ's prefix
subscription, and wrap all message frame parsing and JSON deserialization logic
in try-except blocks to gracefully handle and log malformed payloads without
crashing the worker loop.

In `@src/plugins/steering_wheel/steering_wheel_plugin.cpp`:
- Around line 145-152: In the close_device() method of the SteeringWheelPlugin
class, after closing the file descriptor with close(device_fd_), clear the
cached control state by resetting the member variables axes_, buttons_, and hat_
to their neutral/default states (typically empty or zero values). This prevents
push_current_state() from continuing to publish stale control commands after the
device has been disconnected or failed to open, which could cause stuck pedal or
button states.

In `@src/retargeters/vehicle_control_retargeter.py`:
- Around line 55-75: The issue is that non-finite values (NaN or infinity) in
the steering, throttle, and brake inputs are propagated through the _clamp and
_apply_deadzone functions unsafely, potentially saturating control commands. Add
validation checks for sample.steering, sample.throttle, and sample.brake to
ensure they are finite values before the current processing begins. If any input
is non-finite, replace it with 0.0 (neutral position) before applying the
deadzone and scaling operations to prevent NaN from being clamped to -1.0 which
would cause full steering deflection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d1297dbd-2b3a-45c4-a33b-cf18e841d491

📥 Commits

Reviewing files that changed from the base of the PR and between cfb8f3e and a51280b.

📒 Files selected for processing (60)
  • CMakeLists.txt
  • examples/vehicle_teleop/.gitignore
  • examples/vehicle_teleop/CMakeLists.txt
  • examples/vehicle_teleop/README.md
  • examples/vehicle_teleop/config/steering_wheel_config.yaml
  • examples/vehicle_teleop/python/pyproject.toml
  • examples/vehicle_teleop/python/vehicle_teleop/__init__.py
  • examples/vehicle_teleop/python/vehicle_teleop/command_log.py
  • examples/vehicle_teleop/python/vehicle_teleop/isaac_keyboard_control_worker.py
  • examples/vehicle_teleop/python/vehicle_teleop/isaac_remote_steering_worker.py
  • examples/vehicle_teleop/python/vehicle_teleop/kia_panda_worker.py
  • examples/vehicle_teleop/python/vehicle_teleop/replay_command_mcap.py
  • examples/vehicle_teleop/python/vehicle_teleop/vehicle_command.py
  • examples/vehicle_teleop/scripts/replay_command_mcap.sh
  • examples/vehicle_teleop/scripts/run_isaac_keyboard_control_worker.sh
  • examples/vehicle_teleop/scripts/run_isaac_remote_steering_worker.sh
  • examples/vehicle_teleop/scripts/run_kia_panda_worker.sh
  • src/core/deviceio_base/cpp/inc/deviceio_base/steering_wheel_tracker_base.hpp
  • src/core/deviceio_trackers/cpp/CMakeLists.txt
  • src/core/deviceio_trackers/cpp/inc/deviceio_trackers/steering_wheel_tracker.hpp
  • src/core/deviceio_trackers/cpp/steering_wheel_tracker.cpp
  • src/core/deviceio_trackers/python/deviceio_trackers_init.py
  • src/core/deviceio_trackers/python/tracker_bindings.cpp
  • src/core/live_trackers/cpp/CMakeLists.txt
  • src/core/live_trackers/cpp/inc/live_trackers/live_deviceio_factory.hpp
  • src/core/live_trackers/cpp/live_deviceio_factory.cpp
  • src/core/live_trackers/cpp/live_steering_wheel_tracker_impl.cpp
  • src/core/live_trackers/cpp/live_steering_wheel_tracker_impl.hpp
  • src/core/mcap/cpp/inc/mcap/recording_traits.hpp
  • src/core/python/deviceio_init.py
  • src/core/replay_trackers/cpp/CMakeLists.txt
  • src/core/replay_trackers/cpp/inc/replay_trackers/replay_deviceio_factory.hpp
  • src/core/replay_trackers/cpp/replay_deviceio_factory.cpp
  • src/core/replay_trackers/cpp/replay_steering_wheel_tracker_impl.cpp
  • src/core/replay_trackers/cpp/replay_steering_wheel_tracker_impl.hpp
  • src/core/retargeting_engine/python/deviceio_source_nodes/__init__.py
  • src/core/retargeting_engine/python/deviceio_source_nodes/deviceio_tensor_types.py
  • src/core/retargeting_engine/python/deviceio_source_nodes/steering_wheel_source.py
  • src/core/retargeting_engine/python/tensor_types/__init__.py
  • src/core/retargeting_engine/python/tensor_types/indices.py
  • src/core/retargeting_engine/python/tensor_types/standard_types.py
  • src/core/schema/fbs/steering_wheel.fbs
  • src/core/schema/fbs/vehicle_control.fbs
  • src/core/schema/python/CMakeLists.txt
  • src/core/schema/python/schema_init.py
  • src/core/schema/python/schema_module.cpp
  • src/core/schema/python/steering_wheel_bindings.h
  • src/core/schema/python/vehicle_control_bindings.h
  • src/core/schema_tests/cpp/CMakeLists.txt
  • src/core/schema_tests/cpp/test_steering_wheel.cpp
  • src/core/schema_tests/cpp/test_vehicle_control.cpp
  • src/core/schema_tests/python/test_vehicle_io.py
  • src/plugins/steering_wheel/CMakeLists.txt
  • src/plugins/steering_wheel/README.md
  • src/plugins/steering_wheel/main.cpp
  • src/plugins/steering_wheel/plugin.yaml
  • src/plugins/steering_wheel/steering_wheel_plugin.cpp
  • src/plugins/steering_wheel/steering_wheel_plugin.hpp
  • src/retargeters/__init__.py
  • src/retargeters/vehicle_control_retargeter.py

Comment on lines +106 to +133
def __enter__(self) -> "McapCommandLogger":
self._path.parent.mkdir(parents=True, exist_ok=True)
self._file = self._path.open("wb")
self._writer = Writer(self._file, compression=CompressionType.NONE)
self._writer.start()
schema_id = self._writer.register_schema(
name="vehicle_teleop.VehicleControlCommandRecord",
encoding="jsonschema",
data=json.dumps(VEHICLE_COMMAND_SCHEMA, separators=(",", ":")).encode(
"utf-8"
),
)
self._channel_id = self._writer.register_channel(
topic=VEHICLE_COMMAND_TOPIC,
message_encoding="json",
schema_id=schema_id,
)
return self

def __exit__(self, _exc_type, _exc_value, _traceback) -> None:
if self._writer is not None:
self._writer.finish()
self._writer = None
self._channel_id = None
if self._file is not None:
self._file.close()
self._file = None

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make context-manager cleanup exception-safe.

If setup or finish() throws, Line 130-132 may never run, leaving the file descriptor open and potentially masking the original failure. Guard writer setup/finish with try/finally so file close always executes.

Proposed fix
 class McapCommandLogger:
@@
     def __enter__(self) -> "McapCommandLogger":
         self._path.parent.mkdir(parents=True, exist_ok=True)
         self._file = self._path.open("wb")
-        self._writer = Writer(self._file, compression=CompressionType.NONE)
-        self._writer.start()
-        schema_id = self._writer.register_schema(
-            name="vehicle_teleop.VehicleControlCommandRecord",
-            encoding="jsonschema",
-            data=json.dumps(VEHICLE_COMMAND_SCHEMA, separators=(",", ":")).encode(
-                "utf-8"
-            ),
-        )
-        self._channel_id = self._writer.register_channel(
-            topic=VEHICLE_COMMAND_TOPIC,
-            message_encoding="json",
-            schema_id=schema_id,
-        )
+        try:
+            self._writer = Writer(self._file, compression=CompressionType.NONE)
+            self._writer.start()
+            schema_id = self._writer.register_schema(
+                name="vehicle_teleop.VehicleControlCommandRecord",
+                encoding="jsonschema",
+                data=json.dumps(VEHICLE_COMMAND_SCHEMA, separators=(",", ":")).encode(
+                    "utf-8"
+                ),
+            )
+            self._channel_id = self._writer.register_channel(
+                topic=VEHICLE_COMMAND_TOPIC,
+                message_encoding="json",
+                schema_id=schema_id,
+            )
+        except Exception:
+            if self._file is not None:
+                self._file.close()
+                self._file = None
+            self._writer = None
+            self._channel_id = None
+            raise
         return self
 
     def __exit__(self, _exc_type, _exc_value, _traceback) -> None:
-        if self._writer is not None:
-            self._writer.finish()
-            self._writer = None
-            self._channel_id = None
-        if self._file is not None:
-            self._file.close()
-            self._file = None
+        try:
+            if self._writer is not None:
+                self._writer.finish()
+        finally:
+            self._writer = None
+            self._channel_id = None
+            if self._file is not None:
+                self._file.close()
+                self._file = None
🧰 Tools
🪛 ast-grep (0.44.0)

[info] 113-113: use jsonify instead of json.dumps for JSON output
Context: json.dumps(VEHICLE_COMMAND_SCHEMA, separators=(",", ":"))
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/vehicle_teleop/python/vehicle_teleop/command_log.py` around lines
106 - 133, The context manager's resource cleanup is not exception-safe because
if an exception occurs during writer setup in the __enter__ method or during
finish() in the __exit__ method, the file descriptor may never be closed,
leaving resources open and potentially masking the original error. Wrap the
writer registration logic in __enter__ with a try/finally block to ensure the
file is closed if any exception occurs during setup, and wrap the
self._writer.finish() call in __exit__ with a try/finally block to ensure the
file close operation in the finally clause always executes even if finish()
throws an exception.

self._register_signal_handlers()
self._socket.setsockopt(zmq.LINGER, 0)
self._socket.bind(self._bind)
period_s = 1.0 / self._rate_hz

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject non-positive publish rates.

Line 119 divides by self._rate_hz; 0 crashes and negative values force near-zero sleep at Line 149.

Proposed fix
+def _positive_float(value: str) -> float:
+    parsed = float(value)
+    if parsed <= 0.0:
+        raise argparse.ArgumentTypeError("value must be > 0")
+    return parsed
+
 def build_parser() -> argparse.ArgumentParser:
@@
-    parser.add_argument("--rate-hz", type=float, default=50.0, help="Publish rate.")
+    parser.add_argument(
+        "--rate-hz",
+        type=_positive_float,
+        default=50.0,
+        help="Publish rate (Hz, must be > 0).",
+    )

Also applies to: 149-149, 198-198

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/vehicle_teleop/python/vehicle_teleop/isaac_keyboard_control_worker.py`
at line 119, Add validation for the self._rate_hz parameter to reject
non-positive values (zero or negative). Before the division operation at line
119 where period_s is calculated from 1.0 / self._rate_hz, ensure that
self._rate_hz is greater than zero by either validating it during initialization
of the class or immediately before it is used. This will prevent division by
zero crashes and ensure that sleep duration calculations at lines 149 and 198
receive valid positive rate values.

Comment on lines +192 to +193
period_s = 1.0 / self._rate_hz
print(

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate --rate-hz as strictly positive.

Line 192 divides by self._rate_hz; 0 raises ZeroDivisionError, and negative values create a busy loop at Line 230.

Proposed fix
+def _positive_float(value: str) -> float:
+    parsed = float(value)
+    if parsed <= 0.0:
+        raise argparse.ArgumentTypeError("value must be > 0")
+    return parsed
+
 def build_parser() -> argparse.ArgumentParser:
@@
-    parser.add_argument("--rate-hz", type=float, default=50.0, help="Publish rate.")
+    parser.add_argument(
+        "--rate-hz",
+        type=_positive_float,
+        default=50.0,
+        help="Publish rate (Hz, must be > 0).",
+    )

Also applies to: 230-230, 339-339

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/vehicle_teleop/python/vehicle_teleop/isaac_remote_steering_worker.py`
around lines 192 - 193, The `--rate-hz` parameter stored in `self._rate_hz` is
not validated to be strictly positive, causing a ZeroDivisionError when
computing period_s at line 192 (division by zero) and potentially creating a
busy loop when negative values are used in operations at lines 230 and 339. Add
validation when the rate_hz parameter is parsed or set to ensure it is strictly
greater than 0, raising an appropriate error with a clear message if the
validation fails.

Comment on lines +72 to +73
self._socket.setsockopt_string(zmq.SUBSCRIBE, self._topic)
self._socket.connect(self._connect)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Harden inbound ZMQ parsing (topic filter + malformed payload handling).

Current subscription is prefix-based, so similarly-prefixed topics can pass through, and malformed frames/JSON will raise and terminate the worker loop.

Proposed fix
     def _open_socket(self) -> None:
         self._socket.setsockopt(zmq.LINGER, 0)
         self._socket.setsockopt(zmq.CONFLATE, 1)
-        self._socket.setsockopt_string(zmq.SUBSCRIBE, self._topic)
+        self._socket.setsockopt_string(zmq.SUBSCRIBE, f"{self._topic} ")
         self._socket.connect(self._connect)
         self._poller.register(self._socket, zmq.POLLIN)
@@
     def _poll_once(self) -> bool:
         events = dict(self._poller.poll(timeout=self._poll_ms))
         if self._socket not in events:
             return False
 
-        message = self._socket.recv_string()
-        _topic, payload = message.split(" ", 1)
-        self._last_command = VehicleControlCommand.from_dict(json.loads(payload))
+        try:
+            message = self._socket.recv_string()
+            topic, payload = message.split(" ", 1)
+            if topic != self._topic:
+                return False
+            parsed = VehicleControlCommand.from_dict(json.loads(payload))
+        except (ValueError, json.JSONDecodeError, KeyError, TypeError):
+            return False
+
+        self._last_command = parsed
         self._last_received = time.monotonic()
         self._neutral_sent_after_timeout = False
         return True

Also applies to: 92-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/vehicle_teleop/python/vehicle_teleop/kia_panda_worker.py` around
lines 72 - 73, The ZMQ subscription at the setsockopt_string call is using
prefix-based matching which allows similarly-prefixed topics to pass through
unintended, and the message parsing logic (also referenced in the comment at
lines 92-95) lacks error handling for malformed frames or JSON payloads, causing
worker termination. Implement exact topic matching by filtering received
messages against the exact self._topic value instead of relying on ZMQ's prefix
subscription, and wrap all message frame parsing and JSON deserialization logic
in try-except blocks to gracefully handle and log malformed payloads without
crashing the worker loop.

Comment on lines +145 to +152
void SteeringWheelPlugin::close_device()
{
if (device_fd_ < 0)
return;

close(device_fd_);
device_fd_ = -1;
}

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Clear cached control state on device loss to prevent stuck commands.

close_device() only drops the FD, but push_current_state() continues publishing cached axes_/buttons_/hat_. If disconnect happens while a pedal is pressed, non-zero control can keep being sent after unplug/open failure.

Suggested fix
 void SteeringWheelPlugin::close_device()
 {
     if (device_fd_ < 0)
         return;

     close(device_fd_);
     device_fd_ = -1;
+    std::fill(axes_.begin(), axes_.end(), 0.0f);
+    std::fill(buttons_.begin(), buttons_.end(), 0);
+    hat_ = { 0, 0 };
 }

Also applies to: 154-179

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/steering_wheel/steering_wheel_plugin.cpp` around lines 145 - 152,
In the close_device() method of the SteeringWheelPlugin class, after closing the
file descriptor with close(device_fd_), clear the cached control state by
resetting the member variables axes_, buttons_, and hat_ to their
neutral/default states (typically empty or zero values). This prevents
push_current_state() from continuing to publish stale control commands after the
device has been disconnected or failed to open, which could cause stuck pedal or
button states.

Comment on lines +55 to +75
steer = self._apply_deadzone(
(sample.steering - self._steering_neutral) * self._config.steer_scale,
self._config.steering_deadzone,
)
throttle = self._apply_deadzone(
axis_to_pedal(sample.throttle) * self._config.throttle_scale,
self._config.pedal_deadzone,
)
brake = self._apply_deadzone(
axis_to_pedal(sample.brake) * self._config.brake_scale,
self._config.pedal_deadzone,
)
accel = _clamp(throttle - brake, -1.0, 1.0)

return VehicleControlCommand(
sequence=sequence,
steer=_clamp(steer, -1.0, 1.0),
accel=accel,
throttle=accel if accel > 0.0 else 0.0,
brake=-accel if accel < 0.0 else 0.0,
)

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.

🎯 Functional Correctness | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
def clamp(value, lo, hi):
    return min(hi, max(lo, float(value)))

nan = float("nan")
print("steer clamp:", clamp(nan, -1.0, 1.0))   # currently -1.0
print("pedal clamp:", clamp(nan, 0.0, 1.0))    # currently 0.0
PY

Repository: NVIDIA/IsaacTeleop

Length of output: 191


🏁 Script executed:

# First, explore the repository structure and locate the file
git ls-files | grep -E "(vehicle_control|retargeter)" | head -20

Repository: NVIDIA/IsaacTeleop

Length of output: 1412


🏁 Script executed:

# Check if the file exists and get its size
wc -l src/retargeters/vehicle_control_retargeter.py 2>/dev/null || echo "File not found"

Repository: NVIDIA/IsaacTeleop

Length of output: 205


🏁 Script executed:

# Map the file structure with ast-grep to understand context
ast-grep outline src/retargeters/vehicle_control_retargeter.py --view expanded

Repository: NVIDIA/IsaacTeleop

Length of output: 890


🏁 Script executed:

# Read the actual code at the specified lines
sed -n '1,10p' src/retargeters/vehicle_control_retargeter.py

Repository: NVIDIA/IsaacTeleop

Length of output: 477


🏁 Script executed:

# Read lines 52-90 to see the full context of retarget() and helper functions
sed -n '52,90p' src/retargeters/vehicle_control_retargeter.py

Repository: NVIDIA/IsaacTeleop

Length of output: 1537


🏁 Script executed:

# Check for any existing NaN/finite validation in the file
rg -i "isfinite|nan|math\.is" src/retargeters/vehicle_control_retargeter.py

Repository: NVIDIA/IsaacTeleop

Length of output: 156


🏁 Script executed:

# Check if math module is already imported
rg "^import|^from" src/retargeters/vehicle_control_retargeter.py | head -10

Repository: NVIDIA/IsaacTeleop

Length of output: 327


Guard against non-finite inputs in vehicle control retargeting to prevent saturated commands.

The current _clamp() function propagates NaN values through min/max comparisons in an unsafe way. Testing shows _clamp(NaN, -1.0, 1.0) yields -1.0, which could saturate steering to full left turn. Validate steering, throttle, and brake inputs as finite before applying deadzone/clamp, defaulting invalid values to 0.0 (neutral).

🔧 Proposed fix
+import math
 from dataclasses import dataclass
 from typing import Protocol
@@
     def retarget(
         self, sample: SteeringWheelLike | SteeringWheelOutput, *, sequence: int
     ) -> VehicleControlCommand:
+        steering_in = _finite_or_zero(sample.steering)
+        throttle_in = _finite_or_zero(sample.throttle)
+        brake_in = _finite_or_zero(sample.brake)
+
         steer = self._apply_deadzone(
-            (sample.steering - self._steering_neutral) * self._config.steer_scale,
+            (steering_in - self._steering_neutral) * self._config.steer_scale,
             self._config.steering_deadzone,
         )
         throttle = self._apply_deadzone(
-            axis_to_pedal(sample.throttle) * self._config.throttle_scale,
+            axis_to_pedal(throttle_in) * self._config.throttle_scale,
             self._config.pedal_deadzone,
         )
         brake = self._apply_deadzone(
-            axis_to_pedal(sample.brake) * self._config.brake_scale,
+            axis_to_pedal(brake_in) * self._config.brake_scale,
             self._config.pedal_deadzone,
         )
@@
+def _finite_or_zero(value: float) -> float:
+    v = float(value)
+    return v if math.isfinite(v) else 0.0
+
+
 def _clamp(value: float, lower: float, upper: float) -> float:
     return min(upper, max(lower, float(value)))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/retargeters/vehicle_control_retargeter.py` around lines 55 - 75, The
issue is that non-finite values (NaN or infinity) in the steering, throttle, and
brake inputs are propagated through the _clamp and _apply_deadzone functions
unsafely, potentially saturating control commands. Add validation checks for
sample.steering, sample.throttle, and sample.brake to ensure they are finite
values before the current processing begins. If any input is non-finite, replace
it with 0.0 (neutral position) before applying the deadzone and scaling
operations to prevent NaN from being clamped to -1.0 which would cause full
steering deflection.

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.

2 participants