uHAL quick tutorial

uHAL is the Hardware Access Library (HAL) that provides an end-user C++/Python API for IPbus reads, writes and RMW transactions.

API reference

The doxygen pages containing an exhaustive list of all uHAL classes and functions can be found at https://ipbus.web.cern.ch/ipbus/sw/release/2.7/api/html/ N.B. If you’re a new user it would be best to read the relevant sections of this tutorial at least once before looking at the doxygen pages.

Pre-requisites

Before building/running the source code examples from the following sections, you should:

  • Install the IPbus suite

  • Set the environment:

    export LD_LIBRARY_PATH=/opt/cactus/lib:$LD_LIBRARY_PATH
    export PATH=/opt/cactus/bin:$PATH
    
  • Create a connection file that describes what protocol (UDP, PCIe, …) and IP address or device file should be used to communicate with each IPbus endpoint (i.e. each hardware device - typically an FPGA - containing a control bus master).

  • Create an address table describing the address layout of your IPbus endpoints.

  • (optional) If you want to work with dummy hardware, then just check the Working with dummy hardware section.

  • (optional) If multiple clients/threads/processes have to simulatenously access an IPbus UDP endpoint, then you will need to use the Control Hub

Connecting to the hardware IP endpoint with a connection file

Once you have a connection file describing the location of the hardware endpoint and protocol, and an address table describing its memory layout, then you can create the HwInterface by:

#include "uhal/uhal.hpp"

using namespace uhal;

ConnectionManager manager ( "file://path/to/connection/file/connections.xml" );
HwInterface hw=manager.getDevice ( "hcal.crate1.slot1" );

ValWord< uint32_t > mem = hw.getNode ( "REG" ).read();
hw.dispatch();

std::cout << "REG = " << reg.value() << std::endl;

You can see a working example in read_write_single_register.cxx

Tip: You can inspect the connection file programmatically by means of the vector<string> ConnectionManager::getDevices() and vector<string> ConnectionManager::getDevice(const string& regex) methods.

Connecting to the hardware IP endpoint without a connection file

If you do not want to use a connection file (e.g. debugging purposes), then you can instantiate a HwInterface using the ConnectionManager::getDevice factory method:

HwInterface hw=ConnectionManager::getDevice ( id, uri, address_file );

