ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dmx.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file dmx.h
6 * @brief DMX512 framing + RDM (ANSI E1.20) management codec (PROTOCORE_ENABLE_DMX).
7 *
8 * DMX512 (lighting / stage control over RS-485) is positional: after a break, a start code
9 * octet (0x00 for dimmer data) is followed by up to 512 channel slots, with no checksum or
10 * in-frame addressing. This codec assembles / reads that slot array, and implements **RDM**
11 * (Remote Device Management, ANSI E1.20) - the addressed management layer that shares the
12 * DMX wire: a real packet with 48-bit source / destination UIDs, a command class + parameter
13 * id, and a 16-bit additive checksum.
14 *
15 * The break + RS-485 direction are the application's (a `MAX485`-class transceiver on a UART
16 * at 250 kbit/s, 8N2). This is the byte-level framing layer. Pure and host-tested. Bridge a
17 * lighting rig onto Wi-Fi: drive DMX slots or discover / configure RDM fixtures from the web.
18 *
19 * @author Douglas Quigg (dstroy0)
20 * @date 2026
21 */
22
23#ifndef PROTOCORE_DMX_H
24#define PROTOCORE_DMX_H
25
26#include "protocore_config.h" // the entry point: protocore_types.h for the widths
27
28#if PROTOCORE_ENABLE_DMX
29
31
32// This module holds nothing between calls, so it carves no borrow and states none. An entry
33// takes one all the same, and never reads it, so every namespace in the tree is invoked the
34// same way.
35
36#define DMX_MAX_CHANNELS 512u ///< slots per DMX512 universe
37
38#define DMX_SC_DIMMER 0x00u ///< start code for standard dimmer data
39
40#define RDM_SC 0xCCu ///< RDM start code (SC_RDM)
41
42#define RDM_SUB_SC 0x01u ///< RDM sub-start code (SC_SUB_MESSAGE)
43
44#define RDM_OVERHEAD 26u ///< full packet octets with PDL 0 (24-octet message + 2 checksum)
45
46#define RDM_CC_DISCOVERY 0x10u
47
48#define RDM_CC_DISCOVERY_RESPONSE 0x11u
49
50#define RDM_CC_GET 0x20u
51
52#define RDM_CC_GET_RESPONSE 0x21u
53
54#define RDM_CC_SET 0x30u
55
56#define RDM_CC_SET_RESPONSE 0x31u
57
58#define RDM_RESPONSE_ACK 0x00u
59
60#define RDM_RESPONSE_ACK_TIMER 0x01u
61
62#define RDM_RESPONSE_NACK_REASON 0x02u
63
64#define RDM_RESPONSE_ACK_OVERFLOW 0x03u
65
66#define RDM_PID_DISC_UNIQUE_BRANCH 0x0001u
67
68#define RDM_PID_DISC_MUTE 0x0002u
69
70#define RDM_PID_DISC_UN_MUTE 0x0003u
71
72#define RDM_PID_SUPPORTED_PARAMETERS 0x0050u
73
74#define RDM_PID_DEVICE_INFO 0x0060u
75
76#define RDM_PID_DMX_START_ADDRESS 0x00F0u
77
78#define RDM_PID_IDENTIFY_DEVICE 0x1000u
79
80#define PROTOCORE_RDM_DEVICE_INFO_PDL 19 ///< octets in a DEVICE_INFO (PID 0x0060) GET-response parameter block
81
82/** @brief A parsed / to-be-built RDM packet. UIDs are 48-bit (manufacturer<<32 | device). */
83typedef struct
84{
85 uint64_t dest_uid;
86 uint64_t src_uid;
87 uint8_t tn; ///< transaction number
88 uint8_t port_id; ///< port id (request) / response type (response)
89 uint8_t msg_count; ///< queued message count
90 uint16_t sub_device; ///< sub-device (0 = root)
91 uint8_t cc; ///< command class (RDM_CC_*)
92 uint16_t pid; ///< parameter id
93 uint8_t pdl; ///< parameter data length
94 const uint8_t *pdata; ///< parameter data (points into the parsed buffer); nullptr when pdl 0
95} RdmPacket;
96
97/** @brief Decoded DEVICE_INFO (PID 0x0060) parameter data - the descriptor every RDM responder must
98 * answer, carrying the fields a controller needs to patch and identify the device. */
99typedef struct
100{
101 uint8_t proto_major; ///< RDM protocol version major (1 for E1.20)
102 uint8_t proto_minor; ///< RDM protocol version minor
103 uint16_t device_model_id; ///< manufacturer-specific device model id
104 uint16_t product_category; ///< E1.20 product category code
105 uint32_t software_version_id; ///< manufacturer-specific software version id
106 uint16_t dmx_footprint; ///< number of DMX512 slots the current personality occupies
107 uint8_t current_personality; ///< current DMX personality (1-based)
108 uint8_t personality_count; ///< total number of DMX personalities
109 uint16_t dmx_start_address; ///< DMX512 start address (1-512; 0xFFFF if the device uses no DMX)
110 uint16_t sub_device_count; ///< number of sub-devices (0 = none)
111 uint8_t sensor_count; ///< number of sensors
112} RdmDeviceInfo;
113
114/** @brief What build takes: buf, cap, start_code, channels, n. */
115typedef struct
116{
117 uint8_t *buf;
118 size_t cap;
119 uint8_t start_code;
120 const uint8_t *channels;
121 uint16_t n;
122} DmxBuildArgs;
123
124/** @brief What get_channel takes: buf, len, ch. */
125typedef struct
126{
127 const uint8_t *buf;
128 size_t len;
129 uint16_t ch;
130} DmxGetChannelArgs;
131
132/** @brief What rdm_uid takes: manufacturer, device. */
133typedef struct
134{
135 uint16_t manufacturer;
136 uint32_t device;
137} DmxRdmUidArgs;
138
139/** @brief What rdm_checksum takes: buf, len. */
140typedef struct
141{
142 const uint8_t *buf;
143 size_t len;
144} DmxRdmChecksumArgs;
145
146/** @brief What rdm_build takes: buf, cap, p, pdata, pdl. */
147typedef struct
148{
149 uint8_t *buf;
150 size_t cap;
151 const RdmPacket *p;
152 const uint8_t *pdata;
153 uint8_t pdl;
154} DmxRdmBuildArgs;
155
156/** @brief What rdm_parse takes: buf, len, out, consumed. */
157typedef struct
158{
159 const uint8_t *buf;
160 size_t len;
161 RdmPacket *out;
162 size_t *consumed;
163} DmxRdmParseArgs;
164
165/** @brief What rdm_decode_disc_response takes: buf, len, uid. */
166typedef struct
167{
168 const uint8_t *buf;
169 size_t len;
170 uint64_t *uid;
171} DmxRdmDecodeDiscResponseArgs;
172
173/** @brief What rdm_build_disc_response takes: buf, cap, uid, ... */
174typedef struct
175{
176 uint8_t *buf;
177 size_t cap;
178 uint64_t uid;
179 uint8_t preamble_len;
180} DmxRdmBuildDiscResponseArgs;
181
182/** @brief What rdm_build_device_info takes: pdata, cap, info. */
183typedef struct
184{
185 uint8_t *pdata;
186 size_t cap;
187 const RdmDeviceInfo *info;
188} DmxRdmBuildDeviceInfoArgs;
189
190/** @brief What rdm_parse_device_info takes: pdata, pdl, out. */
191typedef struct
192{
193 const uint8_t *pdata;
194 uint8_t pdl;
195 RdmDeviceInfo *out;
196} DmxRdmParseDeviceInfoArgs;
197
198/**
199 * @brief DMX512 framing + RDM (ANSI E1.20) management codec (PROTOCORE_ENABLE_DMX). DMX512 (lighting / stage control
200 * ...
201 *
202 * A caller sets the members a call takes, invokes it through ::Dmx with the bytes it runs
203 * out of, and reads the outcome off the same handle.
204 *
205 * Dmx.build_args.buf = ...;
206 * Dmx.build_args.cap = ...;
207 * Dmx.build_args.start_code = ...;
208 * Dmx.build_args.channels = ...;
209 * Dmx.build_args.n = ...;
210 * Dmx.build(work);
211 * // Dmx.n is what the call reports
212 *
213 * @var DmxNs::build_args what build takes: buf, cap, start_code, channels, n
214 * @var DmxNs::get_channel_args what get_channel takes: buf, len, ch
215 * @var DmxNs::rdm_uid_args what rdm_uid takes: manufacturer, device
216 * @var DmxNs::rdm_checksum_args what rdm_checksum takes: buf, len
217 * @var DmxNs::rdm_build_args what rdm_build takes: buf, cap, p, pdata, pdl
218 * @var DmxNs::rdm_parse_args what rdm_parse takes: buf, len, out, consumed
219 * @var DmxNs::rdm_decode_disc_response_args what rdm_decode_disc_response takes: buf, len, uid
220 * @var DmxNs::rdm_build_disc_response_args what rdm_build_disc_response takes: buf, cap, uid,
221 * @var DmxNs::rdm_build_device_info_args what rdm_build_device_info takes: pdata, cap, info
222 * @var DmxNs::rdm_parse_device_info_args what rdm_parse_device_info takes: pdata, pdl, out
223 * @var DmxNs::ok true iff the separator is present, the 16 encoded octets fit, and ...
224 * @var DmxNs::n octets written (preamble_len + 17), or 0 on a null buffer, ...
225 * @var DmxNs::u8 what a call reports
226 * @var DmxNs::uid what a call reports
227 * @var DmxNs::checksum what a call reports
228 * @var DmxNs::build assemble a DMX512 packet body: [start code][channel slots]. n <= ...
229 * @var DmxNs::get_channel read channel ch (1-based, per DMX convention) from a received ...
230 * @var DmxNs::rdm_uid compose a 48-bit RDM UID from a manufacturer id and a device id
231 * @var DmxNs::rdm_checksum 16-bit additive checksum over len octets (RDM message block)
232 * @var DmxNs::rdm_build build a full RDM packet (incl. the trailing 16-bit checksum) from p ...
233 * @var DmxNs::rdm_parse parse an RDM packet: validates the start codes, the message length ...
234 * @var DmxNs::rdm_decode_disc_response decode a DISC_UNIQUE_BRANCH discovery response into the responder's ...
235 * @var DmxNs::rdm_build_disc_response build the DISC_UNIQUE_BRANCH discovery response a responder sends ...
236 * @var DmxNs::rdm_build_device_info pack a DEVICE_INFO (PID 0x0060) GET-response parameter block from ...
237 * @var DmxNs::rdm_parse_device_info decode a DEVICE_INFO (PID 0x0060) GET-response parameter block into ...
238 *
239 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
240 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
241 * a caller drives every namespace the same way.
242 */
243typedef struct
244{
245 DmxBuildArgs build_args;
246 DmxGetChannelArgs get_channel_args;
247 DmxRdmUidArgs rdm_uid_args;
248 DmxRdmChecksumArgs rdm_checksum_args;
249 DmxRdmBuildArgs rdm_build_args;
250 DmxRdmParseArgs rdm_parse_args;
251 DmxRdmDecodeDiscResponseArgs rdm_decode_disc_response_args;
252 DmxRdmBuildDiscResponseArgs rdm_build_disc_response_args;
253 DmxRdmBuildDeviceInfoArgs rdm_build_device_info_args;
254 DmxRdmParseDeviceInfoArgs rdm_parse_device_info_args;
255 proto_bool ok;
256 size_t n;
257 uint8_t u8;
258 uint64_t uid;
259 uint16_t checksum;
260} DmxVars;
261
262/** @brief The operands and the outcome. */
263extern DmxVars DmxV;
264
265/** @brief The entries. */
266typedef struct
267{
268 void (*const build)(uint8_t *work);
269 void (*const get_channel)(uint8_t *work);
270 void (*const rdm_uid)(uint8_t *work);
271 void (*const rdm_checksum)(uint8_t *work);
272 void (*const rdm_build)(uint8_t *work);
273 void (*const rdm_parse)(uint8_t *work);
274 void (*const rdm_decode_disc_response)(uint8_t *work);
275 void (*const rdm_build_disc_response)(uint8_t *work);
276 void (*const rdm_build_device_info)(uint8_t *work);
277 void (*const rdm_parse_device_info)(uint8_t *work);
278} DmxNs;
279
280// What the table binds, defined once in the .c and taking one parameter each: everything
281// else an entry needs is an operand in DmxV or a region of the borrow at a fixed offset.
282void protocore_dmx_build(uint8_t *work);
283void protocore_dmx_get_channel(uint8_t *work);
284void protocore_dmx_rdm_uid(uint8_t *work);
285void protocore_dmx_rdm_checksum(uint8_t *work);
286void protocore_dmx_rdm_build(uint8_t *work);
287void protocore_dmx_rdm_parse(uint8_t *work);
288void protocore_dmx_rdm_decode_disc_response(uint8_t *work);
289void protocore_dmx_rdm_build_disc_response(uint8_t *work);
290void protocore_dmx_rdm_build_device_info(uint8_t *work);
291void protocore_dmx_rdm_parse_device_info(uint8_t *work);
292
293// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
294// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
295// `Dmx.build(work)` resolves to a named function and becomes a DIRECT call. An extern table
296// leaves the call indirect and the symbol live at every level, -O2 -flto included.
297static const DmxNs Dmx __attribute__((unused)) = {
298 .build = protocore_dmx_build,
299 .get_channel = protocore_dmx_get_channel,
300 .rdm_uid = protocore_dmx_rdm_uid,
301 .rdm_checksum = protocore_dmx_rdm_checksum,
302 .rdm_build = protocore_dmx_rdm_build,
303 .rdm_parse = protocore_dmx_rdm_parse,
304 .rdm_decode_disc_response = protocore_dmx_rdm_decode_disc_response,
305 .rdm_build_disc_response = protocore_dmx_rdm_build_disc_response,
306 .rdm_build_device_info = protocore_dmx_rdm_build_device_info,
307 .rdm_parse_device_info = protocore_dmx_rdm_parse_device_info,
308};
309
311
312#endif // PROTOCORE_ENABLE_DMX
313
314#endif // PROTOCORE_DMX_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97