sACN  2.0.2
Implementation of ANSI E1.31 (Streaming ACN)
View other versions:
Using the sACN Merge Receiver API

The sACN Merge Receiver API combines the functionality of the sACN Receiver and DMX Merger APIs. It provides the application with merged DMX levels, plus data with other start codes from each source, as sACN packets are received. This API exposes both a C and C++ language interface. The C++ interface is a header-only wrapper around the C interface.

Initialization and Destruction

The sACN library must be globally initialized before using the Merge Receiver API. See Global Initialization and Destruction.

An sACN merge receiver instance can listen on one universe at a time, but the universe it listens on can be changed at any time. A merge receiver begins listening when it is created. To create an sACN merge receiver instance, use the sacn_merge_receiver_create() function in C, or instantiate an sacn::MergeReceiver and call its Startup() function in C++. A merge receiver can later be destroyed by calling sacn_merge_receiver_destroy() in C or Shutdown() in C++.

The sACN Merge Receiver API is an asynchronous, callback-oriented API. Part of the initial configuration for a merge receiver instance is to specify the callbacks for the library to use. In C, these are specified as a set of function pointers. In C++, these are specified as an instance of a class that implements sacn::MergeReceiver::NotifyHandler. Callbacks are dispatched from a background thread which is started when the first merge receiver instance is created.

// Or, to initialize default values at runtime:
config.universe_id = 1; // Listen on universe 1
// Set the callback functions - defined elsewhere
config.callbacks.universe_data = my_universe_data_callback;
config.callbacks.universe_non_dmx = my_universe_non_dmx_callback;
config.callbacks.source_limit_exceeded = my_source_limit_exceeded_callback; // optional, can be NULL
SacnMcastInterface my_netints[NUM_MY_NETINTS];
// Assuming my_netints and NUM_MY_NETINTS are initialized by the application...
SacnNetintConfig netint_config;
netint_config.netints = my_netints;
netint_config.num_netints = NUM_MY_NETINTS;
sacn_merge_receiver_t my_merge_receiver_handle;
// If you want to specify specific network interfaces to use:
sacn_merge_receiver_create(&config, &my_merge_receiver_handle, &netint_config);
// Or, if you just want to use all network interfaces:
sacn_merge_receiver_create(&config, &my_merge_receiver_handle, NULL);
// You can add additional merge receivers as well, in the same way.
// To destroy the merge receiver when you're done with it:
sacn_merge_receiver_destroy(my_merge_receiver_handle);
etcpal_error_t sacn_merge_receiver_destroy(sacn_merge_receiver_t handle)
Destroy a sACN Merge Receiver instance.
Definition: merge_receiver.c:182
etcpal_error_t sacn_merge_receiver_create(const SacnMergeReceiverConfig *config, sacn_merge_receiver_t *handle, const SacnNetintConfig *netint_config)
Create a new sACN Merge Receiver to listen and merge sACN data on a universe.
Definition: merge_receiver.c:88
void sacn_merge_receiver_config_init(SacnMergeReceiverConfig *config)
Initialize an sACN Merge Receiver Config struct to default values.
Definition: merge_receiver.c:55
int sacn_merge_receiver_t
Definition: merge_receiver.h:53
#define SACN_MERGE_RECEIVER_CONFIG_DEFAULT_INIT
Definition: merge_receiver.h:186
Definition: common.h:85
SacnMergeReceiverMergedDataCallback universe_data
Definition: merge_receiver.h:150
SacnMergeReceiverSourceLimitExceededCallback source_limit_exceeded
Definition: merge_receiver.h:152
SacnMergeReceiverNonDmxCallback universe_non_dmx
Definition: merge_receiver.h:151
Definition: merge_receiver.h:158
uint16_t universe_id
Definition: merge_receiver.h:162
SacnMergeReceiverCallbacks callbacks
Definition: merge_receiver.h:164
Definition: common.h:102
size_t num_netints
Definition: common.h:107
SacnMcastInterface * netints
Definition: common.h:105

