aboutsummaryrefslogtreecommitdiffhomepage
path: root/backends
diff options
context:
space:
mode:
Diffstat (limited to 'backends')
-rw-r--r--backends/artnet.c173
-rw-r--r--backends/artnet.h51
-rw-r--r--backends/artnet.md19
-rw-r--r--backends/mqtt.c4
-rw-r--r--backends/sacn.c14
5 files changed, 228 insertions, 33 deletions
diff --git a/backends/artnet.c b/backends/artnet.c
index 7c5f7e0..c0e567d 100644
--- a/backends/artnet.c
+++ b/backends/artnet.c
@@ -19,10 +19,11 @@ 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 descriptor limit reached");
+ LOG("Backend socket limit reached");
return -1;
}
@@ -40,10 +41,25 @@ static int artnet_listener(char* host, char* port){
return -1;
}
- LOGPF("Interface %" 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,8 +114,18 @@ static int artnet_configure(char* option, char* value){
return 1;
}
- if(artnet_listener(host, (port ? port : ARTNET_PORT))){
- LOGPF("Failed to bind descriptor: %s", value);
+ 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;
}
return 0;
@@ -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,22 +521,46 @@ 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 descriptor %" PRIu64, frame->universe, frame->net, (((uint64_t) fds[u].impl) & 0xFF));
+ 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");
}
}
}
@@ -461,7 +592,7 @@ static int artnet_start(size_t n, instance** inst){
};
if(!global_cfg.fds){
- LOG("Failed to start backend: no descriptors bound");
+ LOG("Failed to start backend: no sockets bound");
return 1;
}
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.
diff --git a/backends/mqtt.c b/backends/mqtt.c
index 6d25fd3..c9a303e 100644
--- a/backends/mqtt.c
+++ b/backends/mqtt.c
@@ -314,11 +314,11 @@ static int mqtt_reconnect(instance* inst){
//prepare CONNECT message header
variable_header[6] = data->mqtt_version;
- variable_header[7] = 0x02 /*clean start*/ | (data->user ? 0x80 : 0x00) | (data->user ? 0x40 : 0x00);
+ variable_header[7] = 0x02 /*clean start*/ | (data->user ? 0x80 : 0x00) | (data->password ? 0x40 : 0x00);
if(data->mqtt_version == 0x05){ //mqtt v5 has additional options
//push number of option bytes (as a varint, no less) before actually pushing the option data.
- //obviously someone thought saving 3 whole bytes in exchange for not being able to sequentially creating the package was smart..
+ //obviously someone thought saving 3 whole bytes in exchange for not being able to sequentially create the package was smart..
variable_header[vh_offset++] = 8;
//push maximum packet size option
variable_header[vh_offset++] = 0x27;
diff --git a/backends/sacn.c b/backends/sacn.c
index 5c5b81d..4e01402 100644
--- a/backends/sacn.c
+++ b/backends/sacn.c
@@ -95,7 +95,7 @@ static int sacn_listener(char* host, char* port, uint8_t flags){
return -1;
}
- LOGPF("Interface %" PRIsize_t " bound to %s port %s", global_cfg.fds, host, port);
+ LOGPF("Socket %" PRIsize_t " bound to %s port %s", global_cfg.fds, host, port);
global_cfg.fd[global_cfg.fds].fd = fd;
global_cfg.fd[global_cfg.fds].universes = 0;
global_cfg.fd[global_cfg.fds].universe = NULL;
@@ -160,7 +160,7 @@ static int sacn_configure(char* option, char* value){
}
if(sacn_listener(host, port ? port : SACN_PORT, flags)){
- LOGPF("Failed to bind descriptor: %s", value);
+ LOGPF("Failed to bind socket: %s", value);
return 1;
}
@@ -613,7 +613,7 @@ static int sacn_handle(size_t num, managed_fd* fds){
}
else if(!inst && global_cfg.detect > 1){
//this will only happen with unicast input
- LOGPF("Received data for unconfigured universe %d on descriptor %" PRIu64, be16toh(data->universe), ((uint64_t) fds[u].impl) & 0xFFFF);
+ LOGPF("Received data for unconfigured universe %d on socket %" PRIu64, be16toh(data->universe), ((uint64_t) fds[u].impl) & 0xFFFF);
}
}
}
@@ -641,6 +641,8 @@ static int sacn_start_multicast(instance* inst){
struct sockaddr_storage bound_name = {
0
};
+ char mcast_ifaddr[INET_ADDRSTRLEN] = "";
+
#ifdef _WIN32
struct ip_mreq mcast_req = {
.imr_interface.s_addr = INADDR_ANY,
@@ -661,11 +663,15 @@ static int sacn_start_multicast(instance* inst){
LOGPF("Socket %" PRIsize_t " not bound to a specific IPv4 address, joining multicast input group for instance %s (universe %u) on default interface", data->fd_index, inst->name, data->uni);
}
else{
+ //this relies on the previous check for the socket family (AF_INET / IPv4)
#ifdef _WIN32
mcast_req.imr_interface = ((struct sockaddr_in*) &bound_name)->sin_addr;
#else
mcast_req.imr_address = ((struct sockaddr_in*) &bound_name)->sin_addr;
#endif
+
+ mmbackend_sockaddr_ntop((struct sockaddr*) &bound_name, mcast_ifaddr, sizeof(mcast_ifaddr));
+ LOGPF("Joining multicast input group for instance %s (universe %u) on interface for socket %" PRIsize_t " (%s)", inst->name, data->uni, data->fd_index, mcast_ifaddr);
}
if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){
@@ -685,7 +691,7 @@ static int sacn_start(size_t n, instance** inst){
struct sockaddr_in* dest_v4 = NULL;
if(!global_cfg.fds){
- LOG("Failed to start, no descriptors bound");
+ LOG("Failed to start, no sockets bound");
return 1;
}