where the three parameters are strings:

  • id: String to identify the device.

  • uri: String with the protocol and location of the hardware endpoint in URI format (e.g. ipbusudp-2.0://localhost:50001).

  • address_table: String with the location of the file containing the address file.

For example:

HwInterface hw=ConnectionManager::getDevice( "on.the.fly","chtcp-2.0://localhost:10203?target=127.0.0.1:50001","file:///path/to/address_file.xml" );

You can see a working example in read_write_single_register_without_connection_file.cxx

Reading and writing a register

Let’s suppose that in your address table you have a register named REG in address 0x0001:

<node id="REG" address="0x0001" permission="rw"/>

Each entry in the address table is represented by an instance of the uhal::Node class. You can read and write to the 32-bit register as follows:

ConnectionManager manager("file://path/to/connections.xml");
HwInterface hw = manager.getDevice("dummy.udp.0");

//write 1 in the address 0x0001
hw.getNode ("REG").write(1);

//read back
ValWord< uint32_t > reg = hw.getNode ("REG").read();

//send the IPbus transactions
hw.dispatch();

std::cout << "REG = " << reg.value() << std::endl;

You can see a working example in read_write_single_register.cxx

Reading and writing memory blocks

Let’s suppose that in your address table you have a 1 MByte memory block (i.e. mode="block" and size=262144 words) named MEM in address 0x0001:

<node id="MEM" address="0x1000" mode="incremental" size="262144" permission="rw"/>

The usage is pretty similar that the single register. You can write and read back 256 words (i.e. 1 kByte):

ConnectionManager manager("file://path/to/connections.xml");
HwInterface hw = manager.getDevice("dummy.udp.0");

//fill a vector with random information
const size_t N=256;
std::vector<uint32_t> xx;
for(size_t i=0; i!= N; ++i)
  xx.push_back(static_cast<uint32_t>(rand()));


//write
hw.getNode ("MEM").writeBlock(xx);

//read back the information
ValVector< uint32_t > mem = hw.getNode ( "MEM" ).readBlock (N);

hw.dispatch();

//If there is a single client we should read back the same informaiton
for(size_t i=0; i!= N; ++i)
  assert(xx[i] == mem[i])

You can see a working example in read_write_block_or_fifo.cxx

Reading and writing to FIFOs

The C++ API for reading a FIFO (aka non-incremental memory or port) and for reading a memory block is identical. The only difference is in the corresponding address table entry:

<node id="MEM" address="0x1000" mode="non-incremental" size="262144" permission="rw"/>

You can see a working example in read_write_block_or_fifo.cxx

How to get the node attributes?

You can retrieve programmatically all the attributes from a given node. The following methods are available in the Node API:

  • uint32_t getAddress(): Address of the node.

  • uint32_t getMask(): Mask of the node.

  • const string& getId(): Id of the node.

  • defs::BlockReadWriteMode getMode(): One out of defs::SINGLE (default), defs::INCREMENTAL, and defs::NON_INCREMENTAL, or defs::HIERARCHICAL for top-level nodes nesting other nodes.

  • defs::NodePermission getPermission(): One out of defs::READ, defs::WRITE, and defs::READWRITE (default).

  • uint32_t getSize(): Size of the node. All the single register access and FIFOs have a default size of 1 (i.e. 32 bits).

  • const string& getTags(): User definable attribute with in principle a comma separated list of values.

  • const boost::unordered_map<std::string, std::string>& getParameters (): Set of “parameter=value” pairs, specified with a semi-colon delimeted “param=value” list in the xml address table “parameters” attribute.

You can see a working example in print_node_attributes.cxx

How to traverse the address table tree?

The traversal of the address table tree is pretty straight forward using the methods Node::getNode(). For example, if we apply the following code to this address table (which references uhal-example/example-address-componentA.xml and uhal-example/example-address-componentB.xml) :

ConnectionManager manager ( connection );
HwInterface hw=manager.getDevice ( id );

std::vector<std::string> ids = hw.getNodes();
std::cout << "getNodes(): ";
std::copy(ids.begin(),
          ids.end(),
          std::ostream_iterator<std::string>(std::cout,", "));

std::cout << std::endl << std::endl;

ids = hw.getNodes(".*mem.*");
std::cout << "getNodes(\".*mem.*\").getNodes(): ";
std::copy(ids.begin(),
          ids.end(),
          std::ostream_iterator<std::string>(std::cout,", "));

std::cout << std::endl << std::endl;

ids = hw.getNode("componentA").getNodes();
std::cout << "getNode("componentA").getNodes(): ";
std::copy(ids.begin(),
          ids.end(),
          std::ostream_iterator<std::string>(std::cout,", "));

std::cout << std::endl;

We will get the following result:

getNodes(): componentB.reg1, componentB.reg2, componentB.mem1, componentB, componentA.mem1, mem2, mem1, componentA.fifo, componentA.reg1, componentB.mem2, fifo, reg3.fieldB, componentA.mem2, componentB.fifo, reg3.reset, reg3.fieldC, reg3, componentA.reg2, componentA, reg2, reg1, reg3.fieldA,

getNodes(".*mem.*"): componentA.mem1, componentA.mem2, componentB.mem1, componentB.mem2, mem1, mem2,

getNode("componentA").getNodes(): mem2, mem1, fifo, reg2, reg1,

You can see a working example in print_node_attributes.cxx

How to insert multiple HwInterface objects in the same STL container?

Imagine that you want to create once all the HwInterface objects and then use them. You could store and use them in a std::vector as follows:

ConnectionManager manager ( connection );

//get all the hcal endpoints
std::vector<HwInterface> hws;
std::vector<std::string> ids=manager.getDevices ( "hcal\..*" );
for(std::vector<std::string>::const_iterator i(ids.begin()); i!=ids.end(); ++i)
   hws.push_back(manager.getDevice(*i));

//Request the "FIRMWARE_REVISION" register for all of them
for(std::vector<HwInterface>::iterator hw(hws.begin()); hw != hws.end(); ++hw) {
  ValWord<uint32_t> ver = hw->getNode("FIRMWARE_REVISION").read();
  hw->dispatch();
  std::cout << hw->id() << " FIRMWARE REVISION: " << std::hex << ver << std:endl;
}

How do I create an HwInterface object on the heap?

It is not recommended that you attempt to manually construct an HwInterface object - you should always use a ConnectionManager class to construct the HwInterface objects for you. So… what do you do if you want to create an HwInterface object on the heap? If you want to use “new” to create an HwInterface object, you have to do it via the HwInterface copy constructor like so:

uhal::ConnectionManager connectionManager("file://path/to/connections.xml");
uhal::HwInterface * hw = new uhal::HwInterface(connectionManager.getDevice("MyBoard"));

How to disable the logging?

You can disable the logging by executing uhal::disableLogging() in your program. Or, alternatively, raise the log threshold to something quite high like this: uhal::setLogLevelTo(uhal::Error());

Creating a connection file

Before we can use uHAL to talk to a board, we need to create a configuration file telling uHAL which board to talk to.

A configuration file is an XML file like:

<?xml version="1.0" encoding="UTF-8"?>

<connections>
  <connection id="dummy.udp.0"        uri="ipbusudp-2.0://localhost:50001"                     address_table="file://dummy_address.xml" />
  <connection id="dummy.tcp.0"        uri="ipbustcp-2.0://localhost:50002"                     address_table="file://dummy_address.xml" />
  <connection id="dummy.controlhub.0" uri="chtcp-2.0://localhost:10203?target=127.0.0.1:50001" address_table="file://dummy_address.xml" />
</connections>

Each of the connection tags contain three attributes that describe a device and the protocol used to access to it:

  • id: String identifier. The standard nomenclature for id’s in the CMS trigger upgrade project will be subsystem.crate.slot.

  • uri: Protocol and location to access a target device in URI format. There are 8 protocols currently available:

    • For IPbus 1.3 hardware : chtcp-1.3, ipbusudp-1.3, and ipbustcp-1.3

    • For IPbus 2.0 hardware : chtcp-2.0, ipbusudp-2.0, ipbustcp-2.0, ipbuspcie-2.0 and ipbusmmap-2.0

  • address_file: Location of the address table file which describes the register space of the target device. The URI can be absolute or relative to the connection file in the local file system (e.g. file://my_adress_file.xml)

You can check a working example in this connections file

Creating an address table

Note! In these examples it is worth remembering that IPbus is an A32/D32 bus, that is, it supports addresses up to 32 bits wide and data spaces up to 32 bits wide. Any particular device/firmware may, however, chose to use only a restricted subset of the total address space.

You can check a working example in this address table (which references uhal-example/example-address-componentA.xml and uhal-example/example-address-componentB.xml)

Single register address table

The simplest address table that we may consider is an XML file like:

<?xml version="1.0" encoding="ISO-8859-1"?>

<node>
  <node id="A" address="0x00000000"/>
</node>

This address table can be read as follows: The 32-bit memory space at address 0x00000000 can be accessed with the id “A”.

This means that you will be able to read this memory location with the following C++ code:

ConnectionManager manager("file://path/to/connections.xml");
HwInterface hw = manager.getDevice("x.y.z");

ValWord< uint32_t > reg = hw.getNode ("A").read();

hw.dispatch();

std::cout << "A = " << reg.value() << std::endl;

Single register in a hierarchical address table

uHAL provides a hierarchical address table structure that is designed to have a one-to-one correspondence with the hierarchical structure of firmware modules. A simple example of an address table with a hierarchical structure is:

<node>
  <node id="B">
    <node id="A" address="0x00000100"/>
  </node>
</node>

This address table can be read as follows: The firmware module “B” contains a 32-bit memory location at address 0x00000100 with ID “A”.

This means that you will be able to read this memory location with the following C++ code:

ConnectionManager manager("file://path/to/connections.xml");
HwInterface hw = manager.getDevice("x.y.z");

//This is equivalent to getNode("B.A")
ValWord< uint32_t > reg = hw.getNode("B").getNode("A").read();

hw.dispatch();

std::cout << "B.A = " << reg.value() << std::endl;

Multiple modules with absolute and relative addresses

If you have two firmware modules in different addresses you could have an address table like:

<node>
  <node id="C1">
    <node id="A1" address="0x00000200" />
    <node id="A2" address="0x00000201" />
    <node id="A3" address="0x00000202" />
    <node id="A4" address="0x00000203" />
  </node>

  <node id="C2" address="0x00000300">
    <node id="A1" address="0x000" />
    <node id="A2" address="0x001" />
    <node id="A3" address="0x002" />
    <node id="A4" address="0x003" />
  </node>
</node>

In this example, the modules “C1” and “C2” refer to different firmware modules. For the memory locations in “C1” we have explicitly specified the absolute address, whereas in “C2” we have specified the relative address of the memory locations with respect to the parent module. For example, the address of the “C2.A2” node is calculated as follows:

AbsoluteAddress( C2.A2 ) = Address( C2 ) + RelativeAddress( C2.A2 )
                         =   0x00000300  +         0x001
                         =   0x00000301

Multiple modules with identical structure

Let’s suppose that we have two instances of the same firmware module accessible from a single IPbus device (e.g. “D1” and “D2”). The internal address table structure of the modules will be identical, and therefore it would be a waste of time (and error prone) to type the same structure several times. Instead we can avoid duplication by creating two address tables:

  1. A higher-level address table, containing two nodes (with different base addresses) that correspond to the two instances of the firwmare module:

    <node>
      <node id="D1" module="file://mymodule.xml" address="0x00000400" />
      <node id="D2" module="file://mymodule.xml" address="0x00000500" />
    </node>
    
  2. The mymodule.xml file, that specifies the layout of registers, block RAMs, FIFOs etc within the duplicated module, and specifies relative addresses with respect to the parent node:

    <node>
      <node id="A1" address="0x00000001" />
      <node id="A2" address="0x00000002" />
      <node id="A3" address="0x00000003" />
      <node id="A4" address="0x00000004" />
    </node>
    

In order to access memory location “A2” within module “D1”, you would then write in C++:

ValWord< uint32_t > reg = hw.getNode("D1.A2").read();

Setting read and write access to memory locations

The access permissions (e.g. read-only, read-write, etc) of a node can be set using the permission attribute:

<node>
  <node id="EBA" address="0x600" permission="r"/>
  <node id="EBB" address="0x601" permission="read"/>
  <node id="EBC" address="0x602" permission="rw"/>
  <node id="EBD" address="0x603" permission="wr"/>
  <node id="EBE" address="0x604" permission="readwrite"/>
  <node id="EBF" address="0x605" permission="writeread"/>
  <node id="EBG" address="0x606" permission="w" />
  <node id="EBH" address="0x607" permission="write" />
</node>

The meanings of these attributes are hopefully self-evident: r and read indicate a read-only IPbus endpoint whilst w and write indicate a write-only endpoint. The other four options indicate that the IPbus endpoint is both readable and writeable. IPbus endpoints are assumed, by default, to be both readable and writeable.

Permissions are currently not inherited from parent to child and so setting the permissions on a branch, rather than to a final memory location, has no effect.

Memory blocks and FIFOs

Access to memory blocks and FIFOs can be configured using the mode attribute:

<node>
  <node id="F" address="0x00000700">
    <node id="A1" address="0x000" />
    <node id="A2" address="0x001" mode="single"/>
    <node id="A3" address="0x010" mode="block" size="16" />
    <node id="A4" address="0x020" mode="incremental" size=16"" />
    <node id="A5" address="0x030" mode="inc" size="16" />
    <node id="A6" address="0x040" mode="port" />
    <node id="A7" address="0x041" mode="non-incremental" />
    <node id="A8" address="0x042" mode="non-inc" />
  </node>
</node>

single indicates that the node refers to a single word (i.e. 32 bits) register (e.g. “F.A1” and “F.A2”). single is the default mode if no mode is explicitly declared. This can be used, for example, for a bunch crossing zero (BC0) counter.

block, incremental and inc indicate that the given address is the base address of a block of registers with a continuous address space. In this case, the size attribute is mandatory (e.g. “F.A3”, “F.A4”, and “F.A5”). This can be used, for example, for a capture RAM.

port, non-incremental and non-inc indicate that the given address is an access port which may receive or provide a stream of data but whose address is fixed (e.g. “F.A6”, “F.A7”, and “F.A8”). This can be used, for example, for a JTAG access port.

In order to read from and write to a memory block or FIFO, you could then use the following C++ code:

//read
ValVector< uint32_t > mem = hw.getNode("F.A3").readBlock(16);
ValVector< uint32_t > fifo = hw.getNode("F.A6").readBlock(16);

//write
std::vector<uint32_t> x;
//fill x...

hw.getNode("F.A4").writeBlock(x);
hw.getNode("F.A7").writeBlock(x);

Single register read and write access with bit-masks

Even though IPbus is word-oriented, it is nevertheless convenient to be able to label individual bits. This is done as shown below:

<node>
  <node id="G" address="0x00000800">
    <node id="G0" mask="0x01" />
    <node id="G1" mask="0x02" />
    <node id="G2" mask="0x04" />
    <node id="G3" mask="0x08" />
    <node id="G4" mask="0xF0" />
  </node>
</node>

Performing an IPbus read or write on, for instance, “G.G3”, will perform all of the necessary bit-shifting under-the-hood. For example, reading a value from the node with id “G.G4” will actually perform the appropriate operations and return a value from 0x0 to 0xF, rather than from 0x00 to 0xF0.

It should be noted that performing many operations on bit-masked nodes may add a not insignificant overhead when compared to operating on full 32-bit registers. This is due to the nature of IPbus instructions, rather than a software overhead. There is no way for the software to know if it can merge multiple bit-masked operations into a single instruction and it is up to the user to consider the structure of their code with respect to this.

Documenting your address table

A convenient way of documenting the address table is to use description attribute, for example:

<node>
  <node id="H" address="0x00000900" description="LED control register">
    <node id="H0" mask="0x01" description="Turn LED1 on/off" />
    <node id="H1" mask="0x02" description="Turn LED2 on/off" />
  </node>
</node>

Much like the tags attribute discussed below, the content of description attributes is completely user-definable. An advantage of using the description attribute for address table documentation is that - unlike using XML comments - the content of the attribute can be accessed by your application, through the Node::getDescription method.

What are the “tags” and “parameters” attributes?

The tags and parameters attributes allow the user to specify a custom list and/or map of properties that are associated with a particular node; the values of these attributes can then be accessed from C++/Python through methods of the Node class. These attributes do not have to be set for any nodes within the address table, and the values of these attributes do not change the behaviour of any of the uHAL classes/functions in any way.

The value of the tags attribute can be accessed from C++/Python using the Node::getTags() method.

The parameters attribute is parsed as a semicolon-delimited map of ‘parameter=value’ pairs, producing a std::map<std::string,std::string> that can be accessed from C++/Python using the Node::getParameters() method. For example, parameters="tx0=0x29;tx1=0x2b" would result in a map containing 2 entries: key tx0 with value 0x29, and key tx1 with value 0x2b.

Building your code

Here is an example Makefile for building your project. Once you’ve downloaded the file, rename it to “Makefile” and then follow the documentation found in the section at the top of the file. Note: The example makefile assumes that the software is installed under /opt/cactus (e.g. using the YUM installation instructions <software-install-yum>).


Using IPbus in Python

Since release 1.0.6, the IPbus software suite also includes Python-bindings for uHAL. After following the IPbus installation instructions, just import the uhal module in Python and you can get going.

The Python API (i.e. function and class names) is basically the same as in C++, but with all std::vector instances replaced with Python lists. Several example use-cases are given below. They all assume that you have already done:

import uhal
# Or if using uHAL v1.0.6 (now an old version)
import pycohal as uhal

Getting a HwInterface object

HwInterface objects form the basic interface to your hardware …

  • Creating from an xml connection file:

    manager = uhal.ConnectionManager("file://path/to/connection/file/connections.xml")
    hw = manager.getDevice("hcal.crate1.slot1")
    
  • Without a connection file:

    hw = uhal.getDevice( "id_for_device" , uri, address_table )
    

    where:

    • uri = String with the protocol and location of the hardware endpoint in URI format (e.g. ipbusudp-2.0://localhost:50001). Syntax explained here

    • address_table = String with location of xml address table for this hardware device

  • The attributes of a HwInterface object can then be accessed later on if needed …

    # Grab device's id, or print it to screen
    device_id = hw.id()
    print hw
    # Grab the device's URI
    device_uri = hw.uri()
    

Reading and writing to a single register

For a single register named REG in your address table:

# Queue a write to register
hw.getNode("REG").write(1)

# Read the value back.
# NB: the reg variable below is a uHAL "ValWord", not just a simple integer
reg = hw.getNode("REG").read()

# Send IPbus transactions
hw.dispatch()

# Print the register value to screen as a decimal:
print " REG =", reg

# Print the register value to screen in hex:
print " REG =", hex(reg)

# Get the underlying integer value from the reg "ValWord"
value = int(reg) # Equivalent to reg.value()

A similar working example can be found in read_write_single_register.py and read_write_single_register_without_connection_file.py

Reading/writing to a memory block

For a 1MByte memory block named MEM:

N=256
# Fill list with random info
xx = []
for i in range(N):
   xx.append( rand_uint32() )

# Write
hw.getNode("MEM").writeBlock(xx)
# Read back values
mem = hw.getNode("MEM").readBlock(N)

hw.dispatch()

# Use these values - either by array indexing, in a for loop, or print to screen
print " All values:", mem
firstValue = mem[0]
for x in mem:
   # Do something with each value

A similar working example can be found in read_write_block_or_fifo.py

Traversing the address table

The hierarchy of nodes within your hardware’s address table can be traversed easily using the hw.getNodes(regex_string) and node.getNodes(regex_string) methods. For example:

# Ids of all nodes
list_of_ids = hw.getNodes()
# Use regular expression to select sub-set of nodes
mem_ids = hw.getNodes(".*MEM.*")
# Get list of sub-nodes of "SUBSYSTEM1"
print "The nodes within SUBSYSTEM1 are:"
for id in hw.getNode("SUBSYSTEM1").getNodes():
   print id

A working example of traversing the node tree can be found in print_node_attributes.py

All of a node’s attributes can be accessed through ‘getter’ methods …

print "Node attributes ..."
print node    # Prints id. Equivalent to print node.getId()
print "Address:", node.getAddress()
print "Mask:", node.getMask()   # Bit-mask of node
print "Mode:", node.getMode()   # Mode enum - one of uhal.BlockReadWriteMode.SINGLE (default), INCREMENTAL and NON_INCREMENTAL, or HIERARCHICAL for top-level nodes nesting other nodes.
print "Read/write permissions:", node.getPermission()  # One of uhal.NodePermission.READ, WRITE and READWRITE
print "Size (in units of 32-bits):", node.getSize()     # In units of 32-bits. All single registers and FIFOs have default size of 1
print "Tags:", node.getTags()  # User-definable string from address table - in principle a comma separated list of values
print "Parameters:", node.getParameters() # Map of user-definable, semicolon-delimited parameter=value pairs specified in the "parameters" xml address file attribute.

Some example code that accesses these variables can be found in print_node_attributes.py

Logging

By default all log levels are printed to screen. There are 6 log levels of increasing severity: DEBUG, INFO, NOTICE, WARNING, ERROR and FATAL. The log threshold can be controlled at runtime one of several different ways:

# Only show log messages with levels Warning and above
uhal.setLogLevelTo( uhal.LogLevel.WARNING )
# Don't show any log messages
uhal.disableLogging()
# Use environment variable to set displayed log level
uhal.setLogLevelFromEnvironment( env_variable_name )

Working with dummy hardware

UDP dummy hardware

Start the UDP server emulating the IPbus protocol on localhost:50001:

/opt/cactus/bin/uhal/tests/DummyHardwareUdp.exe -p 50001 -v 2

In this case the connection file will have to contain a URI equal to ipbusudp-2.0://localhost:50001".

ControlHub accessing the UDP dummy hardware

First, start the UDP server emulating the IPbus protocol on localhost:50001:

/opt/cactus/bin/uhal/tests/DummyHardwareUdp.exe -p 50001 -v 2

Second, start the Control Hub to serialize the access to the UDP server on port 10203:

controlhub_start

In this case the connection file will have to contain a URI equal to chtcp-2.0://localhost:10203?target=127.0.0.1:50001.

TCP dummy hardware

Start a TCP server emulating the IPbus protocol on localhost:50002:

/opt/cactus/bin/uhal/tests/DummyHardwareTcp.exe -p 50002 -v 2

In this case the connection file will have to contain a URI equal to ipbustcp-1.3://localhost:50002.


Working with the ControlHub

The Control Hub is required when multiple clients/threads/processes are simultaneously communicating with the same IPbus UDP endpoint. Using the Control Hub does not require any modification of the source code.

Connection file

If initially you were using a connection like:

<connection id="hcal.crat2.slot1" uri="ipbusudp-2.0://1.2.3.4:50001" address_table="file://dummy_address.xml" />

Then in order to serialize the access to the same endpoint using the Control Hub, you will need a connection like:

<connection id="hcal.crat2.slot1" uri="chtcp-2.0://localhost:10203?target=1.2.3.4:50001" address_table="file://dummy_address.xml" />

Usage

The Control Hub is entirely configuration free and can be considered akin to a network switch that you turn on and forget about. Once switched on, the Control Hub listens for TCP connections on port 10203, so you should ensure that the firewall is configured to allow connections on this port if you wish to connect to the Control Hub from an external machine (i.e. not just over localhost).

Further information on the ControlHub can be found here