// Implement the callback functions by inheriting sacn::MergeReceiver::NotifyHandler:
class MyNotifyHandler : public sacn::MergeReceiver::NotifyHandler
{
// Required callbacks that must be implemented:
void HandleMergedData(Handle handle, const SacnRecvMergedData& merged_data) override;
void HandleNonDmxData(Handle receiver_handle, const etcpal::SockAddr& source_addr,
const SacnRemoteSource& source_info, const SacnRecvUniverseData& universe_data) override;
// Optional callback - this doesn't have to be a part of MyNotifyHandler:
void HandleSourceLimitExceeded(Handle handle, uint16_t universe) override;
};
// Now to set up a merge receiver:
sacn::MergeReceiver::Settings config(1); // Instantiate config & listen on universe 1
sacn::MergeReceiver merge_receiver; // Instantiate a merge receiver
MyNotifyHandler my_notify_handler;
merge_receiver.Startup(config, my_notify_handler);
// Or do this if Startup is being called within the NotifyHandler-derived class:
merge_receiver.Startup(config, *this);
// Or do this to specify custom interfaces for the merge receiver to use:
std::vector<SacnMcastInterface> my_netints; // Assuming my_netints is initialized by the application...
merge_receiver.Startup(config, my_notify_handler, my_netints);
// To destroy the merge receiver when you're done with it:
merge_receiver.Shutdown();
A base class for a class that receives notification callbacks from a sACN merge receiver.
Definition: merge_receiver.h:71
virtual void HandleMergedData(Handle handle, const SacnRecvMergedData &merged_data)=0
Notify that a new data packet has been received and merged.
virtual void HandleSourceLimitExceeded(Handle handle, uint16_t universe)
Notify that more than the configured maximum number of sources are currently sending on the universe ...
Definition: merge_receiver.h:127
virtual void HandleNonDmxData(Handle receiver_handle, const etcpal::SockAddr &source_addr, const SacnRemoteSource &source_info, const SacnRecvUniverseData &universe_data)=0
Notify that a non-data packet has been received.
An instance of sACN Merge Receiver functionality; see Using the sACN Merge Receiver API.
Definition: merge_receiver.h:61
etcpal::Error Startup(const Settings &settings, NotifyHandler &notify_handler)
Start listening for sACN data on a universe.
Definition: merge_receiver.h:329
void Shutdown()
Stop listening for sACN data on a universe.
Definition: merge_receiver.h:386
Definition: merge_receiver.h:61
Definition: receiver.h:89
Definition: receiver.h:124
A set of configuration settings that a merge receiver needs to initialize.
Definition: merge_receiver.h:139

Listening on a Universe

A merge receiver can listen to one universe at a time. The initial universe being listened to is specified in the config passed into create/Startup. There are functions that allow you to get the current universe being listened to, or to change the universe to listen to.

// Get the universe currently being listened to
uint16_t current_universe;
sacn_merge_receiver_get_universe(my_merge_receiver_handle, &current_universe);
// Change the universe to listen to
uint16_t new_universe = current_universe + 1;
sacn_merge_receiver_change_universe(my_merge_receiver_handle, new_universe);
etcpal_error_t sacn_merge_receiver_get_universe(sacn_merge_receiver_t handle, uint16_t *universe_id)
Get the universe on which a sACN Merge Receiver is currently listening.
Definition: merge_receiver.c:226
etcpal_error_t sacn_merge_receiver_change_universe(sacn_merge_receiver_t handle, uint16_t new_universe_id)
Change the universe on which a sACN Merge Receiver is listening.
Definition: merge_receiver.c:268

// Get the universe currently being listened to
auto result = merge_receiver.GetUniverse();
if (result)
{
// Change the universe to listen to
uint16_t new_universe = *result + 1;
merge_receiver.ChangeUniverse(new_universe);
}
etcpal::Expected< uint16_t > GetUniverse() const
Get the universe this merge receiver is listening to.
Definition: merge_receiver.h:397
etcpal::Error ChangeUniverse(uint16_t new_universe_id)
Change the universe this class is listening to.
Definition: merge_receiver.h:439

Footprints

TODO: Custom footprints are not yet implemented, so the footprint will always be the full universe.

A merge receiver can also be configured to listen to a specific range of slots within the universe, which is called the footprint. For example, networked fixtures might use this to only retrieve data about slots within their DMX footprint. The footprint is initially specified in the merge receiver config, but it is optional, so it defaults to the full universe if it isn't specified in the config. There are also functions to get and change the footprint, including a function that can change both the universe and footprint at once.

