code-iai / ROSIntegration

Unreal Engine Plugin to enable ROS Support
MIT License
411 stars 133 forks source link

Services Documentation #134

Closed ikhalip closed 3 years ago

ikhalip commented 4 years ago

Currently there isn't much if any documentation regarding services and how to use them. It would be great to see some c++ and or blueprint examples/documentation.

tsender commented 3 years ago

Revision history:

Hi @ikhalip. To all of you who would like documentation on how to use services (in c++) with this plugin, here it is. I hope you all find this useful. If you wanted to use blueprints, I would guess that you would likely have to write some wrapper code in c++ to give you the functionality that you want.

To keep matters simple, I will use the rospy_tutorials/AddTwoInts service, since this is already provided in the source code. I will also write the code such that the callbacks are member functions of the class.

CAUTION: These callbacks are NOT called in the game thread. It is okay to set member variables inside these callbacks, but UE4 will crash if you attempt to invoke commands on UObjects in these callbacks, such as manually moving the position of an actor with SetActorLocation(...). In this case, I suggest you set flags inside these callbacks that are set to true once you have received the data, and then inside the Tick() function you can act on that new data if the flag is true.

Notes:

  1. I have tested the ROS service feature for this plugin with UE 4.26.1 on a Windows 10 computer and both the client-side and server-side code work. I have not tested the examples I wrote below (I simply made these up for instruction purposes), so do not be alarmed if I have a typo by accident or missed an include statement (although I think this code should be fine).

  2. If you have a UE4 client and a ROS service, then you need to make sure your ROS service is able to process and send a response in less than 5 seconds. If you exceed 5 seconds, then the ROSIntegration plugin will immediately send the UE4 client an empty response field and will ignore whatever the ROS service replies. I believe this is because the ROSIntegration plugin constantly monitors its health every 5 seconds. If you experience this, then to get around this issue you should instead split up the service call into 2 service calls. in which sending the request is its own service call (UE4 --> ROS) and sending the response is its own service call (ROS --> UE4).

You are also welcome to use this code structure when creating UTopics.

Case 1: Client Implementation

You have a UE4 actor (or component) that you want to act as the client. Header file: ClientActor.h

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ROSIntegration/Public/ROSBaseServiceResponse.h"
#include "ClientActor.generated.h"

UCLASS()
class YOURAWESOME_API AClientActor: public AActor
{
    GENERATED_BODY()

public:
     // Your public code here...

protected:
     virtual void BeginPlay() override;

private:
     // Your private code here...

     UPROPERTY()
     class UROSIntegrationGameInstance* ROSInst;

     UPROPERTY()
     class UService* AddTwoIntsClient;

     void AddTwoIntsResponseCB(TSharedPtr<FROSBaseServiceResponse> Response);
};

Source file: ClientActor.cpp

#include "ClientActor.h"
#include "ROSIntegration/Classes/ROSIntegrationGameInstance.h"
#include "ROSIntegration/Classes/RI/Service.h"
#include "rospy_tutorials/AddTwoIntsRequest.h"
#include "rospy_tutorials/AddTwoIntsResponse.h"

// Rest of your code

void AClientActor::BeginPlay()
{
     Super::BeginPlay();

     ROSInst = Cast<UROSIntegrationGameInstance>(GetGameInstance());
     if (ROSInst)
     {
          AddTwoIntsClient =  NewObject<UService>(UService::StaticClass());
          AddTwoIntsClient->Init(ROSInst->ROSIntegrationCore, TEXT("/add_two_ints"), TEXT("rospy_tutorials/AddTwoInts"));
     }
}

// For simplicity, I am putting the code to request the service in some member function.
void AClientActor::CallService()
{
     if (ROSInst)
     {
          TSharedPtr<rospy_tutorials::FAddTwoIntsRequest> Request(new rospy_tutorials::FAddTwoIntsRequest());
          Request->_a = 1.0;
          Request->_b = 2.0;
          AddTwoIntsClient->CallService(Request, std::bind(&AClientActor::AddTwoIntsResponseCB, this, std::placeholders::_1));
     }
}

