Skip to main content

opalinx (Python)

A sans-I/O Python client for Opalinx, the Open Protocol for Addressable LEDs. It is the Python counterpart to opalinx and speaks the exact same wire format (verified byte-for-byte in the test suite).

Prerelease: tracks the Opalinx 1.0.0-alpha.0 specification; expect breaking changes before 1.0.0. The protocol, firmware builds, and this library are versioned independently.

Why sans-I/O

The OpalinxClient performs no I/O of its own — it never reads, never blocks, and never starts a thread. You encode requests (which are handed to a write-only transport) and push received bytes in with feed(), getting decoded events back. That single non-blocking core drops cleanly into very different hosts:

  • TouchDesigner (single-threaded): send from the cook loop, feed bytes from the Serial DAT's onReceive callback, poll the pipeline gate once per cook.
  • A pyserial script/CLI: a tiny read loop, with optional blocking helpers for convenience.
  • An asyncio service: wrap events in futures.

Frame pipelining is a non-blocking gate, not an await: show() queues an acknowledged Show and returns immediately, and frame_gate_open() tells your render loop whether there's room for the next frame — the same frame-drop backpressure model real-time hosts already use.

Install from this repository

opalinx-leds is not being published on PyPI yet. From the opalinx-python checkout, install the current development source in editable mode:

python -m pip install -e .             # core only (pure Python)
python -m pip install -e ".[serial]"  # + pyserial transport
python -m pip install -e ".[numpy]"   # + fast NumPy pixel reorder

NumPy is an optional fast path: reorder_to_wire vectorizes when handed a NumPy array and otherwise falls back to pure Python, so the library imports fine without it.

Quick start (pyserial)

from opalinx import OpalinxClient, OutputProfile, PixelFormat, reorder_to_wire
from opalinx.transports.pyserial_transport import SerialTransport, request

transport = SerialTransport("/dev/ttyUSB0")  # or "/dev/tty.usbmodem1234", or "COM5" on Windows
client = OpalinxClient(transport)  # protocol major checked automatically
try:
    transport.open()  # inside the try so a pyserial open failure is reported here too
    # Blocking helpers live in the transport, never in the client core. request() raises a typed
    # OpalinxError on a correlated device ERROR (don't mask it as a timeout).
    info = request(client, transport, client.get_info, "info")
    if info.mismatch:  # protocol major / one-pixel payload mismatch
        raise SystemExit(info.mismatch)
    print(info.info["device_name"], info.info["firmware"])

    request(
        client,
        transport,
        lambda: client.configure(pixel_format=PixelFormat.RGB8, component_order="GRB",
                                 output_profile=OutputProfile.SINGLE_WIRE_PULSE_800K_T1,
                                 led_count=60),
        "config",
    )

    logical = bytes([255, 0, 0] * 60)                # 60 red LEDs, logical RGB
    wire = reorder_to_wire(logical, "GRB", pixel_format=PixelFormat.RGB8)
    # Acknowledged write: a fire-and-forget (TxID 0) set_pixels whose ERROR arrives asynchronously
    # could be missed while the Show still succeeds and displays stale pixels — so this one-shot waits
    # for SET_PIXELS_ACK (request() raises on a correlated device ERROR).
    request(client, transport, lambda: client.set_pixels(0, 60, wire, ack=True), "pixels_ack")
    # Acknowledged Show: wait for its SHOW_ACK so the frame is displayed before the port closes.
    request(client, transport, lambda: client.show(ack=True), "show_ack")
except Exception as exc:
    # Report any failure cleanly — an Opalinx device error or a pyserial transport error during
    # open()/poll()/write() — instead of a raw traceback.
    raise SystemExit(f"Error: {exc}")
finally:
    try:
        transport.close()
    except Exception:
        pass  # never let a close() failure mask the original error

Pipelined streaming loop

from opalinx import OpalinxException
from opalinx.events import DeviceErrorEvent, ShowAckEvent

running, dropped = True, 0
while running:
    if client.frame_gate_open():          # room in the one-deep pipeline?
        client.set_pixels(255, count, next_wire_frame())
        client.show()                     # acked Show; paces the pipeline
    else:
        dropped += 1                      # backpressure: skip this frame
    # Ingest SHOW_ACKs (which reopen the gate), but don't discard the events: a device error or an
    # out-of-order SHOW_ACK means the pipeline is no longer safe to stream against — stop, don't log-and-continue.
    for event in client.feed(transport.poll()):
        if isinstance(event, DeviceErrorEvent):
            raise OpalinxException(f"device error {event.code_name} (0x{event.code:02X})")
        if isinstance(event, ShowAckEvent) and not event.in_order:
            raise OpalinxException("pipeline ordering anomaly (out-of-order SHOW_ACK)")

TouchDesigner

# In the extension:
from opalinx import OpalinxClient
from opalinx.transports.touchdesigner_transport import TouchDesignerTransport
self.transport = TouchDesignerTransport(op("ser_device"))
self.client = OpalinxClient(self.transport)