// Get the current footprint
SacnRecvUniverseSubrange current_footprint;
sacn_merge_receiver_get_footprint(my_receiver_handle, &current_footprint);
// Change the footprint, but keep the universe the same
new_footprint.start_address = 20;
new_footprint.address_count = 10;
sacn_merge_receiver_change_footprint(my_receiver_handle, &new_footprint);
// Change both the universe and the footprint at once
uint16_t new_universe = current_universe + 1;
new_footprint.start_address = 40;
new_footprint.address_count = 20;
sacn_merge_receiver_change_universe_and_footprint(my_receiver_handle, new_universe, &new_footprint);
etcpal_error_t sacn_merge_receiver_change_footprint(sacn_merge_receiver_t handle, const SacnRecvUniverseSubrange *new_footprint)
Change the footprint within the universe on which an sACN receiver is listening. TODO: Not yet implem...
Definition: merge_receiver.c:322
etcpal_error_t sacn_merge_receiver_get_footprint(sacn_merge_receiver_t handle, SacnRecvUniverseSubrange *footprint)
Get the footprint within the universe on which a sACN Merge Receiver is currently listening.
Definition: merge_receiver.c:245
etcpal_error_t sacn_merge_receiver_change_universe_and_footprint(sacn_merge_receiver_t handle, uint16_t new_universe_id, const SacnRecvUniverseSubrange *new_footprint)
Change the universe and footprint on which an sACN merge receiver is listening. TODO: Not yet impleme...
Definition: merge_receiver.c:342
Definition: receiver.h:80
int address_count
Definition: receiver.h:82
int start_address
Definition: receiver.h:81

// Get the universe currently being listened to
auto current_footprint = merge_receiver.GetFootprint();
if (current_footprint)
{
// Change the footprint, but keep the universe the same
SacnRecvUniverseSubrange new_footprint;
new_footprint.start_address = current_footprint.start_address + 10;
new_footprint.address_count = current_footprint.address_count;
merge_receiver.ChangeFootprint(new_footprint);
// Change both the universe and the footprint at once
new_footprint.start_address += 10;
uint16_t new_universe = 100u;
merge_receiver.ChangeUniverseAndFootprint(new_universe, new_footprint);
}
etcpal::Error ChangeFootprint(const SacnRecvUniverseSubrange &new_footprint)
Change the footprint within the universe this merge receiver is listening to. TODO: Not yet implement...
Definition: merge_receiver.h:453
etcpal::Expected< SacnRecvUniverseSubrange > GetFootprint() const
Get the footprint within the universe this merge receiver is listening to.
Definition: merge_receiver.h:414
etcpal::Error ChangeUniverseAndFootprint(uint16_t new_universe_id, const SacnRecvUniverseSubrange &new_footprint)
Change the universe and footprint this merge receiver is listening to. TODO: Not yet implemented.
Definition: merge_receiver.h:468

Merging

Once a merge receiver has been created, it will begin listening for data on the configured universe and footprint. When DMX level or priority data arrives, or a source is lost, it performs a merge. This is where it determines the correct DMX level and source for each slot of the footprint, since there may be multiple sources transmitting on the same universe. It does this by selecting the source with the highest priority. If there are multiple sources at that priority, then it selects the source with the highest level. That is the Highest Takes Precedence (HTP) merge algorithm.

Receiving sACN Data

The merged data callback is called whenever there are new merge results, pending the sampling period. The sampling period occurs when a receiver starts listening on a new universe, new footprint, or new set of interfaces. Universe data is merged as it comes in during this period, but the notification of this data doesn't occur until after the sampling period ends. This removes flicker as various sources in the network are discovered.

This callback will be called in multiple ways:

  1. When a new non-preview data packet or per-address priority packet is received from the sACN Receiver module, it is immediately and synchronously passed to the DMX Merger. If the sampling period has not ended, the merged result is not passed to this callback until the sampling period ends. Otherwise, it is immediately and synchronously passed to this callback.
  2. When a sACN source is no longer sending non-preview data or per-address priority packets, the lost source callback from the sACN Receiver module will be passed to the merger, after which the merged result is passed to this callback pending the sampling period.

Please note that per-address priority is an ETC-specific sACN extension, and is disabled if the library is compiled with SACN_ETC_PRIORITY_EXTENSION set to 0 (in which case per-address priority packets received have no effect).

