Skip to content

Commit 6caa79a

Browse files
KerstinKellerashariff-11
authored andcommitted
[python] Nanobind eCAL Core binding (#2151)
Provide a feature complete Python API for ecal core using nanobind. Co-authored-by: Arifulla Shariff <[email protected]>
1 parent b22388c commit 6caa79a

File tree

119 files changed

+5282
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+5282
-12
lines changed

.github/workflows/build-macos.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jobs:
7979
-DECAL_BUILD_APPS=ON \
8080
-DECAL_BUILD_SAMPLES=ON \
8181
-DECAL_BUILD_TIMEPLUGINS=ON \
82-
-DECAL_BUILD_PY_BINDING=ON \
82+
-DECAL_BUILD_PY_BINDING=OFF \
8383
-DECAL_BUILD_CSHARP_BINDING=OFF \
8484
-DECAL_BUILD_TESTS=ON \
8585
-DECAL_INSTALL_SAMPLE_SOURCES=ON \

.github/workflows/build-ubuntu.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ jobs:
8484
thirdparty/ecaludp/ecaludp \
8585
thirdparty/fineftp/fineftp-server \
8686
thirdparty/ftxui/ftxui \
87+
thirdparty/nanobind/nanobind \
8788
thirdparty/qwt/qwt \
8889
thirdparty/recycle/recycle \
90+
thirdparty/tsl-robin-map/tsl-robin-map \
8991
thirdparty/tcp_pubsub/tcp_pubsub \
9092
thirdparty/termcolor/termcolor
9193

.gitmodules

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@
4949
[submodule "thirdparty/ecaludp/ecaludp"]
5050
path = thirdparty/ecaludp/ecaludp
5151
url = https://github.com/eclipse-ecal/ecaludp.git
52+
[submodule "thirdparty/nanobind/nanobind"]
53+
path = thirdparty/nanobind/nanobind
54+
url = https://github.com/wjakob/nanobind.git
55+
[submodule "thirdparty/tsl-robin-map/tsl-robin-map"]
56+
path = thirdparty/tsl-robin-map/tsl-robin-map
57+
url = https://github.com/Tessil/robin-map.git

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,7 @@ message(STATUS "ECAL_THIRDPARTY_BUILD_FINEFTP : ${ECAL_THI
641641
message(STATUS "ECAL_THIRDPARTY_BUILD_FTXUI : ${ECAL_THIRDPARTY_BUILD_FTXUI}")
642642
message(STATUS "ECAL_THIRDPARTY_BUILD_GTEST : ${ECAL_THIRDPARTY_BUILD_GTEST}")
643643
message(STATUS "ECAL_THIRDPARTY_BUILD_HDF5 : ${ECAL_THIRDPARTY_BUILD_HDF5}")
644+
message(STATUS "ECAL_THIRDPARTY_BUILD_NANOBIND : ${ECAL_THIRDPARTY_BUILD_NANOBIND}")
644645
message(STATUS "ECAL_THIRDPARTY_BUILD_PROTOBUF : ${ECAL_THIRDPARTY_BUILD_PROTOBUF}")
645646
message(STATUS "ECAL_THIRDPARTY_BUILD_QWT : ${ECAL_THIRDPARTY_BUILD_QWT}")
646647
message(STATUS "ECAL_THIRDPARTY_BUILD_RECYCLE : ${ECAL_THIRDPARTY_BUILD_RECYCLE}")

cmake/submodule_dependencies.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ set(ecal_submodule_dependencies
1313
GTest
1414
HDF5
1515
#libssh2
16+
nanobind
1617
Protobuf
1718
qwt
1819
recycle
1920
spdlog
2021
tclap
2122
tcp_pubsub
2223
termcolor
24+
tsl-robin-map
2325
tinyxml2
2426
udpcap
2527
yaml-cpp

doc/rst/license/thirdparty_licenses.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ They are used by Eclipse eCAL, but not associated to it in any way.
149149
- - |fa-file-alt| Copy in repository: :file:`/cpack/innosetup/modpath.iss`
150150
- |fa-windows| Binary distributions for Windows (Installer only)
151151

152+
* - `nanobind <https://github.com/wjakob/nanobind>`_
153+
- :ref:`bsd_3`
154+
- 2022, Wenzel Jakob
155+
- - |fa-github| Git submodule ``/tirdparty/nanobind/nanobind``
156+
152157
* - `nanopb <https://github.com/nanopb/nanopb>`_
153158
- :ref:`zlib_license`
154159
- 2011 Petteri Aimonen \<jpa at nanopb.mail.kapsi.fi\>
@@ -186,6 +191,11 @@ They are used by Eclipse eCAL, but not associated to it in any way.
186191
- - |fa-github| Git submodule ``/tirdparty/recycle/recycle``
187192
- |fa-windows| Binary distributions for Windows
188193
- |fa-ubuntu| Binary distributions for Linux
194+
195+
* - `robin-map <https://github.com/Tessil/robin-map>`_
196+
- :ref:`mit_license`
197+
- 2017, Thibaut Goetghebuer-Planchon
198+
- - |fa-github| Git submodule ``/tirdparty/tsl-robin-map/tsl-robin-map``
189199

190200
* - `spdlog <https://github.com/gabime/spdlog>`_
191201
- :ref:`mit_license`

ecal/core/include/ecal/pubsub/types.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,17 @@ namespace eCAL
159159
using SubEventCallbackT = std::function<void(const STopicId& topic_id_, const SSubEventCallbackData& data_)>;
160160
}
161161

162+
163+
namespace std
164+
{
165+
template<>
166+
class hash<eCAL::STopicId> {
167+
public:
168+
size_t operator()(const eCAL::STopicId& id) const
169+
{
170+
const std::size_t h1 = std::hash<std::string>{}(id.topic_name);
171+
const std::size_t h2 = std::hash<uint64_t>{}(id.topic_id.entity_id);
172+
return h1 ^ (h2 << 1); // basic combination
173+
}
174+
};
175+
}

ecal/core/include/ecal/service/types.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,31 @@ namespace eCAL
205205
using ServerEventCallbackT = std::function<void(const SServiceId& service_id_, const struct SServerEventCallbackData& data_)>;
206206

207207
}
208+
209+
210+
namespace std
211+
{
212+
template<>
213+
class hash<eCAL::SServiceId> {
214+
public:
215+
size_t operator()(const eCAL::SServiceId& id) const
216+
{
217+
const std::size_t h1 = std::hash<std::string>{}(id.service_name);
218+
const std::size_t h2 = std::hash<uint64_t>{}(id.service_id.entity_id);
219+
return h1 ^ (h2 << 1); // basic combination
220+
}
221+
};
222+
223+
template<>
224+
class hash<eCAL::SServiceMethodInformation> {
225+
public:
226+
size_t operator()(const eCAL::SServiceMethodInformation& info) const
227+
{
228+
const std::size_t h1 = std::hash<std::string>{}(info.method_name);
229+
const std::size_t h2 = std::hash<eCAL::SDataTypeInformation>{}(info.request_type);
230+
const std::size_t h3 = std::hash<eCAL::SDataTypeInformation>{}(info.response_type);
231+
return h1 ^ (h2 << 1) ^ (h3 << 2);
232+
}
233+
};
234+
}
235+

ecal/core/include/ecal/types.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,18 @@ namespace eCAL
102102
return os;
103103
}
104104
}
105+
106+
namespace std
107+
{
108+
template<>
109+
class hash<eCAL::SDataTypeInformation> {
110+
public:
111+
size_t operator()(const eCAL::SDataTypeInformation& datatype_info) const
112+
{
113+
const std::size_t h1 = std::hash<std::string>{}(datatype_info.name);
114+
const std::size_t h2 = std::hash<std::string>{}(datatype_info.encoding);
115+
const std::size_t h3 = std::hash<std::string>{}(datatype_info.descriptor);
116+
return h1 ^ (h2 << 1) ^ (h3 << 2); // basic combination
117+
}
118+
};
119+
}

