Skip to content

Commit a40dfa0

Browse files
authored
Merge pull request #1 from innate-inc/codex/advertise-service-support
Support rosbridge advertised services
2 parents c4d0923 + e320b2d commit a40dfa0

14 files changed

Lines changed: 890 additions & 14 deletions

.github/workflows/ci.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
ros-humble:
11+
name: ROS Humble build and tests
12+
runs-on: ubuntu-22.04
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
with:
18+
path: ros_ws/src/rws
19+
20+
- name: Install ROS Humble and dependencies
21+
run: |
22+
sudo apt-get update
23+
sudo apt-get install -y curl git gnupg software-properties-common
24+
sudo add-apt-repository -y universe
25+
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
26+
-o /usr/share/keyrings/ros-archive-keyring.gpg
27+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo "$UBUNTU_CODENAME") main" | \
28+
sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
29+
sudo apt-get update
30+
sudo apt-get install -y --no-install-recommends \
31+
python3-colcon-common-extensions \
32+
ros-humble-ament-cmake \
33+
ros-humble-ament-cmake-gmock \
34+
ros-humble-ament-cmake-gtest \
35+
ros-humble-ament-index-cpp \
36+
ros-humble-ament-lint-auto \
37+
ros-humble-ament-lint-common \
38+
ros-humble-action-msgs \
39+
ros-humble-rcl-interfaces \
40+
ros-humble-rclcpp \
41+
ros-humble-rclcpp-action \
42+
ros-humble-ros-base \
43+
ros-humble-rosidl-typesupport-cpp \
44+
ros-humble-rosidl-typesupport-introspection-cpp \
45+
ros-humble-std-msgs \
46+
ros-humble-std-srvs \
47+
ros-humble-test-msgs \
48+
ros-humble-unique-identifier-msgs
49+
50+
- name: Build
51+
working-directory: ros_ws
52+
run: |
53+
source /opt/ros/humble/setup.bash
54+
colcon build \
55+
--packages-select rws \
56+
--event-handlers console_direct+ \
57+
--cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo
58+
59+
- name: Test
60+
working-directory: ros_ws
61+
run: |
62+
source /opt/ros/humble/setup.bash
63+
colcon test \
64+
--packages-select rws \
65+
--event-handlers console_direct+ \
66+
--return-code-on-test-failure
67+
colcon test-result --verbose

