Skip to content

Commit

Permalink
feat: add communication interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
domire8 committed May 1, 2024
1 parent 92f413b commit 247315d
Show file tree
Hide file tree
Showing 36 changed files with 1,714 additions and 0 deletions.
8 changes: 8 additions & 0 deletions python/include/communication_interfaces_bindings.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#include <pybind11/pybind11.h>

namespace py = pybind11;
using namespace pybind11::literals;

void bind_tcp(py::module_& m);
void bind_udp(py::module_& m);
void bind_zmq(py::module_& m);
27 changes: 27 additions & 0 deletions python/source/communication_interfaces/bind_tcp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#include "communication_interfaces_bindings.hpp"

#include <communication_interfaces/sockets/TCPClient.hpp>
#include <communication_interfaces/sockets/TCPServer.hpp>

using namespace communication_interfaces::sockets;

void bind_tcp(py::module_& m) {
py::class_<TCPClientConfiguration>(m, "TCPClientConfiguration")
.def(
py::init<std::string, int, int>(), "TCPClientConfiguration struct", "ip_address"_a, "port"_a, "buffer_size"_a)
.def_readwrite("ip_address", &TCPClientConfiguration::ip_address)
.def_readwrite("port", &TCPClientConfiguration::port)
.def_readwrite("buffer_size", &TCPClientConfiguration::buffer_size);

py::class_<TCPClient, std::shared_ptr<TCPClient>, ISocket>(m, "TCPClient")
.def(py::init<TCPClientConfiguration>(), "Constructor taking the configuration struct", "configuration"_a);

py::class_<TCPServerConfiguration>(m, "TCPServerConfiguration")
.def(py::init<int, int, bool>(), "TCPServerConfiguration struct", "port"_a, "buffer_size"_a, "enable_reuse"_a)
.def_readwrite("port", &TCPServerConfiguration::port)
.def_readwrite("buffer_size", &TCPServerConfiguration::buffer_size)
.def_readwrite("enable_reuse", &TCPServerConfiguration::enable_reuse);

py::class_<TCPServer, std::shared_ptr<TCPServer>, ISocket>(m, "TCPServer")
.def(py::init<TCPServerConfiguration>(), "Constructor taking the configuration struct", "configuration"_a);
}
24 changes: 24 additions & 0 deletions python/source/communication_interfaces/bind_udp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#include "communication_interfaces_bindings.hpp"

#include <communication_interfaces/sockets/UDPClient.hpp>
#include <communication_interfaces/sockets/UDPServer.hpp>

using namespace communication_interfaces::sockets;

void bind_udp(py::module_& m) {
py::class_<UDPSocketConfiguration>(m, "UDPSocketConfiguration")
.def(
py::init<std::string, int, int, bool, double>(), "UDPSocketConfiguration struct", "ip_address"_a, "port"_a,
"buffer_size"_a, "enable_reuse"_a = false, "timeout_duration_sec"_a = 0.0)
.def_readwrite("ip_address", &UDPSocketConfiguration::ip_address)
.def_readwrite("port", &UDPSocketConfiguration::port)
.def_readwrite("buffer_size", &UDPSocketConfiguration::buffer_size)
.def_readwrite("enable_reuse", &UDPSocketConfiguration::enable_reuse)
.def_readwrite("timeout_duration_sec", &UDPSocketConfiguration::timeout_duration_sec);

py::class_<UDPClient, std::shared_ptr<UDPClient>, ISocket>(m, "UDPClient")
.def(py::init<UDPSocketConfiguration>(), "Constructor taking the configuration struct", "configuration"_a);

py::class_<UDPServer, std::shared_ptr<UDPServer>, ISocket>(m, "UDPServer")
.def(py::init<UDPSocketConfiguration>(), "Constructor taking the configuration struct", "configuration"_a);
}
87 changes: 87 additions & 0 deletions python/source/communication_interfaces/bind_zmq.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#include "communication_interfaces_bindings.hpp"

#include <communication_interfaces/sockets/ZMQPublisherSubscriber.hpp>

using namespace communication_interfaces::sockets;

class PyZMQContext {
public:
PyZMQContext(int io_threads = 1) { context = std::make_shared<zmq::context_t>(io_threads); }

std::shared_ptr<zmq::context_t> context;
};

struct PyZMQSocketConfiguration {
PyZMQContext context;
std::string ip_address;
std::string port;
bool bind = true;
bool wait = false;
};

struct PyZMQCombinedSocketsConfiguration {
PyZMQContext context;
std::string ip_address;
std::string publisher_port;
std::string subscriber_port;
bool bind_publisher = true;
bool bind_subscriber = true;
bool wait = false;
};