lang/python/CMakeLists.txt

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ========================= eCAL LICENSE =================================
22
#
3-
# Copyright (C) 2016 - 2024 Continental Corporation
3+
# Copyright (C) 2016 - 2025 Continental Corporation
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -28,8 +28,18 @@ option(ECAL_PYTHON_BUILD_SAMPLES "Includes the python samples"
2828
option(ECAL_PYTHON_BUILD_TESTS "Includes the python tests" ON)
2929
option(ECAL_PYTHON_USE_HDF5 "Enables eCAL application cmd line interfaces" ON)
3030

31+
if (NOT ECAL_PYTHON_USE_HDF5)
32+
message(FATAL_ERROR "Building python bindings without hdf5 is not supported.")
33+
endif()
34+
35+
3136
set(ECAL_PYTHON_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
3237

38+
# Try to make modules loadable by debug python version
39+
if(WIN32)
40+
set(CMAKE_DEBUG_POSTFIX _d)
41+
endif()
42+
3343
# This function takes a list of python files to be copied to the package directory
3444
# and associates the files with a given python extension target
3545
# Using this function allows to create the whole python package editable in place
@@ -129,15 +139,14 @@ set(CMAKE_INSTALL_RPATH "\$ORIGIN")
129139
# for build tree use.
130140
set(CMAKE_BUILD_WITH_INSTALL_RPATH "ON")
131141

132-
add_subdirectory(core)
142+
add_subdirectory(src/core)
133143
add_dependencies(${PROJECT_NAME} _ecal_core_py)
134144

135-
if(ECAL_USE_HDF5)
136-
add_subdirectory(ecalhdf5)
137-
add_dependencies(${PROJECT_NAME} _ecal_hdf5_py)
138-
else()
139-
message(WARNING "Building Python bindings without HDF5 support")
140-
endif()
145+
add_subdirectory(src/nanobind_core/src)
146+
add_dependencies(${PROJECT_NAME} nanobind_core)
147+
148+
add_subdirectory(src/ecalhdf5)
149+
add_dependencies(${PROJECT_NAME} _ecal_hdf5_py)
141150

142151
if (ECAL_PYTHON_BUILD_SAMPLES)
143152
add_subdirectory(samples)

lang/python/samples/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ set(python_samples_core
2323
core/service/minimal_service_server.py
2424
)
2525

26+
set(python_samples_nanobind_core
27+
nanobind_core/pubsub/nb_binary_rec.py
28+
nanobind_core/pubsub/nb_binary_rec_cb.py
29+
nanobind_core/pubsub/nb_binary_snd.py
30+
nanobind_core/service/nb_minimal_service_client.py
31+
nanobind_core/service/nb_minimal_service_server.py
32+
nanobind_core/nb_logging.py
33+
nanobind_core/nb_monitoring.py
34+
nanobind_core/nb_process.py
35+
nanobind_core/nb_util.py
36+
)
37+
2638
function(add_sample_to_solution python_filenname)
2739
cmake_path(GET python_filenname PARENT_PATH subfolder)
2840
cmake_path(GET python_filenname STEM stem)
@@ -40,7 +52,13 @@ if(ECAL_PYTHON_USE_HDF5)
4052
endforeach()
4153
endif()
4254

55+
# Core samples
4356
foreach(python_sample ${python_samples_core})
4457
add_sample_to_solution(${python_sample})
4558
endforeach()
59+
60+
# Nanobind samples
61+
foreach(python_sample ${python_samples_nanobind_core})
62+
add_sample_to_solution(${python_sample})
63+
endforeach()
4664
endif()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# ========================= eCAL LICENSE =================================
2+
#
3+
# Copyright (C) 2016 - 2025 Continental Corporation
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# ========================= eCAL LICENSE =================================
18+
import ecal.nanobind_core as ecal_core
19+
20+
def main():
21+
22+
# For pure logging, it's not necessary to modify the configuration.
23+
# However, in order to also receive logging information it's necessary to be turned on
24+
config = ecal_core.init.get_configuration()
25+
config.logging.receiver.enable = True
26+
config.logging.provider.udp.log_levels = [ecal_core.logging.LogLevel.INFO, ecal_core.logging.LogLevel.WARNING, ecal_core.logging.LogLevel.ERROR, ecal_core.logging.LogLevel.FATAL]
27+
ecal_core.initialize(config, 'Logging Python Sample', ecal_core.init.ALL)
28+
29+
ecal_core.logging.log(ecal_core.logging.LogLevel.INFO, "Hello Hello")
30+
ecal_core.logging.log(ecal_core.logging.LogLevel.WARNING, "Help")
31+
32+
all_logging = ecal_core.logging.get_logging()
33+
34+
for log in all_logging:
35+
print(log)
36+
print("\n")
37+
38+
ecal_core.finalize()
39+
40+
if __name__ == "__main__":
41+
main()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# ========================= eCAL LICENSE =================================
2+
#
3+
# Copyright (C) 2016 - 2025 Continental Corporation
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# ========================= eCAL LICENSE =================================
18+
import time
19+
20+
import ecal.nanobind_core as ecal_core
21+
22+
def main():
23+
ecal_core.initialize('Monitoring Python Sample', ecal_core.init.ALL)
24+
25+
try:
26+
while ecal_core.ok():
27+
#TODO: is the naming good?
28+
# or ecal_core.monitoring.
29+
monitoring = ecal_core.monitoring.get_monitoring()
30+
31+
print("There are {} publishers and {} subscribers in the system".format(len(monitoring.publishers), len(monitoring.subscribers)))
32+
33+
serialized_monitoring = ecal_core.monitoring.get_serialized_monitoring()
34+
print("Length of serialized monitoring: {}".format(len(serialized_monitoring)))
35+
# You could now use protobuf to deserialize the monitoring string
36+
# We recommend to use the `get_monitoring` instead.
37+
38+
time.sleep(1)
39+
40+
except KeyboardInterrupt:
41+
pass
42+
finally:
43+
ecal_core.finalize()
44+
45+
if __name__ == "__main__":
46+
main()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# ========================= eCAL LICENSE =================================
2+
#
3+
# Copyright (C) 2016 - 2025 Continental Corporation
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# ========================= eCAL LICENSE =================================
18+
import os
19+
import time
20+
21+
import ecal.nanobind_core as ecal_core
22+
23+
def main():
24+
ecal_core.initialize('Process Python Sample', ecal_core.init.ALL)
25+
26+
# We can query the eCAL Unit name which was set via `initialize`
27+
unit_name = ecal_core.process.get_unit_name()
28+
print("The name of this unit is '{}'".format(unit_name))
29+
30+
# We can set a process state that will be shown in eCAL Monitor and eCALSys.
31+
ecal_core.process.set_state(ecal_core.process.Severity.HEALTHY, ecal_core.process.SeverityLevel.LEVEL1, "I am doing fine")
32+
33+
time.sleep(10)
34+
35+
ecal_core.finalize()
36+
37+
if __name__ == "__main__":
38+
main()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# ========================= eCAL LICENSE =================================
2+
#
3+
# Copyright (C) 2016 - 2025 Continental Corporation
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# ========================= eCAL LICENSE =================================
18+
import os
19+
import time
20+
21+
import ecal.nanobind_core as ecal_core
22+
23+
def main():
24+
ecal_core.initialize('Util Python Sample', ecal_core.init.ALL)
25+
26+
# Print the eCAL data directory
27+
print("eCAL Data directory: {}".format(ecal_core.get_ecal_data_dir()))
28+
29+
# Print the eCAL logging directory, where file logs will be placed
30+
print("eCAL Logging directory: {}".format(ecal_core.get_ecal_log_dir()))
31+
32+
# Sets a shutdown signal to person_send, which will cause `eCAL::ok()` in that process to return false;
33+
ecal_core.shutdown_process_by_name("person_send")
34+
35+
ecal_core.finalize()
36+
37+
if __name__ == "__main__":
38+
main()

0 commit comments

Comments
 (0)