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:
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 ofdefs::SINGLE
(default),defs::INCREMENTAL
, anddefs::NON_INCREMENTAL
, ordefs::HIERARCHICAL
for top-level nodes nesting other nodes.defs::NodePermission getPermission()
: One out ofdefs::READ
,defs::WRITE
, anddefs::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 besubsystem.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
, andipbustcp-1.3
For IPbus 2.0 hardware :
chtcp-2.0
,ipbusudp-2.0
,ipbustcp-2.0
,ipbuspcie-2.0
andipbusmmap-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:
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>
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 hereaddress_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
Print node attributes¶
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