AP_DDS: Rally Interface to get-set rally points.

This commit is contained in:
Tiziano Fiorenzani 2024-10-29 15:02:48 +00:00
parent dd37065bb5
commit 92ce630cda
12 changed files with 504 additions and 2 deletions

View File

@ -0,0 +1,200 @@
# Copyright 2023 ArduPilot.org.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Tests the Rally Point interface.
Clear the list, adds points and verifies that they are correctly updated.
colcon test --packages-select ardupilot_dds_tests \
--event-handlers=console_cohesion+ --pytest-args -k test_rally_point
"""
import rclpy
import time
import launch_pytest
from rclpy.node import Node
from builtin_interfaces.msg import Time
from ardupilot_msgs.msg import Rally
from ardupilot_msgs.srv import RallyGet
from ardupilot_msgs.srv import RallySet
import pytest
from launch_pytest.tools import process as process_tools
from launch import LaunchDescription
import threading
RALLY_0 = Rally()
RALLY_0.point.latitude =-35.0
RALLY_0.point.longitude = 149.0
RALLY_0.point.altitude = 400.0
RALLY_0.altitude_frame = 2
class RallyControl(Node):
"""Push/Pull Rally points."""
def __init__(self):
"""Initialise the node."""
super().__init__("rally_service")
self.clear_rally_event = threading.Event()
self.add_rally_event = threading.Event()
self.get_rally_event = threading.Event()
self._client_rally_get = self.create_client(RallyGet, "/ap/rally_get")
self._client_rally_set = self.create_client(RallySet, "/ap/rally_set")
# Add a spin thread.
self.ros_spin_thread = threading.Thread(target=lambda node: rclpy.spin(node), args=(self,))
self.ros_spin_thread.start()
def _clear_list(self):
req = RallySet.Request()
req.clear = True
self.future = self._client_rally_set.call_async(req)
time.sleep(1.0)
if self.future.result() is None:
return False
else:
print(self.future)
print(self.future.result())
return self.future.result().size == 0
def _send_rally(self, rally):
req = RallySet.Request()
req.clear = False
req.rally = rally
self.future = self._client_rally_set.call_async(req)
time.sleep(1.0)
return self.future.result()
def _get_rally(self):
req = RallyGet.Request()
req.index = 0
self.future = self._client_rally_get.call_async(req)
time.sleep(1.0)
return self.future.result()
#-------------- PROCESSES -----------------
def process_clear(self):
print("---> Process start")
while True:
if self._clear_list():
print("---> List Cleared")
self.clear_rally_event.set()
return
time.sleep(1)
def clear_list(self):
try:
self.clear_thread.stop()
except:
print("---> Starting thread")
self.clear_thread = threading.Thread(target=self.process_clear)
self.clear_thread.start()
def process_send(self, rally):
while True:
response = self._send_rally(rally)
if response is not None:
if response.success:
self.add_rally_event.set()
return
time.sleep(1)
def send_rally(self, rally):
try:
self.send_thread.stop()
except:
self.send_thread = threading.Thread(target=self.process_send, args=[rally])
self.send_thread.start()
def process_compare(self, rally):
while True:
response = self._get_rally()
if response is not None:
if response.success:
if (abs(response.rally.point.latitude - rally.point.latitude) < 1e-7
and abs(response.rally.point.longitude - rally.point.longitude) < 1e-7
and response.rally.altitude_frame == rally.altitude_frame):
self.get_rally_event.set()
return
time.sleep(1)
def get_and_compare_rally(self, rally):
try:
self.get_thread.stop()
except:
self.get_thread = threading.Thread(target=self.process_compare, args=[rally])
self.get_thread.start()
@launch_pytest.fixture
def launch_sitl_copter_dds_serial(sitl_copter_dds_serial):
"""Fixture to create the launch description."""
sitl_ld, sitl_actions = sitl_copter_dds_serial
ld = LaunchDescription(
[
sitl_ld,
launch_pytest.actions.ReadyToTest(),
]
)
actions = sitl_actions
yield ld, actions
@launch_pytest.fixture
def launch_sitl_copter_dds_udp(sitl_copter_dds_udp):
"""Fixture to create the launch description."""
sitl_ld, sitl_actions = sitl_copter_dds_udp
ld = LaunchDescription(
[
sitl_ld,
launch_pytest.actions.ReadyToTest(),
]
)
actions = sitl_actions
yield ld, actions
@pytest.mark.launch(fixture=launch_sitl_copter_dds_udp)
def test_dds_udp_rally(launch_context, launch_sitl_copter_dds_udp):
"""Test Rally Points with AP_DDS."""
_, actions = launch_sitl_copter_dds_udp
micro_ros_agent = actions["micro_ros_agent"].action
mavproxy = actions["mavproxy"].action
sitl = actions["sitl"].action
# Wait for process to start.
process_tools.wait_for_start_sync(launch_context, micro_ros_agent, timeout=2)
process_tools.wait_for_start_sync(launch_context, mavproxy, timeout=2)
process_tools.wait_for_start_sync(launch_context, sitl, timeout=2)
rclpy.init()
time.sleep(5)
try:
node = RallyControl()
node.clear_list()
clear_flag = node.clear_rally_event.wait(20)
assert clear_flag, "Could not clear"
node.send_rally(RALLY_0)
send_flag = node.add_rally_event.wait(10)
assert send_flag, "Could not send Rally"
node.get_and_compare_rally(RALLY_0)
send_flag = node.get_rally_event.wait(10)
assert send_flag, "Wrong Rally point back"
finally:
rclpy.shutdown()
yield

View File

@ -5,6 +5,7 @@ project(ardupilot_msgs)
# Find dependencies.
find_package(ament_cmake REQUIRED)
find_package(geographic_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(std_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
@ -14,11 +15,14 @@ find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/GlobalPosition.msg"
"msg/Rally.msg"
"msg/Status.msg"
"srv/ArmMotors.srv"
"srv/ModeSwitch.srv"
"srv/Takeoff.srv"
DEPENDENCIES geometry_msgs std_msgs
"srv/RallyGet.srv"
"srv/RallySet.srv"
DEPENDENCIES geographic_msgs geometry_msgs std_msgs
ADD_LINTER_TESTS
)

View File

@ -0,0 +1,11 @@
# Rally message provides a geographic point and
# its altitude reference frame.
geographic_msgs/GeoPoint point
# Altitude reference frame
uint8 ALTITUDE_FRAME_ABSOLUTE = 0
uint8 ALTITUDE_FRAME_HOME = 1
uint8 ALTITUDE_FRAME_ORIGIN = 2
uint8 ALTITUDE_FRAME_TERRAIN = 3
uint8 altitude_frame

View File

@ -0,0 +1,11 @@
# This service requests a Rally point from its ID.
# Rally Point ID.
uint8 index
---
# True is the retrieved point is valid.
bool success
# Size of Rally Points list.
uint8 size
# Rally point.
ardupilot_msgs/Rally rally

View File

@ -0,0 +1,12 @@
# This service appends a Rally Point to the list or clears the whole list.
# Rally Point.
ardupilot_msgs/Rally rally
# True to clear the whole list (rally entry is ignored).
bool clear
---
# True if the action is successful.
bool success
# Size of Rally Points list.
uint8 size

View File

@ -32,6 +32,11 @@
#if AP_DDS_VTOL_TAKEOFF_SERVER_ENABLED
#include "ardupilot_msgs/srv/Takeoff.h"
#endif // AP_DDS_VTOL_TAKEOFF_SERVER_ENABLED
#if AP_DDS_RALLY_SERVER_ENABLED
#include <AP_Rally/AP_Rally.h>
#include "ardupilot_msgs/srv/RallyGet.h"
#include "ardupilot_msgs/srv/RallySet.h"
#endif // AP_DDS_RALLY_SERVER_ENABLED
#if AP_EXTERNAL_CONTROL_ENABLED
#include "AP_DDS_ExternalControl.h"
@ -976,6 +981,110 @@ void AP_DDS_Client::on_request(uxrSession* uxr_session, uxrObjectId object_id, u
break;
}
#endif //AP_DDS_ARM_CHECK_SERVER_ENABLED
#if AP_DDS_RALLY_SERVER_ENABLED
case services[to_underlying(ServiceIndex::GET_RALLY)].rep_id: {
ardupilot_msgs_srv_RallyGet_Request rally_get_request;
ardupilot_msgs_srv_RallyGet_Response rally_get_response;
const bool deserialize_success = ardupilot_msgs_srv_RallyGet_Request_deserialize_topic(ub, &rally_get_request);
if (deserialize_success == false) {
break;
}
const uxrObjectId replier_id = {
.id = services[to_underlying(ServiceIndex::GET_RALLY)].rep_id,
.type = UXR_REPLIER_ID
};
RallyLocation rally_location {};
const AP_Rally *rally = AP::rally();
if (rally->get_rally_point_with_index(rally_get_request.index, rally_location)) {
rally_get_response.success = true;
rally_get_response.size = rally->get_rally_total();
rally_get_response.rally.point.latitude = rally_location.lat * 1e-7;
rally_get_response.rally.point.longitude = rally_location.lng * 1e-7;
rally_get_response.rally.point.altitude = rally_location.alt;
bool alt_frame_valid = rally_location.flags & (1 << 2);
if (alt_frame_valid) {
rally_get_response.rally.altitude_frame = (rally_location.flags >> 3) & ((1 << 2) - 1);
} else {
rally_get_response.rally.altitude_frame = static_cast<uint8_t>(Location::AltFrame::ABOVE_HOME);
}
} else {
rally_get_response.success = false;
}
uint8_t reply_buffer[ardupilot_msgs_srv_RallyGet_Response_size_of_topic(&rally_get_response, 0)] {};
ucdrBuffer reply_ub;
ucdr_init_buffer(&reply_ub, reply_buffer, sizeof(reply_buffer));
const bool serialize_success = ardupilot_msgs_srv_RallyGet_Response_serialize_topic(&reply_ub, &rally_get_response);
if (serialize_success == false) {
break;
}
uxr_buffer_reply(uxr_session, reliable_out, replier_id, sample_id, reply_buffer, ucdr_buffer_length(&reply_ub));
break;
}
case services[to_underlying(ServiceIndex::SET_RALLY)].rep_id: {
ardupilot_msgs_srv_RallySet_Request rally_set_request;
ardupilot_msgs_srv_RallySet_Response rally_set_response;
const bool deserialize_success = ardupilot_msgs_srv_RallySet_Request_deserialize_topic(ub, &rally_set_request);
if (deserialize_success == false) {
break;
}
const uxrObjectId replier_id = {
.id = services[to_underlying(ServiceIndex::SET_RALLY)].rep_id,
.type = UXR_REPLIER_ID
};
RallyLocation rally_location{};
AP_Rally *rally = AP::rally();
if (rally_set_request.clear) {
rally->truncate(0);
rally_set_response.success = rally->get_rally_total() == 0;
} else {
rally_location.lat = static_cast<int32_t>(rally_set_request.rally.point.latitude * 1e7);
rally_location.lng = static_cast<int32_t>(rally_set_request.rally.point.longitude * 1e7);
rally_location.alt = static_cast<int16_t>(rally_set_request.rally.point.altitude);
rally_location.flags = 0;
rally_location.flags |= (1 << 2); // Use the provided frame.
rally_location.flags |= (rally_set_request.rally.altitude_frame << 3);
if (rally_location.lat != 0 && rally_location.lng != 0
&& (rally_set_request.rally.altitude_frame == static_cast<uint8_t>(Location::AltFrame::ABOVE_HOME)
|| rally_set_request.rally.altitude_frame == static_cast<uint8_t>(Location::AltFrame::ABOVE_ORIGIN)
|| rally_set_request.rally.altitude_frame == static_cast<uint8_t>(Location::AltFrame::ABOVE_TERRAIN)
|| rally_set_request.rally.altitude_frame == static_cast<uint8_t>(Location::AltFrame::ABSOLUTE))) {
if (rally->append(rally_location)) {
rally_set_response.success = true;
} else {
rally_set_response.success = false;
}
} else {
rally_set_response.success = false;
}
}
rally_set_response.size = rally->get_rally_total();
uint8_t reply_buffer[ardupilot_msgs_srv_RallySet_Response_size_of_topic(&rally_set_response, 0)] {};
ucdrBuffer reply_ub;
ucdr_init_buffer(&reply_ub, reply_buffer, sizeof(reply_buffer));
const bool serialize_success = ardupilot_msgs_srv_RallySet_Response_serialize_topic(&reply_ub, &rally_set_response);
if (serialize_success == false) {
break;
}
uxr_buffer_reply(uxr_session, reliable_out, replier_id, sample_id, reply_buffer, ucdr_buffer_length(&reply_ub));
break;
}
#endif // AP_DDS_RALLY_SERVER_ENABLED
#if AP_DDS_PARAMETER_SERVER_ENABLED
case services[to_underlying(ServiceIndex::SET_PARAMETERS)].rep_id: {
const bool deserialize_success = rcl_interfaces_srv_SetParameters_Request_deserialize_topic(ub, &set_parameter_request);

View File

@ -14,6 +14,10 @@ enum class ServiceIndex: uint8_t {
#if AP_DDS_VTOL_TAKEOFF_SERVER_ENABLED
TAKEOFF,
#endif // AP_DDS_VTOL_TAKEOFF_SERVER_ENABLED
#if AP_DDS_RALLY_SERVER_ENABLED
GET_RALLY,
SET_RALLY,
#endif // AP_DDS_RALLY_SERVER_ENABLED
#if AP_DDS_PARAMETER_SERVER_ENABLED
SET_PARAMETERS,
GET_PARAMETERS
@ -81,6 +85,28 @@ constexpr struct AP_DDS_Client::Service_table AP_DDS_Client::services[] = {
.reply_topic_name = "rr/ap/experimental/takeoffReply",
},
#endif // AP_DDS_VTOL_TAKEOFF_SERVER_ENABLED
#if AP_DDS_RALLY_SERVER_ENABLED
{
.req_id = to_underlying(ServiceIndex::GET_RALLY),
.rep_id = to_underlying(ServiceIndex::GET_RALLY),
.service_rr = Service_rr::Replier,
.service_name = "rs/ap/rally_getService",
.request_type = "ardupilot_msgs::srv::dds_::RallyGet_Request_",
.reply_type = "ardupilot_msgs::srv::dds_::RallyGet_Response_",
.request_topic_name = "rq/ap/rally_getRequest",
.reply_topic_name = "rr/ap/rally_getReply",
},
{
.req_id = to_underlying(ServiceIndex::SET_RALLY),
.rep_id = to_underlying(ServiceIndex::SET_RALLY),
.service_rr = Service_rr::Replier,
.service_name = "rs/ap/rally_setService",
.request_type = "ardupilot_msgs::srv::dds_::RallySet_Request_",
.reply_type = "ardupilot_msgs::srv::dds_::RallySet_Response_",
.request_topic_name = "rq/ap/rally_setRequest",
.reply_topic_name = "rr/ap/rally_setReply",
},
#endif // AP_DDS_RALLY_SERVER_ENABLED
#if AP_DDS_PARAMETER_SERVER_ENABLED
{
.req_id = to_underlying(ServiceIndex::SET_PARAMETERS),

View File

@ -145,6 +145,10 @@
#define AP_DDS_VTOL_TAKEOFF_SERVER_ENABLED 1
#endif
#ifndef AP_DDS_RALLY_SERVER_ENABLED
#define AP_DDS_RALLY_SERVER_ENABLED 1
#endif
#ifndef AP_DDS_PARAMETER_SERVER_ENABLED
#define AP_DDS_PARAMETER_SERVER_ENABLED 1
#endif

View File

@ -0,0 +1,23 @@
// generated from rosidl_adapter/resource/msg.idl.em
// with input from ardupilot_msgs/msg/Rally.msg
// generated code does not contain a copyright notice
#include "geographic_msgs/msg/GeoPoint.idl"
module ardupilot_msgs {
module msg {
module Rally_Constants {
@verbatim (language="comment", text=
"Altitude reference frame")
const uint8 ALTITUDE_FRAME_ABSOLUTE = 0;
const uint8 ALTITUDE_FRAME_HOME = 1;
const uint8 ALTITUDE_FRAME_ORIGIN = 2;
const uint8 ALTITUDE_FRAME_TERRAIN = 3;
};
struct Rally {
geographic_msgs::msg::GeoPoint point;
uint8 altitude_frame;
};
};
};

View File

@ -0,0 +1,30 @@
// generated from rosidl_adapter/resource/srv.idl.em
// with input from ardupilot_msgs/srv/RallyGet.srv
// generated code does not contain a copyright notice
#include "ardupilot_msgs/msg/Rally.idl"
module ardupilot_msgs {
module srv {
@verbatim (language="comment", text=
"This service requests a Rally point from its ID.")
struct RallyGet_Request {
@verbatim (language="comment", text=
"Rally Point ID.")
uint8 index;
};
@verbatim (language="comment", text=
"True is the retrieved point is valid.")
struct RallyGet_Response {
boolean success;
@verbatim (language="comment", text=
"Size of Rally Points list.")
uint8 size;
@verbatim (language="comment", text=
"Rally point.")
ardupilot_msgs::msg::Rally rally;
};
};
};

View File

@ -0,0 +1,29 @@
// generated from rosidl_adapter/resource/srv.idl.em
// with input from ardupilot_msgs/srv/RallySet.srv
// generated code does not contain a copyright notice
#include "ardupilot_msgs/msg/Rally.idl"
module ardupilot_msgs {
module srv {
struct RallySet_Request {
@verbatim (language="comment", text=
"This service appends a Rally Point to the list or clears the whole list." "\n"
"Rally Point.")
ardupilot_msgs::msg::Rally rally;
@verbatim (language="comment", text=
"True to clear the whole list (rally entry is ignored).")
boolean clear;
};
@verbatim (language="comment", text=
"True if the action is successful.")
struct RallySet_Response {
boolean success;
@verbatim (language="comment", text=
"Size of Rally Points list.")
uint8 size;
};
};
};

View File

@ -211,6 +211,8 @@ $ ros2 service list
/ap/mode_switch
/ap/prearm_check
/ap/experimental/takeoff
/ap/rally_get
/ap/rally_set
---
```
@ -238,6 +240,8 @@ $ ros2 service list -t
/ap/mode_switch [ardupilot_msgs/srv/ModeSwitch]
/ap/prearm_check [std_srvs/srv/Trigger]
/ap/experimental/takeoff [ardupilot_msgs/srv/Takeoff]
/ap/rally_get [ardupilot_msgs/srv/RallyGet]
/ap/rally_set [ardupilot_msgs/srv/RallySet]
```
Call the arm motors service:
@ -284,6 +288,45 @@ response:
ardupilot_msgs.srv.Takeoff_Response(status=True)
```
Clear the Rally Points list (the provided Point will be ignored):
```bash
ros2 service call /ap/rally_set ardupilot_msgs/srv/RallySet "{clear: true}"
requester: making request: ardupilot_msgs.srv.RallySet_Request(rally=ardupilot_msgs.msg.Rally(point=geographic_msgs.msg.GeoPoint(latitude=0.0, longitude=0.0, altitude=0.0), altitude_frame=0), clear=True)
response:
ardupilot_msgs.srv.RallySet_Response(success=True, size=0)
```
Append a Rally Point:
Rally points units of measure are:
* degrees for the latitude and longitude (up to 7 decimal digits resolution);
* meters for the altitude (with no decimals).
The `altitude_frame` specifies the datum for the altitude, among Home, Absolute, Terrain or EKF Origin
(use the enumerator specified in the message definition).
```bash
ros2 service call /ap/rally_set ardupilot_msgs/srv/RallySet "{rally: {altitude_frame: 1, point: {latitude: -35.12345, longitude: 151.12345, altitude: 400}}}"
requester: making request: ardupilot_msgs.srv.RallySet_Request(rally=ardupilot_msgs.msg.Rally(point=geographic_msgs.msg.GeoPoint(latitude=-35.12345, longitude=151.12345, altitude=400.0), altitude_frame=1), clear=False)
response:
ardupilot_msgs.srv.RallySet_Response(success=True, size=1)
```
Get a specific Rally Point (the same service can be used to request the size of the Rally list):
```bash
ros2 service call /ap/rally_get ardupilot_msgs/srv/RallyGet "index: 0"
requester: making request: ardupilot_msgs.srv.RallyGet_Request(index=0)
response:
ardupilot_msgs.srv.RallyGet_Response(success=True, size=1, rally=ardupilot_msgs.msg.Rally(point=geographic_msgs.msg.GeoPoint(latitude=35.12345504760742, longitude=151.12345176604336, altitude=400.0), altitude_frame=1))
```
## Commanding using ROS 2 Topics
The following topic can be used to control the vehicle.
@ -308,7 +351,7 @@ ros2 topic pub /ap/cmd_gps_pose ardupilot_msgs/msg/GlobalPosition "{latitude: 34
publisher: beginning loop
publishing #1: ardupilot_msgs.msg.GlobalPosition(header=std_msgs.msg.Header(stamp=builtin_interfaces.msg.Time(sec=0, nanosec=0), frame_id=''), coordinate_frame=0, type_mask=0, latitude=34.0, longitude=118.0, altitude=1000.0, velocity=geometry_msgs.msg.Twist(linear=geometry_msgs.msg.Vector3(x=0.0, y=0.0, z=0.0), angular=geometry_msgs.msg.Vector3(x=0.0, y=0.0, z=0.0)), acceleration_or_force=geometry_msgs.msg.Twist(linear=geometry_msgs.msg.Vector3(x=0.0, y=0.0, z=0.0), angular=geometry_msgs.msg.Vector3(x=0.0, y=0.0, z=0.0)), yaw=0.0)
```
## Contributing to `AP_DDS` library
### Adding DDS messages to Ardupilot