{
    "componentChunkName": "component---src-templates-page-js",
    "path": "/isaac/2025-09-robot-radio-control/",
    "result": {"data":{"site":{"siteMetadata":{"title":"Pavel Popov - System Engineer and Algorithmic Designer","author":"Pavel Popov"}},"markdownRemark":{"id":"cf0025c2-d944-57d8-b672-a0307082e25c","excerpt":"I created this code first to control a Spot doggy not by WASD but by my FPV Flysky i6 radio transmitter. Installation Fire up:\n  IsaacSim…","html":"<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 650px; \n                margin: 20px auto 20px auto;\n                padding: 10px;\n                border: 1px solid #eef;\n                border-radius: 5;\n                box-shadow: 0px 0px 5px #ccc;\n              \"\n    >\n      <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/930c3c23add3bfda894ae7e894855a23/2bfc7/image.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n    <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 47.239263803680984%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAACBElEQVQoz2Pg4+MTFBTg5+dXUFBUU1dXUFAQFxeXl5fT0dNVV9cUFBQQFBTk4+OVkZVTU9dQUlKSkJCQlpHW0dVR19BkYGLlYGThYOHgkVfVVFDXUdI20zWx1jYw0Te20DE0YePkZmRhZ2TlklFSl1fTUdQ21Ta21jEy1zE00zMyY1B1iNLwTNULzNcNLbVJrInIzbOOyND0zVZ3S1CzDVKzD9HwTNENzNMLLbVMqI3ILXCITtfyy9FwT1SxD2fQi6i0Tm+3LZgaWddX25gUlRxgn1hgmzfBMr1TLyhfL7jAMq3DJm9SQNXEuuaMpPQAh/hs25we68xuvYhKBse8fvvCqT6NSx1zOyy8A2R1zfWD8zxr5tkVTXPKaHHMaLErmurdsMi5aIKlf5iivrmmV7J71WyH4mmOef0MzkWTPCpm+DYtcUyu5hNXsAzPie1b51o5y7t6jl16q0Nmm2fVbK/6RR75XYJSKvruUXF9a92r53pWzXIunMRgWzwjunNV0oQN0V2rC6ZuKJ6zK7pnbcrE9SGtK6wyu6yzusPaV6VMXB/VtTp38rqyuTtje9cnTVgf2bHCpmg6g4qlu4K5u66Dn6lrkJVnqLlbkLFLoJa9r7yZu7qlm7qlq7y5u7a9j4lroJVXqIV7sLFLoK6Dn4KZu6qlBwMjA07AxsbKzsaKW54BAL0rk92CGtjKAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n  ></span>\n  <img\n        class=\"gatsby-resp-image-image\"\n        alt=\"alt text\"\n        title=\"alt text\"\n        src=\"/static/930c3c23add3bfda894ae7e894855a23/a6d36/image.png\"\n        srcset=\"/static/930c3c23add3bfda894ae7e894855a23/222b7/image.png 163w,\n/static/930c3c23add3bfda894ae7e894855a23/ff46a/image.png 325w,\n/static/930c3c23add3bfda894ae7e894855a23/a6d36/image.png 650w,\n/static/930c3c23add3bfda894ae7e894855a23/e548f/image.png 975w,\n/static/930c3c23add3bfda894ae7e894855a23/2bfc7/image.png 1227w\"\n        sizes=\"(max-width: 650px) 100vw, 650px\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n        loading=\"lazy\"\n      />\n  </a>\n    </span>\nI created this code first to control a Spot doggy not by WASD but by my FPV Flysky i6 radio transmitter.</p>\n<h2>Installation</h2>\n<p>Fire up:\n<br /></p>\n<p> <code>IsaacSim -> Window -> Examples -> Robotics Examples -> Policy -> Qudrupped -> Click 'Open Source Code' -> [it will fire up VSCode] -> paste the code below commenting the initial one</code></p>\n<p>My code just extends the control, if no transmitter converted, it’ll set back to initial Arrow+N/M control over Spot.\nWritten using <a href=\"https://github.com/pygame/pygame?ysclid=mafdjz3m1g617709353\">pygame</a> library, so it should be installed in Python version linked to your IsaacSim installation</p>\n<p>For me\n<br />\n<code>python3.10 -m pip install pygame</code>\n<br />\nworked well, but check that you pip install tthings for the right python</p>\n<p>In my setup the transmitter is connected via Trainer-USB wire to my laptop and is perceived as a PPM joystick device by Windows.</p>\n<p>Some additional setups might be needed for other transmitters or older Windows versions. (Setup work fine on Ubuntu 22.04 also)</p>\n<p>On Windows check the connection of your transmitter by using Win+R -> joy.cpl -> [see your PPM device] -> Properties\nIn Properties you can see how your sticks are set.\nUse smth like <a href=\"\">vJoy</a> or <a href=\"https://www.saashub.com/vjoy-alternatives\">analogues</a> to set up your sticks</p>\n<p>vJoy seems to be compromised, so stick to analogues, I use <a href=\"https://joytokey.net/en/download\">joyToKey</a> instead.</p>\n<p>The plan is to publish extension for a radio control of any movable agent in Isaac by any FPV Controller.</p>\n<h2>Code</h2>\n<p>Здесь код для управления собакой Spot.\n<br/></p>\n<pre><code>import carb\nimport numpy as np\nimport omni\nimport pygame\nfrom isaacsim.examples.interactive.base_sample import BaseSample\nfrom isaacsim.robot.policy.examples.robots import SpotFlatTerrainPolicy\n\nclass QuadrupedExample(BaseSample):\n    def __init__(self) -> None:\n        super().__init__()\n\n        # World settings\n        pygame.init()\n        pygame.display.init()  # Explicitly initialize display (needed for event pump)\n\n\n        self._world_settings = {\n            \"stage_units_in_meters\": 1.0,\n            \"physics_dt\": 1.0 / 500.0,\n            \"rendering_dt\": 10.0 / 500.0\n        }\n        self._base_command = [0.0, 0.0, 0.0]\n        \n        # Pygame initialization\n        pygame.init()\n        pygame.joystick.init()\n        \n        # Controller state\n        self.joystick = None\n        self.controller_connected = False\n        self.last_axis_values = None\n        self.threshold = 0.1  # 10% deadzone\n        self.scale_factor = 2.0\n        \n        self.setup_controller()\n        \n        # Claude suggestion: Add setup for simulation callbacks\n        self.setup_simulation_callbacks()\n        \n        # Fallback keyboard controls\n        self._input_keyboard_mapping = {\n            \"UP\": [2.0, 0.0, 0.0],\n            \"DOWN\": [-2.0, 0.0, 0.0],\n            \"RIGHT\": [0.0, -2.0, 0.0],\n            \"LEFT\": [0.0, 2.0, 0.0],\n            \"N\": [0.0, 0.0, 2.0],\n            \"M\": [0.0, 0.0, -2.0],\n        }\n\n    def setup_scene(self) -> None:\n        self._world.scene.add_default_ground_plane(\n            z_position=0,\n            name=\"default_ground_plane\",\n            prim_path=\"/World/defaultGroundPlane\",\n            static_friction=0.2,\n            dynamic_friction=0.2,\n            restitution=0.01,\n        )\n        self.spot = SpotFlatTerrainPolicy(\n            prim_path=\"/World/Spot\",\n            name=\"Spot\",\n            position=np.array([0, 0, 0.8]),\n        )\n\n    def setup_controller(self):\n        \"\"\"Initialize Pygame joystick connection\"\"\"\n        if pygame.joystick.get_count() > 0:\n            self.joystick = pygame.joystick.Joystick(0)\n            self.joystick.init()\n            self.controller_connected = True\n            self.last_axis_values = [0.0] * self.joystick.get_numaxes()\n            carb.log_info(f\"Controller connected: {self.joystick.get_name()}\")\n            carb.log_info(f\"Axes: {self.joystick.get_numaxes()}, Buttons: {self.joystick.get_numbuttons()}\")\n        else:\n            carb.log_warn(\"No controller found - using keyboard fallback\")\n            self._setup_keyboard_fallback()\n\n    # Claude suggestion: Add method to setup simulation callbacks\n    def setup_simulation_callbacks(self):\n        \"\"\"Set up callbacks for simulation events\"\"\"\n        self._timeline = omni.timeline.get_timeline_interface()\n        self._stop_sub = self._timeline.get_timeline_event_stream().create_subscription_to_pop(\n            self._on_timeline_event\n        )\n\n    # Claude suggestion: Modified timeline event handler with world existence check\n    def _on_timeline_event(self, event):\n        \"\"\"Handle timeline events like stop/play\"\"\"\n        if event.type == int(omni.timeline.TimelineEventType.STOP):\n            carb.log_info(\"Simulation stopped, preparing for reset\")\n            self._physics_ready = False\n        elif event.type == int(omni.timeline.TimelineEventType.PLAY):\n            carb.log_info(\"Simulation started, resetting state\")\n            try:\n                self.reset()\n            except Exception as e:\n                carb.log_error(f\"Error during reset: {str(e)}\")\n                # Set flag to ensure initialization happens later\n                self._physics_ready = False\n\n    def _setup_keyboard_fallback(self):\n        \"\"\"Setup keyboard controls as fallback\"\"\"\n        self._appwindow = omni.appwindow.get_default_app_window()\n        self._input = carb.input.acquire_input_interface()\n        self._keyboard = self._appwindow.get_keyboard()\n        self._sub_keyboard = self._input.subscribe_to_keyboard_events(\n            self._keyboard, self._sub_keyboard_event)\n\n    # Claude suggestion: Add method to handle world initialization\n    def ensure_world_initialized(self):\n        \"\"\"Make sure world reference is initialized\"\"\"\n        if not hasattr(self, '_world') or self._world is None:\n            carb.log_info(\"Initializing world reference\")\n            try:\n                self._world = self.get_world()\n                if self._world is not None:\n                    # Set up physics callback\n                    if not self._world.physics_callback_exists(\"physics_step\"):\n                        self._world.add_physics_callback(\"physics_step\", callback_fn=self.on_physics_step)\n                    return True\n                else:\n                    carb.log_warn(\"World still not available\")\n                    return False\n            except Exception as e:\n                carb.log_error(f\"Error initializing world: {str(e)}\")\n                return False\n        return True\n\n    # Claude suggestion: Modified to ensure world is initialized\n    async def setup_post_load(self) -> None:\n        self._physics_ready = False\n        self.ensure_world_initialized()\n        try:\n            await self.get_world().play_async()\n        except Exception as e:\n            carb.log_error(f\"Error in setup_post_load: {str(e)}\")\n\n    # Claude suggestion: Modified to add additional checks and error handling\n    def on_physics_step(self, step_size) -> None:\n        # Ensure world is properly initialized\n        if not self.ensure_world_initialized():\n            return\n            \n        # Process controller input\n        pygame.event.pump()\n        \n        if self.controller_connected:\n            self._process_controller_input()\n        \n        # Make sure spot robot is initialized\n        if not hasattr(self, 'spot') or self.spot is None:\n            carb.log_warn(\"Spot robot not initialized yet\")\n            self._physics_ready = False\n            return\n            \n        # Update robot physics - with additional safety checks\n        try:\n            if not self._physics_ready:\n                # Initialize or reinitialize on first step or after reload\n                self._physics_ready = True\n                self.spot.initialize()\n                self.spot.post_reset()\n                self.spot.robot.set_joints_default_state(self.spot.default_pos)\n                carb.log_info(\"Robot initialized successfully\")\n            else:\n                # Check if articulation view exists before applying commands\n                if hasattr(self.spot.robot, '_articulation_controller') and \\\n                   hasattr(self.spot.robot._articulation_controller, '_articulation_view') and \\\n                   self.spot.robot._articulation_controller._articulation_view is not None:\n                    self.spot.forward(step_size, self._base_command)\n                else:\n                    # Physics components not ready yet, try to reinitialize\n                    carb.log_warn(\"Physics components not ready, reinitializing...\")\n                    self._physics_ready = False\n        except Exception as e:\n            # Reset physics ready flag to force reinitialization on next step\n            self._physics_ready = False\n            carb.log_error(f\"Error in physics step: {str(e)}\")\n\n    def _process_controller_input(self):\n        \"\"\"Convert joystick input to robot commands\"\"\"\n        new_command = [0.0, 0.0, 0.0]\n        \n        # Get current axis values (Xbox/PS4 mapping)\n        axes = [self.joystick.get_axis(i) for i in range(self.joystick.get_numaxes())]\n\n        print(axes)\n        \n        # # Left Stick: Movement (Axis 0: LR, Axis 1: UD)\n        # roll = axes[0] if len(axes) > 0 else 0.0\n        # pitch = -axes[1] if len(axes) > 1 else 0.0  # Invert for natural control\n        \n        # # Right Stick: Rotation (Axis 2: LR)\n        # yaw = axes[2] if len(axes) > 2 else 0.0\n        \n\n        # WAITASEC!!!\n        # Right Stick: Movement (Axis 0: LR, Axis 1: UD)\n        roll = -axes[0] if len(axes) > 0 else 0.0\n        pitch = axes[1] if len(axes) > 1 else 0.0  # Invert for natural control\n        \n        # Left Stick: Rotation (Axis 2: LR)\n        yaw = -axes[4] if len(axes) > 4 else 0.0  # Claude suggestion: Fixed index check from 2 to 4\n        \n        # Apply deadzone and scaling\n        if abs(roll) > self.threshold:\n            new_command[1] = roll * self.scale_factor  # Left/Right\n            \n        if abs(pitch) > self.threshold:\n            new_command[0] = pitch * self.scale_factor  # Forward/Back\n            \n        if abs(yaw) > self.threshold:\n            new_command[2] = yaw * self.scale_factor  # Rotation\n        \n        # Print diagnostics when values change significantly\n        if self.last_axis_values:\n            changes = [abs(a - b) > 0.05 for a, b in zip(axes, self.last_axis_values)]\n            if any(changes):\n                carb.log_info(f\"Controller Input - Roll: {roll:.2f}, Pitch: {pitch:.2f}, Yaw: {yaw:.2f}\")\n        \n        self._base_command = new_command\n        self.last_axis_values = axes\n\n    def _sub_keyboard_event(self, event, *args, **kwargs) -> bool:\n        \"\"\"Handle keyboard fallback controls\"\"\"\n        if event.type == carb.input.KeyboardEventType.KEY_PRESS:\n            if event.input.name in self._input_keyboard_mapping:\n                self._base_command += np.array(self._input_keyboard_mapping[event.input.name])\n        elif event.type == carb.input.KeyboardEventType.KEY_RELEASE:\n            if event.input.name in self._input_keyboard_mapping:\n                self._base_command -= np.array(self._input_keyboard_mapping[event.input.name])\n        return True\n\n    # Claude suggestion: Add safety checks for None world\n    def reset(self):\n        \"\"\"Reset the simulation state\"\"\"\n        self._physics_ready = False\n        self._base_command = [0.0, 0.0, 0.0]\n        \n        # Reset controller state\n        if self.controller_connected:\n            self.last_axis_values = [0.0] * self.joystick.get_numaxes()\n        \n        # Check if world exists before trying to access it\n        if self.ensure_world_initialized():\n            # Make sure any old physics callbacks are cleaned up\n            if self._world.physics_callback_exists(\"physics_step\"):\n                self._world.remove_physics_callback(\"physics_step\")\n            \n            # Re-add the physics callback\n            self._world.add_physics_callback(\"physics_step\", callback_fn=self.on_physics_step)\n        else:\n            carb.log_warn(\"World not available during reset, will initialize when ready\")\n\n    def world_cleanup(self):\n        \"\"\"Clean up resources\"\"\"\n        if self.controller_connected:\n            pygame.quit()\n        if hasattr(self, '_sub_keyboard'):\n            self._sub_keyboard = None\n        if hasattr(self, '_world') and self._world is not None:\n            if self._world.physics_callback_exists(\"physics_step\"):\n                self._world.remove_physics_callback(\"physics_step\")\n</code></pre>\n<br/>\n<div align=\"center\">\n</div>","frontmatter":{"title":"Robot FPV Radio Controller"}}},"pageContext":{"slug":"/isaac/2025-09-robot-radio-control/","previous":{"fields":{"slug":"/isaac/2025-05-usd-format/"},"frontmatter":{"title":"USD Format 101"}},"kind":"isaac","parentPath":"/isaac/","parentTitle":"IsaacSim","next":{"fields":{"slug":"/isaac/2025-06-pegasus/"},"frontmatter":{"title":"Pegasus Extension"}}}},
    "staticQueryHashes": ["3649515864"]}