CMakeLists.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ fetchcontent_getproperties(websocketpp)
5353
if(NOT websocketpp_POPULATED)
5454
fetchcontent_populate(websocketpp)
5555
add_subdirectory(${websocketpp_SOURCE_DIR} ${websocketpp_BINARY_DIR} EXCLUDE_FROM_ALL)
56-
56+
5757
# Patch hybi13.hpp to skip outgoing UTF-8 validation for TEXT frames
5858
# This improves performance when we know our JSON output is valid UTF-8
5959
file(READ "${websocketpp_SOURCE_DIR}/websocketpp/processors/hybi13.hpp" HYBI13_CONTENT)
60-
string(REPLACE
60+
string(REPLACE
6161
"// validate payload utf8\n if (op == frame::opcode::TEXT && !utf8_validator::validate(i)) {\n return make_error_code(error::invalid_payload);\n }"
6262
"// UTF-8 validation disabled for outgoing TEXT frames (we generate valid UTF-8)\n // if (op == frame::opcode::TEXT && !utf8_validator::validate(i)) {\n // return make_error_code(error::invalid_payload);\n // }"
6363
HYBI13_CONTENT "${HYBI13_CONTENT}")
@@ -105,6 +105,8 @@ if(BUILD_TESTING)
105105
find_package(ament_cmake_gmock REQUIRED)
106106
find_package(ament_lint_auto REQUIRED)
107107
find_package(rcl_interfaces REQUIRED)
108+
find_package(std_msgs REQUIRED)
109+
find_package(std_srvs REQUIRED)
108110

109111
# the following lines skip linters
110112
set(ament_cmake_cppcheck_FOUND TRUE)
@@ -164,6 +166,8 @@ if(BUILD_TESTING)
164166
src/generic_action_client.cpp
165167
)
166168
ament_target_dependencies(client_handler_test ${DEPENDENCIES})
169+
ament_target_dependencies(client_handler_test std_msgs)
170+
ament_target_dependencies(client_handler_test std_srvs)
167171
target_include_directories(client_handler_test PUBLIC
168172
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
169173
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ rws_server_node = Node(
5151
| subscribe | + | + | Subscribe to topic |
5252
| unsubscribe | + | + | Unsubscribe from topic |
5353
| call_service | + | + | Call service (with timeout support) |
54-
| advertise_service | - | + | Advertise external service |
55-
| unadvertise_service | - | + | Stop advertising external service |
54+
| advertise_service | + | + | Advertise external service |
55+
| unadvertise_service | + | + | Stop advertising external service |
5656
| service_request | - | + | Request to external service |
57-
| service_response | - | + | Response from external service |
57+
| service_response | + | + | Response from external service |
5858
| send_action_goal | + | + | Send action goal (with feedback) |
5959
| cancel_action_goal | + | + | Cancel action goal |
6060
| advertise_action | - | + | Advertise action server |

include/rws/client_handler.hpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
#ifndef RWS__NODE_HPP_
1616
#define RWS__NODE_HPP_
1717

18+
#include <atomic>
19+
#include <mutex>
20+
1821
#include <rapidjson/document.h>
1922
#include <rapidjson/writer.h>
2023
#include <rapidjson/stringbuffer.h>
@@ -58,7 +61,19 @@ class ClientHandler
5861
std::map<std::string, std::function<void(std::shared_ptr<const rclcpp::SerializedMessage>)>>
5962
publisher_cb_;
6063
std::map<std::string, std::shared_ptr<rws::GenericClient>> clients_;
64+
std::map<std::string, std::function<void()>> advertised_services_;
65+
std::map<std::string, std::shared_ptr<rws::GenericService>> service_servers_;
66+
std::map<std::string, std::string> advertised_service_type_;
6167
std::map<std::string, std::shared_ptr<rws::GenericActionClient>> action_clients_;
68+
struct PendingAdvertisedServiceRequest
69+
{
70+
std::string service;
71+
std::string service_type;
72+
std::shared_ptr<rmw_request_id_t> request_header;
73+
};
74+
std::mutex advertised_service_mutex_;
75+
std::map<std::string, PendingAdvertisedServiceRequest> pending_advertised_service_requests_;
76+
std::atomic<uint64_t> advertised_service_sequence_{0};
6277

6378
rclcpp::Logger get_logger()
6479
{
@@ -77,6 +92,9 @@ class ClientHandler
7792
bool unadvertise_topic_rapid(const rapidjson::Document & msg, rapidjson::StringBuffer & buf, RapidWriter & w);
7893
bool publish_to_topic_rapid(const rapidjson::Document & msg, rapidjson::StringBuffer & buf, RapidWriter & w);
7994
bool call_service_rapid(const rapidjson::Document & msg, rapidjson::StringBuffer & buf, RapidWriter & w);
95+
bool advertise_service_rapid(const rapidjson::Document & msg, rapidjson::StringBuffer & buf, RapidWriter & w);
96+
bool unadvertise_service_rapid(const rapidjson::Document & msg, rapidjson::StringBuffer & buf, RapidWriter & w);
97+
bool service_response_rapid(const rapidjson::Document & msg, rapidjson::StringBuffer & buf, RapidWriter & w);
8098
bool send_action_goal_rapid(const rapidjson::Document & msg, rapidjson::StringBuffer & buf, RapidWriter & w);
8199
bool cancel_action_goal_rapid(const rapidjson::Document & msg, rapidjson::StringBuffer & buf, RapidWriter & w);
82100

@@ -87,4 +105,4 @@ class ClientHandler
87105

88106
} // namespace rws
89107

90-
#endif // RWS__NODE_HPP_
108+
#endif // RWS__NODE_HPP_

include/rws/generic_service.hpp

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright 2026 Innate
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
6+
#ifndef RWS__GENERIC_SERVICE_HPP_
7+
#define RWS__GENERIC_SERVICE_HPP_
8+
9+
#include <memory>
10+
#include <string>
11+
12+
#include "rcl/service.h"
13+
#include "rclcpp/exceptions.hpp"
14+
#include "rclcpp/expand_topic_or_service_name.hpp"
15+
#include "rclcpp/serialized_message.hpp"
16+
#include "rclcpp/service.hpp"
17+
#include "rclcpp/typesupport_helpers.hpp"
18+
#include "rmw/rmw.h"
19+
#include "rosidl_typesupport_introspection_cpp/service_introspection.hpp"
20+
#include "rws/typesupport_helpers.hpp"
21+
22+
namespace rws
23+
{
24+
25+
using rosidl_typesupport_introspection_cpp::ServiceMembers;
26+
27+
class GenericService : public rclcpp::ServiceBase
28+
{
29+
public:
30+
using SharedRequest = std::shared_ptr<rclcpp::SerializedMessage>;
31+
using SharedResponse = std::shared_ptr<rclcpp::SerializedMessage>;
32+
using CallbackType = std::function<void(std::shared_ptr<rmw_request_id_t>, SharedRequest)>;
33+
34+
RCLCPP_SMART_PTR_DEFINITIONS(GenericService)
35+
36+
GenericService(
37+
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base,
38+
const std::string & service_name,
39+
const std::string & service_type,
40+
rcl_service_options_t & service_options,
41+
CallbackType callback)
42+
: rclcpp::ServiceBase(node_base->get_shared_rcl_node_handle()),
43+
service_type_(service_type),
44+
callback_(callback)
45+
{
46+
srv_ts_lib_ = rclcpp::get_typesupport_library(service_type, rws::ts_identifier_srv);
47+
srv_ts_hdl_ =
48+
rws::get_service_typesupport_handle(service_type, rws::ts_identifier_srv, *srv_ts_lib_);
49+
50+
srv_intro_lib_ = rclcpp::get_typesupport_library(service_type, rws::ts_identifier);
51+
srv_intro_hdl_ =
52+
rws::get_service_typesupport_handle(service_type, rws::ts_identifier, *srv_intro_lib_);
53+
auto srv_members = static_cast<const ServiceMembers *>(srv_intro_hdl_->data);
54+
55+
auto request_type = get_type_from_message_members(srv_members->request_members_);
56+
req_ts_srv_lib_ = rclcpp::get_typesupport_library(request_type, rws::ts_identifier_srv);
57+
req_ts_srv_hdl_ =
58+
rclcpp::get_typesupport_handle(request_type, rws::ts_identifier_srv, *req_ts_srv_lib_);
59+
60+
auto response_type = get_type_from_message_members(srv_members->response_members_);
61+
res_ts_srv_lib_ = rclcpp::get_typesupport_library(response_type, rws::ts_identifier_srv);
62+
res_ts_srv_hdl_ =
63+
rclcpp::get_typesupport_handle(response_type, rws::ts_identifier_srv, *res_ts_srv_lib_);
64+
65+
service_handle_ = std::shared_ptr<rcl_service_t>(
66+
new rcl_service_t, [handle = node_handle_](rcl_service_t * service)
67+
{
68+
if (rcl_service_fini(service, handle.get()) != RCL_RET_OK) {
69+
RCLCPP_ERROR(
70+
rclcpp::get_node_logger(handle.get()).get_child("rclcpp"),
71+
"Error in destruction of rcl service handle: %s",
72+
rcl_get_error_string().str);
73+
rcl_reset_error();
74+
}
75+
delete service;
76+
});
77+
*service_handle_.get() = rcl_get_zero_initialized_service();
78+
79+
rcl_ret_t ret = rcl_service_init(
80+
service_handle_.get(),
81+
node_handle_.get(),
82+
srv_ts_hdl_,
83+
service_name.c_str(),
84+
&service_options);
85+
if (ret != RCL_RET_OK) {
86+
if (ret == RCL_RET_SERVICE_NAME_INVALID) {
87+
rcl_reset_error();
88+
rclcpp::expand_topic_or_service_name(
89+
service_name,
90+
rcl_node_get_name(node_handle_.get()),
91+
rcl_node_get_namespace(node_handle_.get()),
92+
true);
93+
}
94+
95+
rclcpp::exceptions::throw_from_rcl_error(ret, "could not create generic service");
96+
}
97+
}
98+
99+
std::shared_ptr<void> create_request() override
100+
{
101+
auto srv_members = static_cast<const ServiceMembers *>(srv_intro_hdl_->data);
102+
return allocate_message(srv_members->request_members_);
103+
}
104+
105+
std::shared_ptr<rmw_request_id_t> create_request_header() override
106+
{
107+
return std::make_shared<rmw_request_id_t>();
108+
}
109+
110+
void handle_request(
111+
std::shared_ptr<rmw_request_id_t> request_header,
112+
std::shared_ptr<void> request) override
113+
{
114+
auto serialized_request = std::make_shared<rclcpp::SerializedMessage>();
115+
rmw_ret_t ret =
116+
rmw_serialize(request.get(), req_ts_srv_hdl_, &serialized_request->get_rcl_serialized_message());
117+
if (ret != RMW_RET_OK) {
118+
RCUTILS_LOG_ERROR_NAMED(
119+
"rws", "Failed to serialize advertised service request: %s",
120+
rcutils_get_error_string().str);
121+
rcutils_reset_error();
122+
return;
123+
}
124+
125+
callback_(request_header, serialized_request);
126+
}
127+
128+
void send_serialized_response(
129+
std::shared_ptr<rmw_request_id_t> request_header,
130+
SharedResponse serialized_response)
131+
{
132+
auto srv_members = static_cast<const ServiceMembers *>(srv_intro_hdl_->data);
133+
auto response = allocate_message(srv_members->response_members_);
134+
const rmw_serialized_message_t * sm = &serialized_response->get_rcl_serialized_message();
135+
rmw_ret_t rmw_ret = rmw_deserialize(sm, res_ts_srv_hdl_, response.get());
136+
if (rmw_ret != RMW_RET_OK) {
137+
rclcpp::exceptions::throw_from_rcl_error(rmw_ret, "failed to deserialize service response");
138+
}
139+
140+
rcl_ret_t ret = rcl_send_response(get_service_handle().get(), request_header.get(), response.get());
141+
if (ret == RCL_RET_TIMEOUT) {
142+
RCLCPP_WARN(
143+
node_logger_.get_child("rclcpp"),
144+
"failed to send response to %s (timeout): %s",
145+
this->get_service_name(), rcl_get_error_string().str);
146+
rcl_reset_error();
147+
return;
148+
}
149+
if (ret != RCL_RET_OK) {
150+
rclcpp::exceptions::throw_from_rcl_error(ret, "failed to send service response");
151+
}
152+
}
153+
154+
const std::string & service_type() const { return service_type_; }
155+
156+
private:
157+
RCLCPP_DISABLE_COPY(GenericService)
158+
159+
std::string service_type_;
160+
CallbackType callback_;
161+
std::shared_ptr<rcpputils::SharedLibrary> srv_ts_lib_;
162+
const rosidl_service_type_support_t * srv_ts_hdl_;
163+
std::shared_ptr<rcpputils::SharedLibrary> srv_intro_lib_;
164+
const rosidl_service_type_support_t * srv_intro_hdl_;
165+
std::shared_ptr<rcpputils::SharedLibrary> req_ts_srv_lib_;
166+
const rosidl_message_type_support_t * req_ts_srv_hdl_;
167+
std::shared_ptr<rcpputils::SharedLibrary> res_ts_srv_lib_;
168+
const rosidl_message_type_support_t * res_ts_srv_hdl_;
169+
};
170+
171+
} // namespace rws
172+
173+
#endif // RWS__GENERIC_SERVICE_HPP_

include/rws/node_interface.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "rclcpp/generic_publisher.hpp"
55
#include "rclcpp/generic_subscription.hpp"
66
#include "rws/generic_client.hpp"
7+
#include "rws/generic_service.hpp"
78
#include "rws/generic_action_client.hpp"
89

910
namespace rws
@@ -56,6 +57,12 @@ class NodeInterface
5657
const std::string & service_name, const std::string & service_type,
5758
const rmw_qos_profile_t & qos_profile, rclcpp::CallbackGroup::SharedPtr group) = 0;
5859

60+
/// Create a generic service server with a given type.
61+
virtual GenericService::SharedPtr create_generic_service(
62+
const std::string & service_name, const std::string & service_type,
63+
const rmw_qos_profile_t & qos_profile, rclcpp::CallbackGroup::SharedPtr group,
64+
GenericService::CallbackType callback) = 0;
65+
5966
/// Create a generic action client with a given type.
6067
virtual GenericActionClient::SharedPtr create_generic_action_client(
6168
const std::string & action_name, const std::string & action_type,

include/rws/node_interface_impl.hpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "rws/node_interface.hpp"
77
#include "rws/typesupport_helpers.hpp"
88
#include "rws/generic_action_client.hpp"
9+
#include "rws/generic_service.hpp"
910

1011
namespace rws
1112
{
@@ -52,6 +53,22 @@ class NodeInterfaceImpl : public rws::NodeInterface<>
5253
return cli;
5354
}
5455

56+
GenericService::SharedPtr create_generic_service(
57+
const std::string & service_name, const std::string & service_type,
58+
const rmw_qos_profile_t & qos_profile, rclcpp::CallbackGroup::SharedPtr group,
59+
GenericService::CallbackType callback)
60+
{
61+
rcl_service_options_t options = rcl_service_get_default_options();
62+
options.qos = qos_profile;
63+
64+
auto srv = GenericService::make_shared(
65+
node_->get_node_base_interface(), service_name, service_type, options, callback);
66+
67+
auto srv_base_ptr = std::dynamic_pointer_cast<rclcpp::ServiceBase>(srv);
68+
node_->get_node_services_interface()->add_service(srv_base_ptr, group);
69+
return srv;
70+
}
71+
5572
GenericActionClient::SharedPtr create_generic_action_client(
5673
const std::string & action_name, const std::string & action_type,
5774
const rcl_action_client_options_t & options = rcl_action_client_get_default_options())
@@ -120,4 +137,4 @@ class NodeInterfaceImpl : public rws::NodeInterface<>
120137

121138
} // namespace rws
122139

123-
#endif // RWS__NODE_INTERFACE_IMPL_HPP_
140+
#endif // RWS__NODE_INTERFACE_IMPL_HPP_

0 commit comments

Comments
 (0)