RDMnet  HEAD (unstable)
Implementation of ANSI E1.33 (RDMnet)
View other versions:
Using the Controller API

The RDMnet Controller API exposes both a C and C++ language interface. The C++ interface is a header-only wrapper around the C interface.

Initialization

The RDMnet library must be globally initialized before using the RDMnet controller API. See Global Initialization and Destruction.

To create a controller instance, use the rdmnet_controller_create() function in C, or instantiate an rdmnet::Controller and call its Startup() function in C++. Most apps will only need a single controller instance. One controller can monitor an arbitrary number of RDMnet scopes at once.

The RDMnet controller API is an asynchronous, callback-oriented API. Part of the initial configuration for a controller instance is a set of function pointers (or abstract interface) for the library to use as callbacks. Callbacks are dispatched from the background thread that is started when the RDMnet library is initialized.

// Sets optional values to defaults. Must pass your ESTA manufacturer ID. If you have not yet
// requested an ESTA manufacturer ID, the range 0x7ff0 to 0x7fff can be used for prototyping.
RdmnetControllerConfig config = RDMNET_CONTROLLER_CONFIG_DEFAULT_INIT(MY_ESTA_MANUFACTURER_ID_VAL);
// Each controller is a component that must have a Component ID (CID), which is simply a UUID.
// Software should generate and save a CID so that the same one is used on each run of the software.
// Set the callback functions - defined elsewhere
// p_some_opaque_data is an opaque data pointer that will be passed back to each callback function
rdmnet_controller_set_callbacks(&config, my_controller_connected_cb, my_controller_connect_failed_cb,
my_controller_disconnected_cb, my_controller_client_list_update_received_cb,
my_controller_rdm_response_received_cb, my_controller_status_received_cb,
p_some_opaque_data);
// Needed to identify this controller to other controllers on the network. More on this later.
config.rdm_data.model_id = 0x0001;
config.rdm_data.software_version_id = 0x01000000;
config.rdm_data.manufacturer_label = "My Manufacturer Name";
config.rdm_data.device_model_description = "My Product Name";
config.rdm_data.device_label = "My Device Label";
rdmnet_controller_t my_controller_handle;
etcpal_error_t result = rdmnet_controller_create(&config, &my_controller_handle);
if (result == kEtcPalErrOk)
{
// Handle is valid and may be referenced in later calls to API functions.
}
else
{
printf("RDMnet controller creation failed with error: %s\n", etcpal_strerror(result));
}
Definitions for the RDMnet Controller API.
T printf(T... args)
const char * etcpal_strerror(etcpal_error_t code)
etcpal_error_t
kEtcPalErrOk
etcpal_error_t etcpal_generate_os_preferred_uuid(EtcPalUuid *uuid)
etcpal_error_t rdmnet_controller_create(const RdmnetControllerConfig *config, rdmnet_controller_t *handle)
Create a new instance of RDMnet controller functionality.
Definition: controller.c:255
void rdmnet_controller_set_callbacks(RdmnetControllerConfig *config, RdmnetControllerConnectedCallback connected, RdmnetControllerConnectFailedCallback connect_failed, RdmnetControllerDisconnectedCallback disconnected, RdmnetControllerClientListUpdateReceivedCallback client_list_update_received, RdmnetControllerRdmResponseReceivedCallback rdm_response_received, RdmnetControllerStatusReceivedCallback status_received, RdmnetControllerResponderIdsReceivedCallback responder_ids_received, void *context)
Set the main callbacks in an RDMnet controller configuration structure.
Definition: controller.c:182
#define RDMNET_CONTROLLER_CONFIG_DEFAULT_INIT(manu_id)
A default-value initializer for an RdmnetControllerConfig struct.
Definition: controller.h:327
int rdmnet_controller_t
Definition: controller.h:54
A set of information that defines the startup parameters of an RDMnet Controller.
Definition: controller.h:273
EtcPalUuid cid
Definition: controller.h:279
RdmnetControllerRdmData rdm_data
Definition: controller.h:291
const char * software_version_label
Definition: controller.h:230
uint32_t software_version_id
A number representing the version of the controller software.
Definition: controller.h:224
uint16_t model_id
A number representing the product model which implements the controller.
Definition: controller.h:219
const char * device_model_description
Definition: controller.h:228
bool device_label_settable
Whether the library should allow the device_label to be changed remotely.
Definition: controller.h:249
const char * manufacturer_label
Definition: controller.h:226
const char * device_label
Definition: controller.h:232