The merger will prioritize sources with the highest per-address priority (or universe priority if the source doesn't provide per-address priorities). If two sources have the same highest priority, the one with the highest NULL start code level wins (HTP).

There is a key distinction in how the merger interprets the lowest priority. The lowest universe priority is 0, but the lowest per-address priority is 1. This is because a per-address priority of 0 indicates that the source is not sending any levels to the corresponding slot. The solution the merger uses is to always track priorities per-address. If a source only has a universe priority, that priority is used for each slot, except if it equals 0 - in that case, it is converted to 1. This means the merger will treat a universe priority of 0 as equivalent to a universe priority of 1, as well as per-address priorities equal to 1.

Also keep in mind that if less than 512 per-address priorities are received, then the remaining slots will be treated as if they had a per-address priority of 0. Likewise, if less than 512 levels are received, the remaining slots will be treated as if they had a level of 0, but they may still have non-zero priorities.

This callback should be processed quickly, since it will interfere with the receipt and processing of other sACN packets on the universe.

void my_universe_data_callback(sacn_merge_receiver_t handle, const SacnRecvMergedData* merged_data, void* context)
{
// Check handle and/or context as necessary...
// You wouldn't normally print a message on each sACN update, but this is just to demonstrate the
// fields available:
printf("Got new merge results on universe %u\n", merged_data->universe_id);
// Example for an sACN-enabled fixture...
for (int i = 0; i < merged_data->slot_range.address_count; ++i) // For each slot in my DMX footprint
{
// merged_data->owners[0] always represents the owner of the first slot in the footprint
if (merged_data->owners[i] == SACN_REMOTE_SOURCE_INVALID)
{
// One of the slots in my DMX footprint does not have a valid source
return;
}
}
// merged_data->levels[0] will always be the level of the first slot of the footprint
memcpy(my_data_buf, merged_data->levels, merged_data->slot_range.address_count);
// Act on the data somehow
}
T printf(T... args)
#define SACN_REMOTE_SOURCE_INVALID
Definition: common.h:60
T memcpy(T... args)
const uint8_t * levels
Definition: merge_receiver.h:73
uint16_t universe_id
Definition: merge_receiver.h:65
SacnRecvUniverseSubrange slot_range
Definition: merge_receiver.h:69
const sacn_remote_source_t * owners
Definition: merge_receiver.h:79

void MyNotifyHandler::HandleMergedData(Handle handle, const SacnRecvMergedData& merged_data)
{
// You wouldn't normally print a message on each sACN update, but this is just to demonstrate the
// fields available:
std::cout << "Got new merge results on universe " << merged_data.universe_id << "\n";
// Example for an sACN-enabled fixture...
for (int i = 0; i < merged_data.slot_range.address_count; ++i) // For each slot in my DMX footprint
{
// merged_data.owners[0] always represents the owner of the first slot in the footprint
if (merged_data.owners[i] == kInvalidRemoteSourceHandle)
{
// One of the slots in my DMX footprint does not have a valid source
return;
}
}
// merged_data.levels[0] will always be the level of the first slot of the footprint
memcpy(my_data_buf, merged_data.levels, merged_data.slot_range.address_count);
// Act on the data somehow
}
constexpr RemoteSourceHandle kInvalidRemoteSourceHandle
Definition: common.h:54

If non-NULL start code, non-PAP sACN data is received, it is passed through directly to the non-DMX data callback.

void my_universe_non_dmx_callback(sacn_merge_receiver_t receiver_handle, const EtcPalSockAddr* source_addr,
const SacnRemoteSource* source_info, const SacnRecvUniverseData* universe_data,
void* context)
{
// Check receiver_handle and/or context as necessary...
// You wouldn't normally print a message on each sACN update, but this is just to demonstrate the
// header fields available:
char addr_str[ETCPAL_IP_STRING_BYTES];
etcpal_ip_to_string(&source_addr->ip, addr_str);
char cid_str[ETCPAL_UUID_STRING_BYTES];
etcpal_uuid_to_string(&source_info->cid, cid_str);
printf("Got non-DMX sACN update from source %s (address %s:%u, name %s) on universe %u, priority %u, start code %u\n",
cid_str, addr_str, source_addr->port, source_info->name, universe_data->universe_id, universe_data->priority,
universe_data->start_code);
// Act on the data somehow
}
#define ETCPAL_IP_STRING_BYTES
etcpal_error_t etcpal_ip_to_string(const EtcPalIpAddr *src, char *dest)
bool etcpal_uuid_to_string(const EtcPalUuid *uuid, char *buf)
#define ETCPAL_UUID_STRING_BYTES
EtcPalIpAddr ip
uint8_t start_code
Definition: receiver.h:111
uint8_t priority
Definition: receiver.h:97
uint16_t universe_id
Definition: receiver.h:93
EtcPalUuid cid
Definition: receiver.h:128
char name[SACN_SOURCE_NAME_MAX_LEN]
Definition: receiver.h:130

void MyNotifyHandler::HandleNonDmxData(Handle receiver_handle, const etcpal::SockAddr& source_addr,
const SacnRemoteSource& source_info, const SacnRecvUniverseData& universe_data)
{
// You wouldn't normally print a message on each sACN update, but this is just to demonstrate the
// header fields available:
std::cout << "Got non-DMX sACN update from source " << etcpal::Uuid(source_info.cid).ToString() << " (address "
<< source_addr.ToString() << ", name " << source_info.name << ") on universe " << universe_data.universe_id
<< ", priority " << universe_data.priority << ", start code " << universe_data.start_code << "\n";
// Act on the data somehow
}
std::string ToString() const
std::string ToString() const

Tracking Sources

The data callbacks include data originating from one or more sources transmitting on the current universe. Each source has a handle that serves as a primary key, differentiating it from other sources. The merge receiver provides information about each source, including the name, IP, and CID. This information is provided directly in the non-DMX callback. However, in the merged data callback, only the source handles are passed in. To obtain more details about a source, use the get source function.

void my_universe_data_callback(sacn_merge_receiver_t handle, const SacnRecvMergedData* merged_data, void* context)
{
// Check handle and/or context as necessary...
for(unsigned int i = 0; i < merged_data->slot_range.address_count; ++i)
{
etcpal_error_t result = sacn_merge_receiver_get_source(handle, merged_data->owners[i], &source_info);
if(result == kEtcPalErrOk)
{
// You wouldn't normally print a message on each sACN update, but this is just for demonstration:
char cid_str[ETCPAL_UUID_STRING_BYTES];
etcpal_uuid_to_string(&source_info.cid, cid_str);
char ip_str[ETCPAL_IP_STRING_BYTES];
etcpal_ip_to_string(&source_info.addr.ip, ip_str);
printf("Slot %u -\n\tCID: %s\n\tName: %s\n\tAddress: %s:%u\n", (merged_data->slot_range.start_address + i), cid_str,
source_info.name, ip_str, source_info.addr.port);
}
}
}
etcpal_error_t
kEtcPalErrOk
etcpal_error_t sacn_merge_receiver_get_source(sacn_merge_receiver_t merge_receiver_handle, sacn_remote_source_t source_handle, SacnMergeReceiverSource *source_info)
Gets a copy of the information for the specified merge receiver source.
Definition: merge_receiver.c:491
Definition: merge_receiver.h:206
EtcPalUuid cid
Definition: merge_receiver.h:210
EtcPalSockAddr addr
Definition: merge_receiver.h:214
char name[SACN_SOURCE_NAME_MAX_LEN]
Definition: merge_receiver.h:212

void MyNotifyHandler::HandleMergedData(Handle handle, const SacnRecvMergedData& merged_data)
{
// How to get the merge receiver instance from the handle is application-defined. For example:
auto merge_receiver = my_app_state.GetMergeReceiver(handle);
for(unsigned int i = 0; i < merged_data.slot_range.address_count; ++i)
{
auto source = merge_receiver.GetSource(merged_data.owners[i]);
if(source)
{
// You wouldn't normally print a message on each sACN update, but this is just for demonstration:
std::cout << "Slot " << (merged_data.slot_range.start_address + i) << " -\n\tCID: " << source->cid.ToString()
<< "\n\tName: " << source->name << "\n\tAddress: " << source->addr.ToString() << "\n";
}
}
}
etcpal::Expected< Source > GetSource(sacn_remote_source_t source_handle)
Gets a copy of the information for the specified merge receiver source.
Definition: merge_receiver.h:507

Source Limit Exceeded Conditions

The sACN library will only forward data from sources that it is able to track. When the library is compilied with SACN_DYNAMIC_MEM set to 0, this means that it will only track up to SACN_RECEIVER_MAX_SOURCES_PER_UNIVERSE sources at a time. (You can change these compile options in your sacn_config.h; see Building and Integrating the sACN Library Into Your Project.)

When the library encounters a source that it does not have room to track, it will send a single source limit exceeded notification. Additional source limit exceeded callbacks will not be delivered until the number of tracked sources falls below the limit and then exceeds it again.

If sACN was compiled with SACN_DYNAMIC_MEM set to 1 (the default on non-embedded platforms), the library will check against the source_count_max value from the merge receiver config/settings, instead of SACN_RECEIVER_MAX_SOURCES_PER_UNIVERSE. The source_count_max value may be set to SACN_RECEIVER_INFINITE_SOURCES, in which case the library will track as many sources as it is able to dynamically allocate memory for, and this callback will not be called in normal program operation (in this case it can be set to NULL in the config struct in C).

void my_source_limit_exceeded_callback(sacn_merge_receiver_t handle, uint16_t universe, void* context)
{
// Check handle and/or context as necessary...
// Handle the condition in an application-defined way. Maybe log it?
}

void MyNotifyHandler::HandleSourceLimitExceeded(Handle handle, uint16_t universe)
{
// Handle the condition in an application-defined way. Maybe log it?
}