hilspy
HILS (hardware-in-the-loop simulation) I/O for Python on a Rust core, built to
be embedded in a scenario library (such as
autoware_carla_scenario).
One call opens a channel: a transport (how packets travel) composed with a codec (what they mean). Both are pluggable, in Rust or in Python.
import hilspy
with hilspy.open("can://vcan0", "dbc:vehicle.dbc") as ecu:
ecu.send("VehicleSpeed", {"Speed": 12.5, "Accel": 0.0, "Gear": "D", "Counter": 0})
for msg in ecu: # until ecu.close()
print(msg.name, msg.fields) # "SteeringCmd" {"SteeringAngle": -3.5, ...}
| Transport URL | |
|---|---|
can://vcan0 |
Linux SocketCAN (?fd, ?echo, ?filter=0x100:0x700,...) |
udp://host:port |
UDP to a peer (?bind=addr:port); udp://addr:port?listen replies to the last sender |
tcp://host:port |
TCP client; tcp://addr:port?listen serves every connected client |
mem://name |
in-process bus (tests, simulated ECUs in one process) |
file://path |
replay a recording; ?write / ?append records one |
| Codec | |
|---|---|
raw (default) |
none: messages carry data (and id for CAN) |
dbc:FILE |
CAN frames <-> DBC messages, via can-dbc (options strict, scaling, decode_choices) |
sbf |
Septentrio Binary Format stream <-> blocks, via libsbf; encodes PVTGeodetic and AttEuler |
lidar:MODEL |
LiDAR UDP data packets <-> point clouds: Hesai and Velodyne line-ups, described by profiles |
Install
Python 3.10+. One abi3 wheel per platform serves every Python version; wheels
are built for Linux (x86_64, aarch64), macOS (arm64, x86_64) and Windows x64.
can:// (SocketCAN) is Linux-only; every other transport and codec works everywhere.
pip install hilspy
uv add hilspy
pip install git+https://github.com/hakuturu583/hilspy # from source (needs Rust)
From a checkout:
uv sync # builds the extension with maturin
uv run pytest
Versions follow semver and are released by labelling the PR (bump patch /
bump minor / bump major); see docs/releasing.md.
API
The whole public API:
| Name | |
|---|---|
hilspy.open(url, codec="raw", **codec_options) -> Channel |
open a channel |
Channel.send(message) / send(name, fields) |
encode and send |
Channel.recv(timeout=None) -> Message | None |
next message (iterate the channel for all) |
Channel.every(period, message_or_name, fields=None, *, duration=None) -> Periodic |
cyclic send from a Rust thread; update(), stop(), period, sent_count |
Channel.close() |
also via with / async with |
await Channel.asend(...), await Channel.arecv(timeout=None), async for msg in channel |
asyncio |
hilspy.merge(*channels) |
(channel, message) from several buses as they arrive (for / async for) |
hilspy.Message(name=None, fields=None, *, data=None, id=None, extended=None, fd=False) |
.name, .fields, msg["field"], .data, .id, .timestamp |
hilspy.register_transport(scheme, factory) / register_codec(name, factory) |
plugins |
hilspy.Error |
bad URL/codec/option, encode/decode failure, closed channel |
asyncio and several buses at once
Every channel also has await ch.asend(...), await ch.arecv(timeout),
async for msg in ch and async with. hilspy.merge(*channels) receives from
many buses at once, yielding (channel, message) as messages arrive, with
for or async for:
async with (
hilspy.open("can://can0", "dbc:powertrain.dbc") as can0,
hilspy.open("can://can1", "dbc:chassis.dbc") as can1,
hilspy.open("tcp://192.168.3.1:28784", "sbf") as gnss,
hilspy.merge(can0, can1, gnss) as buses,
):
async for bus, msg in buses:
if bus is gnss and msg.name == "PVTGeodetic":
await can1.asend("GnssPosition", {"Lat": msg["latitude"], "Lon": msg["longitude"]})
Each bus is received on its own Rust thread, so awaiting never blocks the
event loop and a quiet or slow bus does not delay the others. A channel has
one consumer at a time: once received with arecv/async for/merge, its
synchronous recv refuses.
A message with a name goes through the codec; a nameless one
(Message(data=b"...", id=0x123)) is sent as is. Messages the codec does not
know (a CAN id missing from the DBC) arrive nameless.
GNSS receiver simulation (SBF over TCP)
with hilspy.open("tcp://0.0.0.0:28784?listen", "sbf") as receiver:
receiver.send(
"PVTGeodetic", {"mode": 4, "latitude": lat_rad, "longitude": lon_rad, "height": 40.0}
)
receiver.send("AttEuler", {"heading": 90.0, "pitch": 0.0, "roll": 0.0})
# tow/wnc default to the current GPS time
LiDAR (Hesai, Velodyne)
lidar:MODEL decodes a sensor's UDP data packets into point clouds, and
encodes point clouds into the packets a sensor would send (to stand in for
one in the loop). Sensors are profiles, not code:
| Vendor | Models (lidar:<name>; aliases in brackets) |
|---|---|
| Hesai | pandar_40p, pandar_64, pandar_qt64, pandar_qt128, pandar_xt16, pandar_xt32, pandar_xt32m, pandar_128e3x (Pandar128), pandar_128e4x (OT128), pandar_at128 |
| Velodyne | vlp16 (Puck), vlp16_hires, vlp32c, vlp32mr, hdl32e, hdl64e, vls128 (Alpha Prime) |
Names ignore case, - and _ (lidar:VLP-16, lidar:XT32).
# Driver side: one message per revolution.
with hilspy.open("udp://0.0.0.0:2368?listen", "lidar:pandar_xt32") as lidar:
for scan in lidar:
xyz = np.column_stack([np.frombuffer(scan[k], np.float32) for k in "xyz"])
# Sensor side: send one revolution of a simulated point cloud.
with hilspy.open("udp://192.168.1.100:2368", "lidar:vlp16") as sensor:
sensor.send("scan", {"x": xs, "y": ys, "z": zs, "intensity": 100})
A scan has arrays (array.array, readable by numpy without copying) x,
y, z (metres; x forward, y left, z up), intensity, channel,
azimuth (degrees, clockwise from forward), distance, return and time
(seconds after timestamp), plus timestamp, model, return_mode, rpm
and packets. Sending takes x/y/z (each point goes to the channel of
the nearest elevation) or distance/azimuth/channel, as lists, tuples,
array.array or numpy arrays; a received scan can be sent back as is.
Sending one revolution spreads its packets over the revolution's duration
(100 ms at 600 rpm), as the sensor does, so a driver's socket buffer is not
flooded; send returns when the last packet is out (pace=False sends
them at once). Codec options: frame="packet" (a message per packet),
phase (where scans start, degrees), rpm, return_mode and pace
(encoding), profiles=FILE.
Profiles are protobuf messages (schema:
lidar_profile.proto):
packet layout, per-channel elevation / azimuth offset / firing time, return
modes, timing and range. The defaults are written as text in
lidar/profiles/ and committed compiled
as lidar/profiles.binpb, which hilspy embeds. To describe another sensor,
write a ProfileSet in text format, compile it with any protobuf tool
(protoc --encode=hilspy.lidar.ProfileSet lidar_profile.proto < my_lidars.txtpb > my_lidars.binpb)
and pass it with profiles="my_lidars.binpb"; it is searched before the
defaults. Channel angles are
nominal values from tier4/nebula's
calibration files; per-unit calibration is not applied.
Plugins
A transport is any object with send(packet) and recv(timeout) (and
optionally close()); a codec has encode(message) and decode(packet).
Packets are nameless Messages.
class SerialTransport:
def __init__(self, address: str, options: dict[str, str]):
self.port = serial.Serial(address, int(options.get("baud", 115200)))
def send(self, packet):
self.port.write(packet.data)
def recv(self, timeout):
self.port.timeout = timeout
data = self.port.read(self.port.in_waiting or 1)
return hilspy.Message(data=data) if data else None
hilspy.register_transport("serial", SerialTransport)
gnss = hilspy.open("serial:///dev/ttyACM0?baud=921600", "sbf")
Rust crates register through hilspy_core::transport::register and
hilspy_core::codec::register.
A virtual CAN interface for local testing:
sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set vcan0 mtu 72 # CAN FD
sudo ip link set up vcan0
Examples
uv run python examples/virtual_ecu.py # cyclic DBC message, in-process
uv run python examples/virtual_ecu.py --url can://vcan0
uv run python examples/gnss_simulator.py "tcp://127.0.0.1:28784?listen"
uv run python examples/gnss_simulator.py "file://sim.sbf?write" --seconds 5
uv run python examples/sbf_dump.py file://sim.sbf
uv run python examples/multi_bus.py # asyncio: CAN + GNSS at once
Development
uv sync # build + dev dependencies (cantools for cross-checks)
cargo test -p hilspy-core # Rust unit tests
uv run pytest # Python tests
HILSPY_TEST_VCAN=vcan0 uv run pytest tests/test_socketcan.py # needs a vcan interface
uv run pre-commit run --all-files # ruff, mypy, cargo fmt, clippy
Design notes: docs/design.md.
License
Apache-2.0
Metadata
Release files for hilspy 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hilspy-0.5.0.tar.gz | 118.4 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| hilspy-0.5.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| hilspy-0.5.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| hilspy-0.5.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| hilspy-0.5.0-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| hilspy-0.5.0-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 4.8 MB
Release files / hilspy-0.5.0.tar.gz
| Download URL | hilspy-0.5.0.tar.gz |
|---|---|
| Size | 118.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4106a742719c458efd010fcd1989d3c397874309f03293d6bbf7073042076944
|
|
BLAKE2b-256 checksum How to use checksums |
9e3ea7bb24f1b072f30621892b4d30271f6c446dc96297029fd91975355721fe
|
| 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 Oct 15, 2026.
Transparency logRelease files / hilspy-0.5.0-cp310-abi3-win_amd64.whl
| Download URL | hilspy-0.5.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 871.3 kB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
f9797f8f33f20149fefce883d71c26b2ac7b7d4bb55c9525bc7edb5b2c4f081e
|
|
BLAKE2b-256 checksum How to use checksums |
c050483c6a6cbabf1d36b5e81496460dc7193ad6476be01897f1f588dae586ef
|
| 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 Oct 15, 2026.
Transparency logRelease files / hilspy-0.5.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | hilspy-0.5.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 998.2 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
45136efee25d3e21ed29cfbf1170fc7d9e7a50dd95268643fa72e43a5a2b5d2a
|
|
BLAKE2b-256 checksum How to use checksums |
5101c249243a76c80ec9ccf618906db9768513d8f4d033d287b3f85cf421eb94
|
| 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 Oct 15, 2026.
Transparency logRelease files / hilspy-0.5.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | hilspy-0.5.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 979.9 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
bc28edb22ef9d9d5afb74dc4bc54d3bc7bd5db8bc4f44370ba71ac9db35fff1d
|
|
BLAKE2b-256 checksum How to use checksums |
9ece7ec2b1174d674ed486d141532f151ae600cc98d51d3c28e9ad4b6651fb3e
|
| 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 Oct 15, 2026.
Transparency logRelease files / hilspy-0.5.0-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | hilspy-0.5.0-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 904.2 kB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
f9d43aa6e47936b89d146885821d16d4d9a3f27c517ac69ecf8193b9f5360997
|
|
BLAKE2b-256 checksum How to use checksums |
dcdce3b2d9fd3b442fb562cc5717b7a49033b7c56f1a1f31a3ca4cebb670a780
|
| 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 Oct 15, 2026.
Transparency logRelease files / hilspy-0.5.0-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | hilspy-0.5.0-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 925.1 kB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
5735b2eb3b729b01ec68f897f3367e5df061f5bc3f59c27610295abffab2ba05
|
|
BLAKE2b-256 checksum How to use checksums |
6e10b03f7d23476b985fa2980c5cb4d7378f435dd22b89d0f714c5ecec4e4e3e
|
| 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 Oct 15, 2026.
Transparency log