From 41c2d135a4dc4d794094aa6ff0fe159455cd982a Mon Sep 17 00:00:00 2001
From: cbdev <cb@cbcdn.com>
Date: Sat, 27 Nov 2021 14:46:49 +0100
Subject: Implement ArtNet ArtPollReply (Fixes #95)

---
 backends/artnet.c  | 165 +++++++++++++++++++++++++++++++++++++++++++++++------
 backends/artnet.h  |  51 ++++++++++++++++-
 backends/artnet.md |  19 ++++--
 3 files changed, 212 insertions(+), 23 deletions(-)

(limited to 'backends')

diff --git a/backends/artnet.c b/backends/artnet.c
index 75bcce3..c0e567d 100644
--- a/backends/artnet.c
+++ b/backends/artnet.c
@@ -19,8 +19,9 @@ static struct {
 	0
 };
 
-static int artnet_listener(char* host, char* port){
+static int artnet_listener(char* host, char* port, struct sockaddr_storage* announce){
 	int fd;
+	char announce_addr[INET_ADDRSTRLEN];
 	if(global_cfg.fds >= MAX_FDS){
 		LOG("Backend socket limit reached");
 		return -1;
@@ -40,10 +41,25 @@ static int artnet_listener(char* host, char* port){
 		return -1;
 	}
 
-	LOGPF("Socket %" PRIsize_t " bound to %s port %s", global_cfg.fds, host, port);
+	if(announce->ss_family != AF_INET){
+		LOGPF("Socket %" PRIsize_t " bound to %s port %s", global_cfg.fds, host, port);
+	}
+	else{
+		mmbackend_sockaddr_ntop((struct sockaddr*) announce, announce_addr, sizeof(announce_addr));
+		LOGPF("Socket %" PRIsize_t " bound to %s port %s, announced as %s",
+				global_cfg.fds, host, port, announce_addr);
+	}
+
+	//set announce port if no address is set
+	//this is used for artpollreply frames
+	if(!((struct sockaddr_in*) announce)->sin_port){
+		((struct sockaddr_in*) announce)->sin_port = htobe16(strtoul(port, NULL, 0));
+	}
+
 	global_cfg.fd[global_cfg.fds].fd = fd;
 	global_cfg.fd[global_cfg.fds].output_instances = 0;
 	global_cfg.fd[global_cfg.fds].output_instance = NULL;
+	memcpy(&global_cfg.fd[global_cfg.fds].announce_addr, announce, sizeof(global_cfg.fd[global_cfg.fds].announce_addr));
 	global_cfg.fds++;
 	return 0;
 }
@@ -84,6 +100,7 @@ static uint32_t artnet_interval(){
 
 static int artnet_configure(char* option, char* value){
 	char* host = NULL, *port = NULL, *fd_opts = NULL;
+	struct sockaddr_storage announce = {0};
 	if(!strcmp(option, "net")){
 		//configure default net
 		global_cfg.default_net = strtoul(value, NULL, 0);
@@ -97,7 +114,17 @@ static int artnet_configure(char* option, char* value){
 			return 1;
 		}
 
-		if(artnet_listener(host, (port ? port : ARTNET_PORT))){
+		if(fd_opts){
+			DBGPF("Parsing fd options %s", fd_opts);
+			//as there is currently only one additional option, parse only for that
+			if(!strncmp(fd_opts, "announce=", 9)){
+				if(mmbackend_parse_sockaddr(fd_opts + 9, port ? port : ARTNET_PORT, &announce, NULL)){
+					return 1;
+				}
+			}
+		}
+
+		if(artnet_listener(host, (port ? port : ARTNET_PORT), &announce)){
 			LOGPF("Failed to bind socket: %s", value);
 			return 1;
 		}
@@ -230,7 +257,7 @@ static int artnet_transmit(instance* inst, artnet_output_universe* output){
 	artnet_instance_data* data = (artnet_instance_data*) inst->impl;
 
 	//build output frame
-	artnet_pkt frame = {
+	artnet_dmx frame = {
 		.magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
 		.opcode = htobe16(OpDmx),
 		.version = htobe16(ARTNET_VERSION),
@@ -323,7 +350,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v)
 	return 0;
 }
 
-static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){
+static inline int artnet_process_dmx(instance* inst, artnet_dmx* frame){
 	size_t p, max_mark = 0;
 	uint16_t wide_val = 0;
 	channel* chan = NULL;
@@ -381,17 +408,97 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){
 	return 0;
 }
 
-static int artnet_handle(size_t num, managed_fd* fds){
-	size_t u, c;
-	uint64_t timestamp = mm_timestamp();
-	uint32_t synthesize_delta = 0;
-	ssize_t bytes_read;
-	char recv_buf[ARTNET_RECV_BUF];
+static int artnet_process_poll(uint8_t fd, struct sockaddr* source, socklen_t source_len){
+	size_t n = 0, u, i = 1;
+	instance** instances = NULL;
+	artnet_instance_data* data = NULL;
+	struct sockaddr_in* announce = (struct sockaddr_in*) &(global_cfg.fd[fd].announce_addr);
 	artnet_instance_id inst_id = {
 		.label = 0
 	};
+	artnet_poll_reply frame = {
+		.magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
+		.opcode = htobe16(OpPollReply),
+		.oem = htobe16(ARTNET_OEM),
+		.status = 0xD0, //indicators normal, address set by frontpanel
+		.manufacturer = htole16(ARTNET_ESTA_MANUFACTURER),
+		.longname = "MIDIMonster - ",
+		.ports = htobe16(1),
+		.video = 0x01, //deprecated, but mark as playing ethernet data
+		.status2 = 0x08, //supports 15bit port address
+		.port_out_b = {0xC0} //no rdm, delta output
+	};
+
+	//for some stupid reason, the standard insists on including the peer address not once
+	//but TWICE in the PollReply frame (instead of just using the sender address).
+	//it also completely ignores the existence of anything other than ipv4.
+	if(announce->sin_family == AF_INET){
+		memcpy(frame.ip4, &(announce->sin_addr.s_addr), 4);
+		memcpy(frame.parent_ip, &(announce->sin_addr.s_addr), 4);
+	}
+	//the announce port is always valid
+	frame.port = htole16(be16toh(announce->sin_port));
+
+	//prepare listing of all instances on this socket
+	if(mm_backend_instances(BACKEND_NAME, &n, &instances)){
+		LOG("Failed to query backend instances");
+		return 1;
+	}
+
+	for(u = 0; u < n; u++){
+		inst_id.label = instances[u]->ident;
+		if(inst_id.fields.fd_index == fd){
+			data = (artnet_instance_data*) instances[u]->impl;
+			DBGPF("Poll reply %" PRIsize_t " for socket %d: Instance %s net %d universe %d",
+					i, fd, instances[u]->name, inst_id.fields.net, inst_id.fields.uni);
+
+			frame.parent_index = i;
+			frame.port_address = htobe16(((inst_id.fields.net & 0x7F) << 8) | (inst_id.fields.uni >> 4));
+			//we can always do output (as seen by the artnet spec)
+			frame.port_types[0] = 0x80; //output from artnet network enabled
+			frame.subaddr_out[0] = inst_id.fields.uni & 0x0F;
+
+			//data output status as seen from artnet, ie. midimonster input status
+			frame.port_out[0] = data->last_input ? 0x82 /*transmitting, ltp*/ : 0x02 /*ltp*/;
+
+			//default artnet input (ie. midimonster output) state
+			frame.port_in[0] = 0x08 /*input disabled*/;
+
+			//if this instance is enabled for output (input in artnet spec terminology), announce that
+			if(data->dest_len){
+				frame.port_types[0] |= 0x40; //input to artnet network enabled
+				frame.subaddr_in[0] = inst_id.fields.uni & 0x0F;
+				frame.port_in[0] = 0x80 /*receiving - well, transmitting*/;
+			}
+
+			strncpy((char*) frame.shortname, instances[u]->name, sizeof(frame.shortname) - 1);
+			strncpy((char*) frame.longname + 14, instances[u]->name, sizeof(frame.longname) - 15);
+
+			//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
+			//we just do the latter because it is easier (and IMO makes more sense)
+			if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &frame, sizeof(frame), 0, source, source_len) < 0){
+				#ifdef _WIN32
+				if(WSAGetLastError() != WSAEWOULDBLOCK){
+				#else
+				if(errno != EAGAIN){
+				#endif
+					LOGPF("Failed to send poll reply for instance %s: %s", instances[u]->name, mmbackend_socket_strerror(errno));
+					return 1;
+				}
+			}
+			i++;
+		}
+	}
+
+	free(instances);
+	return 0;
+}
+
+static int artnet_maintenance(){
+	size_t u, c;
+	uint64_t timestamp = mm_timestamp();
+	uint32_t synthesize_delta = 0;
 	instance* inst = NULL;
-	artnet_pkt* frame = (artnet_pkt*) recv_buf;
 
 	//transmit keepalive & synthesized frames
 	global_cfg.next_frame = 0;
@@ -414,24 +521,48 @@ static int artnet_handle(size_t num, managed_fd* fds){
 			}
 		}
 	}
+	return 0;
+}
+
+static int artnet_handle(size_t num, managed_fd* fds){
+	size_t u;
+	struct sockaddr_storage peer_addr;
+	socklen_t peer_len = sizeof(peer_addr);
+	ssize_t bytes_read;
+	char recv_buf[ARTNET_RECV_BUF];
+	artnet_instance_id inst_id = {
+		.label = 0
+	};
+	instance* inst = NULL;
+	artnet_dmx* frame = (artnet_dmx*) recv_buf;
+
+	if(artnet_maintenance()){
+		return 1;
+	}
 
 	for(u = 0; u < num; u++){
 		do{
-			bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
-			if(bytes_read > 0 && bytes_read > sizeof(artnet_hdr)){
-				if(!memcmp(frame->magic, "Art-Net\0", 8) && be16toh(frame->opcode) == OpDmx){
+			bytes_read = recvfrom(fds[u].fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*) &peer_addr, &peer_len);
+			if(bytes_read > 0 && bytes_read > sizeof(artnet_hdr) && !memcmp(frame->magic, "Art-Net\0", 8)){
+				//DBGPF("Frame with opcode %04X, size %" PRIsize_t " on socket %" PRIu64, be16toh(frame->opcode), bytes_read, ((uint64_t) fds[u].impl) & 0xFF);
+				if(be16toh(frame->opcode) == OpDmx && bytes_read >= (sizeof(artnet_dmx) - 512)){
 					//find matching instance
 					inst_id.fields.fd_index = ((uint64_t) fds[u].impl) & 0xFF;
 					inst_id.fields.net = frame->net;
 					inst_id.fields.uni = frame->universe;
 					inst = mm_instance_find(BACKEND_NAME, inst_id.label);
-					if(inst && artnet_process_frame(inst, frame)){
-						LOG("Failed to process frame");
+					if(inst && artnet_process_dmx(inst, frame)){
+						LOG("Failed to process DMX frame");
 					}
 					else if(!inst && global_cfg.detect > 1){
 						LOGPF("Received data for unconfigured universe %d (net %d) on socket %" PRIu64, frame->universe, frame->net, (((uint64_t) fds[u].impl) & 0xFF));
 					}
 				}
+				else if(be16toh(frame->opcode) == OpPoll && bytes_read >= sizeof(artnet_poll)){
+					if(artnet_process_poll(((uint64_t) fds[u].impl) & 0xFF, (struct sockaddr*) &peer_addr, peer_len)){
+						LOG("Failed to process discovery frame");
+					}
+				}
 			}
 		} while(bytes_read > 0);
 
diff --git a/backends/artnet.h b/backends/artnet.h
index b42646d..8bf83f5 100644
--- a/backends/artnet.h
+++ b/backends/artnet.h
@@ -16,6 +16,8 @@ static int artnet_shutdown(size_t n, instance** inst);
 
 #define ARTNET_PORT "6454"
 #define ARTNET_VERSION 14
+#define ARTNET_ESTA_MANUFACTURER 0x4653 //"FS" as registered with ESTA
+#define ARTNET_OEM 0x2B93 //as registered with artistic license
 #define ARTNET_RECV_BUF 4096
 
 #define ARTNET_KEEPALIVE_INTERVAL 1000
@@ -70,6 +72,7 @@ typedef struct /*_artnet_fd*/ {
 	int fd;
 	size_t output_instances;
 	artnet_output_universe* output_instance;
+	struct sockaddr_storage announce_addr; //used for pollreplies if ss_family == AF_INET, port is always valid
 } artnet_descriptor;
 
 #pragma pack(push, 1)
@@ -79,7 +82,7 @@ typedef struct /*_artnet_hdr*/ {
 	uint16_t version;
 } artnet_hdr;
 
-typedef struct /*_artnet_pkt*/ {
+typedef struct /*_artnet_dmx*/ {
 	uint8_t magic[8];
 	uint16_t opcode;
 	uint16_t version;
@@ -89,9 +92,53 @@ typedef struct /*_artnet_pkt*/ {
 	uint8_t net;
 	uint16_t length;
 	uint8_t data[512];
-} artnet_pkt;
+} artnet_dmx;
+
+typedef struct /*_artnet_poll*/ {
+	uint8_t magic[8];
+	uint16_t opcode;
+	uint16_t version;
+	uint8_t flags;
+	uint8_t priority;
+} artnet_poll;
+
+typedef struct /*_artnet_poll_reply*/ {
+	uint8_t magic[8];
+	uint16_t opcode; //little-endian
+	uint8_t ip4[4]; //stop including l2/3 addresses in the payload, just use the sender address ffs
+	uint16_t port; //little-endian, who does that?
+	uint16_t firmware; //big-endian
+	uint16_t port_address; //big-endian
+	uint16_t oem; //big-endian
+	uint8_t bios_version;
+	uint8_t status;
+	uint16_t manufacturer; //little-endian
+	uint8_t shortname[18];
+	uint8_t longname[64];
+	uint8_t report[64];
+	uint16_t ports; //big-endian
+	uint8_t port_types[4]; //only use the first member, we report every universe in it's own reply
+	uint8_t port_in[4];
+	uint8_t port_out[4];
+	uint8_t subaddr_in[4];
+	uint8_t subaddr_out[4];
+	uint8_t video; //deprecated
+	uint8_t macro; //deprecatd
+	uint8_t remote; //deprecated
+	uint8_t spare[3];
+	uint8_t style;
+	uint8_t mac[6]; //come on
+	uint8_t parent_ip[4]; //COME ON
+	uint8_t parent_index; //i don't even know
+	uint8_t status2;
+	uint8_t port_out_b[4];
+	uint8_t status3;
+	uint8_t spare2[21];
+} artnet_poll_reply;
 #pragma pack(pop)
 
 enum artnet_pkt_opcode {
+	OpPoll = 0x0020,
+	OpPollReply = 0x0021,
 	OpDmx = 0x0050
 };
diff --git a/backends/artnet.md b/backends/artnet.md
index f035ad7..0770c92 100644
--- a/backends/artnet.md
+++ b/backends/artnet.md
@@ -9,16 +9,16 @@ Art-Netâ„¢ Designed by and Copyright Artistic Licence Holdings Ltd.
 
 | Option	| Example value		| Default value 	| Description		|
 |---------------|-----------------------|-----------------------|-----------------------|
-| `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. |
-| `net`		| `0`			| `0`			| The default net to use |
+| `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. |
+| `net`		| `0`			| `0`			| The default net to use (upper 7 bits of the 15-bit port address) |
 | `detect`	| `on`, `verbose`	| `off`			| Output additional information on received data packets to help with configuring complex scenarios |
 
 #### Instance configuration
 
 | Option	| Example value		| Default value 	| Description		|
 |---------------|-----------------------|-----------------------|-----------------------|
-| `net`		| `0`			| `0`			| ArtNet `net` to use	|
-| `universe`	| `0`			| `0`			| Universe identifier	|
+| `net`		| `0`			| `0`			| ArtNet `net` to use (upper 7 bits of the 15-bit port address |
+| `universe`	| `0`			| `0`			| Universe identifier (lower 8 bits of the 15-bit port address) |
 | `destination`	| `10.2.2.2`		| none			| Destination address for sent ArtNet frames. Setting this enables the universe for output |
 | `interface`	| `1`			| `0`			| The bound address to use for data input/output |
 | `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.
 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
 mandated by the [ArtNet specification](https://artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf) (Section `Refresh rate`).
 This limit can be disabled on a per-instance basis using the `realtime` instance option.
+
+This backend will reply to PollRequests from ArtNet controllers if binding an interface with an IPv4 address.
+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
+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
+announced in the ArtPollReply.
+
+When binding a specific IP address on Linux and OSX, no broadcast data (including ArtPoll requests) are received. There will be mechanism
+to bind to a specified interface in a future release. As a workaround, bind to the wildcard interface `0.0.0.0`.
+
+The backend itself supports IPv6, but the ArtNet spec hardcodes IPv4 address fields in some responses.
+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.
-- 
cgit v1.2.3