class MyControllerNotifyHandler : public rdmnet::Controller::NotifyHandler
{
// Implement the NotifyHandler callbacks...
};
MyControllerNotifyHandler my_controller_notify_handler;
// Each controller is a component that must have a Component ID (CID), which is simply a UUID.
// Software should generate and save a CID so that the same one is used on each run of the software.
// Contains the configuration settings that the controller needs to operate. Some of these are set
// to default values and can be changed if necessary. Must pass your ESTA manufacturer ID. If you
// have not yet requested an ESTA manufacturer ID, the range 0x7ff0 to 0x7fff can be used for
// prototyping.
rdmnet::Controller::Settings my_settings(my_cid, MY_ESTA_MANUFACTURER_ID_VAL);
// Needed to identify this controller to other controllers on the network. More on this later.
rdmnet::Controller::RdmData my_rdm_data(0x0001, // Device Model ID
0x01000000, // Software Version ID
"My Manufacturer Name", // Manufacturer Label
"My Product Name", // Device Model Description
"1.0.0", // Software Version Label
"My Device Label"); // Device Label
rdmnet::Controller controller;
etcpal::Error result = controller.Startup(my_controller_notify_handler, my_settings, my_rdm_data);
if (result)
{
// Controller is valid and running.
rdmnet::Controller::Handle handle = controller.handle();
// Store handle for later lookup from the NotifyHandler callback functions.
}
else
{
std::cout << "RDMnet controller startup failed with error: " << result.ToString() << '\n';
}
std::string ToString() const
static Uuid OsPreferred() noexcept
A base class for a class that receives notification callbacks from a controller.
Definition: controller.h:77
An instance of RDMnet controller functionality.
Definition: controller.h:67
etcpal::Error Startup(NotifyHandler &notify_handler, const Settings &settings, const RdmData &rdm_data)
Allocate resources and start up this controller with the given configuration.
Definition: controller.h:521
Handle handle() const
Retrieve the handle of a controller instance.
Definition: controller.h:1045
C++ wrapper for the RDMnet Controller API.
A set of initial identifying RDM data to use for a controller.
Definition: controller.h:201
A set of configuration settings that a controller needs to initialize.
Definition: controller.h:182

Deinitialization

The controller should be shut down and destroyed gracefully before program termination. This will send a graceful disconnect message to any connected brokers and deallocate the controller's resources.

// At this point, the controller is no longer running and its handle is no longer valid.
@ kRdmnetDisconnectShutdown
Definition: common.h:87
etcpal_error_t rdmnet_controller_destroy(rdmnet_controller_t controller_handle, rdmnet_disconnect_reason_t reason)
Destroy a controller instance.
Definition: controller.c:293

controller.Shutdown();
// At this point, the controller is no longer running and its handle is no longer valid. It can be
// started again (with a new handle) by calling Startup() again.
void Shutdown(rdmnet_disconnect_reason_t disconnect_reason=kRdmnetDisconnectShutdown)
Shut down this controller and deallocate resources.
Definition: controller.h:636

Managing Scopes

A controller instance is initially created without any configured scopes. If the app has not been reconfigured by a user, the E1.33 RDMnet standard requires that the RDMnet default scope be configured automatically. There is a shortcut function for this. Otherwise, you can add a custom scope.

Per the requirements of RDMnet, a scope string is always UTF-8 and is thus represented by a char[] in C and a std::string in C++.

See the "Scopes" section in How RDMnet Works for more information on scopes.

// Add a default scope
rdmnet_client_scope_t default_scope_handle;
etcpal_error_t result = rdmnet_controller_add_default_scope(my_controller_handle, &default_scope_handle);
// Add a custom scope
RdmnetScopeConfig scope_config;
RDMNET_CLIENT_SET_SCOPE(&scope_config, "custom_scope_name");
rdmnet_client_scope_t custom_scope_handle;
etcpal_error_t result = rdmnet_controller_add_scope(my_controller_handle, &scope_config, &custom_scope_handle);
#define RDMNET_CLIENT_SET_SCOPE(configptr, scope_str)
Initialize an RdmnetScopeConfig struct with a scope string.
Definition: client.h:262
int rdmnet_client_scope_t
Definition: client.h:41
etcpal_error_t rdmnet_controller_add_scope(rdmnet_controller_t controller_handle, const RdmnetScopeConfig *scope_config, rdmnet_client_scope_t *scope_handle)
Add a new scope to a controller instance.
Definition: controller.c:330
etcpal_error_t rdmnet_controller_add_default_scope(rdmnet_controller_t controller_handle, rdmnet_client_scope_t *scope_handle)
Add a new scope representing the default RDMnet scope to a controller instance.
Definition: controller.c:366
Definition: client.h:224

// Or...
etcpal::Expected<rdmnet::ScopeHandle> add_res = controller.AddScope("custom_scope_name");
if (add_res)
{
rdmnet::ScopeHandle default_scope_handle = *add_res;
}
else
{
std::cout << "Error adding default scope: '" << add_res.error().ToString() << "'\n"
}
constexpr Error error() const noexcept
etcpal::Expected< ScopeHandle > AddDefaultScope(const etcpal::SockAddr &static_broker_addr=etcpal::SockAddr{})
Shortcut to add the default RDMnet scope to a controller instance.
Definition: controller.h:707
etcpal::Expected< ScopeHandle > AddScope(const char *id, const etcpal::SockAddr &static_broker_addr=etcpal::SockAddr{})
Add a new scope to this controller instance.
Definition: controller.h:653

