Skip to main content

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 command
  • GET - Read properties
  • SET - Write properties
  • INT - 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 to min/max/step and 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

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rigup-0.1.0.tar.gz (29.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rigup-0.1.0-py3-none-any.whl (38.7 kB view details)

Uploaded Python 3

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

Hashes for rigup-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2fd7d0fca3f46f5f9e084a4e0e4af6f6596f22f74141337fdcb7eff1f8333012
MD5 413500eb2a174d3edcd457c50aef5191
BLAKE2b-256 618e2e968dbcf316aa54247be52e47aae4264bd53e441a84e995811513f005f1

See more details on using hashes here.

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

Hashes for rigup-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d59fc57423ad2d6b29cdea1bd289ba81bff76f1fc4a931672c07536b9eae446f
MD5 ffe6b24990e6544c33adc88edd187eaa
BLAKE2b-256 e8a44c0c087051356227cf0054a878d19639913e47186d1eb7f873643144f040

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page