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.
Files changed (56) hide show
  1. pyrdmnet-0.5.0/LICENSE +21 -0
  2. pyrdmnet-0.5.0/PKG-INFO +243 -0
  3. pyrdmnet-0.5.0/README.md +215 -0
  4. pyrdmnet-0.5.0/pyproject.toml +42 -0
  5. pyrdmnet-0.5.0/pyrdmnet/__init__.py +18 -0
  6. pyrdmnet-0.5.0/pyrdmnet/__main__.py +463 -0
  7. pyrdmnet-0.5.0/pyrdmnet/artnet_rdm.py +265 -0
  8. pyrdmnet-0.5.0/pyrdmnet/broker.py +713 -0
  9. pyrdmnet-0.5.0/pyrdmnet/broker_responder.py +140 -0
  10. pyrdmnet-0.5.0/pyrdmnet/client.py +475 -0
  11. pyrdmnet-0.5.0/pyrdmnet/client_responder.py +168 -0
  12. pyrdmnet-0.5.0/pyrdmnet/connection.py +62 -0
  13. pyrdmnet-0.5.0/pyrdmnet/constants.py +344 -0
  14. pyrdmnet-0.5.0/pyrdmnet/controller.py +309 -0
  15. pyrdmnet-0.5.0/pyrdmnet/dnssd.py +389 -0
  16. pyrdmnet-0.5.0/pyrdmnet/ept.py +266 -0
  17. pyrdmnet-0.5.0/pyrdmnet/facade.py +297 -0
  18. pyrdmnet-0.5.0/pyrdmnet/live_test.py +562 -0
  19. pyrdmnet-0.5.0/pyrdmnet/llrp.py +796 -0
  20. pyrdmnet-0.5.0/pyrdmnet/netif.py +103 -0
  21. pyrdmnet-0.5.0/pyrdmnet/pdu.py +423 -0
  22. pyrdmnet-0.5.0/pyrdmnet/rdm.py +201 -0
  23. pyrdmnet-0.5.0/pyrdmnet/rdm_params.py +346 -0
  24. pyrdmnet-0.5.0/pyrdmnet/readout.py +261 -0
  25. pyrdmnet-0.5.0/pyrdmnet/settings_check.py +207 -0
  26. pyrdmnet-0.5.0/pyrdmnet/simulator.py +328 -0
  27. pyrdmnet-0.5.0/pyrdmnet/storage.py +330 -0
  28. pyrdmnet-0.5.0/pyrdmnet/write_test.py +249 -0
  29. pyrdmnet-0.5.0/pyrdmnet.egg-info/PKG-INFO +243 -0
  30. pyrdmnet-0.5.0/pyrdmnet.egg-info/SOURCES.txt +54 -0
  31. pyrdmnet-0.5.0/pyrdmnet.egg-info/dependency_links.txt +1 -0
  32. pyrdmnet-0.5.0/pyrdmnet.egg-info/entry_points.txt +2 -0
  33. pyrdmnet-0.5.0/pyrdmnet.egg-info/requires.txt +3 -0
  34. pyrdmnet-0.5.0/pyrdmnet.egg-info/top_level.txt +1 -0
  35. pyrdmnet-0.5.0/setup.cfg +4 -0
  36. pyrdmnet-0.5.0/tests/test_artnet_rdm.py +100 -0
  37. pyrdmnet-0.5.0/tests/test_broker.py +178 -0
  38. pyrdmnet-0.5.0/tests/test_broker_responder.py +130 -0
  39. pyrdmnet-0.5.0/tests/test_client_entry_update.py +113 -0
  40. pyrdmnet-0.5.0/tests/test_devices.py +304 -0
  41. pyrdmnet-0.5.0/tests/test_encoding.py +182 -0
  42. pyrdmnet-0.5.0/tests/test_ept.py +205 -0
  43. pyrdmnet-0.5.0/tests/test_facade.py +139 -0
  44. pyrdmnet-0.5.0/tests/test_fair_queuing.py +121 -0
  45. pyrdmnet-0.5.0/tests/test_ipv6_broker.py +187 -0
  46. pyrdmnet-0.5.0/tests/test_live_checks.py +138 -0
  47. pyrdmnet-0.5.0/tests/test_live_test.py +182 -0
  48. pyrdmnet-0.5.0/tests/test_llrp.py +447 -0
  49. pyrdmnet-0.5.0/tests/test_mdns.py +122 -0
  50. pyrdmnet-0.5.0/tests/test_reconnect.py +261 -0
  51. pyrdmnet-0.5.0/tests/test_scope_guard.py +209 -0
  52. pyrdmnet-0.5.0/tests/test_simulator_llrp.py +129 -0
  53. pyrdmnet-0.5.0/tests/test_storage_migration.py +143 -0
  54. pyrdmnet-0.5.0/tests/test_tcp_comms.py +233 -0
  55. pyrdmnet-0.5.0/tests/test_write_policy.py +68 -0
  56. 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.
@@ -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).
@@ -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"