Dynamic vs Static Scopes

Adding a scope will immediately begin the scope connection state machine from the RDMnet tick thread. If a static configuration is not provided (using the RDMNET_CLIENT_SET_SCOPE() macro to initialize an RdmnetScopeConfig, or calling rdmnet::Controller::AddScope() with only one argument) the first action will be to attempt to discover brokers for this scope using DNS-SD. Once a broker is found, connection will be initiated automatically, and the result will be delivered via either the connected or connect_failed callback.

If a broker for a given scope has been configured with a static IP address and port, you can skip DNS-SD discovery by providing a static configuration:

// Get configured static broker address
EtcPalSockAddr static_broker_addr;
etcpal_string_to_ip(kEtcPalIpTypeV4, "192.168.2.1", &static_broker_addr.ip);
static_broker_addr.port = 8000;
RDMNET_CLIENT_SET_STATIC_SCOPE(&config, "my_custom_scope", static_broker_addr);
// Or:
RDMNET_CLIENT_SET_STATIC_DEFAULT_SCOPE(&config, static_broker_addr);
rdmnet_client_scope_t scope_handle;
etcpal_error_t result = rdmnet_controller_add_scope(my_controller_handle, &config, &scope_handle);
etcpal_error_t etcpal_string_to_ip(etcpal_iptype_t type, const char *src, EtcPalIpAddr *dest)
kEtcPalIpTypeV4
#define RDMNET_CLIENT_SET_STATIC_SCOPE(configptr, scope_str, broker_addr)
Initialize an RdmnetScopeConfig struct with a scope string and static broker address.
Definition: client.h:295
#define RDMNET_CLIENT_SET_STATIC_DEFAULT_SCOPE(configptr, broker_addr)
Initialize an RdmnetScopeConfig struct with the default RDMnet scope and a static broker address.
Definition: client.h:312
EtcPalIpAddr ip

// Get configured static broker address
etcpal::Sockaddr static_broker_addr(etcpal::IpAddr::FromString("192.168.2.1"), 8000);
etcpal::Expected<rdmnet::ScopeHandle> add_res = controller.AddScope("my_custom_scope", static_broker_addr);
// Or:
etcpal::Expected<rdmnet::ScopeHandle> add_res = controller.AddDefaultScope(static_broker_addr);
static IpAddr FromString(const char *ip_str) noexcept

Handling Callbacks

The library will dispatch callback notifications from the background thread which is started when rdmnet_init() is called. It is safe to call any RDMnet API function from any callback; in fact, this is the recommended way of handling many callbacks.

For example, a very common controller behavior will be to fetch a client list from the broker after a successful connection:

void controller_connected_callback(rdmnet_controller_t controller_handle, rdmnet_client_scope_t scope_handle,
const RdmnetClientConnectedInfo* info, void* context)
{
char addr_str[ETCPAL_IP_STRING_BYTES];
etcpal_ip_to_string(&info->broker_addr.ip, addr_str);
printf("Connected to broker '%s' at address %s:%d\n", info->broker_name, addr_str, info->broker_addr.port);
// Check handles and/or context as necessary...
rdmnet_controller_request_client_list(controller_handle, scope_handle);
}
#define ETCPAL_IP_STRING_BYTES
etcpal_error_t etcpal_ip_to_string(const EtcPalIpAddr *src, char *dest)
etcpal_error_t rdmnet_controller_request_client_list(rdmnet_controller_t controller_handle, rdmnet_client_scope_t scope_handle)
Request a client list from a broker.
Definition: controller.c:542
Definition: client.h:151
EtcPalSockAddr broker_addr
Definition: client.h:153
const char * broker_name
Definition: client.h:155

