pyrdmnet 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrdmnet-0.5.0/LICENSE +21 -0
- pyrdmnet-0.5.0/PKG-INFO +243 -0
- pyrdmnet-0.5.0/README.md +215 -0
- pyrdmnet-0.5.0/pyproject.toml +42 -0
- pyrdmnet-0.5.0/pyrdmnet/__init__.py +18 -0
- pyrdmnet-0.5.0/pyrdmnet/__main__.py +463 -0
- pyrdmnet-0.5.0/pyrdmnet/artnet_rdm.py +265 -0
- pyrdmnet-0.5.0/pyrdmnet/broker.py +713 -0
- pyrdmnet-0.5.0/pyrdmnet/broker_responder.py +140 -0
- pyrdmnet-0.5.0/pyrdmnet/client.py +475 -0
- pyrdmnet-0.5.0/pyrdmnet/client_responder.py +168 -0
- pyrdmnet-0.5.0/pyrdmnet/connection.py +62 -0
- pyrdmnet-0.5.0/pyrdmnet/constants.py +344 -0
- pyrdmnet-0.5.0/pyrdmnet/controller.py +309 -0
- pyrdmnet-0.5.0/pyrdmnet/dnssd.py +389 -0
- pyrdmnet-0.5.0/pyrdmnet/ept.py +266 -0
- pyrdmnet-0.5.0/pyrdmnet/facade.py +297 -0
- pyrdmnet-0.5.0/pyrdmnet/live_test.py +562 -0
- pyrdmnet-0.5.0/pyrdmnet/llrp.py +796 -0
- pyrdmnet-0.5.0/pyrdmnet/netif.py +103 -0
- pyrdmnet-0.5.0/pyrdmnet/pdu.py +423 -0
- pyrdmnet-0.5.0/pyrdmnet/rdm.py +201 -0
- pyrdmnet-0.5.0/pyrdmnet/rdm_params.py +346 -0
- pyrdmnet-0.5.0/pyrdmnet/readout.py +261 -0
- pyrdmnet-0.5.0/pyrdmnet/settings_check.py +207 -0
- pyrdmnet-0.5.0/pyrdmnet/simulator.py +328 -0
- pyrdmnet-0.5.0/pyrdmnet/storage.py +330 -0
- pyrdmnet-0.5.0/pyrdmnet/write_test.py +249 -0
- pyrdmnet-0.5.0/pyrdmnet.egg-info/PKG-INFO +243 -0
- pyrdmnet-0.5.0/pyrdmnet.egg-info/SOURCES.txt +54 -0
- pyrdmnet-0.5.0/pyrdmnet.egg-info/dependency_links.txt +1 -0
- pyrdmnet-0.5.0/pyrdmnet.egg-info/entry_points.txt +2 -0
- pyrdmnet-0.5.0/pyrdmnet.egg-info/requires.txt +3 -0
- pyrdmnet-0.5.0/pyrdmnet.egg-info/top_level.txt +1 -0
- pyrdmnet-0.5.0/setup.cfg +4 -0
- pyrdmnet-0.5.0/tests/test_artnet_rdm.py +100 -0
- pyrdmnet-0.5.0/tests/test_broker.py +178 -0
- pyrdmnet-0.5.0/tests/test_broker_responder.py +130 -0
- pyrdmnet-0.5.0/tests/test_client_entry_update.py +113 -0
- pyrdmnet-0.5.0/tests/test_devices.py +304 -0
- pyrdmnet-0.5.0/tests/test_encoding.py +182 -0
- pyrdmnet-0.5.0/tests/test_ept.py +205 -0
- pyrdmnet-0.5.0/tests/test_facade.py +139 -0
- pyrdmnet-0.5.0/tests/test_fair_queuing.py +121 -0
- pyrdmnet-0.5.0/tests/test_ipv6_broker.py +187 -0
- pyrdmnet-0.5.0/tests/test_live_checks.py +138 -0
- pyrdmnet-0.5.0/tests/test_live_test.py +182 -0
- pyrdmnet-0.5.0/tests/test_llrp.py +447 -0
- pyrdmnet-0.5.0/tests/test_mdns.py +122 -0
- pyrdmnet-0.5.0/tests/test_reconnect.py +261 -0
- pyrdmnet-0.5.0/tests/test_scope_guard.py +209 -0
- pyrdmnet-0.5.0/tests/test_simulator_llrp.py +129 -0
- pyrdmnet-0.5.0/tests/test_storage_migration.py +143 -0
- pyrdmnet-0.5.0/tests/test_tcp_comms.py +233 -0
- pyrdmnet-0.5.0/tests/test_write_policy.py +68 -0
- pyrdmnet-0.5.0/tests/test_write_test.py +148 -0
pyrdmnet-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stefan Schlagbauer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyrdmnet-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrdmnet
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: RDMnet (ANSI E1.33) broker and controller in pure Python
|
|
5
|
+
Author: Stefan Schlagbauer
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Schlagi-lx/pyrdmnet
|
|
8
|
+
Project-URL: Changelog, https://github.com/Schlagi-lx/pyrdmnet/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/Schlagi-lx/pyrdmnet/issues
|
|
10
|
+
Keywords: rdmnet,rdm,e1.33,e1.20,dmx,lighting,broker
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
+
Classifier: Topic :: System :: Networking
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: zeroconf
|
|
26
|
+
Requires-Dist: zeroconf; extra == "zeroconf"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# pyrdmnet
|
|
30
|
+
|
|
31
|
+
**A self-contained RDMnet (ANSI E1.33) broker and controller in pure Python – no external dependencies.**
|
|
32
|
+
|
|
33
|
+
pyrdmnet discovers RDM devices behind RDMnet gateways, reads device info, personalities and sensors, and can set
|
|
34
|
+
DMX personality and start address under strict safeguards. It runs standalone and can be embedded in other programs
|
|
35
|
+
through a synchronous interface.
|
|
36
|
+
|
|
37
|
+
> **Coming from 0.4 (German names)?** Version 0.5 is a breaking change: all names (modules, classes, functions, CLI commands and
|
|
38
|
+
> options, database tables and JSON keys) are now English. See [CHANGELOG.md](https://github.com/Schlagi-lx/pyrdmnet/blob/main/CHANGELOG.md) and the step-by-step
|
|
39
|
+
> guide [docs/MIGRATING_TO_0.5.md](https://github.com/Schlagi-lx/pyrdmnet/blob/main/docs/MIGRATING_TO_0.5.md). Existing SQLite databases are migrated automatically
|
|
40
|
+
> when opened (back them up first – the migration is one-way).
|
|
41
|
+
|
|
42
|
+
## Status
|
|
43
|
+
|
|
44
|
+
| | |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Software tests | 238/238 passing on Linux, macOS and Windows (Python 3.10–3.13, standard library only) |
|
|
47
|
+
| Real-world test | with an **LSS MasterPort 2** (firmware 2.13.0028) as RDMnet gateway and an **ETC ColorSource PAR**: broker discovery via mDNS, connection, finding devices, reading, setting mode/start address and reading back; with 0.5 also `TCP_COMMS_STATUS`, two controllers at once (fair queuing), reconnection after a broker restart (about 16 s) and LLRP discovery and reading. The MasterPort acknowledges `COMPONENT_SCOPE` SETs (LLRP and RDMnet) but does not apply them – change its scope in its menu. |
|
|
48
|
+
| Other gateways/devices | not tested – feedback welcome |
|
|
49
|
+
|
|
50
|
+
No E1.33 conformance testing, no certification. Use at your own risk (see license).
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **Broker** per ANSI E1.33 (asyncio): connect with scope check, dynamic UIDs, client list, RPT forwarding
|
|
55
|
+
(including broadcast), heartbeat, packet capture as JSONL, Client Entry Update (change an entry without reconnecting)
|
|
56
|
+
- **Fair queuing** (E1.33 9.2.10): the broker keeps at most `device_window` requests (default 4) in flight per device
|
|
57
|
+
and sends waiting requests of several controllers round-robin, so a busy controller cannot starve the others
|
|
58
|
+
- **EPT** (E1.33 section 8): the broker accepts EPT clients, keeps their client list and forwards EPT data between
|
|
59
|
+
them, including one-to-many messages packed with PDU inheritance; `EptClient` for own tools and tests
|
|
60
|
+
- **Broker RDM responder**: controllers can query the broker itself (`COMPONENT_SCOPE`, `SEARCH_DOMAIN`,
|
|
61
|
+
`BROKER_STATUS`, device info). Scope and state cannot be changed via RDM.
|
|
62
|
+
- **mDNS registration** `_rdmnet._tcp` with scope subtype – via Bonjour (`dns-sd`, available on macOS), otherwise
|
|
63
|
+
optionally via the `zeroconf` package
|
|
64
|
+
- **One broker per scope** (E1.33 9.1.4): before announcing itself the broker looks for other brokers on its scope;
|
|
65
|
+
if there is one it stays in STANDBY (reported via `BROKER_STATUS`) until the other one is gone, and while running it
|
|
66
|
+
warns when another broker appears. `brokers` lists all brokers of a scope.
|
|
67
|
+
- **Controller**: RDM GET, ACK_TIMER, ACK_OVERFLOW, NACK, status messages; `read_tcp_comms_status()` for the broker
|
|
68
|
+
address and unhealthy-connection count of a device or controller (`TCP_COMMS_STATUS`)
|
|
69
|
+
- **Default responders of controllers and devices** (E1.33 7.2.1, 7.4.8): the controller answers RDM requests to its
|
|
70
|
+
own UID (required E1.20 PIDs, `COMPONENT_SCOPE`, `SEARCH_DOMAIN`, `TCP_COMMS_STATUS`); a SET of `COMPONENT_SCOPE`
|
|
71
|
+
moves a client to the new scope (and static broker). Unhealthy TCP connections are counted per scope.
|
|
72
|
+
- **Automatic reconnection** (E1.33 6.2.5) for controllers, devices and EPT clients: after a broker restart or a
|
|
73
|
+
lost connection they reconnect by themselves with the back-off of the standard; Client Redirect (IPv4 and IPv6) is
|
|
74
|
+
followed, and the broker can redirect a client itself (`redirect_client`).
|
|
75
|
+
- **IPv6**: the broker listens on IPv4 and/or IPv6 (`BrokerConfig.extra_hosts`, CLI `broker --ipv6 ::`), clients
|
|
76
|
+
connect to either (`--broker [fd00::5]:8888`); static IPv6 broker addresses in `COMPONENT_SCOPE`.
|
|
77
|
+
The facade waits for the reconnection instead of failing.
|
|
78
|
+
- **Reading** via gateway endpoints or native RDMnet devices: manufacturer, model, firmware, DMX personalities with
|
|
79
|
+
footprint, start address, sensors, device hours
|
|
80
|
+
- **Settings check** of the device setting against an expected setting (mode, footprint, start address, universe)
|
|
81
|
+
- **Restricted RDM SET** (see below)
|
|
82
|
+
- **Sensor snapshots** before/after an action, stored in SQLite (tables `rdm_*`)
|
|
83
|
+
- **RDM over Art-Net** (ArtTod, ArtRdm)
|
|
84
|
+
- **LLRP** (E1.33 section 5): find RDMnet components without a broker, read and – after confirmation – set scope,
|
|
85
|
+
static broker address and search domain. The broker can respond as an LLRP target itself (`--llrp`). IPv4 and IPv6.
|
|
86
|
+
- **Simulator** for tests without hardware; with `--llrp` also an LLRP target, e.g. started with a wrong scope and then
|
|
87
|
+
fixed with the `llrp` command
|
|
88
|
+
|
|
89
|
+
## Write safety
|
|
90
|
+
|
|
91
|
+
The module only reads by default. Which RDM SET commands may be sent is set by a `WritePolicy` (controller,
|
|
92
|
+
Art-Net controller, LLRP manager, `RdmConfig.write_policy`):
|
|
93
|
+
|
|
94
|
+
- default `WritePolicy()`: any PID, but only after explicit write permission per session (`write_enabled = True`,
|
|
95
|
+
facade: `set_write_enabled(True)`)
|
|
96
|
+
- `WritePolicy.lab()`: measurement-lab profile, only `DMX_PERSONALITY`, `DMX_START_ADDRESS` and `IDENTIFY_DEVICE`;
|
|
97
|
+
`WritePolicy.llrp_lab()`: over LLRP only `COMPONENT_SCOPE`, `SEARCH_DOMAIN` and `IDENTIFY_DEVICE`
|
|
98
|
+
- own lists: `WritePolicy({C.PID_DEVICE_LABEL, ...})`; for scripts without the per-session permission:
|
|
99
|
+
`WritePolicy(require_write_enabled=False)`
|
|
100
|
+
- the settings workflow reads every change back and logs it as old → new
|
|
101
|
+
- the module never sends discovery commands (`DISC_*`), whatever the policy; discovery is done by the gateway
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from pyrdmnet.controller import WritePolicy
|
|
105
|
+
module = RdmModule(RdmConfig(broker_host=adapter_ip, write_policy=WritePolicy.lab()))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Quick start
|
|
109
|
+
|
|
110
|
+
Requires Python 3.10 or later. No further packages needed.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install pyrdmnet # optionally with mDNS via zeroconf: pip install "pyrdmnet[zeroconf]"
|
|
114
|
+
pyrdmnet demo # offline run with the simulator (same as: python3 -m pyrdmnet demo)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
From source, with the tests:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
git clone https://github.com/Schlagi-lx/pyrdmnet.git
|
|
121
|
+
cd pyrdmnet
|
|
122
|
+
python3 -m unittest discover -s tests -t tests # tests
|
|
123
|
+
python3 -m pyrdmnet demo # offline run with the simulator
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or install the checkout as a package: `pip install .`
|
|
127
|
+
|
|
128
|
+
### Commands
|
|
129
|
+
|
|
130
|
+
| Command | Purpose |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `broker` | start the broker (runs until Ctrl+C) |
|
|
133
|
+
| `simulator` | connect a simulated RDMnet device to a broker |
|
|
134
|
+
| `read` | find and read all devices on the broker (read only) |
|
|
135
|
+
| `livetest` | test with a real gateway, read only |
|
|
136
|
+
| `livetest --checks all` | additionally `TCP_COMMS_STATUS`, two controllers at once, LLRP (read only), broker restart |
|
|
137
|
+
| `writetest` | set mode/start address on a real device, verify and restore (with on-site confirmation) |
|
|
138
|
+
| `llrp` | find components via LLRP (no broker needed); set with `--uid` and `--scope`/`--broker`/`--domain` |
|
|
139
|
+
| `brokers` | list the brokers announced via mDNS for a scope; warns if there is more than one |
|
|
140
|
+
| `demo` | complete offline run (simulation) |
|
|
141
|
+
|
|
142
|
+
Example with separate processes:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python3 -m pyrdmnet broker --host 127.0.0.1 --port 8888 --log events.jsonl
|
|
146
|
+
python3 -m pyrdmnet simulator --broker 127.0.0.1:8888
|
|
147
|
+
python3 -m pyrdmnet read --broker 127.0.0.1:8888 --json devices.json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Gateway with the wrong scope or without broker discovery? Find and reconfigure it via LLRP:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
python3 -m pyrdmnet llrp --adapter 192.168.1.10 # search, show scope
|
|
154
|
+
python3 -m pyrdmnet llrp --adapter 192.168.1.10 --uid 7ff0:00000100 \
|
|
155
|
+
--scope default --broker 192.168.1.10:8888 # set scope + static broker
|
|
156
|
+
python3 -m pyrdmnet llrp --adapter 192.168.1.10 --interface6 eth0 # IPv6 on a named interface
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Try it without hardware: a simulator with the wrong scope keeps trying to connect and answers LLRP; fixing its scope
|
|
160
|
+
connects it at once.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
python3 -m pyrdmnet broker --port 8888
|
|
164
|
+
python3 -m pyrdmnet simulator --broker 127.0.0.1:8888 --scope wrong --llrp
|
|
165
|
+
python3 -m pyrdmnet llrp # finds the simulator, scope "wrong"
|
|
166
|
+
python3 -m pyrdmnet llrp --uid 7ff0:00000100 --scope default --yes # it connects to the broker
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
LLRP runs over IPv4 and IPv6 at the same time. `--interface6` selects the IPv6 interface (name such as `eth0`/`en0`,
|
|
170
|
+
or the index; on Windows see `netsh interface ipv6 show interfaces`); without it the system default is used. If the
|
|
171
|
+
computer has no IPv6, LLRP continues over IPv4 with a warning.
|
|
172
|
+
|
|
173
|
+
On a real network, set `--host` to the IP of the network adapter and register via mDNS with `--dnssd`. On the
|
|
174
|
+
gateway, set broker discovery to mDNS and enter the same scope (default: `default`, case-sensitive). Avoid competing
|
|
175
|
+
brokers in the same scope.
|
|
176
|
+
|
|
177
|
+
### Windows
|
|
178
|
+
|
|
179
|
+
pyrdmnet runs on Windows with the standard Python from python.org (tested in CI with Python 3.10–3.13). Points to
|
|
180
|
+
note:
|
|
181
|
+
|
|
182
|
+
- **mDNS:** broker registration (`--dnssd`, default for `livetest`/`writetest`) needs either Apple's Bonjour
|
|
183
|
+
service (provides `dns-sd`, e.g. "Bonjour Print Services for Windows") or the `zeroconf` package:
|
|
184
|
+
`pip install ".[zeroconf]"`. Without either, use `--no-mdns` and enter the broker address in the gateway.
|
|
185
|
+
- **Firewall:** on the first broker start, Windows Defender Firewall asks whether Python may accept connections.
|
|
186
|
+
Allow it for the network the gateway is on. If that network is classified as "Public", either change it to
|
|
187
|
+
"Private" or allow inbound TCP 8888 (broker), UDP 5569 (LLRP) and UDP 6454 (Art-Net) for Python.
|
|
188
|
+
- **Adapter selection:** `livetest`/`writetest` without `--host` list the adapters from `ipconfig`.
|
|
189
|
+
|
|
190
|
+
## Embedding in your own programs
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from pyrdmnet.facade import RdmModule, RdmConfig
|
|
194
|
+
from pyrdmnet.settings_check import ExpectedSetting
|
|
195
|
+
|
|
196
|
+
module = RdmModule(RdmConfig(broker_host=adapter_ip, db="rdm.sqlite"))
|
|
197
|
+
module.start()
|
|
198
|
+
profile = module.read_device(module.find_devices()[0])
|
|
199
|
+
expected = ExpectedSetting(gdtf_mode, footprint, start_address, universe)
|
|
200
|
+
check = module.check_setting(profile, expected, reference="session-1")
|
|
201
|
+
if not check.measurement_allowed:
|
|
202
|
+
module.set_write_enabled(True) # only after on-site confirmation
|
|
203
|
+
application, profile, check = module.apply_setting(profile, expected, reference="session-1")
|
|
204
|
+
with module.accompany_measurement(profile, measurement_ref="1"):
|
|
205
|
+
... # your own action; sensors are stored before and after
|
|
206
|
+
module.stop()
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
A mode change requires a confirmed mapping RDM personality ↔ mode (per model and firmware); nothing is guessed.
|
|
210
|
+
|
|
211
|
+
## Structure
|
|
212
|
+
|
|
213
|
+
| File | Content |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `constants.py` | protocol constants E1.20, E1.33, E1.37-7 |
|
|
216
|
+
| `rdm.py`, `rdm_params.py` | RDM messages, UID, checksum, parameter data |
|
|
217
|
+
| `pdu.py`, `connection.py` | ACN TCP preamble, root/broker/RPT PDUs, TCP connection |
|
|
218
|
+
| `broker.py`, `broker_responder.py` | broker, the broker's RDM responder |
|
|
219
|
+
| `client.py`, `client_responder.py`, `controller.py` | RPT client, default responder of clients, controller |
|
|
220
|
+
| `readout.py`, `settings_check.py` | reading devices, settings check, applying with read-back |
|
|
221
|
+
| `storage.py` | SQLite tables `rdm_*`, migration of 0.4 databases |
|
|
222
|
+
| `facade.py` | synchronous interface (asyncio in its own thread) |
|
|
223
|
+
| `dnssd.py` | mDNS registration |
|
|
224
|
+
| `artnet_rdm.py` | RDM over Art-Net |
|
|
225
|
+
| `llrp.py` | LLRP manager and target |
|
|
226
|
+
| `ept.py` | EPT PDUs (with inheritance), EPT client |
|
|
227
|
+
| `simulator.py`, `live_test.py`, `write_test.py`, `__main__.py` | simulator, tools, command line |
|
|
228
|
+
|
|
229
|
+
## Notes
|
|
230
|
+
|
|
231
|
+
- The protocol constants were checked against ETC's public reference implementations
|
|
232
|
+
([ETCLabs/RDMnet](https://github.com/ETCLabs/RDMnet), [ETCLabs/RDM](https://github.com/ETCLabs/RDM)); no code was
|
|
233
|
+
taken from them.
|
|
234
|
+
- By default the broker uses the ESTA prototype manufacturer ID `0x7FF0` (intended for development). If you use the
|
|
235
|
+
module in a product, set your own ESTA manufacturer ID in `RdmConfig.broker_uid`.
|
|
236
|
+
- The ANSI E1.20 and E1.33 standards are available from ESTA (tsp.esta.org).
|
|
237
|
+
- `COMPONENT_SCOPE`: E1.33-2019 states a data length of 0x54, but the fields add up to 88 bytes (0x58). pyrdmnet sends
|
|
238
|
+
and expects 88 bytes, like ETC's reference implementation.
|
|
239
|
+
- "RDMnet" refers to the ESTA standard ANSI E1.33. This project is not affiliated with ESTA, ETC or LSS.
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT – see [LICENSE](https://github.com/Schlagi-lx/pyrdmnet/blob/main/LICENSE).
|
pyrdmnet-0.5.0/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# pyrdmnet
|
|
2
|
+
|
|
3
|
+
**A self-contained RDMnet (ANSI E1.33) broker and controller in pure Python – no external dependencies.**
|
|
4
|
+
|
|
5
|
+
pyrdmnet discovers RDM devices behind RDMnet gateways, reads device info, personalities and sensors, and can set
|
|
6
|
+
DMX personality and start address under strict safeguards. It runs standalone and can be embedded in other programs
|
|
7
|
+
through a synchronous interface.
|
|
8
|
+
|
|
9
|
+
> **Coming from 0.4 (German names)?** Version 0.5 is a breaking change: all names (modules, classes, functions, CLI commands and
|
|
10
|
+
> options, database tables and JSON keys) are now English. See [CHANGELOG.md](https://github.com/Schlagi-lx/pyrdmnet/blob/main/CHANGELOG.md) and the step-by-step
|
|
11
|
+
> guide [docs/MIGRATING_TO_0.5.md](https://github.com/Schlagi-lx/pyrdmnet/blob/main/docs/MIGRATING_TO_0.5.md). Existing SQLite databases are migrated automatically
|
|
12
|
+
> when opened (back them up first – the migration is one-way).
|
|
13
|
+
|
|
14
|
+
## Status
|
|
15
|
+
|
|
16
|
+
| | |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Software tests | 238/238 passing on Linux, macOS and Windows (Python 3.10–3.13, standard library only) |
|
|
19
|
+
| Real-world test | with an **LSS MasterPort 2** (firmware 2.13.0028) as RDMnet gateway and an **ETC ColorSource PAR**: broker discovery via mDNS, connection, finding devices, reading, setting mode/start address and reading back; with 0.5 also `TCP_COMMS_STATUS`, two controllers at once (fair queuing), reconnection after a broker restart (about 16 s) and LLRP discovery and reading. The MasterPort acknowledges `COMPONENT_SCOPE` SETs (LLRP and RDMnet) but does not apply them – change its scope in its menu. |
|
|
20
|
+
| Other gateways/devices | not tested – feedback welcome |
|
|
21
|
+
|
|
22
|
+
No E1.33 conformance testing, no certification. Use at your own risk (see license).
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Broker** per ANSI E1.33 (asyncio): connect with scope check, dynamic UIDs, client list, RPT forwarding
|
|
27
|
+
(including broadcast), heartbeat, packet capture as JSONL, Client Entry Update (change an entry without reconnecting)
|
|
28
|
+
- **Fair queuing** (E1.33 9.2.10): the broker keeps at most `device_window` requests (default 4) in flight per device
|
|
29
|
+
and sends waiting requests of several controllers round-robin, so a busy controller cannot starve the others
|
|
30
|
+
- **EPT** (E1.33 section 8): the broker accepts EPT clients, keeps their client list and forwards EPT data between
|
|
31
|
+
them, including one-to-many messages packed with PDU inheritance; `EptClient` for own tools and tests
|
|
32
|
+
- **Broker RDM responder**: controllers can query the broker itself (`COMPONENT_SCOPE`, `SEARCH_DOMAIN`,
|
|
33
|
+
`BROKER_STATUS`, device info). Scope and state cannot be changed via RDM.
|
|
34
|
+
- **mDNS registration** `_rdmnet._tcp` with scope subtype – via Bonjour (`dns-sd`, available on macOS), otherwise
|
|
35
|
+
optionally via the `zeroconf` package
|
|
36
|
+
- **One broker per scope** (E1.33 9.1.4): before announcing itself the broker looks for other brokers on its scope;
|
|
37
|
+
if there is one it stays in STANDBY (reported via `BROKER_STATUS`) until the other one is gone, and while running it
|
|
38
|
+
warns when another broker appears. `brokers` lists all brokers of a scope.
|
|
39
|
+
- **Controller**: RDM GET, ACK_TIMER, ACK_OVERFLOW, NACK, status messages; `read_tcp_comms_status()` for the broker
|
|
40
|
+
address and unhealthy-connection count of a device or controller (`TCP_COMMS_STATUS`)
|
|
41
|
+
- **Default responders of controllers and devices** (E1.33 7.2.1, 7.4.8): the controller answers RDM requests to its
|
|
42
|
+
own UID (required E1.20 PIDs, `COMPONENT_SCOPE`, `SEARCH_DOMAIN`, `TCP_COMMS_STATUS`); a SET of `COMPONENT_SCOPE`
|
|
43
|
+
moves a client to the new scope (and static broker). Unhealthy TCP connections are counted per scope.
|
|
44
|
+
- **Automatic reconnection** (E1.33 6.2.5) for controllers, devices and EPT clients: after a broker restart or a
|
|
45
|
+
lost connection they reconnect by themselves with the back-off of the standard; Client Redirect (IPv4 and IPv6) is
|
|
46
|
+
followed, and the broker can redirect a client itself (`redirect_client`).
|
|
47
|
+
- **IPv6**: the broker listens on IPv4 and/or IPv6 (`BrokerConfig.extra_hosts`, CLI `broker --ipv6 ::`), clients
|
|
48
|
+
connect to either (`--broker [fd00::5]:8888`); static IPv6 broker addresses in `COMPONENT_SCOPE`.
|
|
49
|
+
The facade waits for the reconnection instead of failing.
|
|
50
|
+
- **Reading** via gateway endpoints or native RDMnet devices: manufacturer, model, firmware, DMX personalities with
|
|
51
|
+
footprint, start address, sensors, device hours
|
|
52
|
+
- **Settings check** of the device setting against an expected setting (mode, footprint, start address, universe)
|
|
53
|
+
- **Restricted RDM SET** (see below)
|
|
54
|
+
- **Sensor snapshots** before/after an action, stored in SQLite (tables `rdm_*`)
|
|
55
|
+
- **RDM over Art-Net** (ArtTod, ArtRdm)
|
|
56
|
+
- **LLRP** (E1.33 section 5): find RDMnet components without a broker, read and – after confirmation – set scope,
|
|
57
|
+
static broker address and search domain. The broker can respond as an LLRP target itself (`--llrp`). IPv4 and IPv6.
|
|
58
|
+
- **Simulator** for tests without hardware; with `--llrp` also an LLRP target, e.g. started with a wrong scope and then
|
|
59
|
+
fixed with the `llrp` command
|
|
60
|
+
|
|
61
|
+
## Write safety
|
|
62
|
+
|
|
63
|
+
The module only reads by default. Which RDM SET commands may be sent is set by a `WritePolicy` (controller,
|
|
64
|
+
Art-Net controller, LLRP manager, `RdmConfig.write_policy`):
|
|
65
|
+
|
|
66
|
+
- default `WritePolicy()`: any PID, but only after explicit write permission per session (`write_enabled = True`,
|
|
67
|
+
facade: `set_write_enabled(True)`)
|
|
68
|
+
- `WritePolicy.lab()`: measurement-lab profile, only `DMX_PERSONALITY`, `DMX_START_ADDRESS` and `IDENTIFY_DEVICE`;
|
|
69
|
+
`WritePolicy.llrp_lab()`: over LLRP only `COMPONENT_SCOPE`, `SEARCH_DOMAIN` and `IDENTIFY_DEVICE`
|
|
70
|
+
- own lists: `WritePolicy({C.PID_DEVICE_LABEL, ...})`; for scripts without the per-session permission:
|
|
71
|
+
`WritePolicy(require_write_enabled=False)`
|
|
72
|
+
- the settings workflow reads every change back and logs it as old → new
|
|
73
|
+
- the module never sends discovery commands (`DISC_*`), whatever the policy; discovery is done by the gateway
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pyrdmnet.controller import WritePolicy
|
|
77
|
+
module = RdmModule(RdmConfig(broker_host=adapter_ip, write_policy=WritePolicy.lab()))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
Requires Python 3.10 or later. No further packages needed.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install pyrdmnet # optionally with mDNS via zeroconf: pip install "pyrdmnet[zeroconf]"
|
|
86
|
+
pyrdmnet demo # offline run with the simulator (same as: python3 -m pyrdmnet demo)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
From source, with the tests:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/Schlagi-lx/pyrdmnet.git
|
|
93
|
+
cd pyrdmnet
|
|
94
|
+
python3 -m unittest discover -s tests -t tests # tests
|
|
95
|
+
python3 -m pyrdmnet demo # offline run with the simulator
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or install the checkout as a package: `pip install .`
|
|
99
|
+
|
|
100
|
+
### Commands
|
|
101
|
+
|
|
102
|
+
| Command | Purpose |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `broker` | start the broker (runs until Ctrl+C) |
|
|
105
|
+
| `simulator` | connect a simulated RDMnet device to a broker |
|
|
106
|
+
| `read` | find and read all devices on the broker (read only) |
|
|
107
|
+
| `livetest` | test with a real gateway, read only |
|
|
108
|
+
| `livetest --checks all` | additionally `TCP_COMMS_STATUS`, two controllers at once, LLRP (read only), broker restart |
|
|
109
|
+
| `writetest` | set mode/start address on a real device, verify and restore (with on-site confirmation) |
|
|
110
|
+
| `llrp` | find components via LLRP (no broker needed); set with `--uid` and `--scope`/`--broker`/`--domain` |
|
|
111
|
+
| `brokers` | list the brokers announced via mDNS for a scope; warns if there is more than one |
|
|
112
|
+
| `demo` | complete offline run (simulation) |
|
|
113
|
+
|
|
114
|
+
Example with separate processes:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
python3 -m pyrdmnet broker --host 127.0.0.1 --port 8888 --log events.jsonl
|
|
118
|
+
python3 -m pyrdmnet simulator --broker 127.0.0.1:8888
|
|
119
|
+
python3 -m pyrdmnet read --broker 127.0.0.1:8888 --json devices.json
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Gateway with the wrong scope or without broker discovery? Find and reconfigure it via LLRP:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python3 -m pyrdmnet llrp --adapter 192.168.1.10 # search, show scope
|
|
126
|
+
python3 -m pyrdmnet llrp --adapter 192.168.1.10 --uid 7ff0:00000100 \
|
|
127
|
+
--scope default --broker 192.168.1.10:8888 # set scope + static broker
|
|
128
|
+
python3 -m pyrdmnet llrp --adapter 192.168.1.10 --interface6 eth0 # IPv6 on a named interface
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Try it without hardware: a simulator with the wrong scope keeps trying to connect and answers LLRP; fixing its scope
|
|
132
|
+
connects it at once.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python3 -m pyrdmnet broker --port 8888
|
|
136
|
+
python3 -m pyrdmnet simulator --broker 127.0.0.1:8888 --scope wrong --llrp
|
|
137
|
+
python3 -m pyrdmnet llrp # finds the simulator, scope "wrong"
|
|
138
|
+
python3 -m pyrdmnet llrp --uid 7ff0:00000100 --scope default --yes # it connects to the broker
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
LLRP runs over IPv4 and IPv6 at the same time. `--interface6` selects the IPv6 interface (name such as `eth0`/`en0`,
|
|
142
|
+
or the index; on Windows see `netsh interface ipv6 show interfaces`); without it the system default is used. If the
|
|
143
|
+
computer has no IPv6, LLRP continues over IPv4 with a warning.
|
|
144
|
+
|
|
145
|
+
On a real network, set `--host` to the IP of the network adapter and register via mDNS with `--dnssd`. On the
|
|
146
|
+
gateway, set broker discovery to mDNS and enter the same scope (default: `default`, case-sensitive). Avoid competing
|
|
147
|
+
brokers in the same scope.
|
|
148
|
+
|
|
149
|
+
### Windows
|
|
150
|
+
|
|
151
|
+
pyrdmnet runs on Windows with the standard Python from python.org (tested in CI with Python 3.10–3.13). Points to
|
|
152
|
+
note:
|
|
153
|
+
|
|
154
|
+
- **mDNS:** broker registration (`--dnssd`, default for `livetest`/`writetest`) needs either Apple's Bonjour
|
|
155
|
+
service (provides `dns-sd`, e.g. "Bonjour Print Services for Windows") or the `zeroconf` package:
|
|
156
|
+
`pip install ".[zeroconf]"`. Without either, use `--no-mdns` and enter the broker address in the gateway.
|
|
157
|
+
- **Firewall:** on the first broker start, Windows Defender Firewall asks whether Python may accept connections.
|
|
158
|
+
Allow it for the network the gateway is on. If that network is classified as "Public", either change it to
|
|
159
|
+
"Private" or allow inbound TCP 8888 (broker), UDP 5569 (LLRP) and UDP 6454 (Art-Net) for Python.
|
|
160
|
+
- **Adapter selection:** `livetest`/`writetest` without `--host` list the adapters from `ipconfig`.
|
|
161
|
+
|
|
162
|
+
## Embedding in your own programs
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from pyrdmnet.facade import RdmModule, RdmConfig
|
|
166
|
+
from pyrdmnet.settings_check import ExpectedSetting
|
|
167
|
+
|
|
168
|
+
module = RdmModule(RdmConfig(broker_host=adapter_ip, db="rdm.sqlite"))
|
|
169
|
+
module.start()
|
|
170
|
+
profile = module.read_device(module.find_devices()[0])
|
|
171
|
+
expected = ExpectedSetting(gdtf_mode, footprint, start_address, universe)
|
|
172
|
+
check = module.check_setting(profile, expected, reference="session-1")
|
|
173
|
+
if not check.measurement_allowed:
|
|
174
|
+
module.set_write_enabled(True) # only after on-site confirmation
|
|
175
|
+
application, profile, check = module.apply_setting(profile, expected, reference="session-1")
|
|
176
|
+
with module.accompany_measurement(profile, measurement_ref="1"):
|
|
177
|
+
... # your own action; sensors are stored before and after
|
|
178
|
+
module.stop()
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
A mode change requires a confirmed mapping RDM personality ↔ mode (per model and firmware); nothing is guessed.
|
|
182
|
+
|
|
183
|
+
## Structure
|
|
184
|
+
|
|
185
|
+
| File | Content |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `constants.py` | protocol constants E1.20, E1.33, E1.37-7 |
|
|
188
|
+
| `rdm.py`, `rdm_params.py` | RDM messages, UID, checksum, parameter data |
|
|
189
|
+
| `pdu.py`, `connection.py` | ACN TCP preamble, root/broker/RPT PDUs, TCP connection |
|
|
190
|
+
| `broker.py`, `broker_responder.py` | broker, the broker's RDM responder |
|
|
191
|
+
| `client.py`, `client_responder.py`, `controller.py` | RPT client, default responder of clients, controller |
|
|
192
|
+
| `readout.py`, `settings_check.py` | reading devices, settings check, applying with read-back |
|
|
193
|
+
| `storage.py` | SQLite tables `rdm_*`, migration of 0.4 databases |
|
|
194
|
+
| `facade.py` | synchronous interface (asyncio in its own thread) |
|
|
195
|
+
| `dnssd.py` | mDNS registration |
|
|
196
|
+
| `artnet_rdm.py` | RDM over Art-Net |
|
|
197
|
+
| `llrp.py` | LLRP manager and target |
|
|
198
|
+
| `ept.py` | EPT PDUs (with inheritance), EPT client |
|
|
199
|
+
| `simulator.py`, `live_test.py`, `write_test.py`, `__main__.py` | simulator, tools, command line |
|
|
200
|
+
|
|
201
|
+
## Notes
|
|
202
|
+
|
|
203
|
+
- The protocol constants were checked against ETC's public reference implementations
|
|
204
|
+
([ETCLabs/RDMnet](https://github.com/ETCLabs/RDMnet), [ETCLabs/RDM](https://github.com/ETCLabs/RDM)); no code was
|
|
205
|
+
taken from them.
|
|
206
|
+
- By default the broker uses the ESTA prototype manufacturer ID `0x7FF0` (intended for development). If you use the
|
|
207
|
+
module in a product, set your own ESTA manufacturer ID in `RdmConfig.broker_uid`.
|
|
208
|
+
- The ANSI E1.20 and E1.33 standards are available from ESTA (tsp.esta.org).
|
|
209
|
+
- `COMPONENT_SCOPE`: E1.33-2019 states a data length of 0x54, but the fields add up to 88 bytes (0x58). pyrdmnet sends
|
|
210
|
+
and expects 88 bytes, like ETC's reference implementation.
|
|
211
|
+
- "RDMnet" refers to the ESTA standard ANSI E1.33. This project is not affiliated with ESTA, ETC or LSS.
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT – see [LICENSE](https://github.com/Schlagi-lx/pyrdmnet/blob/main/LICENSE).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyrdmnet"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "RDMnet (ANSI E1.33) broker and controller in pure Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Stefan Schlagbauer" }]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = []
|
|
15
|
+
keywords = ["rdmnet", "rdm", "e1.33", "e1.20", "dmx", "lighting", "broker"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Operating System :: MacOS",
|
|
25
|
+
"Operating System :: Microsoft :: Windows",
|
|
26
|
+
"Operating System :: POSIX :: Linux",
|
|
27
|
+
"Topic :: System :: Networking",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
zeroconf = ["zeroconf"]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
pyrdmnet = "pyrdmnet.__main__:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/Schlagi-lx/pyrdmnet"
|
|
38
|
+
Changelog = "https://github.com/Schlagi-lx/pyrdmnet/blob/main/CHANGELOG.md"
|
|
39
|
+
Issues = "https://github.com/Schlagi-lx/pyrdmnet/issues"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools]
|
|
42
|
+
packages = ["pyrdmnet"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""pyrdmnet – a standalone RDMnet broker (ANSI E1.33) and controller.
|
|
2
|
+
|
|
3
|
+
Originated in the "GelMatch" project (measurement of fixtures). The module runs
|
|
4
|
+
standalone; :mod:`pyrdmnet.facade` provides a synchronous interface for embedding.
|
|
5
|
+
|
|
6
|
+
Principles:
|
|
7
|
+
* RDM SET only with per-session write permission; which PIDs may be set is a
|
|
8
|
+
:class:`~pyrdmnet.controller.WritePolicy` (default: any; ``WritePolicy.lab()``:
|
|
9
|
+
DMX mode, start address and identify only). Discovery commands are never sent.
|
|
10
|
+
The settings workflow reads every change back.
|
|
11
|
+
* RDM is supplementary information: an RDM failure does not block a measurement,
|
|
12
|
+
a detected settings deviation does.
|
|
13
|
+
* No external dependencies (Python standard library only).
|
|
14
|
+
mDNS via Bonjour (dns-sd, available on macOS), otherwise optionally ``zeroconf``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.5.0"
|
|
18
|
+
MODULE_NAME = "pyrdmnet"
|