Siddharth Saha's NDoF Sensor Driver for Machina Labs
v1.0.0
ROS 2 Driver for Machina Labs' NDoF Sensor
|
My solution to the challenged posted by Machina Labs here with 2 days spent on it
Documentation website can be seen here
There are 6 packages in this repo. They are
machina_ndof_sensor_driver
rmw_cyclonedds_cpp
. It isn't a strict requirement but it's what I have found most convenient to use in ROS 2 especially with services. Feel free to comment it out / remove it if you don't need / want it (Note the Dockerfile automatically uses CycloneDDS with a config)machina_tcp_interface
arpa
and sys
libraries to connect to a TCP socket, read and send buffers as well as check status information of the connectionmachina_ndof_sensor_driver_msgs
example_interfaces/msg/Float64MultiArray
with a std_msgs/Header
to form SensorOutput.msg
. I believe it's important to record the sensor's frame ID and time it was collected for downstream processing laterJointSensorOutput.msg
that has it's own builtin_interfaces/Time
. In this case each sensor still retains it's own timestamp and frame ID. But we are also recording the time the message was generated to be used downstream as an indication for whether or not the data is frozenGetLatestSensorData.srv
. It takes an empty request and sends back a boolean flag if any data was filled, a timestamp for when the data was collected and the data itself. The latter 2 are combined together through the SensorOutput.msg
machina_ndof_sensor_driver_service
machina_tcp_client
to a sensor, constantly polls it for data and exposes that data for other nodes to receive through the ROS 2 Servicestd::atomic
to check) then the consumer can read from the other element. This way we can avoid blocking. Ommited for the sake of timemachina_ndof_sensor_driver_client
machina_ndof_sensor_driver_service
, combine the data into 1 topic and publish it outMachinaNDoFSensorDriverClient
and MachinaNDoFSensorDriverClientNode
. The MachinaNDoFSensorDriverClient
is responsible for maintaining thread safety within it's class, spawn a thread to send requests to a single sensor's service and store the latest data to be accessed by MachinaNDoFSensorDriverClientNode
later. The MachinaNDoFSensorDriverClientNode
is responsible for periodically polling each instance of MachinaNDoFSensorDriverClient
for the latest data and then publishing out the joint outputTACO
from iceoryx and a std::shared_mutex
StaticSingleThreadedExecutor
. In the client implementation we have 1 timer callback and M
sensor client callbacks (where M
is the number of sensors) that are all being handled by the ROS 2 executor. Due to the thread safety modes implemented above, they can all be handled concurrently. Thus, we switch the executor for the client implementation to a MultiThreadedExecutor
and have each of the client callbacks and the timer callbacks be on their own callback group so that the executor runs them in parallelLifecycleNode
, Composition
and TypeAdapters
from the service implementation still applies here.machina_ndof_sensor_driver_system_tests
tmux
config to record a ROS bag for post analysis. There is a script in tools/scripts/examine_sensor_output.py
which will print out some stats for you as well as copy a Markdown representation into your clipboardtests
. This was adapted from the base one provided by Machina. I modified it to accept a uint64
for sample size instead of string, to replace the last element in the data with the sum of all elements before it so that it could act as a CRC of sorts, specify some of the dtypes in Numpy arrays and accept arguments from the command line.Outside of these core packages there are the following:
tools/image
).devcontainer
).github/workflows
to build and test the driver implementation as well as generate the documentation pageYou can build the Docker container by using
The IMG_NAME can be swapped with anything you find convenient
To build the stack, just type
This will build the stack with locks enabled. If you want to turn it off
Both the above make
targets build with Release
. If you want a Debug
build then
will build with Debug
enabled.
If you want to go for a fresh build
And then type the make
command based off how you want to build
If you want to generate the docs
If you want to delete the docs
If you want to delete everything
If you want to run tests (right now just runs style tests)
If you want to clean up the results
If you want to reformat your code
where you can change the PATHS
as needed
All the build and test target also accept a PACKAGES=
in case you want to run up to specific packages. For example
If you want to just run everything at once there is a reference tmux
config provided under tools/tmux_configs
. You can run it by
It will spawn 2 sensors, run a launch file with 2 services to connect to each sensor and the client. It will also run the lifecycle transitions for you and after a minute deactivate them. Finally, it will record a MCAP for you. Make sure to close the MCAP using Ctrl + C before you kill the session so that it can close properly
Once you have that bag you can run
It will print out some summary statistics and copy a Markdown version of the statistics to your clipboard. You will see some examples later in the next section
The data for these comparisons has been uploaded here
The summary with locks enabled was
publisher_time_diff_sec | sensor_0_time_diff_sec | sensor_0_average_deviation | sensor_0_data_valid | sensor_1_time_diff_sec | sensor_1_average_deviation | sensor_1_data_valid | |
---|---|---|---|---|---|---|---|
count | 1565 | 1565 | 1565 | 1565 | 1565 | 1565 | 1565 |
mean | 0.0369636 | 0.0369822 | 0.177633 | 1 | 0.0369916 | 0.177166 | 1 |
std | 0.0189857 | 0.0361484 | 0.173388 | 0 | 0.0403624 | 0.173364 | 0 |
min | 0.000128512 | 0 | 0 | 1 | 0 | 0 | 1 |
25% | 0.022933 | 0 | 0 | 1 | 0 | 0 | 1 |
50% | 0.0367444 | 0.0623309 | 0.319733 | 1 | 0.0438513 | 0.32158 | 1 |
75% | 0.0501245 | 0.0722235 | 0.345725 | 1 | 0.0545754 | 0.34565 | 1 |
max | 0.109515 | 0.0800883 | 0.403066 | 1 | 0.157095 | 0.393948 | 1 |
The summary with locks disabled (so lock free) was
publisher_time_diff_sec | sensor_0_time_diff_sec | sensor_0_average_deviation | sensor_0_data_valid | sensor_1_time_diff_sec | sensor_1_average_deviation | sensor_1_data_valid | |
---|---|---|---|---|---|---|---|
count | 13063 | 13063 | 13063 | 13063 | 13063 | 13063 | 13063 |
mean | 0.00444128 | 0.00444002 | 0.0212402 | 1 | 0.0044335 | 0.0312561 | 1 |
std | 0.00160622 | 0.017368 | 0.083015 | 0 | 0.014126 | 0.0993279 | 0 |
min | 7.0144e-05 | 0 | 0 | 1 | 0 | 0 | 1 |
25% | 0.00496973 | 0 | 0 | 1 | 0 | 0 | 1 |
50% | 0.00505574 | 0 | 0 | 1 | 0 | 0 | 1 |
75% | 0.00508288 | 0 | 0 | 1 | 0 | 0 | 1 |
max | 0.0500941 | 0.0818102 | 0.397955 | 1 | 0.061439 | 0.396857 | 1 |
Couple of observations
sensor_i_data_valid
true at all times during publishpublisher_time_diff_sec
is basically a measure of how often the timer callback was triggered in the client implementation. In these experiments they were set to poll at 500Hz.sensor_i_time_diff_sec
is a measure of how often the sensor data was updated in b/w service calls. We can see that it has been similarly throttled like the publisher_time_diff_sec
. However, in the lock free case we see a median of 0 which means that very often we were sending out outdated data even though it was polling at 2000Hz from the TCP clientsensor_i_average_deviation
we can see that the data deviation was much lower in the lock free case than in the lock case. Again the median hits 0 here for the lock free case which is a sign that we are constantly receiving outdated dataFrom the debugging I did, the issue still seems to be in thread contention. I believe as the number of threads/sensors scale up the lock free implementation will do a better job of producing data reliably. I do think the 2 element container
method mentioned earlier would significantly reduce the contention. But it needs to be tested