void MyControllerNotifyHandler::HandleConnectedToBroker(rdmnet::Controller::Handle controller_handle,
rdmnet::ScopeHandle scope_handle,
{
std::cout << "Connected to broker '" << info.broker_name()
<< "' at IP address " << info.broker_addr().ToString() << '\n';
// Check handles as necessary and get controller instance...
controller.RequestClientList(scope_handle);
}
std::string ToString() const
Information about a successful connection to a broker delivered to an RDMnet callback function.
Definition: client.h:158
std::string broker_name() const
Get the DNS name of the broker (if it was discovered via DNS-SD; otherwise this will be an empty stri...
Definition: client.h:193
constexpr etcpal::SockAddr broker_addr() const noexcept
Get the IP address and port of the remote broker to which we have connected.
Definition: client.h:187
etcpal::Error RequestClientList(ScopeHandle scope_handle)
Request a client list from a broker.
Definition: controller.h:882

Connection Failure

It's worth noting connection failure as a special case here. RDMnet connections can fail for many reasons, including user misconfiguration, network misconfiguration, components starting up or shutting down, programming errors, and more.

The ClientConnectFailedInfo and ClientDisconnectedInfo structs passed back with the "connect failed" and "disconnected" callbacks respectively have comprehensive information about the failure, including enum values containing the library's categorization of the failure, protocol reason codes and socket errors where applicable. This information is typically used mostly for logging and debugging. Each of these codes has a respective to_string() function to aid in logging.

For programmatic use, the structs also contain a will_retry member which indicates whether the library plans to retry this connection in the background. The only circumstances under which the library will not retry is when a connection failure is determined to be either a programming error or a user misconfiguration. Some examples of these circumstances are:

  • The broker explicitly rejected a connection with a reason code indicating a configuration error, such as CONNECT_SCOPE_MISMATCH or CONNECT_DUPLICATE_UID.
  • The library failed to create a network socket before the connection was initiated.

Discovering Devices

To discover devices in RDMnet, you need to request a Client List from the broker you're connected to. In our API, this is very easy - as we saw in the callbacks section earlier, we can just call rdmnet_controller_request_client_list() or rdmnet::Controller::RequestClientList(). This sends the appropriate request to the broker, and the reply will come back in the "Client List Update" callback:

void my_client_list_update_cb(rdmnet_controller_t controller_handle, rdmnet_client_scope_t scope_handle,
client_list_action_t list_action, const RdmnetRptClientList* list, void* context)
{
// Check handles and/or context as necessary...
switch (list_action)
{
// These are new entries to be added to the list of clients. Append the new entry to our
// bookkeeping.
add_new_clients(list->client_entries, list->num_client_entries);
break;
// These are entries to be removed from the list of clients. Remove the entry from our
// bookkeeping.
remove_clients(list->client_entries, list->num_client_entries);
break;
// These are entries to be updated in the list of clients. Update the corresponding entry
// in our bookkeeping.
update_clients(list->client_entries, list->num_client_entries);
break;
// This is the full client list currently connected to the broker - our existing client
// list should be replaced wholesale with this one. This will be the response to
// rdmnet_controller_request_client_list(); the other cases are sent unsolicited.
replace_client_list(list->client_entries, list->num_client_entries);
break;
}
if (list->more_coming)
{
// The library ran out of memory pool space while allocating client entries - after this
// callback returns, another will be delivered with the continuation of this response.
// If RDMNET_DYNAMIC_MEM == 1 (the default on non-embedded platforms), this flag will never be
// set to true and does not need to be checked.
}
}
client_list_action_t
Definition: client.h:50
@ kRdmnetClientListAppend
Definition: client.h:52
@ kRdmnetClientListRemove
Definition: client.h:54
@ kRdmnetClientListUpdate
Definition: client.h:56
@ kRdmnetClientListReplace
Definition: client.h:58
Definition: message.h:399
RdmnetRptClientEntry * client_entries
Definition: message.h:401
bool more_coming
Definition: message.h:410
size_t num_client_entries
Definition: message.h:403

void MyControllerNotifyHandler::HandleClientListUpdate(rdmnet::Controller::Handle controller_handle,
rdmnet::ScopeHandle scope_handle,
client_list_action_t list_action,
const rdmnet::RptClientList& list)
{
// Check handles as necessary...
switch (list_action)
{
// These are new entries to be added to the list of clients. Append the new entry to our
// bookkeeping.
AddNewClients(list.GetClientEntries());
break;
// These are entries to be removed from the list of clients. Remove the entry from our
// bookkeeping.
RemoveClients(list.GetClientEntries());
break;
// These are entries to be updated in the list of clients. Update the corresponding entry
// in our bookkeeping.
UpdateClients(list.GetClientEntries());
break;
// This is the full client list currently connected to the broker - our existing client
// list should be replaced wholesale with this one. This will be the response to
// Controller::RequestClientList(); the other cases are sent unsolicited.
ReplaceClientList(list.GetClientEntries());
break;
}
if (list.more_coming())
{
// The library ran out of memory pool space while allocating client entries - after this
// callback returns, another will be delivered with the continuation of this response.
// If RDMNET_DYNAMIC_MEM == 1 (the default on non-embedded platforms), this flag will never be
// set to true and does not need to be checked.
}
}
A list of RPT client entries.
Definition: rpt_client.h:89
constexpr bool more_coming() const noexcept
This message contains a partial list.
Definition: rpt_client.h:134
std::vector< RptClientEntry > GetClientEntries() const
Copy out the list of client entries.
Definition: rpt_client.h:119

Brokers also send Client List Update messages asynchronously when things change; these messages contain only the differences from the last client list sent to a given controller. This means you should only have to request a full client list when a new connection completes; after that, expect periodic callbacks notifying you of changes, with the client_list_action_t set appropriately as shown above.

Clients include both devices and other controllers; to differentiate the two, check the type field in each RPT client entry structure.

Sending RDM Commands

Sending RDM commands requires a destination address structure to indicate the RDMnet component and RDM responder to which the command is addressed. See Devices and Gateways for more information on the fields of the destination address structure.

The caller retains ownership of any data buffers supplied to the RDM command sending API functions. See Data Ownership Paradigms in the RDMnet Library for more information.

After sending a command, the library will provide a sequence number that will be echoed in the corresponding RDM response. This sequence number should be saved.

uint32_t cmd_seq_num;
etcpal_error_t result = rdmnet_controller_send_get_command(my_controller_handle, my_scope_handle, &dest,
E120_DEVICE_LABEL, NULL, 0, &cmd_seq_num);
if (result == kEtcPalErrOk)
{
// cmd_seq_num identifies this command transaction. Store it for when a response is received.
}
#define RDMNET_ADDR_TO_DEFAULT_RESPONDER(manu_id, dev_id)
Initialize an RdmnetDestinationAddr with a default responder address.
Definition: client.h:106
etcpal_error_t rdmnet_controller_send_get_command(rdmnet_controller_t controller_handle, rdmnet_client_scope_t scope_handle, const RdmnetDestinationAddr *destination, uint16_t param_id, const uint8_t *data, uint8_t data_len, uint32_t *seq_num)
Send an RDM GET command from a controller on a scope.
Definition: controller.c:666
A destination address for an RDM command in RDMnet's RPT protocol.
Definition: client.h:84

auto dest_addr = rdmnet::DestinationAddr::ToDefaultResponder(0x6574, 0x12345678);
etcpal::Expected<uint32_t> result = controller.SendGetCommand(my_scope_handle, dest_addr, E120_DEVICE_LABEL);
if (result)
{
// *result identifies this command transaction. Store it for when a response is received.
}
etcpal::Expected< uint32_t > SendGetCommand(ScopeHandle scope_handle, const DestinationAddr &destination, uint16_t param_id, const uint8_t *data=nullptr, uint8_t data_len=0)
Send an RDM GET command from a controller on a scope.
Definition: controller.h:833
static constexpr DestinationAddr ToDefaultResponder(const rdm::Uid &rdmnet_uid, uint16_t subdevice=0)
Get a DestinationAddr representing a message addressed to a component's default responder.
Definition: client.h:103

Handling RDM Responses

Responses to commands you send will be delivered asynchronously through the "RDM response" callback. You may also receive "unsolicited responses" - asynchronous state updates from devices that don't correspond to changes you requested.

Responses are always delivered atomically in RDMnet (contrast this with RDM, which uses the ACK_OVERFLOW mechanism to deliver responses with oversized data). This means that RDM response data in RDMnet can be larger than the 231-byte limit that is customary in RDM. The only time a response can be fragmented is if the RDMnet library was compiled with dynamic memory allocation disabled (see the note on the more_coming flag below for more information on this).

Note that response callbacks reference data buffers owned by the library, which will be invalid when the callback returns. See Data Ownership Paradigms in the RDMnet Library for more information.

bool rdm_response_callback(rdmnet_controller_t controller_handle, rdmnet_client_scope_t scope_handle,
const RdmnetRdmResponse* resp, void* context)
{
// Check handles and/or context as necessary...
if (resp->is_response_to_me)
{
// Verify resp->seq_num against the cmd_seq_num you stored earlier. This command transaction is
// now finished.
}
else
{
// This is either an unsolicited RDM response (an asynchronous update about a change you didn't initiate)
// or a response to a command by another controller. Use it to update your cached data about this
// RDMnet responder.
}
// If RDMNET_RESP_ORIGINAL_COMMAND_INCLUDED(resp) == true, resp->cmd will contain the original command you sent.
handle_response_data(resp->rdm_header.param_id, resp->rdm_data, resp->rdm_data_len);
// RdmnetRdmResponse structures do not own their data and the data will be invalid when this callback
// returns. To save the data for later processing:
rdmnet_save_rdm_response(resp, &saved_resp);
if (resp->more_coming)
{
// The library ran out of memory pool space while allocating responses - after this callback
// returns, another will be delivered with the continuation of this response data.
// If RDMNET_DYNAMIC_MEM == 1 (the default on non-embedded platforms), this flag will never be
// set to true.
// When more responses come, you can append their data to your saved response:
rdmnet_append_to_saved_rdm_response(resp, &previously_saved_resp);
}
// Ready to process the next response, so return true.
return true;
}
etcpal_error_t rdmnet_append_to_saved_rdm_response(const RdmnetRdmResponse *new_response, RdmnetSavedRdmResponse *previously_saved_response)
Append more data to a SavedRdmResponse's parameter data.
Definition: message.c:152
etcpal_error_t rdmnet_save_rdm_response(const RdmnetRdmResponse *response, RdmnetSavedRdmResponse *saved_response)
Save the data in a received RDM response for later use from a different context.
Definition: message.c:102
Definition: message.h:91
const uint8_t * rdm_data
Definition: message.h:117
size_t rdm_data_len
Definition: message.h:119
RdmResponseHeader rdm_header
Definition: message.h:115
bool more_coming
Definition: message.h:128
bool is_response_to_me
Whether the response was sent in response to a command previously sent by this controller.
Definition: message.h:102
An RDM response received over RDMnet and saved for later processing.
Definition: message.h:139

bool MyControllerNotifyHandler::HandleRdmResponse(rdmnet::Controller::Handle controller_handle,
rdmnet::ScopeHandle scope_handle,
const rdmnet::RdmResponse& resp)
{
// Check handles as necessary...
if (resp.is_response_to_me())
{
// Verify resp.seq_num() against the command seq_num you stored earlier. This command transaction is
// now finished.
}
else
{
// This is either an unsolicited RDM response (an asynchronous update about a change you didn't initiate)
// or a response to a command by another controller. Use it to update your cached data about this
// RDMnet responder.
}
// If resp.OriginalCommandIncluded() == true, the original_cmd_...() getters will contain the data
// of the original command that instigated this response.
HandleResponseData(resp.param_id(), resp.data(), resp.data_len());
// RdmResponse classes do not own their data and the data will be invalid when this callback returns.
// To save the data for later processing:
rdmnet::SavedRdmResponse saved_resp = resp.Save();
if (resp.more_coming())
{
// The library ran out of memory pool space while allocating responses - after this callback
// returns, another will be delivered with the continuation of this response.
// If RDMNET_DYNAMIC_MEM == 1 (the default on non-embedded platforms), this flag will never be set to true.
// When more responses come, you can append their data to your saved response:
previously_saved_resp.AppendData(resp);
}
// Ready to process the next response, so return true.
return true;
}
An RDM response received over RDMnet and delivered to an RDMnet callback function.
Definition: rdm_response.h:47
SavedRdmResponse Save() const
Save the data in this response for later use from a different context.
Definition: rdm_response.h:411
constexpr const uint8_t * data() const noexcept
Get a pointer to the RDM parameter data buffer contained within this response.
Definition: rdm_response.h:279
constexpr bool more_coming() const noexcept
This message contains partial RDM data.
Definition: rdm_response.h:296
constexpr size_t data_len() const noexcept
Get the length of the RDM parameter data contained within this response.
Definition: rdm_response.h:285
constexpr uint16_t param_id() const noexcept
Get the RDM parameter ID (PID) of this response.
Definition: rdm_response.h:267
An RDM response received over RDMnet and saved for later processing.
Definition: rdm_response.h:111
void AppendData(const RdmResponse &new_resp)
Append more data to this response's parameter data.
Definition: rdm_response.h:672

Delaying Processing of RDM Responses

In the case where your application doesn't have the resources to process an RDM response, you can return false to trigger another notification for the same response at a later time:

bool rdm_response_callback(rdmnet_controller_t controller_handle, rdmnet_client_scope_t scope_handle,
const RdmnetRdmResponse* resp, void* context)
{
// Check handles and/or context as necessary...
if(my_app.rdm_response_queue_has_room)
{
// Save response and add it to the queue...
// Ready for the next response, so return true.
return true;
}
// Can't queue this response yet, so return false. This function will be called again later with the same response.
return false;
}

bool MyControllerNotifyHandler::HandleRdmResponse(rdmnet::Controller::Handle controller_handle,
rdmnet::ScopeHandle scope_handle,
const rdmnet::RdmResponse& resp)
{
// Check handles as necessary...
if(my_app_.RdmResponseQueueHasRoom())
{
// Save response and add it to the queue...
// Ready for the next response, so return true.
return true;
}
// Can't queue this response yet, so return false. This function will be called again later with the same response.
return false;
}

The application can do this consecutively for as long as needed, but keep in mind that this will delay the library's processing of future messages from the broker (ultimately delaying potential notifications), and may also narrow the TCP window of the connection with the broker, potentially resulting in backpressure.

Handling RPT Status Messages

If something went wrong while either a broker or device was processing your message, you will get a response called an "RPT Status". There is a separate callback for handling these messages.

When you send an RDM command, you start a transaction that is identified by a 32-bit sequence number. That transaction is considered completed when you get either an RDM Response or an RPT Status containing that same sequence number.

void rpt_status_callback(rdmnet_controller_t controller_handle, rdmnet_client_scope_t scope_handle,
const RdmnetRptStatus* status, void* context)
{
// Check handles and/or context as necessary...
// Verify status->seq_num against the cmd_seq_num you stored earlier.
char uid_str[RDM_UID_STRING_BYTES];
rdm_uid_to_string(&status->source_uid, uid_str);
printf("Error sending RDM command to device %s: '%s'\n", uid_str,
// Other logic as needed; remove our internal storage of the RDM transaction, etc.
}
const char * rdmnet_rpt_status_code_to_string(rpt_status_code_t code)
Get a string representation of an RPT status code.
Definition: common.c:305
Definition: message.h:194
RdmUid source_uid
Definition: message.h:196
rpt_status_code_t status_code
Definition: message.h:202

void MyControllerNotifyHandler::HandleRptStatus(rdmnet::Controller::Handle controller_handle,
rdmnet::ScopeHandle scope_handle,
const rdmnet::RptStatus& status)
{
// Check handles as necessary...
// Verify status.seq_num() against the result of rdmnet::Controller::SendRdmCommand() you stored earlier.
std::cout << "Error sending RDM command to device " << status.source_uid().ToString() << ": '"
<< status.CodeToString() << "'\n";
// Other logic as needed; remove our internal storage of the RDM transaction, etc.
}
An RPT status message received over RDMnet and delivered to an RDMnet callback function.
Definition: rpt_status.h:42
constexpr rdm::Uid source_uid() const noexcept
Get the UID of the RDMnet component that sent this RPT status message.
Definition: rpt_status.h:110
std::string CodeToString() const
Convert the status message's code to a string representation.
Definition: rpt_status.h:152

Getting Responder IDs

Controllers may encounter RDMnet responders which have dynamic UIDs. Base RDMnet components such as controllers and devices can have dynamic UIDs, as can virtual responders present on devices. See Devices and Gateways for more information on virtual responders, and Roles and Addressing for more information on dynamic UIDs. As a reminder, a dynamic UID is identified by the top bit of the Manufacturer ID portion being set and the UID not being a broadcast UID; this can be tested using the convenience methods in the RDM library: RDMNET_UID_IS_DYNAMIC() and/or rdm::Uid::IsDynamic().

A responder using a dynamic UID may get a different UID at any time. For this reason, it is useful for a controller to be able to get a more permanent identifier for an RDMnet responder.

For controllers, devices and brokers, the permanent identifier is the CID, which is present alongside the UID in an RPT client list entry. If you want to track these components beyond a single RDMnet session, be sure to store the CID.

Virtual responders present on a device have a similar identifier called a Responder ID (RID) which has the same function as a CID for that virtual responder. The UIDs of a device's virtual responders are obtained using the ENDPOINT_RESPONDERS RDM command; however, this command's data does not include the RID. To obtain RIDs for a set of virtual responders, it is necessary to query the connected broker.

void handle_response_data(uint16_t param_id, const uint8_t* param_data, size_t param_data_len)
{
// This code is simplified and meant to illustrate an example of discovering virtual responders
// using the ENDPOINT_RESPONDERS command.
if (param_id == E137_7_ENDPOINT_RESPONDERS)
{
size_t num_uids = ((param_data_len - 6) / 6); // 6 bytes per UID, subtract the other data field sizes
RdmUid* responder_uids = (RdmUid*)calloc(num_uids, sizeof(RdmUid));
// Unpack the response data into the responder_uids array...
// Now request the RIDs from the broker for the responder UIDs.
etcpal_error_t result = rdmnet_controller_request_responder_ids(my_controller_handle, my_scope_handle,
responder_uids, num_uids);
if (result == kEtcPalErrOk)
{
// The broker's response will be forwarded to the RdmnetControllerResponderIdsReceivedCallback,
// defined in the next snippet.
}
free(responder_uids);
}
}
T calloc(T... args)
T free(T... args)
etcpal_error_t rdmnet_controller_request_responder_ids(rdmnet_controller_t controller_handle, rdmnet_client_scope_t scope_handle, const RdmUid *uids, size_t num_uids)
Request a set of responder IDs corresponding with dynamic responder UIDs from a broker.
Definition: controller.c:574

void MyControllerNotifyHandler::HandleResponseData(uint16_t param_id, const uint8_t* param_data, size_t param_data_len)
{
// This code is simplified and meant to illustrate an example of discovering virtual responders
// using the ENDPOINT_RESPONDERS command.
if (param_id == E137_7_ENDPOINT_RESPONDERS)
{
std::vector<RdmUid> responder_uids;
// Unpack the response data into the responder_uids array...
// Now request the RIDs from the broker for the responder UIDs.
etcpal::Error result = controller.RequestResponderIds(responder_uids);
if (result)
{
// The broker's response will be forwarded to the MyControllerNotifyHandler::HandleResponderIdsReceived()
// callback, defined in the next snippet.
}
}
}
etcpal::Error RequestResponderIds(ScopeHandle scope_handle, const rdm::Uid *uids, size_t num_uids)
Request mappings from dynamic UIDs to Responder IDs (RIDs).
Definition: controller.h:897

The broker will respond with a list of mappings between dynamic UIDs and RIDs known as a Dynamic UID Assignment List. Each entry in the list will contain a status code indicating whether the RID was looked up successfully, followed by the UID and RID.

void handle_responder_ids_received(rdmnet_controller_t controller_handle, rdmnet_client_scope_t scope_handle,
const RdmnetDynamicUidAssignmentList* list, void* context)
{
// Check handles and/or context as necessary...
for (const RdmnetDynamicUidMapping* mapping = list->mappings; mapping < list->mappings + list->num_mappings;
++mapping)
{
if (mapping->status_code == kRdmnetDynamicUidStatusOk)
{
// This function is assumed to add the RID to the responder's cached data.
add_responder_rid(&mapping->uid, &mapping->rid);
}
else
{
char uid_str[RDM_UID_STRING_BYTES];
rdm_uid_to_string(&mapping->uid, uid_str);
printf("Error obtaining RID for responder %s: '%s'\n", uid_str,
rdmnet_dynamic_uid_status_to_string(mapping->status_code));
}
}
}
const char * rdmnet_dynamic_uid_status_to_string(rdmnet_dynamic_uid_status_t code)
Get a string description of an RDMnet Dynamic UID status code.
Definition: common.c:466
@ kRdmnetDynamicUidStatusOk
Definition: common.h:136
Definition: message.h:246
RdmnetDynamicUidMapping * mappings
Definition: message.h:248
size_t num_mappings
Definition: message.h:250
Definition: message.h:235

void MyControllerNotifyHandler::HandleResponderIdsReceived(rdmnet::Controller::Handle controller_handle,
rdmnet::ScopeHandle scope_handle,
{
// Check handles as necessary...
for (const rdmnet::DynamicUidMapping& mapping : list.GetMappings())
{
if (mapping.IsOk())
{
// This function is assumed to add the RID to the responder's cached data.
AddResponderRid(mapping.uid, mapping.rid);
}
else
{
std::cout << "Error obtaining RID for responder " << mapping.uid.ToString() << ": '"
<< mapping.CodeToString() << "'\n";
}
}
}
A list of mappings from dynamic UIDs to responder IDs received from an RDMnet broker.
Definition: dynamic_uid.h:99
std::vector< DynamicUidMapping > GetMappings() const
Copy out the list of dynamic UID mappings.
Definition: dynamic_uid.h:130
A mapping from a dynamic UID to a responder ID (RID).
Definition: dynamic_uid.h:42

Handling RDM Commands

In addition to getting information about responders, RDMnet controllers are required to respond to a basic set of RDM commands, which allows them to be identified by other controllers on the network. By default, this behavior is implemented completely within the library.

The default implementation provides read-only access to the standard set of data that is required to be readable from a controller. This includes the current scope(s) (COMPONENT_SCOPE), search domain (SEARCH_DOMAIN), and RDMnet communication diagnostic data (TCP_COMMS_STATUS), as well as some basic RDM data like the manufacturer (MANUFACTURER_LABEL), a description of the product (DEVICE_MODEL_DESCRIPTION), the software version in string form (SOFTWARE_VERSION_LABEL) and a user-settable label for the controller (DEVICE_LABEL). The library has access to all the information necessary for the first three items, since that information is necessary for RDMnet communication. Initial values for the last four items are provided to the library when creating a new controller instance, through the rdmnet_controller_set_rdm_data() function or the rdmnet::Controller::RdmData structure.

If you want to provide richer RDM responder functionality from your controller implementation, you can provide a set of callbacks to handle RDM commands. In this case, the library only handle TCP_COMMS_STATUS, and you must handle the rest of the above PIDs, as well as SUPPORTED_PARAMETERS and any additional ones you choose to support.

To use the library this way, you can:

RdmnetControllerConfig config = RDMNET_CONTROLLER_CONFIG_DEFAULT_INIT(MY_ESTA_MANUFACTURER_ID_VAL);
// Generate CID and call rdmnet_controller_set_callbacks() as above...
rdmnet_controller_set_rdm_cmd_callbacks(&config, my_rdm_command_received_cb, my_llrp_rdm_command_received_cb);
rdmnet_controller_t my_controller_handle;
etcpal_error_t result = rdmnet_controller_create(&config, &my_controller_handle);
void rdmnet_controller_set_rdm_cmd_callbacks(RdmnetControllerConfig *config, RdmnetControllerRdmCommandReceivedCallback rdm_command_received, RdmnetControllerLlrpRdmCommandReceivedCallback llrp_rdm_command_received, uint8_t *response_buf, void *context)
Set callbacks to handle RDM commands in an RDMnet controller configuration structure.
Definition: controller.c:225

class MyControllerRdmCommandHandler : public rdmnet::Controller::RdmCommandHandler
{
// Implement the RdmCommandHandler functions...
};
MyControllerRdmCommandHandler my_rdm_cmd_handler;
rdmnet::Controller::Settings my_settings(my_cid, MY_ESTA_MANUFACTURER_ID_VAL);
etcpal::Error result = controller.Startup(my_controller_notify_handler, my_settings, my_rdm_cmd_handler);
A base class for a class that receives RDM commands addressed to a controller.
Definition: controller.h:153

See Handling RDM Commands for information about how to handle commands through the callbacks.