class PyZMQPublisher : public ZMQPublisher, public std::enable_shared_from_this<PyZMQPublisher> {
public:
PyZMQPublisher(PyZMQSocketConfiguration config)
: ZMQPublisher({config.context.context, config.ip_address, config.port, config.bind, config.wait}) {}
};

class PyZMQSubscriber : public ZMQSubscriber, public std::enable_shared_from_this<PyZMQSubscriber> {
public:
PyZMQSubscriber(PyZMQSocketConfiguration config)
: ZMQSubscriber({config.context.context, config.ip_address, config.port, config.bind, config.wait}) {}
};

class PyZMQPublisherSubscriber : public ZMQPublisherSubscriber,
public std::enable_shared_from_this<PyZMQPublisherSubscriber> {
public:
PyZMQPublisherSubscriber(PyZMQCombinedSocketsConfiguration config)
: ZMQPublisherSubscriber(
{config.context.context, config.ip_address, config.publisher_port, config.subscriber_port,
config.bind_publisher, config.bind_subscriber, config.wait}) {}
};

void bind_zmq(py::module_& m) {
py::class_<PyZMQContext>(m, "ZMQContext").def(py::init<int>(), "Create a ZMQ context", "io_threads"_a = 1);

py::class_<PyZMQSocketConfiguration>(m, "ZMQSocketConfiguration")
.def(
py::init<PyZMQContext, std::string, std::string, bool, bool>(), "ZMQSocketConfiguration struct", "context"_a,
"ip_address"_a, "port"_a, "bind"_a = true, "wait"_a = false)
.def_readwrite("ip_address", &PyZMQSocketConfiguration::ip_address)
.def_readwrite("port", &PyZMQSocketConfiguration::port)
.def_readwrite("bind", &PyZMQSocketConfiguration::bind)
.def_readwrite("wait", &PyZMQSocketConfiguration::wait);

py::class_<PyZMQCombinedSocketsConfiguration>(m, "ZMQCombinedSocketsConfiguration")
.def(
py::init<PyZMQContext, std::string, std::string, std::string, bool, bool, bool>(),
"ZMQCombinedSocketsConfiguration struct", "context"_a, "ip_address"_a, "publisher_port"_a,
"subscriber_port"_a, "bind_publisher"_a = true, "bind_subscriber"_a = true, "wait"_a = false)
.def_readwrite("ip_address", &PyZMQCombinedSocketsConfiguration::ip_address)
.def_readwrite("publisher_port", &PyZMQCombinedSocketsConfiguration::publisher_port)
.def_readwrite("subscriber_port", &PyZMQCombinedSocketsConfiguration::subscriber_port)
.def_readwrite("bind_publisher", &PyZMQCombinedSocketsConfiguration::bind_publisher)
.def_readwrite("bind_subscriber", &PyZMQCombinedSocketsConfiguration::bind_subscriber)
.def_readwrite("wait", &PyZMQCombinedSocketsConfiguration::wait);

py::class_<PyZMQPublisher, std::shared_ptr<PyZMQPublisher>, ISocket>(m, "ZMQPublisher")
.def(py::init<PyZMQSocketConfiguration>(), "Constructor taking the configuration struct", "configuration"_a);

py::class_<PyZMQSubscriber, std::shared_ptr<PyZMQSubscriber>, ISocket>(m, "ZMQSubscriber")
.def(py::init<PyZMQSocketConfiguration>(), "Constructor taking the configuration struct", "configuration"_a);

py::class_<PyZMQPublisherSubscriber, std::shared_ptr<PyZMQPublisherSubscriber>, ISocket>(m, "ZMQPublisherSubscriber")
.def(
py::init<PyZMQCombinedSocketsConfiguration>(), "Constructor taking the configuration struct",
"configuration"_a);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#include "communication_interfaces_bindings.hpp"

#include <communication_interfaces/exceptions/SocketConfigurationException.hpp>
#include <communication_interfaces/sockets/ISocket.hpp>

#define STRINGIFY(x) #x
#define MACRO_STRINGIFY(x) STRINGIFY(x)

using namespace communication_interfaces;

PYBIND11_MODULE(communication_interfaces, m) {
m.doc() = "Python bindings for communication interfaces";

#ifdef MODULE_VERSION_INFO
m.attr("__version__") = MACRO_STRINGIFY(MODULE_VERSION_INFO);
#else
m.attr("__version__") = "dev";
#endif

auto m_sub_ex = m.def_submodule("exceptions", "Submodule for custom communication interfaces exceptions");
py::register_exception<exceptions::SocketConfigurationException>(
m_sub_ex, "SocketConfigurationError", PyExc_RuntimeError);

auto m_sub_sock = m.def_submodule("sockets", "Submodule for communication interfaces sockets");

py::class_<sockets::ISocket, std::shared_ptr<sockets::ISocket>>(m_sub_sock, "ISocket")
.def("open", &sockets::ISocket::open, "Perform configuration steps to open the socket for communication")
.def(
"receive_bytes",
[](sockets::ISocket& socket) -> py::object {
std::string buffer;
auto res = socket.receive_bytes(buffer);
if (res) {
return py::bytes(buffer);
} else {
return py::none();
}
},
"Receive bytes from the socket")
.def("send_bytes", &sockets::ISocket::send_bytes, "Receive bytes from the socket", "buffer"_a)
.def("close", &sockets::ISocket::close, "Perform steps to disconnect and close the socket communication");

bind_tcp(m_sub_sock);
bind_udp(m_sub_sock);
bind_zmq(m_sub_sock);
}
56 changes: 56 additions & 0 deletions python/test/communication_interfaces/test_tcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import multiprocessing
import pytest
import time

from communication_interfaces.exceptions import SocketConfigurationError
from communication_interfaces.sockets import TCPClientConfiguration, TCPClient, TCPServerConfiguration, TCPServer


class TestTCPCommunication:
server = TCPServer(TCPServerConfiguration(6000, 50, True))
client = TCPClient(TCPClientConfiguration("127.0.0.1", 6000, 50))
server_message = "Hello client"
client_message = "Hello server"

def serve(self):
self.server.open()
response = self.server.receive_bytes()
if response is None:
pytest.fail("No response from client")
assert response.decode("utf-8").rstrip("\x00") == self.client_message
assert self.server.send_bytes(self.server_message)

def test_comm(self):
t = multiprocessing.Process(target=self.serve)
t.start()
time.sleep(1.0)

self.client.open()
assert self.client.send_bytes(self.client_message)
response = self.client.receive_bytes()
assert response
assert response.decode("utf-8").rstrip("\x00") == self.server_message

buffer = ""
self.server.close()
with pytest.raises(SocketConfigurationError):
self.server.receive_bytes()
with pytest.raises(SocketConfigurationError):
self.server.send_bytes(buffer)
self.client.close()
with pytest.raises(SocketConfigurationError):
self.client.receive_bytes()
with pytest.raises(SocketConfigurationError):
self.client.send_bytes(buffer)

def test_not_open(self):
buffer = ""
with pytest.raises(SocketConfigurationError):
self.server.receive_bytes()
with pytest.raises(SocketConfigurationError):
self.server.send_bytes(buffer)

with pytest.raises(SocketConfigurationError):
self.client.receive_bytes()
with pytest.raises(SocketConfigurationError):
self.client.send_bytes(buffer)
94 changes: 94 additions & 0 deletions python/test/communication_interfaces/test_udp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import pytest

from communication_interfaces.sockets import UDPSocketConfiguration, UDPClient, UDPServer
from communication_interfaces.exceptions import SocketConfigurationError


@pytest.fixture
def udp_config():
return UDPSocketConfiguration("127.0.0.1", 5000, 100)


@pytest.fixture
def server(udp_config):
socket = UDPServer(udp_config)
yield socket
socket.close()


@pytest.fixture
def client(udp_config):
socket = UDPClient(udp_config)
yield socket
socket.close()


def test_send_receive(server, client):
send_string = "Hello world"

try:
server.open()
except SocketConfigurationError as e:
pytest.fail(e)

try:
client.open()
except SocketConfigurationError as e:
pytest.fail(e)

assert client.send_bytes(send_string)
response = server.receive_bytes()
assert response
assert response.decode("utf-8").rstrip("\x00") == send_string


def test_timeout(udp_config):
udp_config.timeout_duration_sec = 2.0

server = UDPServer(udp_config)
server.open()

assert server.receive_bytes() is None
server.close()


def test_port_reuse(udp_config, server):
server.open()

server2 = UDPServer(udp_config)
with pytest.raises(Exception):
server2.open()


def test_open_close(udp_config):
buffer = ""
udp_config.timeout_duration_sec = 0.5
server = UDPServer(udp_config)
with pytest.raises(SocketConfigurationError):
server.send_bytes(buffer)
with pytest.raises(SocketConfigurationError):
server.receive_bytes()
server.open()
assert server.send_bytes("test")
assert not server.receive_bytes()

server.close()
with pytest.raises(SocketConfigurationError):
server.send_bytes(buffer)
with pytest.raises(SocketConfigurationError):
server.receive_bytes()

client = UDPClient(udp_config)
with pytest.raises(SocketConfigurationError):
client.send_bytes(buffer)
with pytest.raises(SocketConfigurationError):
client.receive_bytes()
client.open()
assert client.send_bytes("test")
assert not client.receive_bytes()

client.close()
with pytest.raises(SocketConfigurationError):
client.send_bytes(buffer)
with pytest.raises(SocketConfigurationError):
client.receive_bytes()
Loading

0 comments on commit 247315d

Please sign in to comment.