roswell (Python)
pip install-able Python that speaks ROS 2 with zero ROS installation: no
rosidl codegen, no PyO3 build step, and no callback-group ceremony.
The package is a small asyncio-native client over Roswell's Rust runtime.
Roswell keeps the ROS message semantics — parsing, layout, CDR serialization,
QoS, transport, and reply correlation — in one shared implementation, then
exposes a pleasant Python surface through ctypes.
Install
pip install roswell # self-contained wheel: bundles the compiled
# runtime + a set of common ROS interfaces
pip install roswell[numpy] # + numpy for zero-copy array views
Published wheels are platform wheels (py3-none-<platform>) carrying the prebuilt
roswell_c shared library — no ROS, no rosidl, no Rust toolchain, no compiler
needed to use it. Common interface definitions (std_msgs, geometry_msgs,
sensor_msgs, example_interfaces, …) ship inside the wheel, so
load_type("geometry_msgs/msg/Twist") works with no file paths.
Raspberry Pi / ARM Linux
Roswell targets embedded nodes: the runtime is a compact cdylib (idle Node
resident memory is a few MB over the interpreter) with a fast import, and the
package is pure ctypes — numpy is optional. Install the manylinux
aarch64 wheel on Pi-class ARM Linux and skip the numpy extra to keep the
footprint minimal; primitive arrays then come back as plain Python lists instead
of zero-copy numpy views:
pip install roswell # no numpy — lists for array fields
pip install roswell[numpy] # + numpy for zero-copy views (if you want them)
Quickstart
import asyncio, roswell
async def main():
node = roswell.Node("listener", domain=0)
Twist = node.load_type("geometry_msgs/msg/Twist") # bundled — no path, no deps
async with node.subscribe("/cmd_vel", Twist) as sub:
async for msg in sub:
print(msg.linear.x)
asyncio.run(main())
load_type also takes an explicit .msg path plus deps=[...] for your own
interface files:
T = node.load_type("my_pkg/msg/Custom.msg", deps=["my_pkg/msg/Helper.msg"])
Publish, call a service, and serve one:
node = roswell.Node("talker", domain=0)
Str = node.load_type("std_msgs/msg/String")
pub = node.publisher("/chatter", Str)
msg = pub.new(); msg.data = "hello"; pub.publish(msg)
req_t, resp_t = node.load_service("example_interfaces/srv/AddTwoInts")
client = node.client("/add_two_ints", req_t, resp_t)
req = client.new_request(); req.a, req.b = 41, 1
reply = await client.call(req, timeout=5.0) # -> reply.sum == 42
def handler(request):
resp = resp_t.alloc(); resp.sum = request.a + request.b; return resp
node.serve("/add_two_ints", (req_t, resp_t), handler) # sync or async handler
Everyday Use
- asyncio-native:
async for msg in sub,await client.call(req). One background thread per node multiplexes all readers (ros_wait) and dispatches into your event loop — never a thread per subscription, never a busy-poll. - Sync too:
sub.take(),for msg in sub.messages(timeout=...),client.call_sync(req). - numpy zero-copy: primitive arrays/sequences (e.g.
float64[],uint8[]) are exposed asnumpyviews over the underlying buffer when numpy is present (valid until the owning message is finalized), and as plain lists otherwise. - Clear QoS warnings: incompatible QoS with a peer surfaces automatically as
a
roswell.QosIncompatibleWarning. - Messages are runtime-typed: fields are plain attributes
(
msg.header.frame_id = "map",msg.data = np.zeros(...)).
All parsing, layout, CDR (de)serialization, QoS, transport, and reply correlation live in Rust; this package is ctypes bindings + asyncio plumbing.
Parameters
Declare, read, and update node parameters. The first parameter call stands up a
parameter server (a background thread in the Rust runtime) so ros2 param get/set/list sees the node and every change publishes /parameter_events:
node = roswell.Node("driver", domain=0)
node.declare_parameter("speed", 1.5) # float
node.declare_parameter("gain", 7) # int
node.declare_parameter("frame", "base_link")# str
node.declare_parameter("enabled", True) # bool
node.get_parameter("speed") # -> 1.5
node.set_parameter("gain", 9) # publishes a /parameter_events update
node.list_parameters() # -> ["enabled", "frame", "gain", "speed"]
Scalar types (bool, int, float, str) are supported — the common case for
node configuration. Values live in Rust; the Python surface is just the typed
call across the FFI.
Timers
create_timer runs a periodic callback (sync or async) on the asyncio loop —
the idiomatic "publish at N Hz" node loop. Timing is plumbing, so it lives in
Python; cancel it explicitly or let node.close() stop it:
async def main():
node = roswell.Node("ticker", domain=0)
pub = node.publisher("/chatter", node.load_type("std_msgs/msg/String"))
def tick():
m = pub.new(); m.data = "hi"; pub.publish(m)
node.create_timer(0.1, tick) # 10 Hz
await asyncio.Event().wait() # spin
asyncio.run(main())
Develop
cargo build -p roswell-c # builds the shared library into target/
pip install -e "python[test]" # editable install; numpy + pytest
python -m pytest python/tests -v
# or, without activating a venv:
uv run --project python --extra test pytest python/tests
python python/tests/bench_pointcloud.py
At import time, the shared library is located via ROSWELL_LIB (if set), then the bundled
roswell/_lib/ (wheels), then the dev target/{release,debug} tree. Bundled
interfaces are found under roswell/interfaces/ (wheels) or samples/ (checkout),
overridable with ROSWELL_INTERFACES.
Build release wheels
uv run --project python --extra release cibuildwheel --output-dir python/dist python
That one command builds the configured wheel set: Linux x86_64, Linux aarch64,
macOS universal2, and Windows AMD64. Each wheel bundles the compiled Rust
runtime plus common interfaces and is tagged py3-none-<platform>, so it is not
tied to one CPython ABI. Build the sdist with python -m build --sdist python/
and check everything with twine check python/dist/*.
A Rust toolchain is required to build from source, but not to install or use a
published wheel. The package version is single-sourced from the crate
(roswell-c/Cargo.toml).
QoS
from roswell import QosProfile
pub = node.publisher("/scan", T, qos=QosProfile.preset("sensor_data"))
sub = node.subscribe("/scan", T, qos=QosProfile(reliability="best_effort", depth=5))
Visualize With Foxglove
Roswell has no in-process visualization yet, but it interoperates with
Foxglove Studio through MCAP files. Record any topics
your Python nodes publish with the bag_record binary from the Rust workspace,
then open the resulting .mcap in Foxglove.
Record (type-blind — no ROS install needed), from the repo root:
# Named topics with their ROS types (matches the domain your Node uses):
cargo run --release -p roswell-ros2-compat --bin bag_record -- \
--output chatter.mcap --domain 0 --topic /chatter:std_msgs/msg/String
# ...or discover and record everything on the graph for 10 seconds:
cargo run --release -p roswell-ros2-compat --bin bag_record -- \
--output session.mcap --domain 0 --all --duration 10
Recording stops on --duration, on Enter/Ctrl-D at a terminal, or on Ctrl-C
(already-flushed chunks stay readable). A per-topic message-count summary prints
on a clean stop.
Then in Foxglove Studio choose Open local file… and select the .mcap — the
schemas travel inside the file, so panels (Raw Messages, Plot, 3D, …) work with
no extra setup. To sanity-check a recording without Foxglove, replay it:
cargo run --release -p roswell-ros2-compat --bin bag_play -- session.mcap --domain 0
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file roswell-0.1.0.tar.gz.
File metadata
- Download URL: roswell-0.1.0.tar.gz
- Upload date:
- Size: 309.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19a3e44952ff04fb59794fa344a8cef08965ec5e8b642eab402ae5bc08128cca
|
|
| MD5 |
160ae33d53cfc541b19ace7719eda4da
|
|
| BLAKE2b-256 |
0a49c692aa92e63956512e10ef974c243e3adec866428fdfb6c6f2e7f4a40073
|
File details
Details for the file roswell-0.1.0-py3-none-manylinux_2_28_x86_64.whl.
File metadata
- Download URL: roswell-0.1.0-py3-none-manylinux_2_28_x86_64.whl
- Upload date:
- Size: 1.8 MB
- Tags: Python 3, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df0432a3c7e368ffd5af16bc2786ac73503dddbccd62796f8f8e6c772c2d1e6f
|
|
| MD5 |
6c6f3cb71a311e90896b96df04828dc5
|
|
| BLAKE2b-256 |
9db620ec4b6c3c3755ea49eb9c52ae5b3df6e83e76bd78931989f5182677f43a
|
File details
Details for the file roswell-0.1.0-py3-none-manylinux_2_28_aarch64.whl.
File metadata
- Download URL: roswell-0.1.0-py3-none-manylinux_2_28_aarch64.whl
- Upload date:
- Size: 1.8 MB
- Tags: Python 3, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1847cf9b26bcfb7f59497677456b24018db3e258dcf9400b736aa5053f91c41
|
|
| MD5 |
c5f82a7bceab08ba8fd6b96fbd4c5cda
|
|
| BLAKE2b-256 |
3541d297e163a5fd7ea7c831fa43db3a552fe7a1b523224b1e7607a5bbf9dc57
|
File details
Details for the file roswell-0.1.0-py3-none-macosx_11_0_universal2.whl.
File metadata
- Download URL: roswell-0.1.0-py3-none-macosx_11_0_universal2.whl
- Upload date:
- Size: 3.3 MB
- Tags: Python 3, macOS 11.0+ universal2 (ARM64, x86-64)
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e92e9941cdc3d3cb820f2e8a0b497337fdd7f36477771fd067d861c092392f1b
|
|
| MD5 |
78cf37400ab18866f03b6f9f23d372f4
|
|
| BLAKE2b-256 |
a17d43311c4b073fb449c12939ad1906ad87846c4e91edbcf9bd754e94d7f040
|