Skip to content

Commit 41c2d13

Browse files
committed
Implement ArtNet ArtPollReply (Fixes #95)
1 parent 6cedb80 commit 41c2d13

File tree

4 files changed

+214
-23
lines changed

4 files changed

+214
-23
lines changed

assets/TODO

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Core roadmap
1818
- debounce
1919
- toggle
2020
- libmmbackend: interface bind
21+
- When binding an interface instead of an address, multicast and interface addressing should work naturally
22+
-> ArtNet ArtPoll handling
2123
- gtk ui
2224

2325
Backend internals

backends/artnet.c

+148-17
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ static struct {
1919
0
2020
};
2121

22-
static int artnet_listener(char* host, char* port){
22+
static int artnet_listener(char* host, char* port, struct sockaddr_storage* announce){
2323
int fd;
24+
char announce_addr[INET_ADDRSTRLEN];
2425
if(global_cfg.fds >= MAX_FDS){
2526
LOG("Backend socket limit reached");
2627
return -1;
@@ -40,10 +41,25 @@ static int artnet_listener(char* host, char* port){
4041
return -1;
4142
}
4243

43-
LOGPF("Socket %" PRIsize_t " bound to %s port %s", global_cfg.fds, host, port);
44+
if(announce->ss_family != AF_INET){
45+
LOGPF("Socket %" PRIsize_t " bound to %s port %s", global_cfg.fds, host, port);
46+
}
47+
else{
48+
mmbackend_sockaddr_ntop((struct sockaddr*) announce, announce_addr, sizeof(announce_addr));
49+
LOGPF("Socket %" PRIsize_t " bound to %s port %s, announced as %s",
50+
global_cfg.fds, host, port, announce_addr);
51+
}
52+
53+
//set announce port if no address is set
54+
//this is used for artpollreply frames
55+
if(!((struct sockaddr_in*) announce)->sin_port){
56+
((struct sockaddr_in*) announce)->sin_port = htobe16(strtoul(port, NULL, 0));
57+
}
58+
4459
global_cfg.fd[global_cfg.fds].fd = fd;
4560
global_cfg.fd[global_cfg.fds].output_instances = 0;
4661
global_cfg.fd[global_cfg.fds].output_instance = NULL;
62+
memcpy(&global_cfg.fd[global_cfg.fds].announce_addr, announce, sizeof(global_cfg.fd[global_cfg.fds].announce_addr));
4763
global_cfg.fds++;
4864
return 0;
4965
}
@@ -84,6 +100,7 @@ static uint32_t artnet_interval(){
84100

85101
static int artnet_configure(char* option, char* value){
86102
char* host = NULL, *port = NULL, *fd_opts = NULL;
103+
struct sockaddr_storage announce = {0};
87104
if(!strcmp(option, "net")){
88105
//configure default net
89106
global_cfg.default_net = strtoul(value, NULL, 0);
@@ -97,7 +114,17 @@ static int artnet_configure(char* option, char* value){
97114
return 1;
98115
}
99116

100-
if(artnet_listener(host, (port ? port : ARTNET_PORT))){
117+
if(fd_opts){
118+
DBGPF("Parsing fd options %s", fd_opts);
119+
//as there is currently only one additional option, parse only for that
120+
if(!strncmp(fd_opts, "announce=", 9)){
121+
if(mmbackend_parse_sockaddr(fd_opts + 9, port ? port : ARTNET_PORT, &announce, NULL)){
122+
return 1;
123+
}
124+
}
125+
}
126+
127+
if(artnet_listener(host, (port ? port : ARTNET_PORT), &announce)){
101128
LOGPF("Failed to bind socket: %s", value);
102129
return 1;
103130
}
@@ -230,7 +257,7 @@ static int artnet_transmit(instance* inst, artnet_output_universe* output){
230257
artnet_instance_data* data = (artnet_instance_data*) inst->impl;
231258

232259
//build output frame
233-
artnet_pkt frame = {
260+
artnet_dmx frame = {
234261
.magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
235262
.opcode = htobe16(OpDmx),
236263
.version = htobe16(ARTNET_VERSION),
@@ -323,7 +350,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v)
323350
return 0;
324351
}
325352

326-
static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){
353+
static inline int artnet_process_dmx(instance* inst, artnet_dmx* frame){
327354
size_t p, max_mark = 0;
328355
uint16_t wide_val = 0;
329356
channel* chan = NULL;
@@ -381,17 +408,97 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){
381408
return 0;
382409
}
383410

384-
static int artnet_handle(size_t num, managed_fd* fds){
385-
size_t u, c;
386-
uint64_t timestamp = mm_timestamp();
387-
uint32_t synthesize_delta = 0;
388-
ssize_t bytes_read;
389-
char recv_buf[ARTNET_RECV_BUF];
411+
static int artnet_process_poll(uint8_t fd, struct sockaddr* source, socklen_t source_len){
412+
size_t n = 0, u, i = 1;
413+
instance** instances = NULL;
414+
artnet_instance_data* data = NULL;
415+
struct sockaddr_in* announce = (struct sockaddr_in*) &(global_cfg.fd[fd].announce_addr);
390416
artnet_instance_id inst_id = {
391417
.label = 0
392418
};
419+
artnet_poll_reply frame = {
420+
.magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
421+
.opcode = htobe16(OpPollReply),
422+
.oem = htobe16(ARTNET_OEM),
423+
.status = 0xD0, //indicators normal, address set by frontpanel
424+
.manufacturer = htole16(ARTNET_ESTA_MANUFACTURER),
425+
.longname = "MIDIMonster - ",
426+
.ports = htobe16(1),
427+
.video = 0x01, //deprecated, but mark as playing ethernet data
428+
.status2 = 0x08, //supports 15bit port address
429+
.port_out_b = {0xC0} //no rdm, delta output
430+
};
431+
432+
//for some stupid reason, the standard insists on including the peer address not once
433+
//but TWICE in the PollReply frame (instead of just using the sender address).
434+
//it also completely ignores the existence of anything other than ipv4.
435+
if(announce->sin_family == AF_INET){
436+
memcpy(frame.ip4, &(announce->sin_addr.s_addr), 4);
437+
memcpy(frame.parent_ip, &(announce->sin_addr.s_addr), 4);
438+
}
439+
//the announce port is always valid
440+
frame.port = htole16(be16toh(announce->sin_port));
441+
442+
//prepare listing of all instances on this socket
443+
if(mm_backend_instances(BACKEND_NAME, &n, &instances)){
444+
LOG("Failed to query backend instances");
445+
return 1;
446+
}
447+
448+
for(u = 0; u < n; u++){
449+
inst_id.label = instances[u]->ident;
450+
if(inst_id.fields.fd_index == fd){
451+
data = (artnet_instance_data*) instances[u]->impl;
452+
DBGPF("Poll reply %" PRIsize_t " for socket %d: Instance %s net %d universe %d",
453+
i, fd, instances[u]->name, inst_id.fields.net, inst_id.fields.uni);
454+
455+
frame.parent_index = i;
456+
frame.port_address = htobe16(((inst_id.fields.net & 0x7F) << 8) | (inst_id.fields.uni >> 4));
457+
//we can always do output (as seen by the artnet spec)
458+
frame.port_types[0] = 0x80; //output from artnet network enabled
459+
frame.subaddr_out[0] = inst_id.fields.uni & 0x0F;
460+
461+
//data output status as seen from artnet, ie. midimonster input status
462+
frame.port_out[0] = data->last_input ? 0x82 /*transmitting, ltp*/ : 0x02 /*ltp*/;
463+
464+
//default artnet input (ie. midimonster output) state
465+
frame.port_in[0] = 0x08 /*input disabled*/;
466+
467+
//if this instance is enabled for output (input in artnet spec terminology), announce that
468+
if(data->dest_len){
469+
frame.port_types[0] |= 0x40; //input to artnet network enabled
470+
frame.subaddr_in[0] = inst_id.fields.uni & 0x0F;
471+
frame.port_in[0] = 0x80 /*receiving - well, transmitting*/;
472+
}
473+
474+
strncpy((char*) frame.shortname, instances[u]->name, sizeof(frame.shortname) - 1);
475+
strncpy((char*) frame.longname + 14, instances[u]->name, sizeof(frame.longname) - 15);
476+
477+
//the most recent spec document says to always send ArtPollReply frames to the directed broadcast address, while earlier standards just unicast it to the sender
478+
//we just do the latter because it is easier (and IMO makes more sense)
479+
if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &frame, sizeof(frame), 0, source, source_len) < 0){
480+
#ifdef _WIN32
481+
if(WSAGetLastError() != WSAEWOULDBLOCK){
482+
#else
483+
if(errno != EAGAIN){
484+
#endif
485+
LOGPF("Failed to send poll reply for instance %s: %s", instances[u]->name, mmbackend_socket_strerror(errno));
486+
return 1;
487+
}
488+
}
489+
i++;
490+
}
491+
}
492+
493+
free(instances);
494+
return 0;
495+
}
496+
497+
static int artnet_maintenance(){
498+
size_t u, c;
499+
uint64_t timestamp = mm_timestamp();
500+
uint32_t synthesize_delta = 0;
393501
instance* inst = NULL;
394-
artnet_pkt* frame = (artnet_pkt*) recv_buf;
395502

396503
//transmit keepalive & synthesized frames
397504
global_cfg.next_frame = 0;
@@ -414,24 +521,48 @@ static int artnet_handle(size_t num, managed_fd* fds){
414521
}
415522
}
416523
}
524+
return 0;
525+
}
526+
527+
static int artnet_handle(size_t num, managed_fd* fds){
528+
size_t u;
529+
struct sockaddr_storage peer_addr;
530+
socklen_t peer_len = sizeof(peer_addr);
531+
ssize_t bytes_read;
532+
char recv_buf[ARTNET_RECV_BUF];
533+
artnet_instance_id inst_id = {
534+
.label = 0
535+
};
536+
instance* inst = NULL;
537+
artnet_dmx* frame = (artnet_dmx*) recv_buf;
538+
539+
if(artnet_maintenance()){
540+
return 1;
541+
}
417542

418543
for(u = 0; u < num; u++){
419544
do{
420-
bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
421-
if(bytes_read > 0 && bytes_read > sizeof(artnet_hdr)){
422-
if(!memcmp(frame->magic, "Art-Net\0", 8) && be16toh(frame->opcode) == OpDmx){
545+
bytes_read = recvfrom(fds[u].fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*) &peer_addr, &peer_len);
546+
if(bytes_read > 0 && bytes_read > sizeof(artnet_hdr) && !memcmp(frame->magic, "Art-Net\0", 8)){
547+
//DBGPF("Frame with opcode %04X, size %" PRIsize_t " on socket %" PRIu64, be16toh(frame->opcode), bytes_read, ((uint64_t) fds[u].impl) & 0xFF);
548+
if(be16toh(frame->opcode) == OpDmx && bytes_read >= (sizeof(artnet_dmx) - 512)){
423549
//find matching instance
424550
inst_id.fields.fd_index = ((uint64_t) fds[u].impl) & 0xFF;
425551
inst_id.fields.net = frame->net;
426552
inst_id.fields.uni = frame->universe;
427553
inst = mm_instance_find(BACKEND_NAME, inst_id.label);
428-
if(inst && artnet_process_frame(inst, frame)){
429-
LOG("Failed to process frame");
554+
if(inst && artnet_process_dmx(inst, frame)){
555+
LOG("Failed to process DMX frame");
430556
}
431557
else if(!inst && global_cfg.detect > 1){
432558
LOGPF("Received data for unconfigured universe %d (net %d) on socket %" PRIu64, frame->universe, frame->net, (((uint64_t) fds[u].impl) & 0xFF));
433559
}
434560
}
561+
else if(be16toh(frame->opcode) == OpPoll && bytes_read >= sizeof(artnet_poll)){
562+
if(artnet_process_poll(((uint64_t) fds[u].impl) & 0xFF, (struct sockaddr*) &peer_addr, peer_len)){
563+
LOG("Failed to process discovery frame");
564+
}
565+
}
435566
}
436567
} while(bytes_read > 0);
437568

backends/artnet.h

+49-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ static int artnet_shutdown(size_t n, instance** inst);
1616

1717
#define ARTNET_PORT "6454"
1818
#define ARTNET_VERSION 14
19+
#define ARTNET_ESTA_MANUFACTURER 0x4653 //"FS" as registered with ESTA
20+
#define ARTNET_OEM 0x2B93 //as registered with artistic license
1921
#define ARTNET_RECV_BUF 4096
2022

2123
#define ARTNET_KEEPALIVE_INTERVAL 1000
@@ -70,6 +72,7 @@ typedef struct /*_artnet_fd*/ {
7072
int fd;
7173
size_t output_instances;
7274
artnet_output_universe* output_instance;
75+
struct sockaddr_storage announce_addr; //used for pollreplies if ss_family == AF_INET, port is always valid
7376
} artnet_descriptor;
7477

7578
#pragma pack(push, 1)
@@ -79,7 +82,7 @@ typedef struct /*_artnet_hdr*/ {
7982
uint16_t version;
8083
} artnet_hdr;
8184

82-
typedef struct /*_artnet_pkt*/ {
85+
typedef struct /*_artnet_dmx*/ {
8386
uint8_t magic[8];
8487
uint16_t opcode;
8588
uint16_t version;
@@ -89,9 +92,53 @@ typedef struct /*_artnet_pkt*/ {
8992
uint8_t net;
9093
uint16_t length;
9194
uint8_t data[512];
92-
} artnet_pkt;
95+
} artnet_dmx;
96+
97+
typedef struct /*_artnet_poll*/ {
98+
uint8_t magic[8];
99+
uint16_t opcode;
100+
uint16_t version;
101+
uint8_t flags;
102+
uint8_t priority;
103+
} artnet_poll;
104+
105+
typedef struct /*_artnet_poll_reply*/ {
106+
uint8_t magic[8];
107+
uint16_t opcode; //little-endian
108+
uint8_t ip4[4]; //stop including l2/3 addresses in the payload, just use the sender address ffs
109+
uint16_t port; //little-endian, who does that?
110+
uint16_t firmware; //big-endian
111+
uint16_t port_address; //big-endian
112+
uint16_t oem; //big-endian
113+
uint8_t bios_version;
114+
uint8_t status;
115+
uint16_t manufacturer; //little-endian
116+
uint8_t shortname[18];
117+
uint8_t longname[64];
118+
uint8_t report[64];
119+
uint16_t ports; //big-endian
120+
uint8_t port_types[4]; //only use the first member, we report every universe in it's own reply
121+
uint8_t port_in[4];
122+
uint8_t port_out[4];
123+
uint8_t subaddr_in[4];
124+
uint8_t subaddr_out[4];
125+
uint8_t video; //deprecated
126+
uint8_t macro; //deprecatd
127+
uint8_t remote; //deprecated
128+
uint8_t spare[3];
129+
uint8_t style;
130+
uint8_t mac[6]; //come on
131+
uint8_t parent_ip[4]; //COME ON
132+
uint8_t parent_index; //i don't even know
133+
uint8_t status2;
134+
uint8_t port_out_b[4];
135+
uint8_t status3;
136+
uint8_t spare2[21];
137+
} artnet_poll_reply;
93138
#pragma pack(pop)
94139

95140
enum artnet_pkt_opcode {
141+
OpPoll = 0x0020,
142+
OpPollReply = 0x0021,
96143
OpDmx = 0x0050
97144
};

backends/artnet.md

+15-4
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ Art-Net™ Designed by and Copyright Artistic Licence Holdings Ltd.
99

1010
| Option | Example value | Default value | Description |
1111
|---------------|-----------------------|-----------------------|-----------------------|
12-
| `bind` | `127.0.0.1 6454` | none | Binds a network address to listen for data. This option may be set multiple times, with each interface being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one interface is required for transmission. |
13-
| `net` | `0` | `0` | The default net to use |
12+
| `bind` | `127.0.0.1 6454` | none | Binds a network address to listen for data (a socket/interface). This option may be set multiple times, with each interface being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one socket is required for operation. |
13+
| `net` | `0` | `0` | The default net to use (upper 7 bits of the 15-bit port address) |
1414
| `detect` | `on`, `verbose` | `off` | Output additional information on received data packets to help with configuring complex scenarios |
1515

1616
#### Instance configuration
1717

1818
| Option | Example value | Default value | Description |
1919
|---------------|-----------------------|-----------------------|-----------------------|
20-
| `net` | `0` | `0` | ArtNet `net` to use |
21-
| `universe` | `0` | `0` | Universe identifier |
20+
| `net` | `0` | `0` | ArtNet `net` to use (upper 7 bits of the 15-bit port address |
21+
| `universe` | `0` | `0` | Universe identifier (lower 8 bits of the 15-bit port address) |
2222
| `destination` | `10.2.2.2` | none | Destination address for sent ArtNet frames. Setting this enables the universe for output |
2323
| `interface` | `1` | `0` | The bound address to use for data input/output |
2424
| `realtime` | `1` | `0` | Disable the recommended rate-limiting (approx. 44 packets per second) for this instance |
@@ -44,3 +44,14 @@ A normal channel that is part of a wide channel can not be mapped individually.
4444
When using this backend for output with a fast event source, some events may appear to be lost due to the packet output rate limiting
4545
mandated by the [ArtNet specification](https://artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf) (Section `Refresh rate`).
4646
This limit can be disabled on a per-instance basis using the `realtime` instance option.
47+
48+
This backend will reply to PollRequests from ArtNet controllers if binding an interface with an IPv4 address.
49+
When binding to a wildcard address (e.g. `0.0.0.0`), the IP address reported by controllers in a `node overview` may be wrong. This can
50+
be fixed by specifying the bind `announce` address using the syntax `bind = 0.0.0.0 6454 announce=10.0.0.1`, which will override the address
51+
announced in the ArtPollReply.
52+
53+
When binding a specific IP address on Linux and OSX, no broadcast data (including ArtPoll requests) are received. There will be mechanism
54+
to bind to a specified interface in a future release. As a workaround, bind to the wildcard interface `0.0.0.0`.
55+
56+
The backend itself supports IPv6, but the ArtNet spec hardcodes IPv4 address fields in some responses.
57+
Normal input and output are well supported, while extended features such as device discovery may not work with IPv6 due to the specification ignoring the existence of anything but IPv4.

0 commit comments

Comments
 (0)