void AClientActor::AddTwoIntsResponseCB(TSharedPtr<FROSBaseServiceResponse> Response)
{
     auto CastResponse = StaticCastSharedPtr<rospy_tutorials::FAddTwoIntsResponse>(Response);
     if (!CastResponse)
     {
          UE_LOG(LogTemp, Warning, TEXT("Failed to cast Response to rospy_tutorials/AddTwoIntsResponse."));
          return;
     }
     UE_LOG(LogTemp, Warning, TEXT("The sum is: %f"), CastResponse->_sum);
}

Case 2: Server Implementation

You have a UE4 actor (or component) that you want to act as the server. Header file: ServerActor.h

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ROSIntegration/Public/ROSBaseServiceRequest.h"
#include "ROSIntegration/Public/ROSBaseServiceResponse.h"
#include "ServerActor.generated.h"

UCLASS()
class YOURAWESOME_API AServerActor: public AActor
{
    GENERATED_BODY()

public:
     // Your public code here...

protected:
     virtual void BeginPlay() override;

     virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

private:
     // Your private code here...

     UPROPERTY()
     class UROSIntegrationGameInstance* ROSInst;

     UPROPERTY()
     class UService* AddTwoIntsServer;

     void AddTwoIntsServerCB(TSharedPtr<FROSBaseServiceRequest> Request, TSharedPtr<FROSBaseServiceResponse> Response);
};

Source file: ServerActor.cpp

#include "ServerActor.h"
#include "ROSIntegration/Classes/ROSIntegrationGameInstance.h"
#include "ROSIntegration/Classes/RI/Service.h"
#include "rospy_tutorials/AddTwoIntsRequest.h"
#include "rospy_tutorials/AddTwoIntsResponse.h"

// Rest of your code

void AServerActor::BeginPlay()
{
     Super::BeginPlay();

     ROSInst = Cast<UROSIntegrationGameInstance>(GetGameInstance());
     if (ROSInst)
     {
          AddTwoIntsServer =  NewObject<UService>(UService::StaticClass());
          AddTwoIntsServer->Init(ROSInst->ROSIntegrationCore, TEXT("/add_two_ints"), TEXT("rospy_tutorials/AddTwoInts"));

          // The second param indicates if we wish to execute the callback in the game thread. 
          // I chose false, to be consistent with how topic callbacks work.
          AddTwoIntsServer->Advertise(std::bind(&AServerAActor::AddTwoIntsServerCB, this, std::placeholders::_1, std::placeholders::_2), false);
     }
}

void AServerActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
     Super::EndPlay(EndPlayReason);
     AddTwoIntsServer->Unadvertise(); // If you do not unadvertise/unsubscribe in EndPlay, weird behavior may arise
}

void AServerActor::AddTwoIntsServerCB(TSharedPtr<FROSBaseServiceRequest> Request, TSharedPtr<FROSBaseServiceResponse> Response)
{
     auto CastRequest = StaticCastSharedPtr<rospy_tutorials::FAddTwoIntsRequest>(Request);
     auto CastResponse = StaticCastSharedPtr<rospy_tutorials::FAddTwoIntsResponse>(Response);
     if (!CastRequest)
     {
          UE_LOG(LogTemp, Warning, TEXT("Failed to cast Request to rospy_tutorials/AddTwoIntsRequest."));
          return;
     }
     if (!CastResponse)
     {
          UE_LOG(LogTemp, Warning, TEXT("Failed to cast Response to rospy_tutorials/AddTwoIntsResponse."));
          return;
     }
     CastResponse->_sum = CastRequest->_a + CastRequest->_b;
}
Sanic commented 3 years ago

Hi @tsender ! Thanks for providing these examples. I've linked to your post from the main README.md of this repo 👍

With respect to executing on the game thread: You can also check out AsyncTasks: https://answers.unrealengine.com/questions/548541/view.html