diff --git a/docker/compose.yml b/docker/compose.yml
index e2a06f2..f786a71 100644
--- a/docker/compose.yml
+++ b/docker/compose.yml
@@ -12,4 +12,5 @@ services:
&& ros2 run sas_common sas_object_test_node
&& ros2 run sas_common sas_object_client_manager_test_node
&& ros2 run sas_common test_python_wrapper.py
+ && ros2 run sas_common test_object_client_manager_wrapper.py
&& ros2 run sas_common sas_simulator_test_node"
\ No newline at end of file
diff --git a/sas_common/__init__.py b/sas_common/__init__.py
index e1163ea..9b20bbd 100644
--- a/sas_common/__init__.py
+++ b/sas_common/__init__.py
@@ -28,12 +28,13 @@
compiled extension module :mod:`sas_common._sas_common` for convenience.
Exports:
-- rclcpp_Init: Initialize rclcpp
+- rclcpp_init: Initialize rclcpp
- rclcpp_Node: Node wrapper exposed from the C++ extension
- rclcpp_spin_some: Spin the node briefly
- rclcpp_shutdown: Shutdown rclcpp
- ObjectClient, SimulatorClient: C++ client wrappers for object/simulator control
+- ObjectClientManager: Manager for multiple ObjectClient instances
"""
-from sas_common._sas_common import rclcpp_init, rclcpp_Node, rclcpp_spin_some, rclcpp_shutdown, ObjectClient, SimulatorClient
+from sas_common._sas_common import rclcpp_init, rclcpp_Node, rclcpp_spin_some, rclcpp_shutdown, ObjectClient, SimulatorClient, ObjectClientManager
diff --git a/scripts/test_object_client_manager_wrapper.py b/scripts/test_object_client_manager_wrapper.py
new file mode 100644
index 0000000..06c260a
--- /dev/null
+++ b/scripts/test_object_client_manager_wrapper.py
@@ -0,0 +1,75 @@
+#!/usr/bin/python3
+"""
+# Copyright (c) 2026 Murilo Marques Marinho
+#
+# This file is part of sas_common.
+#
+# sas_common is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# sas_common is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with sas_common. If not, see .
+#
+# #######################################################################################
+#
+# Author: Murilo M. Marinho, email: murilomarinho@ieee.org
+#
+# #######################################################################################
+"""
+"""Test script for the ObjectClientManager Python bindings.
+
+This script demonstrates usage of the ObjectClientManager exposed via
+the sas_common Python package. It initializes rclcpp, creates a node,
+instantiates an ObjectClientManager, and exercises add/remove/get/list operations.
+
+Run with: python3 test_object_client_manager_wrapper.py
+"""
+from dqrobotics import *
+from sas_common import rclcpp_init, rclcpp_Node, rclcpp_spin_some, rclcpp_shutdown, ObjectClientManager
+
+def main():
+ """Run a basic smoke-test for the ObjectClientManager Python bindings."""
+ try:
+ rclcpp_init()
+ node = rclcpp_Node("sas_common_object_client_manager_test_node")
+
+ mgr = ObjectClientManager(node)
+ print("Initial size:", mgr.size())
+
+ mgr.add_client("arm")
+ mgr.add_client("hand")
+ print("Size after adding arm and hand:", mgr.size())
+
+ print("Has arm:", mgr.has_client("arm"))
+ print("Has hand:", mgr.has_client("hand"))
+ print("Has gripper:", mgr.has_client("gripper"))
+ print("Client names:", mgr.get_client_names())
+
+ arm = mgr.get_client("arm")
+ print("Arm enabled:", arm.is_enabled())
+ print("Arm topic prefix:", arm.get_topic_prefix())
+
+ x = DQ([1])
+ arm.send_pose(x)
+
+ removed = mgr.remove_client("hand")
+ print("Removed hand:", removed)
+ print("Size after removal:", mgr.size())
+ print("Client names after removal:", mgr.get_client_names())
+
+ rclcpp_spin_some(node)
+ rclcpp_shutdown()
+ except KeyboardInterrupt:
+ print("Interrupted by user")
+ except Exception as e:
+ print("Unhandled exception:", e)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/sas_common_py.cpp b/src/sas_common_py.cpp
index 98c8dc4..fa51f80 100644
--- a/src/sas_common_py.cpp
+++ b/src/sas_common_py.cpp
@@ -22,13 +22,16 @@
#
# ################################################################*/
#include
+#include
#include
#include
+#include
#include
namespace py = pybind11;
using OC = sas::ObjectClient;
+using OCM = sas::ObjectClientManager;
using SC = sas::SimulatorClient;
PYBIND11_MODULE(_sas_common, m) {
@@ -65,4 +68,28 @@ PYBIND11_MODULE(_sas_common, m) {
.def("is_enabled",&SC::is_enabled,"Returns true if the SimulatorClient is enabled.")
.def("get_topic_prefix",&SC::get_topic_prefix,
"Return the topic/service prefix configured for this client.");
+
+ py::class_(m, "ObjectClientManager")
+ .def(py::init&>(),
+ py::arg("node"),
+ "Construct an ObjectClientManager that manages multiple ObjectClient instances.")
+ .def("add_client", &OCM::add_client,
+ py::arg("name"),
+ "Add (or replace) an ObjectClient identified by name.")
+ .def("remove_client", &OCM::remove_client,
+ py::arg("name"),
+ "Remove an ObjectClient by name. Returns True if found and removed.")
+ .def("get_client", [](OCM& self, const std::string& name) -> OC& {
+ return self.get_client(name);
+ },
+ py::arg("name"),
+ py::return_value_policy::reference_internal,
+ "Return a reference to the managed ObjectClient named `name`.")
+ .def("has_client", &OCM::has_client,
+ py::arg("name"),
+ "Return True if a client with the given name exists.")
+ .def("get_client_names", &OCM::get_client_names,
+ "Return a list of all managed client names.")
+ .def("size", &OCM::size,
+ "Return the number of managed clients.");
}