A repository of drivers, protocols and libraries for communicating between software and hardware devices.
A library for simple socket communication. It currently implements sockets for UPD, TCP, and ZMQ communication.
The ISocket
class is an interface for simple socket communication, defining functions for opening a socket,
sending and receiving bytes, and closing the socket connection.
The ISocket
class defines an open()
method to perform configuration steps to open the socket for communication.
If opening the socket fails, an exception is thrown. The close()
method is also provided to perform steps to disconnect
and close the socket communication.
The functions receive_bytes(std::string&)
and send_bytes(const std::string&)
perform the read and write logic of the socket
respectively.
To use this class, create a subclass that inherits from it and implement its pure virtual functions. The pure virtual
functions are on_open()
, on_receive_bytes(std::string&)
, and on_send_bytes(const std::string&)
.
Configuration parameters should be passed with a configuration struct, resulting in a single argument constructor.
The on_close()
function can optionally be overridden to perform steps to disconnect and close the socket communication.
If a derived class defines any cleanup behavior in on_close()
, it should also be invoked statically and explicitly
in the destructor of the derived class.
An example is given below.
// DerivedSocket.hpp
struct DerivedSocketConfig {
int param1;
double param2;
};
class DerivedSocket : ISocket {
public:
DerivedSocket(DerivedSocketConfig configuration);
~DerivedSocket() override;
private:
void on_open() override;
bool on_receive_bytes(std::string& buffer) override;
bool on_send_bytes(const std::string& buffer) override;
void on_close() override;
}
// DerivedSocket.cpp
DerivedSocket::DerivedSocket(DerivedSocketConfig configuraiton) {
// save configuration parameters for later use
}
DerivedSocket::~DerivedSocket() {
DerivedSocket::on_close();
}
void DerivedSocket::on_open() {
// Configure and open the socket
}
bool DerivedSocket::on_receive_bytes(std::string& buffer) {
// Read the contents of the socket into the buffer and return true on success. Otherwise, return false.
return true;
}
bool DerivedSocket::on_send_bytes(const std::string& buffer) {
// Write the contents of the buffer onto the socket and return true on success. Otherwise, return false.
return true;
}
void DerivedSocket::on_close() {
// Perform clean-up steps here
}
It can be difficult to set up a working configuration for ZMQ communication. The examples below assume that there are two ZMQ sockets, one that has the robot state is on port 1601 and the command on 1602:
If all applications run in the same container, or on the same host, the situation is:
- The robot publishes its state on
0.0.0.0:1601
and listens to commands on0.0.0.0:1602
with both sockets non-binding. - The controller sends the command on
*:1602
and receives the state on*:1601
with both sockets binding.
Same as above.
If the containers all run on a user-defined bridge network, the connecting sockets need to be provided with the hostname of the binding sockets. For example, if the containers are running on network aicanet and have hostnames robot and controller, respectively.
- The robot publishes its state on
controller.aicanet:1601
and listens to commands oncontroller.aicanet:1602
with both sockets non-binding. - The controller sends the command on
*:1602
and receives the state on*:1601
with both sockets binding.
- This list of combinations is not exhaustive.
- The binding sockets always have a URI like
*:port
whilst the connecting sockets need to provide the complete address version (0.0.0.0:port
if on localhost orhostname.network:port
if on bridge network).
The Python bindings require an additional step of sanitizing the data when sending and receiving bytes. To illustrate this, an example is provided here.
# First a server and a client is connected
context = ZMQContext()
server = ZMQPublisher(ZMQSocketConfiguration(context, "127.0.0.1", "5001", True))
client = ZMQSubscriber(ZMQSocketConfiguration(context, "127.0.0.1", "5001", False))
server.open()
client.open()
# Then a string is sent through
str_msg = "Hello!"
server.send_bytes(str_msg)
received_str_msg = client.receive_bytes()
if received_str_msg is not None:
print(received_str_msg)
Here we expect the printed value to be Hello!
, however due to the way strings and bytes are processed, the string message is left as a byte literal and b'Hello!'
is printed instead. We can correct this as follows:
# Instead, decode the value
print(received_str_msg.decode("utf-8")) # will print Hello! as expected
More examples can be found in the Python unit tests.