807 lines
30 KiB
C++
807 lines
30 KiB
C++
#include "service/grpc/server/include/grpc_agv_service.h"
|
|
|
|
#include <chrono>
|
|
#include <future>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include <grpcpp/grpcpp.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
|
|
#include "manager/control_authority_manager/include/control_authority_manager.h"
|
|
#include "manager/device_manager/include/device_manager.h"
|
|
#include "service/grpc/stop_all/include/stop_all_admission_gate.h"
|
|
|
|
namespace cmvr::service {
|
|
namespace {
|
|
|
|
constexpr char kNativeErrorMessage[] =
|
|
"SEER Robokit command failed: ret_code=41200, err_msg=speed_illegal";
|
|
constexpr char kNativeNavigationErrorMessage[] =
|
|
"SEER Robokit command failed: ret_code=43051, err_msg=planner_rejected_pose";
|
|
|
|
class FakeAgv final : public device::AbstractAGV {
|
|
public:
|
|
FakeAgv()
|
|
{
|
|
id_ = "test-agv";
|
|
}
|
|
|
|
std::string typeName() const override { return "FakeAgv"; }
|
|
|
|
device::AgvResult emergencyStop() override
|
|
{
|
|
++emergency_stop_calls_;
|
|
emergency_stop_barrier_observed_ = normalLeaseIsBlocked_(
|
|
"test-probe:emergencyStop");
|
|
return device::AgvResult::success();
|
|
}
|
|
|
|
device::AgvResult navigateToPose(
|
|
const math::Pose2d& pose,
|
|
const device::AgvMotionOptions& options,
|
|
const device::AgvAdapterParams&) override
|
|
{
|
|
++navigate_pose_calls_;
|
|
pose_cancellation_bound_ =
|
|
static_cast<bool>(options.cancellation_requested);
|
|
pose_cancellation_requested_during_call_ =
|
|
pose_cancellation_bound_ && options.cancellation_requested();
|
|
if (pose_cancellation_requested_during_call_) {
|
|
return device::AgvResult::failure(
|
|
device::AgvErrorCode::TaskCanceled,
|
|
"navigation canceled before fake device dispatch");
|
|
}
|
|
pose_ = pose;
|
|
pose_options_ = options;
|
|
pose_options_.cancellation_requested = {};
|
|
return pose_result_;
|
|
}
|
|
|
|
device::AgvResult navigateToStation(
|
|
const std::string& station_id,
|
|
const device::AgvMotionOptions& options,
|
|
const device::AgvAdapterParams& adapter_params) override
|
|
{
|
|
station_id_ = station_id;
|
|
station_options_ = options;
|
|
station_adapter_params_ = adapter_params;
|
|
station_cancellation_bound_ =
|
|
static_cast<bool>(options.cancellation_requested);
|
|
station_cancellation_requested_during_call_ =
|
|
station_cancellation_bound_ && options.cancellation_requested();
|
|
station_options_.cancellation_requested = {};
|
|
return device::AgvResult::success();
|
|
}
|
|
|
|
device::AgvResult followPath(
|
|
const std::vector<device::AgvPathSegment>& path,
|
|
const device::AgvMotionOptions& options) override
|
|
{
|
|
path_ = path;
|
|
path_options_ = options;
|
|
path_cancellation_bound_ =
|
|
static_cast<bool>(options.cancellation_requested);
|
|
path_cancellation_requested_during_call_ =
|
|
path_cancellation_bound_ && options.cancellation_requested();
|
|
path_options_.cancellation_requested = {};
|
|
return device::AgvResult::success();
|
|
}
|
|
|
|
device::AgvResult setVelocity(const device::AgvVelocity&) override
|
|
{
|
|
++set_velocity_calls_;
|
|
return device::AgvResult::failure(
|
|
device::AgvErrorCode::CommandFailed,
|
|
kNativeErrorMessage);
|
|
}
|
|
|
|
device::AgvResult cancelNavigation() override
|
|
{
|
|
++cancel_navigation_calls_;
|
|
cancel_navigation_barrier_observed_ = normalLeaseIsBlocked_(
|
|
"test-probe:cancelNavigation");
|
|
return device::AgvResult::success();
|
|
}
|
|
|
|
device::AgvResult stopVelocityControl() override
|
|
{
|
|
++stop_velocity_calls_;
|
|
stop_velocity_barrier_observed_ = normalLeaseIsBlocked_(
|
|
"test-probe:stopVelocityControl");
|
|
return device::AgvResult::success();
|
|
}
|
|
|
|
device::AgvResult confirmMotionStopped() override
|
|
{
|
|
++confirm_stopped_calls_;
|
|
confirm_stopped_barrier_observed_ = normalLeaseIsBlocked_(
|
|
"test-probe:confirmMotionStopped");
|
|
return confirm_stopped_result_;
|
|
}
|
|
|
|
bool normalLeaseIsBlocked_(const std::string& owner)
|
|
{
|
|
auto& authority = control::ControlAuthorityManager::instance();
|
|
const auto probe = authority.tryAcquire(
|
|
id_, owner, std::chrono::hours(1));
|
|
if (probe.acquired) {
|
|
authority.release(probe.token);
|
|
}
|
|
return !probe.acquired;
|
|
}
|
|
|
|
math::Pose2d pose_{};
|
|
device::AgvMotionOptions pose_options_;
|
|
device::AgvResult pose_result_{device::AgvResult::success()};
|
|
std::string station_id_;
|
|
device::AgvMotionOptions station_options_;
|
|
device::AgvAdapterParams station_adapter_params_;
|
|
std::vector<device::AgvPathSegment> path_;
|
|
device::AgvMotionOptions path_options_;
|
|
bool pose_cancellation_bound_{false};
|
|
bool pose_cancellation_requested_during_call_{false};
|
|
bool station_cancellation_bound_{false};
|
|
bool station_cancellation_requested_during_call_{false};
|
|
bool path_cancellation_bound_{false};
|
|
bool path_cancellation_requested_during_call_{false};
|
|
int emergency_stop_calls_{0};
|
|
int cancel_navigation_calls_{0};
|
|
int stop_velocity_calls_{0};
|
|
int confirm_stopped_calls_{0};
|
|
int set_velocity_calls_{0};
|
|
int navigate_pose_calls_{0};
|
|
bool emergency_stop_barrier_observed_{false};
|
|
bool cancel_navigation_barrier_observed_{false};
|
|
bool stop_velocity_barrier_observed_{false};
|
|
bool confirm_stopped_barrier_observed_{false};
|
|
device::AgvResult confirm_stopped_result_{
|
|
device::AgvResult::success()};
|
|
};
|
|
|
|
class LegacyFollowPathAgv final : public device::AbstractAGV {
|
|
public:
|
|
std::string typeName() const override { return "LegacyFollowPathAgv"; }
|
|
|
|
device::AgvResult followPath(
|
|
const std::vector<device::AgvPathSegment>& path) override
|
|
{
|
|
path_ = path;
|
|
return device::AgvResult::success();
|
|
}
|
|
|
|
std::vector<device::AgvPathSegment> path_;
|
|
};
|
|
|
|
class GrpcAgvServiceTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override
|
|
{
|
|
control::ControlAuthorityManager::instance().clear();
|
|
globalStopAllAdmissionGate().clearForTesting();
|
|
config::DeviceManagerConfig config;
|
|
auto& manager = device::DeviceManager::getInstance(config);
|
|
agv_ = std::make_shared<FakeAgv>();
|
|
manager.registerDevice(agv_);
|
|
service_ = std::make_unique<gRPCAgvServiceImpl>();
|
|
}
|
|
|
|
void TearDown() override
|
|
{
|
|
service_.reset();
|
|
agv_.reset();
|
|
device::DeviceManager::destroyInstance();
|
|
control::ControlAuthorityManager::instance().clear();
|
|
globalStopAllAdmissionGate().clearForTesting();
|
|
}
|
|
|
|
std::shared_ptr<FakeAgv> agv_;
|
|
std::unique_ptr<gRPCAgvServiceImpl> service_;
|
|
};
|
|
|
|
void setMotionOptions(msgs::AgvMotionOptions* options)
|
|
{
|
|
options->set_max_speed(0.4);
|
|
options->set_max_angular_speed(0.5);
|
|
options->set_max_acceleration(0.6);
|
|
options->set_max_angular_acceleration(0.7);
|
|
options->set_reach_distance(0.08);
|
|
options->set_reach_angle(0.09);
|
|
options->set_wait_timeout_ms(1234);
|
|
options->set_poll_interval_ms(55);
|
|
}
|
|
|
|
void expectMotionOptions(const device::AgvMotionOptions& options)
|
|
{
|
|
EXPECT_DOUBLE_EQ(options.max_speed, 0.4);
|
|
EXPECT_DOUBLE_EQ(options.max_angular_speed, 0.5);
|
|
EXPECT_DOUBLE_EQ(options.max_acceleration, 0.6);
|
|
EXPECT_DOUBLE_EQ(options.max_angular_acceleration, 0.7);
|
|
EXPECT_DOUBLE_EQ(options.reach_distance, 0.08);
|
|
EXPECT_DOUBLE_EQ(options.reach_angle, 0.09);
|
|
EXPECT_EQ(options.wait_timeout_ms, 1234);
|
|
EXPECT_EQ(options.poll_interval_ms, 55);
|
|
EXPECT_FALSE(options.asynchronous);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, NavigationRpcsForwardSpeedAndAccelerationOptions)
|
|
{
|
|
api::AgvNavigateToPoseCommand_Request pose_request;
|
|
pose_request.mutable_header()->set_device_id("test-agv");
|
|
pose_request.mutable_pose()->set_x(1.0);
|
|
pose_request.mutable_pose()->set_y(2.0);
|
|
pose_request.mutable_pose()->set_theta(0.5);
|
|
setMotionOptions(pose_request.mutable_options());
|
|
api::AgvNavigateToPoseCommand_Feedback pose_response;
|
|
grpc::ServerContext pose_context;
|
|
|
|
const auto pose_status = service_->navigateToPose(
|
|
&pose_context,
|
|
&pose_request,
|
|
&pose_response);
|
|
|
|
ASSERT_TRUE(pose_status.ok()) << pose_status.error_message();
|
|
EXPECT_TRUE(pose_response.header().success());
|
|
EXPECT_DOUBLE_EQ(agv_->pose_.x, 1.0);
|
|
EXPECT_DOUBLE_EQ(agv_->pose_.y, 2.0);
|
|
EXPECT_DOUBLE_EQ(agv_->pose_.theta, 0.5);
|
|
expectMotionOptions(agv_->pose_options_);
|
|
EXPECT_TRUE(agv_->pose_cancellation_bound_);
|
|
EXPECT_FALSE(agv_->pose_cancellation_requested_during_call_);
|
|
|
|
api::AgvNavigateToStationCommand_Request station_request;
|
|
station_request.mutable_header()->set_device_id("test-agv");
|
|
station_request.set_station_id("station-1");
|
|
setMotionOptions(station_request.mutable_options());
|
|
api::AgvNavigateToStationCommand_Feedback station_response;
|
|
grpc::ServerContext station_context;
|
|
|
|
const auto station_status = service_->navigateToStation(
|
|
&station_context,
|
|
&station_request,
|
|
&station_response);
|
|
|
|
ASSERT_TRUE(station_status.ok()) << station_status.error_message();
|
|
EXPECT_TRUE(station_response.header().success());
|
|
EXPECT_EQ(agv_->station_id_, "station-1");
|
|
expectMotionOptions(agv_->station_options_);
|
|
EXPECT_TRUE(agv_->station_cancellation_bound_);
|
|
EXPECT_FALSE(agv_->station_cancellation_requested_during_call_);
|
|
|
|
api::AgvFollowPathCommand_Request path_request;
|
|
path_request.mutable_header()->set_device_id("test-agv");
|
|
auto* segment = path_request.add_path();
|
|
segment->set_source_station("station-1");
|
|
segment->set_target_station("station-2");
|
|
setMotionOptions(path_request.mutable_options());
|
|
api::AgvFollowPathCommand_Feedback path_response;
|
|
grpc::ServerContext path_context;
|
|
|
|
const auto path_status = service_->followPath(
|
|
&path_context,
|
|
&path_request,
|
|
&path_response);
|
|
|
|
ASSERT_TRUE(path_status.ok()) << path_status.error_message();
|
|
EXPECT_TRUE(path_response.header().success());
|
|
ASSERT_EQ(agv_->path_.size(), 1U);
|
|
EXPECT_EQ(agv_->path_[0].source_station, "station-1");
|
|
EXPECT_EQ(agv_->path_[0].target_station, "station-2");
|
|
expectMotionOptions(agv_->path_options_);
|
|
EXPECT_TRUE(agv_->path_cancellation_bound_);
|
|
EXPECT_FALSE(agv_->path_cancellation_requested_during_call_);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, NativeControllerCodeIsReturnedInGrpcMessage)
|
|
{
|
|
api::AgvSetVelocityCommand_Request request;
|
|
request.mutable_header()->set_device_id("test-agv");
|
|
request.mutable_velocity()->set_vx(0.1);
|
|
api::AgvSetVelocityCommand_Feedback response;
|
|
grpc::ServerContext context;
|
|
|
|
const auto status = service_->setVelocity(
|
|
&context,
|
|
&request,
|
|
&response);
|
|
|
|
EXPECT_EQ(status.error_code(), grpc::StatusCode::INTERNAL);
|
|
EXPECT_EQ(status.error_message(), kNativeErrorMessage);
|
|
EXPECT_FALSE(response.header().success());
|
|
EXPECT_EQ(response.header().error_message(), kNativeErrorMessage);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, NativeNavigationCodeIsReturnedInGrpcMessage)
|
|
{
|
|
agv_->pose_result_ = device::AgvResult::failure(
|
|
device::AgvErrorCode::CommandFailed,
|
|
kNativeNavigationErrorMessage);
|
|
api::AgvNavigateToPoseCommand_Request request;
|
|
request.mutable_header()->set_device_id("test-agv");
|
|
request.mutable_pose()->set_x(1.0);
|
|
request.mutable_pose()->set_y(2.0);
|
|
api::AgvNavigateToPoseCommand_Feedback response;
|
|
grpc::ServerContext context;
|
|
|
|
const auto status = service_->navigateToPose(
|
|
&context,
|
|
&request,
|
|
&response);
|
|
|
|
EXPECT_EQ(status.error_code(), grpc::StatusCode::INTERNAL);
|
|
EXPECT_EQ(status.error_message(), kNativeNavigationErrorMessage);
|
|
EXPECT_FALSE(response.header().success());
|
|
EXPECT_EQ(
|
|
response.header().error_message(),
|
|
kNativeNavigationErrorMessage);
|
|
EXPECT_FALSE(agv_->pose_options_.asynchronous);
|
|
EXPECT_EQ(agv_->pose_options_.wait_timeout_ms, 0);
|
|
EXPECT_EQ(agv_->pose_options_.poll_interval_ms, 0);
|
|
EXPECT_TRUE(agv_->pose_cancellation_bound_);
|
|
EXPECT_FALSE(agv_->pose_cancellation_requested_during_call_);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, ExplicitAsynchronousNavigationIsForwarded)
|
|
{
|
|
api::AgvNavigateToStationCommand_Request request;
|
|
request.mutable_header()->set_device_id("test-agv");
|
|
request.set_station_id("station-async");
|
|
request.mutable_options()->set_asynchronous(true);
|
|
api::AgvNavigateToStationCommand_Feedback response;
|
|
grpc::ServerContext context;
|
|
|
|
const auto status = service_->navigateToStation(
|
|
&context,
|
|
&request,
|
|
&response);
|
|
|
|
ASSERT_TRUE(status.ok()) << status.error_message();
|
|
EXPECT_TRUE(agv_->station_options_.asynchronous);
|
|
EXPECT_TRUE(agv_->station_cancellation_bound_);
|
|
EXPECT_FALSE(agv_->station_cancellation_requested_during_call_);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, ActionLeaseBlocksOrdinaryMutatingRpcs)
|
|
{
|
|
auto& authority = control::ControlAuthorityManager::instance();
|
|
const auto action_lease = authority.tryAcquire(
|
|
"test-agv",
|
|
"action-sequence:test-action",
|
|
std::chrono::hours(1));
|
|
ASSERT_TRUE(action_lease.acquired) << action_lease.detail;
|
|
|
|
api::AgvNavigateToPoseCommand_Request navigation_request;
|
|
navigation_request.mutable_header()->set_device_id("test-agv");
|
|
api::AgvNavigateToPoseCommand_Feedback navigation_response;
|
|
grpc::ServerContext navigation_context;
|
|
const auto navigation_status = service_->navigateToPose(
|
|
&navigation_context,
|
|
&navigation_request,
|
|
&navigation_response);
|
|
EXPECT_EQ(
|
|
navigation_status.error_code(),
|
|
grpc::StatusCode::FAILED_PRECONDITION);
|
|
EXPECT_FALSE(navigation_response.header().success());
|
|
|
|
api::CommandHeader_Request clear_fault_request;
|
|
clear_fault_request.set_device_id("test-agv");
|
|
api::CommandHeader_Feedback clear_fault_response;
|
|
grpc::ServerContext clear_fault_context;
|
|
const auto clear_fault_status = service_->clearFault(
|
|
&clear_fault_context,
|
|
&clear_fault_request,
|
|
&clear_fault_response);
|
|
EXPECT_EQ(
|
|
clear_fault_status.error_code(),
|
|
grpc::StatusCode::FAILED_PRECONDITION);
|
|
EXPECT_FALSE(clear_fault_response.success());
|
|
|
|
api::AgvMapCommand_Request switch_map_request;
|
|
switch_map_request.mutable_header()->set_device_id("test-agv");
|
|
switch_map_request.set_map_name("map-1");
|
|
api::AgvMapCommand_Feedback switch_map_response;
|
|
grpc::ServerContext switch_map_context;
|
|
const auto switch_map_status = service_->switchMap(
|
|
&switch_map_context,
|
|
&switch_map_request,
|
|
&switch_map_response);
|
|
EXPECT_EQ(
|
|
switch_map_status.error_code(),
|
|
grpc::StatusCode::FAILED_PRECONDITION);
|
|
EXPECT_FALSE(switch_map_response.header().success());
|
|
|
|
authority.release(action_lease.token);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, NavigationCancellationIncludesControlLease)
|
|
{
|
|
auto& authority = control::ControlAuthorityManager::instance();
|
|
const auto barrier = authority.preemptAcquire(
|
|
"test-agv", "stop-all-test", std::chrono::hours(1));
|
|
ASSERT_TRUE(barrier.acquired) << barrier.detail;
|
|
|
|
api::AgvNavigateToPoseCommand_Request request;
|
|
request.mutable_header()->set_device_id("test-agv");
|
|
api::AgvNavigateToPoseCommand_Feedback response;
|
|
grpc::ServerContext context;
|
|
const auto status = service_->navigateToPose(
|
|
&context, &request, &response);
|
|
|
|
EXPECT_EQ(
|
|
status.error_code(),
|
|
grpc::StatusCode::FAILED_PRECONDITION);
|
|
EXPECT_EQ(agv_->navigate_pose_calls_, 0);
|
|
authority.release(barrier.token);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, SetVelocityDispatchRunsUnderControlFence)
|
|
{
|
|
api::AgvSetVelocityCommand_Request request;
|
|
request.mutable_header()->set_device_id("test-agv");
|
|
request.mutable_velocity()->set_vx(0.1);
|
|
api::AgvSetVelocityCommand_Feedback response;
|
|
grpc::ServerContext context;
|
|
|
|
const auto status = service_->setVelocity(
|
|
&context, &request, &response);
|
|
|
|
EXPECT_EQ(status.error_code(), grpc::StatusCode::INTERNAL);
|
|
EXPECT_EQ(agv_->set_velocity_calls_, 1);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest,
|
|
StopAllGateRejectsMutatingCommandsButAllowsReadsAndStops)
|
|
{
|
|
auto& admission = globalStopAllAdmissionGate();
|
|
const auto ticket = admission.beginStopAll();
|
|
ASSERT_TRUE(ticket.valid());
|
|
|
|
api::AgvNavigateToPoseCommand_Request navigation_request;
|
|
navigation_request.mutable_header()->set_device_id("test-agv");
|
|
api::AgvNavigateToPoseCommand_Feedback navigation_response;
|
|
grpc::ServerContext navigation_context;
|
|
const auto navigation_status = service_->navigateToPose(
|
|
&navigation_context, &navigation_request, &navigation_response);
|
|
|
|
api::AgvSetVelocityCommand_Request velocity_request;
|
|
velocity_request.mutable_header()->set_device_id("test-agv");
|
|
velocity_request.mutable_velocity()->set_vx(0.1);
|
|
api::AgvSetVelocityCommand_Feedback velocity_response;
|
|
grpc::ServerContext velocity_context;
|
|
const auto velocity_status = service_->setVelocity(
|
|
&velocity_context, &velocity_request, &velocity_response);
|
|
|
|
api::AgvRuntimeStateCommand_Request state_request;
|
|
state_request.mutable_header()->set_device_id("test-agv");
|
|
api::AgvRuntimeStateCommand_Feedback state_response;
|
|
grpc::ServerContext state_context;
|
|
const auto state_status = service_->getRuntimeState(
|
|
&state_context, &state_request, &state_response);
|
|
|
|
api::CommandHeader_Request stop_request;
|
|
stop_request.set_device_id("test-agv");
|
|
api::CommandHeader_Feedback stop_response;
|
|
grpc::ServerContext stop_context;
|
|
const auto stop_status = service_->cancelNavigation(
|
|
&stop_context, &stop_request, &stop_response);
|
|
|
|
EXPECT_EQ(
|
|
navigation_status.error_code(),
|
|
grpc::StatusCode::UNAVAILABLE);
|
|
EXPECT_FALSE(navigation_response.header().success());
|
|
EXPECT_EQ(agv_->navigate_pose_calls_, 0);
|
|
EXPECT_EQ(
|
|
velocity_status.error_code(),
|
|
grpc::StatusCode::UNAVAILABLE);
|
|
EXPECT_FALSE(velocity_response.header().success());
|
|
EXPECT_EQ(agv_->set_velocity_calls_, 0);
|
|
|
|
EXPECT_TRUE(state_status.ok()) << state_status.error_message();
|
|
EXPECT_TRUE(state_response.header().success());
|
|
EXPECT_TRUE(stop_status.ok()) << stop_status.error_message();
|
|
EXPECT_TRUE(stop_response.success())
|
|
<< stop_response.error_message();
|
|
EXPECT_EQ(agv_->cancel_navigation_calls_, 2);
|
|
|
|
EXPECT_TRUE(admission.finishStopAll(ticket, true));
|
|
const auto resumed_status = service_->navigateToPose(
|
|
&navigation_context, &navigation_request, &navigation_response);
|
|
EXPECT_TRUE(resumed_status.ok()) << resumed_status.error_message();
|
|
EXPECT_TRUE(navigation_response.header().success());
|
|
EXPECT_EQ(agv_->navigate_pose_calls_, 1);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, QueriesBypassAndSafetyStopsPreemptActionLease)
|
|
{
|
|
auto& authority = control::ControlAuthorityManager::instance();
|
|
const auto action_lease = authority.tryAcquire(
|
|
"test-agv",
|
|
"action-sequence:test-action",
|
|
std::chrono::hours(1));
|
|
ASSERT_TRUE(action_lease.acquired) << action_lease.detail;
|
|
|
|
api::AgvRuntimeStateCommand_Request state_request;
|
|
state_request.mutable_header()->set_device_id("test-agv");
|
|
api::AgvRuntimeStateCommand_Feedback state_response;
|
|
grpc::ServerContext state_context;
|
|
const auto state_status = service_->getRuntimeState(
|
|
&state_context,
|
|
&state_request,
|
|
&state_response);
|
|
EXPECT_TRUE(state_status.ok()) << state_status.error_message();
|
|
EXPECT_TRUE(state_response.header().success());
|
|
EXPECT_TRUE(authority.validate(action_lease.token));
|
|
|
|
api::CommandHeader_Request stop_request;
|
|
stop_request.set_device_id("test-agv");
|
|
|
|
api::CommandHeader_Feedback emergency_response;
|
|
grpc::ServerContext emergency_context;
|
|
auto emergency = std::async(
|
|
std::launch::async,
|
|
[this, &emergency_context, &stop_request, &emergency_response]() {
|
|
return service_->emergencyStop(
|
|
&emergency_context,
|
|
&stop_request,
|
|
&emergency_response);
|
|
});
|
|
const auto emergency_deadline =
|
|
std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
|
while (authority.validate(action_lease.token) &&
|
|
std::chrono::steady_clock::now() < emergency_deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
EXPECT_FALSE(authority.validate(action_lease.token));
|
|
authority.release(action_lease.token);
|
|
const auto emergency_status = emergency.get();
|
|
EXPECT_TRUE(emergency_status.ok()) << emergency_status.error_message();
|
|
EXPECT_TRUE(agv_->emergency_stop_barrier_observed_);
|
|
|
|
const auto released_lease_after_emergency = authority.tryAcquire(
|
|
"test-agv",
|
|
"action-sequence:after-emergency-release",
|
|
std::chrono::hours(1));
|
|
ASSERT_TRUE(released_lease_after_emergency.acquired)
|
|
<< released_lease_after_emergency.detail;
|
|
|
|
api::CommandHeader_Feedback cancel_response;
|
|
grpc::ServerContext cancel_context;
|
|
auto cancel = std::async(
|
|
std::launch::async,
|
|
[this, &cancel_context, &stop_request, &cancel_response]() {
|
|
return service_->cancelNavigation(
|
|
&cancel_context,
|
|
&stop_request,
|
|
&cancel_response);
|
|
});
|
|
const auto cancel_deadline =
|
|
std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
|
while (authority.validate(released_lease_after_emergency.token) &&
|
|
std::chrono::steady_clock::now() < cancel_deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
EXPECT_FALSE(authority.validate(
|
|
released_lease_after_emergency.token));
|
|
authority.release(released_lease_after_emergency.token);
|
|
const auto cancel_status = cancel.get();
|
|
EXPECT_TRUE(cancel_status.ok()) << cancel_status.error_message();
|
|
EXPECT_TRUE(agv_->cancel_navigation_barrier_observed_);
|
|
|
|
const auto released_lease_after_cancel = authority.tryAcquire(
|
|
"test-agv",
|
|
"action-sequence:after-cancel-release",
|
|
std::chrono::hours(1));
|
|
ASSERT_TRUE(released_lease_after_cancel.acquired)
|
|
<< released_lease_after_cancel.detail;
|
|
|
|
api::CommandHeader_Feedback velocity_response;
|
|
grpc::ServerContext velocity_context;
|
|
auto velocity = std::async(
|
|
std::launch::async,
|
|
[this, &velocity_context, &stop_request, &velocity_response]() {
|
|
return service_->stopVelocityControl(
|
|
&velocity_context,
|
|
&stop_request,
|
|
&velocity_response);
|
|
});
|
|
const auto velocity_deadline =
|
|
std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
|
while (authority.validate(released_lease_after_cancel.token) &&
|
|
std::chrono::steady_clock::now() < velocity_deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
EXPECT_FALSE(authority.validate(
|
|
released_lease_after_cancel.token));
|
|
authority.release(released_lease_after_cancel.token);
|
|
const auto velocity_status = velocity.get();
|
|
EXPECT_TRUE(velocity_status.ok()) << velocity_status.error_message();
|
|
EXPECT_TRUE(agv_->stop_velocity_barrier_observed_);
|
|
|
|
const auto lease_after_stops = authority.tryAcquire(
|
|
"test-agv",
|
|
"action-sequence:after-stops",
|
|
std::chrono::hours(1));
|
|
ASSERT_TRUE(lease_after_stops.acquired)
|
|
<< lease_after_stops.detail;
|
|
|
|
EXPECT_EQ(agv_->emergency_stop_calls_, 2);
|
|
EXPECT_EQ(agv_->cancel_navigation_calls_, 2);
|
|
EXPECT_EQ(agv_->stop_velocity_calls_, 2);
|
|
EXPECT_EQ(agv_->confirm_stopped_calls_, 3);
|
|
EXPECT_TRUE(agv_->confirm_stopped_barrier_observed_);
|
|
|
|
authority.release(lease_after_stops.token);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest,
|
|
SafetyStopQuarantinesControlWhenStoppedStateIsUnconfirmed)
|
|
{
|
|
agv_->confirm_stopped_result_ = device::AgvResult::failure(
|
|
device::AgvErrorCode::Timeout,
|
|
"two zero-velocity samples were not observed");
|
|
|
|
api::CommandHeader_Request request;
|
|
request.set_device_id("test-agv");
|
|
api::CommandHeader_Feedback response;
|
|
grpc::ServerContext context;
|
|
const auto status = service_->cancelNavigation(
|
|
&context, &request, &response);
|
|
|
|
EXPECT_EQ(status.error_code(), grpc::StatusCode::DEADLINE_EXCEEDED);
|
|
EXPECT_FALSE(response.success());
|
|
EXPECT_NE(
|
|
response.error_message().find("confirmed stopped state"),
|
|
std::string::npos);
|
|
EXPECT_TRUE(agv_->cancel_navigation_barrier_observed_);
|
|
EXPECT_TRUE(agv_->confirm_stopped_barrier_observed_);
|
|
|
|
const auto lease =
|
|
control::ControlAuthorityManager::instance().tryAcquire(
|
|
"test-agv",
|
|
"normal-control-after-unconfirmed-stop",
|
|
std::chrono::hours(1));
|
|
EXPECT_FALSE(lease.acquired);
|
|
|
|
auto& authority = control::ControlAuthorityManager::instance();
|
|
const auto recovery = authority.preemptAcquire(
|
|
"test-agv", "confirmed-stop-recovery", std::chrono::hours(1));
|
|
ASSERT_TRUE(recovery.acquired) << recovery.detail;
|
|
ASSERT_TRUE(authority.waitForPreemptedRelease(
|
|
recovery.token, std::chrono::milliseconds::zero()));
|
|
ASSERT_TRUE(authority.recoverRetiredSafetyHolders(recovery.token));
|
|
authority.release(recovery.token);
|
|
const auto recovered = authority.tryAcquire(
|
|
"test-agv", "normal-control-after-recovery", std::chrono::hours(1));
|
|
EXPECT_TRUE(recovered.acquired) << recovered.detail;
|
|
authority.release(recovered.token);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, StopMappingFailureReleasesOrdinaryControlLease)
|
|
{
|
|
api::CommandHeader_Request request;
|
|
request.set_device_id("test-agv");
|
|
api::CommandHeader_Feedback response;
|
|
grpc::ServerContext context;
|
|
|
|
const auto status = service_->stopMapping(
|
|
&context, &request, &response);
|
|
|
|
EXPECT_EQ(status.error_code(), grpc::StatusCode::INTERNAL);
|
|
EXPECT_FALSE(response.success());
|
|
auto& authority = control::ControlAuthorityManager::instance();
|
|
const auto lease = authority.tryAcquire(
|
|
"test-agv", "normal-control-after-mapping-failure",
|
|
std::chrono::hours(1));
|
|
EXPECT_TRUE(lease.acquired) << lease.detail;
|
|
authority.release(lease.token);
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, NavigateToStationForwardsPgvAdapterParams)
|
|
{
|
|
api::AgvNavigateToStationCommand_Request request;
|
|
request.mutable_header()->set_device_id("test-agv");
|
|
request.set_station_id("AP1");
|
|
auto* values = request.mutable_adapter_params()->mutable_values();
|
|
(*values)["use_pgv"] = "true";
|
|
(*values)["pgv_adjust_dist"] = "0.3";
|
|
(*values)["pgv_adjust_cx"] = "-0.3";
|
|
(*values)["pgv_adjust_cy"] = "0";
|
|
api::AgvNavigateToStationCommand_Feedback response;
|
|
grpc::ServerContext context;
|
|
|
|
const auto status = service_->navigateToStation(
|
|
&context,
|
|
&request,
|
|
&response);
|
|
|
|
ASSERT_TRUE(status.ok()) << status.error_message();
|
|
EXPECT_TRUE(response.header().success());
|
|
EXPECT_EQ(agv_->station_id_, "AP1");
|
|
EXPECT_EQ(
|
|
agv_->station_adapter_params_.getString("use_pgv").value_or(""),
|
|
"true");
|
|
EXPECT_EQ(
|
|
agv_->station_adapter_params_.getString("pgv_adjust_dist").value_or(""),
|
|
"0.3");
|
|
EXPECT_EQ(
|
|
agv_->station_adapter_params_.getString("pgv_adjust_cx").value_or(""),
|
|
"-0.3");
|
|
EXPECT_EQ(
|
|
agv_->station_adapter_params_.getString("pgv_adjust_cy").value_or(""),
|
|
"0");
|
|
}
|
|
|
|
TEST_F(GrpcAgvServiceTest, NavigationErrorsMapToGrpcCodesAndPreserveDetails)
|
|
{
|
|
struct ErrorCase {
|
|
device::AgvErrorCode device_code;
|
|
grpc::StatusCode grpc_code;
|
|
};
|
|
const ErrorCase cases[] = {
|
|
{device::AgvErrorCode::InvalidArgument,
|
|
grpc::StatusCode::INVALID_ARGUMENT},
|
|
{device::AgvErrorCode::TaskCanceled,
|
|
grpc::StatusCode::CANCELLED},
|
|
{device::AgvErrorCode::Timeout,
|
|
grpc::StatusCode::DEADLINE_EXCEEDED},
|
|
};
|
|
|
|
for (const auto& test_case : cases) {
|
|
const std::string detail =
|
|
"SEER Robokit navigation detail for code="
|
|
+ std::to_string(static_cast<int>(test_case.device_code));
|
|
agv_->pose_result_ = device::AgvResult::failure(
|
|
test_case.device_code,
|
|
detail);
|
|
api::AgvNavigateToPoseCommand_Request request;
|
|
request.mutable_header()->set_device_id("test-agv");
|
|
api::AgvNavigateToPoseCommand_Feedback response;
|
|
grpc::ServerContext context;
|
|
|
|
const auto status = service_->navigateToPose(
|
|
&context,
|
|
&request,
|
|
&response);
|
|
|
|
EXPECT_EQ(status.error_code(), test_case.grpc_code);
|
|
EXPECT_EQ(status.error_message(), detail);
|
|
EXPECT_FALSE(response.header().success());
|
|
EXPECT_EQ(response.header().error_message(), detail);
|
|
}
|
|
}
|
|
|
|
TEST(AbstractAgvCompatibilityTest, FollowPathOptionsDelegateToLegacyOverride)
|
|
{
|
|
LegacyFollowPathAgv legacy;
|
|
device::AbstractAGV* abstract = &legacy;
|
|
const std::vector<device::AgvPathSegment> path = {
|
|
{"station-1", "station-2"},
|
|
};
|
|
device::AgvMotionOptions options;
|
|
|
|
const auto result = abstract->followPath(path, options);
|
|
|
|
ASSERT_TRUE(result.ok()) << result.message;
|
|
ASSERT_EQ(legacy.path_.size(), 1U);
|
|
EXPECT_EQ(legacy.path_[0].source_station, "station-1");
|
|
EXPECT_EQ(legacy.path_[0].target_station, "station-2");
|
|
}
|
|
|
|
TEST(AbstractAgvCompatibilityTest, SynchronousActionSupportDefaultsToFalse)
|
|
{
|
|
LegacyFollowPathAgv legacy;
|
|
|
|
EXPECT_FALSE(legacy.supportsSynchronousAction(
|
|
device::AgvActionKind::NavigateToPose));
|
|
EXPECT_FALSE(legacy.supportsSynchronousAction(
|
|
device::AgvActionKind::NavigateToStation));
|
|
EXPECT_FALSE(legacy.supportsSynchronousAction(
|
|
device::AgvActionKind::FollowPath));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace cmvr::service
|