Distributed device control for experimental rigs
Project description
rigup
Distributed device control framework for experimental rigs: Provides control of hardware devices across networked nodes with ZeroMQ.
🚧 Heads up: rigup is under active development. Expect rapid changes and occasional breaking updates while the core APIs settle. 🚧
Quick Start
# Install dependencies
uv sync --all-packages --all-extras
# Run basic examples
uv run python -m examples.simple.demo
uv run python -m examples.imaging.demo
Voxel: A complete microscope rig implementation using rigup with web UI and hardware drivers.
Example code
from rigup import Rig, RigConfig
config = RigConfig.from_yaml("system.yaml")
rig = Rig(zctx, config)
await rig.start()
# Generic access
temp = rig.controllers["temp_controller"]
await temp.call("start_regulation")
# Or with typed clients (ImagingRig example)
laser = rig.lasers["laser_488"]
await laser.turn_on() # IDE autocomplete!
Architecture
Three layers:
Device - Hardware abstraction (talks to SDK/driver) Service - Network wrapper (ZeroMQ server) Client - Remote proxy (ZeroMQ client)
from rigup import Device, DeviceService, DeviceClient, describe
# Device (server-side)
class Camera(Device):
def capture(self) -> np.ndarray:
return self._sdk.acquire()
# Service (server-side, optional)
class CameraService(DeviceService[Camera]):
@describe(label="Start Stream", desc="Stream frames to file")
def start_stream(self, n_frames: int):
for i in range(n_frames):
self._writer.write(self.device.capture())
# Client (controller-side, optional)
class CameraRHandle(DeviceClient):
async def capture(self) -> np.ndarray:
return await self.call("capture")
async def start_stream(self, n_frames: int):
return await self.call("start_stream", n_frames)
Devices can run on separate machines. Configuration in YAML:
metadata:
name: MyRig
control_port: 9000
nodes:
primary:
devices:
camera_1:
target: myrig.devices.Camera
kwargs: { serial: "12345" }
remote_node:
hostname: 192.168.1.50
devices:
stage_x:
target: myrig.devices.MotorStage
kwargs: { axis: "X" }
Communication
Commands/Properties: REQ/REP sockets State streaming: PUB/SUB sockets Connection monitoring: Heartbeats Logging: PUB/SUB aggregation
Each device service exposes:
REQ- Execute commandGET- Read propertiesSET- Write propertiesINT- Introspection
Logging
rigup uses Python's stdlib logging with ZeroMQ log aggregation.
Enable logging:
import logging
logging.basicConfig(level=logging.INFO) # See all rigup and node logs
from rigup import Rig, RigConfig
rig = Rig(zctx, config)
await rig.start()
The Rig automatically receives logs from all nodes and forwards them to Python's logging system under the node.<node_id> logger. You'll see logs like:
2025-11-05 20:58:00 - rigup.rig - INFO - Starting MyRig...
2025-11-05 20:58:00 - rigup.nodes - INFO - [node.primary.INFO] Node primary started
2025-11-05 20:58:02 - rigup.rig - INFO - MyRig ready with 4 devices
Users opt-in by configuring Python logging. No logs appear by default (library best practice).
Customization
Base Rig: Generic device access via rig.controllers["id"]
Custom Rig: Typed collections with autocomplete
class ImagingRig(Rig):
NODE_SERVICE_CLASS = ImagingRigNode # Custom services
def __init__(self, zctx, config):
super().__init__(zctx, config)
self.lasers: dict[str, LaserClient] = {}
self.cameras: dict[str, CameraRHandle] = {}
def _create_client(self, device_id, prov):
if prov.device_type == DeviceType.LASER:
client = LaserClient(...)
self.lasers[device_id] = client
return client
# ...
Property Helpers
Many hardware knobs expose constrained values (bounded ranges, enumerated modes). rigup ships specialized property descriptors under rigup.device.props so those constraints stay declarative and travel with the data:
@deliminated_float/@deliminated_int: clamp values tomin/max/stepand report those bounds to clients.@enumerated_string/@enumerated_int: restrict values to a predefined list and expose the options in RPC responses.
Descriptors return PropertyModel objects, so DeviceService and DeviceClient automatically serialize both the value and its metadata. UI layers can render sliders or dropdowns without guessing constraints.
from rigup.device.props import deliminated_float, enumerated_string
class Laser(Device):
@deliminated_float(min_value=0.0, max_value=100.0, step=0.5)
def power_setpoint(self) -> float:
return self._power
@power_setpoint.setter
def power_setpoint(self, value: float) -> None:
self._power = value
@enumerated_string(options=["cw", "pulsed", "burst"])
def mode(self) -> str:
return self._mode
On the client side, call await client.get_prop("power_setpoint") to receive the full PropertyModel (value + bounds), or await client.get_prop_value("mode") for just the primitive.
Examples
Simple: Base classes, generic access Imaging: Custom rig with typed clients (cameras, lasers)
cd examples
uv run python -m simple.demo
uv run python -m imaging.demo
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 rigup-0.1.0.tar.gz.
File metadata
- Download URL: rigup-0.1.0.tar.gz
- Upload date:
- Size: 29.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2fd7d0fca3f46f5f9e084a4e0e4af6f6596f22f74141337fdcb7eff1f8333012
|
|
| MD5 |
413500eb2a174d3edcd457c50aef5191
|
|
| BLAKE2b-256 |
618e2e968dbcf316aa54247be52e47aae4264bd53e441a84e995811513f005f1
|
File details
Details for the file rigup-0.1.0-py3-none-any.whl.
File metadata
- Download URL: rigup-0.1.0-py3-none-any.whl
- Upload date:
- Size: 38.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d59fc57423ad2d6b29cdea1bd289ba81bff76f1fc4a931672c07536b9eae446f
|
|
| MD5 |
ffe6b24990e6544c33adc88edd187eaa
|
|
| BLAKE2b-256 |
e8a44c0c087051356227cf0054a878d19639913e47186d1eb7f873643144f040
|