Metadata-Version: 2.4
Name: rdm-dmx-async
Version: 1.0.0
Summary: Modern async-first library for RDM and DMX protocols
Author: Arvin Lobo
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Hardware
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyserial>=3.5
Provides-Extra: dev
Requires-Dist: pytest>=7.4.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
Requires-Dist: mypy>=1.5.0; extra == "dev"
Requires-Dist: pre-commit>=4.6.0; extra == "dev"
Requires-Dist: ruff>=0.16.0; extra == "dev"
Requires-Dist: httpx>=0.27.0; extra == "dev"
Provides-Extra: docs
Requires-Dist: pdoc>=14.0.0; extra == "docs"
Provides-Extra: api
Requires-Dist: fastapi>=0.110.0; extra == "api"
Requires-Dist: uvicorn[standard]>=0.29.0; extra == "api"
Provides-Extra: exe
Requires-Dist: fastapi>=0.110.0; extra == "exe"
Requires-Dist: uvicorn[standard]>=0.29.0; extra == "exe"
Requires-Dist: pyinstaller>=6.10.0; extra == "exe"
Dynamic: license-file

# rdm-dmx-async

`rdm-dmx-async` is an async-first Python library for controlling DMX512
universes and managing RDM devices. It provides packet encoding and decoding,
serial transport, RDM discovery, request/response correlation, retry policies,
device parameter APIs, and high-level network lifecycle management.

The project currently targets the ENTTEC DMX USB Pro interface. DMXKing
adapter types are present as extension points, but their framing
implementation is not yet complete.

> **Project status:** Alpha. The public API may change before the first stable
> release.

## Requirements