# In the Serial DAT's onReceive callback:
def onReceive(dat, rowIndex, message, bytes_, **kwargs):
    ext = op("opalinx").ext.OpalinxExt
    for event in ext.client.feed(bytes_):
        ext.handle_event(event)

API at a glance

An LED configuration is the combination of pixel_format, component_order, and output_profile needed by an LED product. The output profile is specifically the waveform and reset timing.

Requests (return a TxID unless noted): get_info(), get_config(), configure(pixel_format=, component_order=, output_profile=, led_count=, channel=), get_network_config(), configure_network(mode=, address=, prefix_length=, gateway=, hostname=), and reset(). Network methods require a device that advertises Capability.NETWORK_CONFIG. Streaming — fire-and-forget by default (TxID 0), or acknowledged with ack=True (uses a tracked TxID and returns it, so a rejected write surfaces via its *_ACK/ERROR instead of being lost): set_pixels(channel, count, wire_bytes, offset=, ack=False), set_channel(channel, count, wire_bytes, offset=, ack=False) (splits a large channel into payload-sized set_pixels messages, validating the whole span first; with ack=True the final chunk is acknowledged), fill_channel(channel, {"r","g","b","w","cw","ww"}, component_order=, pixel_format=, ack=False). Commit: show(channel=BROADCAST, ack=True) — for a Show, ack chooses a tracked pipelined Show (returns its TxID) vs. a fire-and-forget one.

Inbound: feed(bytes) -> [events]; optional add_listener(type, cb). Events: InfoEvent, ConfigEvent, NetworkConfigEvent, ShowAckEvent, PixelsAckEvent, FillAckEvent, ResetAckEvent, DeviceErrorEvent, UnknownResponseEvent.

Pipelining: frame_gate_open(), pending_shows, pipeline_idle().

reset() is an immediate ordering barrier: the active Show completes, any pending Show is canceled, and RESET_ACK retires every preceding Show transaction still awaiting acknowledgement.

Codec/helpers: encode_frame, parse_frame, FrameDecoder, cobs_encode/decode, crc16, reorder_to_wire, components_per_pixel, bytes_per_pixel, pixel_format_value/name, component_order_value/name, validate_component_order, and output_profile_value.

Roadmap

  • Add opt-in gamma-correction helpers for logical RGB and RGBW pixel data before wire-order conversion. Gamma 1.0 must be an exact identity; per-component curves, rounding, clamping, and 8-bit lookup-table output must match shared golden vectors used by the JavaScript and TouchDesigner libraries. This is a host-side image transform, not an Opalinx protocol feature.

Development

pip install -e .[dev]
pytest

Publishing

Releases are published from GitHub Actions through PyPI Trusted Publishing; no PyPI token is stored in GitHub. The pypi GitHub environment should require approval from a repository maintainer.

Before publishing, update the version in pyproject.toml and src/opalinx/__init__.py, add the matching changelog section, and push those changes. Then create and push a tag that exactly matches the package version with a v prefix:

git tag v0.4.5
git push origin v0.4.5

The publish workflow builds both distributions, verifies their metadata, and refuses to publish if the tag and package version differ.

The test suite embeds golden frames generated by opalinx and asserts byte-for-byte parity.

Licence

The library is available under the Opalinx Noncommercial Licence 1.0. Noncommercial use is free. Commercial use requires a separate written licence; contact Jean-Philippe Cô at jp@djip.co.

Download files

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

Source Distribution

opalinx-0.4.5.tar.gz (45.7 kB view details)

Uploaded Source

Built Distribution

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

opalinx-0.4.5-py3-none-any.whl (35.2 kB view details)

Uploaded Python 3

File details

Details for the file opalinx-0.4.5.tar.gz.

File metadata

  • Download URL: opalinx-0.4.5.tar.gz
  • Upload date:
  • Size: 45.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for opalinx-0.4.5.tar.gz
Algorithm Hash digest
SHA256 e7d55abf057a83c21f2d71afe49cf7cc275771e0de7d945cca5e86b16abc7f6a
MD5 7dd6c2af3aa72a715217038caff01a53
BLAKE2b-256 314df86a8caba010bc996c3cf22905157d185a7e71ac2c473ab72eb160a5c399

See more details on using hashes here.

File details

Details for the file opalinx-0.4.5-py3-none-any.whl.

File metadata

  • Download URL: opalinx-0.4.5-py3-none-any.whl
  • Upload date:
  • Size: 35.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for opalinx-0.4.5-py3-none-any.whl
Algorithm Hash digest
SHA256 310a4758b400b97517af82c772c85097e831213d7c6a6b2850b4ab6c9d79a476
MD5 213a708cf5bb9e337c577558e22fd4db
BLAKE2b-256 f7f23c56d642a97552fb5d69094633e77b7e5c75accd7db73626696869627428

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.4.5 This release

2 files

0.4.3

2 files

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