Skip to main content

openral_hal

Hardware Abstraction Layer for OpenRAL — Layer 0 in the eight-layer architecture (CLAUDE.md §2, §6.1). This package owns the HAL Protocol that every robot adapter must satisfy and ships concrete adapters for the robots OpenRAL targets today.

Synopsis

from openral_hal import HAL, SO100FollowerHAL, SO100DigitalTwin
from openral_core import Action, ControlMode

hal: HAL = SO100FollowerHAL(port="/dev/ttyUSB0")        # real SO-100 follower arm
# or, for offline / CI:
hal = SO100DigitalTwin()                          # in-process simulator

hal.connect()
state = hal.read_state()                          # → openral_core.JointState
hal.send_action(
    Action(control_mode=ControlMode.JOINT_POSITION, joint_positions=[0.0]*6)
)
hal.disconnect()

The HAL Protocol is structural: any class with connect / disconnect / read_state / send_action / estop and an embodiment_tag: str field satisfies it. There is no inheritance requirement.

What's in here

Module Role
protocol.py The HAL Protocol — the canonical interface (RFC §8.2).
lifecycle.py Lifecycle state machine helpers shared by ROS 2 lifecycle adapters.
ros_control.py RosControlHAL — adapter on top of ros2_control (no real ROS 2 import; works against SimTransport for unit tests and the live transport at runtime).
sim_transport.py SimTransport — typed in-memory ros2_control transport for unit tests.
so100_follower.py SO100FollowerHAL + SO100_DESCRIPTION + so100_with_sensors — real SO-100 follower arm via the lerobot SDK.
galaxea_a1.py GalaxeaA1HAL + GALAXEA_A1_DESCRIPTION — real-only six-axis A1 and normalized gripper over a literal IPv4-loopback JSON-lines transport. The operator-provided ROS 1 SDK stays in an isolated sidecar; OpenRAL bundles no vendor code.
so100_sim.py SO100DigitalTwin + SO100DigitalTwinConfig — pure-Python in-process simulator (used by smoketests, sim tests, and as a stand-in when no hardware is connected).
so100_mujoco.py SO100MujocoHAL — MuJoCo digital twin for the SO-100 follower, driving the mujoco_menagerie MJCF with the same 6-DoF action layout as SO100FollowerHAL.
_mujoco_arm.py MujocoArmHAL + MujocoArmHAL.from_description (MJCF resolved via openral_core.assets.resolve_asset) — shared MuJoCo backend for the arms below. It self-constructs from RobotDescription.sim (SimDescription), so per-robot subclasses are 5-line wrappers and no Python file is required to add a new MuJoCo HAL — declare a sim: block in robots/<id>/robot.yaml and call MujocoArmHAL.from_description(desc).
franka_panda.py FrankaPandaHAL + FRANKA_PANDA_DESCRIPTION (sim baseline) + franka_panda_with_sensors (MuJoCo sim).
franka_panda_real.py FrankaPandaRealHAL + FRANKA_PANDA_REAL_DESCRIPTION — real-hardware adapter over franka_ros2 / FCI (issue #56).
sawyer_real.py SawyerRealHAL + SAWYER_DESCRIPTION (sim baseline) + SAWYER_REAL_DESCRIPTION — real-hardware adapter over sawyer_robot / intera_sdk (issue #57).
aloha.py AlohaHAL + ALOHA_DESCRIPTION (sim baseline) + ALOHA_REAL_DESCRIPTION (real HW; Trossen ALOHA via the Interbotix XS SDK + ros2_control, issue #58) and AlohaMujocoHAL (MuJoCo digital twin via the gym-aloha bimanual MJCF, same 14-DoF action layout as AlohaHAL).
g1.py G1MujocoHAL + G1_DESCRIPTION — Unitree G1 humanoid (29-DoF) MuJoCo digital twin. ADR-0087 glide is the default; walking_enabled=True uses ADR-0089's pinned Apache-2.0 MuJoCo Playground ONNX policy + matching MJCF for gravity-on sim walking.
h1.py H1MujocoHAL + H1_DESCRIPTION — Unitree H1 humanoid (19-DoF, predecessor to the G1) MuJoCo digital twin on the mujoco_menagerie/unitree_h1 MJCF. Same contract-validator scope as the G1; falls without gravity disabled. First HAL whose underlying MJCF uses torque actuators rather than position actuators — runs a software PD position loop every mj_step via the MujocoArmHAL._per_step_update hook so the public action contract stays "position targets in radians".
flexiv_rizon4.py Rizon4MujocoHAL + RIZON4_DESCRIPTION — Flexiv Rizon 4 cobot (7-DoF, whole-body force sensitivity) MuJoCo digital twin on the mujoco_menagerie/flexiv_rizon4 MJCF. Drop-in MujocoArmHAL subclass — position actuators, no gripper, no floating base. Real-HW path wrapping flexivrobotics/flexiv_rdk is a planned follow-up.
openarm.py OpenArmMujocoHAL + OPENARM_DESCRIPTION — Enactic OpenArm v2 bimanual humanoid arm (16-DoF: 7 arm + 1 gripper per side) on the enactic/openarm_mujoco v2 bimanual MJCF. Fresh HALBase subclass — v2 ships native <position> actuators with per-class PD gains baked into the MJCF, so the HAL just writes target → ctrl and steps. v2 MJCF fetched lazily via openral_hal._openarm_v2_assets (the robot_descriptions package still pins to a pre-v2 commit; helper drops out when upstream bumps).
anvil_openarm_v2.py AnvilOpenArmV2MujocoHAL + ANVIL_OPENARM_V2_DESCRIPTION — Anvil OpenARM 2.0 (Anvil Robotics' manufactured variant of the standard OpenArm v2; 16-DoF: 7 arm + 1 gripper per side). Thin manifest-driven subclass like openarm.py — the difference is entirely in the fetched MJCF: J1 clamped to ±135 deg, J6 radial deviation widened to -45..+70 deg, and the wrist support bracket (visual-only CAD meshes) that enables it. MJCF fetched at a pinned SHA from bensonlee5/anvil-openarm-mujoco by openral_hal._anvil_openarm_v2_assets.
_real_description.py make_real_description(base, *, sdk_kind) — internal helper deriving a real-HW RobotDescription from a sim baseline (flips sdk_kind; the hal entrypoints are shared); used by all real-HW adapters.
resolver.py build_hal(description, *, mode, transport=None) — the single seam mapping a manifest + mode to a constructed sim/real HAL.
ur.py UR5eHAL / UR5e_DESCRIPTION + UR10eHAL / UR10e_DESCRIPTION (and the *_with_sensors factories) — MuJoCo-backed sim.
ur_real.py UR5eRealHAL / UR5e_REAL_DESCRIPTION + UR10eRealHAL / UR10e_REAL_DESCRIPTION — real UR hardware via ros2_control + ur_robot_driver (URCap / RTDE; BSD-3-Clause driver).
_sensor_wiring.py Catalog-backed sensor wiring shared by the *_with_sensors factories.

Supported robots

Robot Adapter Status Notes
LeRobot SO-100 follower arm SO100FollowerHAL ✓ unit + sim Embodiment tag: so100_follower.
Galaxea A1 (real-HW) GalaxeaA1HAL ✓ unit + offline socket · HIL fixture Joint-position + normalized-gripper only; official ROS 1 driver and joint tracker run in a separately owned sidecar. Lab-gated HIL via tests/hil/test_galaxea_a1.py.
SO-100 (pure-Python sim) SO100DigitalTwin ✓ sim In-process; no MuJoCo, no GPU. Used by the smoketest and CI.
SO-100 (MuJoCo digital twin) SO100MujocoHAL ✓ sim (tests/sim/test_so100_follower_hal_mujoco.py) Real MuJoCo physics on the mujoco_menagerie MJCF; same 6-DoF action layout as SO100FollowerHAL.
Franka Panda (sim) FrankaPandaHAL ✓ sim MuJoCo-backed.
Franka Panda (real-HW) FrankaPandaRealHAL ✓ unit + HIL fixture franka_ros2 / libfranka / FCI. Lab-gated HIL via tests/hil/test_franka_panda.py. Issue #56.
Rethink Sawyer (real-HW) SawyerRealHAL ✓ unit + HIL fixture sawyer_robot (community fork of intera_sdk). Lab-gated HIL via tests/hil/test_sawyer.py. Issue #57. No MuJoCo twin by design (vendor defunct; no real hardware will ever connect).
Trossen ALOHA bimanual (real-HW) AlohaHAL ✓ unit + HIL fixture Interbotix XS SDK + ros2_control (4 controllers, 14-DoF). Lab-gated HIL via tests/hil/test_aloha.py. Issue #58.
Trossen ALOHA bimanual (MuJoCo digital twin) AlohaMujocoHAL ✓ sim (tests/sim/test_aloha_bimanual_hal_mujoco.py) Real MuJoCo physics on gym-aloha's bimanual_viperx_transfer_cube.xml; same 14-DoF action layout as AlohaHAL.
Unitree G1 (MuJoCo digital twin) G1MujocoHAL ✓ sim (tests/sim/test_g1_hal_mujoco.py) 29-DoF humanoid: stock Menagerie contract twin + ADR-0087 glide, or explicit ADR-0089 gravity-on walking with the pinned MuJoCo Playground ONNX controller. Real-HW G1 still requires the M2 C++ S0 controller.
Unitree H1 (MuJoCo digital twin) H1MujocoHAL ✓ sim (tests/sim/test_h1_hal_mujoco.py) 19-DoF humanoid (G1's predecessor) on the mujoco_menagerie/unitree_h1 MJCF. Same contract-validator scope as G1; falls without gravity disabled. Software PD position loop wraps the MJCF's torque actuators (mirrors unitree_sdk2 on hardware). Real-HW H1 also planned under M2.
Flexiv Rizon 4 (MuJoCo digital twin) Rizon4MujocoHAL ✓ sim (tests/sim/test_rizon4_hal_mujoco.py) 7-DoF cobot with whole-body force sensitivity (0.1 N) on the mujoco_menagerie/flexiv_rizon4 MJCF. Drop-in MujocoArmHAL subclass — position actuators, no gripper. Real-HW Rizon 4 path wraps flexivrobotics/flexiv_rdk (Python + C++ SDK; closed-with-api per CLAUDE.md §7.4).
Enactic OpenArm v2 (MuJoCo digital twin) OpenArmMujocoHAL ✓ sim (tests/sim/test_openarm_hal_mujoco.py) 16-DoF bimanual (7 arm + 1 gripper per side) on the enactic/openarm_mujoco v2 bimanual MJCF (PR #19 on master). v2's native <position> actuators with per-class PD baked in let the HAL just write target → ctrl. LeRobot upstream has an OpenArm driver — real-HW path planned.
Anvil OpenARM 2.0 (MuJoCo digital twin) AnvilOpenArmV2MujocoHAL ✓ sim (tests/sim/test_anvil_openarm_v2_hal_mujoco.py) 16-DoF bimanual on the standard v2 MJCF plus Anvil's documented deltas (J1 ±135 deg; J6 -45..+70 deg radial deviation via the wrist support bracket, shipped as visual-only CAD meshes). Same native <position> actuators as the Enactic v2 twin — thin manifest-driven HAL, no PD workarounds. Real-HW path wraps Anvil's driver stack (anvil-robotics/openarm).
Universal Robots UR5e (sim) UR5eHAL ✓ sim MuJoCo-backed.
Universal Robots UR5e (real HW) UR5eRealHAL ✓ unit + HIL fixture ros2_control + ur_robot_driver (URCap / RTDE; BSD-3-Clause driver). Lab-gated HIL via tests/hil/test_ur5e.py. Issue #54.
Universal Robots UR10e (sim) UR10eHAL ✓ sim MuJoCo-backed.
Universal Robots UR10e (real HW) UR10eRealHAL ✓ unit + HIL fixture Same driver as the UR5e real-HW adapter; only the URDF / payload envelope differ. Lab-gated HIL via tests/hil/test_ur10e.py. Issue #55.
ros2_control-driven arms RosControlHAL ✓ generic Any ros2_control controller manager that exposes the standard topics/services.

The Pydantic descriptions above (e.g. SO100_DESCRIPTION, FRANKA_PANDA_DESCRIPTION) are paired with the canonical YAML manifests under robots/<robot_id>/robot.yaml — the YAML is the source of truth for sensors and capabilities, while the Python description pins the kinematic / safety values that don't change at runtime.

Adding a new MuJoCo-backed robot

For any robot — single-arm, floating-base humanoid, or bimanual — whose MJCF lives in robot_descriptions, gym_aloha, or on disk, no Python file is required:

  1. Add robots/<id>/robot.yaml with the usual joints, capabilities, safety blocks plus a sim: block:

    sim:
      # mjcf_uri schemes: robot_descriptions:<module> | gym_aloha:<scene>
      #                    | openarm_v2:bimanual | file:<abs-path>
      mjcf_uri: "robot_descriptions:<module>"
      floating_base: false                       # true for humanoids (G1, H1)
      # Optional joint-index overrides — only needed when the MJCF's qpos
      # order doesn't match description.joints (e.g. OpenArm skips qpos 8
      # and qpos 17 for passive follower fingers).
      # joint_qpos_addr: { joint_a: 0, joint_b: 1 }
      # actuator_index:  { joint_a: 0, joint_b: 1 }
      grippers:                                  # zero, one, or two entries
        - joint: "<gripper_joint_name>"
          ctrl_range: [low, high]
          qpos_addrs: [<qpos_index>, ...]
          qpos_scale: <float>
          read_mode: "sum_over_scale"            # | "affine_low_high" | "passthrough"
          write_mode: "normalised"               # | "passthrough"
          # actuator_index: 6                    # override; defaults to actuator_index map
          # mirror_actuator_index: 7             # Aloha: writes -ctrl to the negative finger
      # keyframe_index: 0                        # mj_resetDataKeyframe at connect (Aloha)
      # seed_ctrl_from_qpos: true                # ctrl = qpos at connect (OpenArm v2)
    
  2. Load the manifest and instantiate the HAL — no per-robot class:

    from openral_core import RobotDescription
    from openral_hal import MujocoArmHAL
    desc = RobotDescription.from_yaml("robots/<id>/robot.yaml")
    hal = MujocoArmHAL.from_description(desc, gravity_enabled=False)
    hal.connect()
    

The default 1:1 joint→qpos/actuator mapping (offset by 7/6 if floating_base: true) is correct for every robot in tree without passive-follower qpos quirks; sim.joint_qpos_addr / sim.actuator_index are only needed when the MJCF declares joints in a different order than description.joints (Aloha) or interleaves passive follower-finger slots (OpenArm).

Worked patterns in tree

Pattern Example robot
single-arm + revolute jaw gripper (affine_low_high) so100_follower
single-arm + parallel jaw (sum_over_scale) franka_panda
single-arm, no gripper ur5e, ur10e, rizon4
floating-base humanoid (floating_base: true) g1, h1
bimanual + two PASSTHROUGH grippers + mirror_actuator_index + keyframe_index aloha_bimanual
bimanual + two PASSTHROUGH grippers + joint_qpos_addr override + seed_ctrl_from_qpos openarm

Pairing with ROS 2 lifecycle nodes

Each HAL adapter has (or will have) a thin ROS 2 lifecycle node under packages/openral_hal_<robot>/:

ROS package Wraps Status
openral_hal_so100 SO100FollowerHAL ✓ working (unit + sim coverage)
openral_hal_galaxea_a1 GalaxeaA1HAL ✓ real observation/hold/joint/gripper + full C++ kernel graph HIL
openral_hal_franka FrankaPandaHAL (planned) skeleton
openral_hal_ur5e UR5eHAL (sim) / UR5eRealHAL (real HW) skeleton
openral_hal_ur10e UR10eHAL (sim) / UR10eRealHAL (real HW) skeleton

See each ROS package's README.md for the lifecycle contract, parameters, and topic names.

Tests

  • Unit: tests/unit/test_hal.py (RosControlHAL × SimTransport), tests/unit/test_so100_follower_hal.py, tests/unit/test_franka_panda.py, tests/unit/test_franka_panda_real.py, tests/unit/test_sawyer_real.py, tests/unit/test_aloha.py, tests/unit/test_mujoco_arm.py, tests/unit/test_sim_transport.py.
  • Protocol conformance: tests/unit/test_hal_protocol_conformance.py parametrizes the HAL invariants across 10 implementations (7 invariants × 10 HALs = 70 cases) — RosControlHAL, SO100FollowerHAL+SO100DigitalTwin, UR5eHAL, UR10eHAL, FrankaPandaHAL, FrankaPandaRealHAL, SawyerRealHAL, AlohaHAL, UR5eRealHAL+SimTransport, UR10eRealHAL+SimTransport.
  • Sim (per-HAL contract): tests/sim/test_ur5e_hal_mujoco.py, tests/sim/test_ur10e_hal_mujoco.py, tests/sim/test_franka_panda_hal_mujoco.py, tests/sim/test_so100_follower_hal_mujoco.py, tests/sim/test_aloha_bimanual_hal_mujoco.py, tests/sim/test_g1_hal_mujoco.py, tests/sim/test_h1_hal_mujoco.py, tests/sim/test_smolvla_so100.py.
  • Sim (cross-HAL integration): tests/sim/test_all_hals_via_runner.py drives every HAL twin through the production DeployRunner with a real WorldStateAggregator + a trivial echo-current-pose skill. Catches any joint-indexing / units / lifecycle / aggregator / safety wiring breakage at the integration boundary — every per-robot sim test could pass while this one still fails.
  • One-line full sweep: just hal-twin-sweep runs both the per-HAL contract suite and the cross-HAL DeployRunner integration suite (~200 tests covering 7 robot families) with the ROS 2 plugin workaround baked in.
  • HIL: tests/hil/test_franka_panda.py, tests/hil/test_sawyer.py, tests/hil/test_aloha.py, tests/hil/test_ur5e.py, tests/hil/test_ur10e.py, tests/hil/test_galaxea_a1.py — lab runners only (UR5E_HOST / UR10E_HOST env vars + [self-hosted, lab-ur*] runner labels for the UR pair).
  • HIL transport bridges (lab-only): tests/hil/_ros_control_transport.py exposes RosControlHILTransport + make_hil_transport for the single-controller real-HW HALs (UR5e / UR10e / Franka Panda / Sawyer); tests/hil/_aloha_ros_transport.py exposes AlohaHILTransport + make_aloha_hil_transport for the bimanual ALOHA fan-out (4 controllers). Both raise at import time if rclpy is not installed, so HIL tests guard with importlib.util.find_spec("rclpy") before importing them.

Doctest coverage: every adapter file is in Justfile::test-doctest's curated DOCTEST_TARGETS; run just test-doctest to exercise the docstring examples.

See also

  • openral_core.RobotDescription, JointSpec, Action, JointState, SafetyEnvelope — the typed contract this package consumes and emits.
  • robots/<robot_id>/robot.yaml — canonical description manifests.
  • packages/openral_hal_*/README.md — ROS 2 lifecycle node docs.
  • CLAUDE.md §6 (architecture discipline) and §10 (exception hierarchy: ROSConfigError / ROSRuntimeError / ROSSafetyViolation are the only exceptions a HAL adapter raises).

Metadata

Release files for openral-hal 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for openral-hal 0.3.1
File Size Uploaded Provenance Actions
openral_hal-0.3.1.tar.gz 218.0 kB With provenance Details for openral_hal-0.3.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for openral-hal 0.3.1
File Interpreter ABI Platform Actions
openral_hal-0.3.1-py3-none-any.whl Python 3 none any Details for openral_hal-0.3.1-py3-none-any.whl

Total release size: 438.3 kB

Release files / openral_hal-0.3.1.tar.gz

Download URL openral_hal-0.3.1.tar.gz
Size 218.0 kB
Tags Source
SHA-256 checksum
How to use checksums
7ff534573c27a7f4c9367b4ddbd60e5ec36e661440016206ad83a7d25811aa4a
BLAKE2b-256 checksum
How to use checksums
91ec28f9de5893b8e1ab8afeaa842a6f9193a13fb249fa4e60365eb2c8010f7d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.

Transparency log

Release files / openral_hal-0.3.1-py3-none-any.whl

Download URL openral_hal-0.3.1-py3-none-any.whl
Size 220.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
70e16f521beb91eca7aec09cef4b48b971f0b43a99f90dc07c8d0a2df33625d1
BLAKE2b-256 checksum
How to use checksums
2787ffbaea5c37c5a14d68086c13a731120365714f6b7990ef6d52491cf7475b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.

Transparency log

Security for openral-hal 0.3.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for openral_hal-0.3.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page