- [uv](https://docs.astral.sh/uv/) for Python installation, dependency management,
  and running Python commands
- Git, to clone the repository
- Python 3.11 or newer (uv downloads a compatible version automatically if
    one is not already installed)
- A supported USB DMX/RDM interface
- Appropriate serial drivers for the interface
- A DMX cable and, where required, bus termination
- Node.js 18 or newer, only if you plan to run the [Web UI](#web-ui-rest-api--react-frontend)

## Getting started after cloning

All Python setup and commands in this project use `uv`. You do not need to
create or activate a virtual environment, run `pip`, or install the package
globally. `uv sync` creates a local `.venv`, installs the project in editable
mode, and keeps its dependencies synchronized with `pyproject.toml`.

### 1. Install uv

If `uv` is not already installed, follow the
[official uv installation instructions](https://docs.astral.sh/uv/getting-started/installation/).
For example, on Windows PowerShell:

```powershell
irm https://astral.sh/uv/install.ps1 | iex
```

On macOS or Linux:

```console
curl -LsSf https://astral.sh/uv/install.sh | sh
```

Open a new terminal if the installer updated your `PATH`, then verify the
installation:

```console
uv --version
```

### 2. Clone and enter the repository

```console
git clone https://github.com/arvinlobo/rdm-dmx-async.git
cd rdm-dmx-async
```

### 3. Install the required dependencies

Choose the setup that matches what you want to run.

Core library and CLI only:

```console
uv sync
```

Backend API and Web UI:

```console
uv sync --extra api
```

Development, tests, linting, type checking, and documentation:

```console
uv sync --extra dev --extra docs
```

Build the standalone Windows executable:

```console
uv sync --extra exe
```

Extras may be combined. For example, a contributor working on the API can
run:

```console
uv sync --extra dev --extra docs --extra api
```

### 4. Verify the installation

Run the CLI from the uv-managed environment:

```console
uv run rdm-dmx --help
uv run rdm-dmx list-ports
```

From this point onward, run Python tools and scripts with `uv run`. There is
no need to activate `.venv`. Run `uv sync` again after pulling changes that
modify `pyproject.toml`.

## Command-line interface

The synchronized environment provides the `rdm-dmx` command.

List available serial ports:

```console
uv run rdm-dmx list-ports
```

Discover RDM devices using an automatically detected ENTTEC interface:

```console
uv run rdm-dmx discover
```

Specify the serial port and discovery timeout when needed:

```console
uv run rdm-dmx --verbose discover --port COM3 --timeout 10
```

On Linux, a port will typically look like `/dev/ttyUSB0` instead of `COM3`.

## RDM discovery

`NetworkManager` owns the transport, protocol, discovery service, and device
collection. Using it as an async context manager ensures that all background
tasks and the serial connection are cleaned up.

```python
import asyncio

from rdm_dmx_async import NetworkConfig, NetworkManager


async def main() -> None:
    config = NetworkConfig(port="COM3")

    async with NetworkManager(config) as manager:
        devices = await manager.discover_devices()

        for device in devices:
            print(f"{device.uid:012X}: {device.state.device_label}")


asyncio.run(main())
```

Omit `port` to auto-detect the first compatible ENTTEC interface:

```python
config = NetworkConfig()
```

Discovered devices expose focused API groups for common RDM parameters:

```python
async with NetworkManager(NetworkConfig(port="COM3")) as manager:
    devices = await manager.discover_devices()
    if not devices:
        return

    device = devices[0]

    await device.device_label.set("Front Wash")
    await device.dmx_config.set_start_address(1)
    await device.control.identify(True)

    label = await device.device_label.get()
    sensor_definitions = (
        await device.sensor_definitions.get_all_sensor_definitions()
    )

    print(label, sensor_definitions)
```

Other API groups include sensors, maintenance, device information, DMX slots
and modes, lamp control, display settings, position configuration, power,
self-test, presets, and system information.

## DMX output

DMX fixtures require a continuously refreshed stream. Sending a universe once
may produce only a brief flash.

```python
import asyncio

from rdm_dmx_async import NetworkConfig, NetworkManager


async def main() -> None:
    # Channels 1–3 at full intensity; all remaining channels are zero.
    universe = bytes([255, 255, 255] + [0] * 509)
    refresh_interval = 1 / 44

    async with NetworkManager(NetworkConfig(port="COM3")) as manager:
        try:
            while True:
                await manager.send_dmx(universe)
                await asyncio.sleep(refresh_interval)
        finally:
            # Send several blackout frames before disconnecting.
            blackout = bytes(512)
            for _ in range(10):
                await manager.send_dmx(blackout)
                await asyncio.sleep(refresh_interval)


asyncio.run(main())
```

Runnable demonstrations are available in [`examples/`](examples/):

```console
uv run python examples/simple_dmx_example.py --port COM3
uv run python examples/simple_dmx_example.py --port COM3 --example fade
uv run python examples/simple_dmx_example.py --port COM3 --example rgb
uv run python examples/srp_network_manager_example.py
```

## Public API

Frequently used objects are re-exported from `rdm_dmx_async`:

- Application: `NetworkManager`, `NetworkConfig`
- Services: `RdmDevice`, `DeviceRepository`, `DiscoveryService`
- Protocol: `RDME120Protocol`, `ResponseCorrelator`, `RdmValidator`
- Transport: `AsyncSerialTransport`, `EnttecAdapter`, `InterfaceAdapter`
- Packets: `RDMRequest`, `RDMResponse`, `PacketEncoder`, `PacketDecoder`
- Transactions: `AsyncTransaction`, `RetryPolicy`, `TransactionResult`
- Scheduling: `DmxFrameScheduler`
- Types and helpers: `UID`, `PID`, `CommandClass`, UID conversion helpers

Importing from the top-level package is recommended for these stable entry
points:

```python
from rdm_dmx_async import (
    CommandClass,
    NetworkConfig,
    NetworkManager,
    RDMRequest,
    UID,
    uid_from_string,
)
```

## API documentation with pdoc

Install the documentation dependencies and generate the full API reference:

```console
uv sync --extra docs
uv run pdoc rdm_dmx_async --output-directory docs/api
```

To serve the documentation locally with live reload:

```console
uv run pdoc rdm_dmx_async
```

## Web UI (REST API + React frontend)

A FastAPI backend (`api/`) exposes device discovery/control and DMX output
over HTTP, and a Vite + React frontend (`frontend/`) provides a browser UI on
top of it. Both are optional and separate from the core library. `uv` manages
all Python dependencies and backend commands. The frontend uses npm because
it is a Node.js project.

Install the API extra and start the backend (defaults to port 8000):

```console
uv sync --extra api
uv run uvicorn api.app:app --reload --reload-dir api --reload-dir rdm_dmx_async
```

The `--reload-dir` flags scope the file watcher to the source directories;
without them, uvicorn watches the whole working directory (including
`.venv`), which triggers a reload loop on Windows.

The `NetworkManager` is not started automatically - use the UI's Connect
button, or `POST /network/connect`, to open the serial connection.

In a second terminal, start the frontend:

```console
cd frontend
npm install
npm run dev
```

Open the printed URL (default `http://localhost:5173`) - it expects the
backend at `http://localhost:8000` (CORS is preconfigured for this).

### Production build (single process)

For day-to-day use on an operator's machine, you don't need two dev
processes (Vite + uvicorn) or CORS. Build the frontend once, and the
FastAPI backend will serve it directly:

```console
cd frontend
npm install
npm run build
cd ..
uv sync --extra api
uv run uvicorn api.app:app --host 0.0.0.0 --port 8000
```

Then open `http://localhost:8000/` - the API and UI are served from the
same origin/port, so no `--reload` and no separate Node/Vite process are
needed at runtime. `api/app.py` automatically mounts `frontend/dist` at `/`
when it exists; if it's missing (no build was run), the backend still
serves the API only. Rebuild the frontend (`npm run build`) after any
frontend code change to pick it up.

See `docs/ARCHITECTURE.md` and `frontend/README.md` for more detail.

### Standalone .exe (Windows)

For non-developer end users, the production build can be packaged as a
single Windows executable with PyInstaller - no Python, Node, or `uv`
needs to be installed on the target machine:

```console
cd frontend
npm install
npm run build
cd ..
uv sync --extra exe
uv run pyinstaller packaging/rdm_dmx.spec
```

This produces `dist/rdm-dmx.exe`, which bundles the Python runtime, all
dependencies, and the built frontend. Double-clicking it starts the
server on `http://127.0.0.1:8000` and opens it in the default browser;
closing the console window stops the server. Rebuild both the frontend
and the exe after any code change - nothing is watched/hot-reloaded in
this mode.

Every public module, class, function, method, and property has a pdoc-compatible
docstring.

## Development

From the repository root, install all contributor dependencies and the
pre-commit hooks:

```console
uv sync --extra dev --extra docs
uv run pre-commit install
```

Run the automated tests:

```console
uv run pytest
```

Run static checks:

```console
uv run ruff check rdm_dmx_async tests
uv run mypy rdm_dmx_async
```

Run every pre-commit hook manually:

```console
uv run pre-commit run --all-files
```

Tests that require connected DMX/RDM hardware are kept separately in
[`hardware_tests/`](hardware_tests/).

## Project layout

```text
rdm_dmx_async/
├── application/       High-level network orchestration
├── domain/            Standard parameter identifiers
├── packets/           RDM packet types, encoding, and decoding
├── protocols/         E1.20 operations, validation, and correlation
├── scheduling/        DMX refresh and RDM request windows
├── services/          Discovery, devices, repositories, and PID APIs
├── transaction/       Transactions, retries, allocation, and results
└── transport/         Async serial transport and hardware adapters
```

See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for a detailed description
of the layers and their responsibilities.

## Troubleshooting

If an interface or fixture is not responding:

1. Confirm the serial port with `rdm-dmx list-ports`.
2. Close other software that may have opened the same serial port.
3. Verify the fixture's DMX address, personality, and operating mode.
4. Use a proper DMX cable and check signal direction and termination.
5. Ensure DMX output is refreshed continuously rather than sent once.

Additional guides:

- [`DMX_QUICK_START.md`](DMX_QUICK_START.md)
- [`DMX_TROUBLESHOOTING.md`](DMX_TROUBLESHOOTING.md)
- [`NEW_ENTTEC_DMX_USB_PRO_API.pdf`](NEW_ENTTEC_DMX_USB_PRO_API.pdf)

## Standards and references

- ANSI E1.11, USITT DMX512-A
- ANSI E1.20, Remote Device Management
- ANSI E1.37-1, additional RDM parameter messages
- ENTTEC DMX USB Pro API
