Based on the *.proto files, the NuGet Gallery | Grpc.Tools tools create C# code, including classes etc. This is done automatically within Visual Studio. The generated code is not particularly easy to read:
c#/// <summary> ///Tool describes a robotic tool. /// </summary> [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class Tool : pb::IMessage<Tool> #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE , pb::IBufferMessage #endif { private static readonly pb::MessageParser<Tool> _parser = new pb::MessageParser<Tool>(() => new Tool()); private pb::UnknownFieldSet _unknownFields; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pb::MessageParser<Tool> Parser { get { return _parser; } }
When using "raw" gRPC from C#, start by creating the client, as auto-generated from the Protobuf file:
c#string ip = "https://localhost:5001"; PRC.GRPC.ParametricRobotControlService.ParametricRobotControlServiceClient client = new PRC.GRPC.ParametricRobotControlService.ParametricRobotControlServiceClient(GrpcChannel.ForAddress(ip));
The GrpcChannel can also be define with more parameters, defining retry policies and more importantly Gzip compression. Take care to use very high limits for the send and receive message size (null is the maximum value for C#).
c#var defaultMethodConfig = new MethodConfig { Names = { MethodName.Default }, RetryPolicy = new RetryPolicy { MaxAttempts = 5, InitialBackoff = TimeSpan.FromSeconds(1), MaxBackoff = TimeSpan.FromSeconds(3), BackoffMultiplier = 1.5, RetryableStatusCodes = { StatusCode.Unavailable } } }; var grpcChannel = GrpcChannel.ForAddress(ip, new GrpcChannelOptions { ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }, MaxReceiveMessageSize = null, MaxSendMessageSize = null, CompressionProviders = new List<ICompressionProvider>() { new Grpc.Net.Compression.GzipCompressionProvider(System.IO.Compression.CompressionLevel.Fastest) } }); client = new PRC.GRPC.ParametricRobotControlService.ParametricRobotControlServiceClient(grpcChannel);
The next step is to define a robot setup. The robotID has to be unique and will be reused for subsequent calls. The
RobotDriverClass
and PresetRobotClass
relate to the PRC.Library
namespace. Currently, only three robots are integrated: KUKA.KUKA_KR610R11002
, KUKA.KUKA_KR816R20102
, and KUKA.KUKA_KR50R2500
c#string robotID = "PRC_Test"; var setupData = await client.SetupRobotAsync(new SetupRobotRequest { ClientId = robotID, SoftwareVersion = "0.1", RobotSetup = new Robot() { FriendlyId = "KUKA KR10", InitialBase = new Base() { BaseFrame = new CartesianPosition() { Cs = new CoordinateSystem() { Origin = new Vector3 { X = 0, Y = 0, Z = 0 }, XAxis = new Vector3 { X = 1, Y = 0, Z = 0 }, YAxis = new Vector3 { X = 0, Y = 1, Z = 0 }, } }, BaseId = "0" }, RobotDriverClass = "KUKA.KSS_KRL_Driver", PresetRobotClass = "KUKA.KUKA_KR610R11002", ToolDictionary = { { "0", new Tool() { Tcp = new CartesianPosition() { Cs = new CoordinateSystem() { Origin = new Vector3 { X = 0, Y = 0, Z = 0 }, XAxis = new Vector3 { X = 1, Y = 0, Z = 0 }, YAxis = new Vector3 { X = 0, Y = 1, Z = 0 }, } }, ToolId = "0", ToolType = FrameType.Fixed } } } } });
To receive feedback to the robot, subscribe to the
SubscribeRobotFeedback
stream. In the example below this is done as a separate task. The feedback stream may contain a regular heartbeat to keep up the communication, a current RobotState
(e.g. when the simulation is refreshed) or new settings when the user changes the settings in the browser UIc#try { dataStreamingCall = client.SubscribeRobotFeedback(new SubscribeRobotFeedbackRequest { Id = robotID }, null, null, cancellationTokenSource.Token); var readTask = System.Threading.Tasks.Task.Run(async () => { await foreach (var response in dataStreamingCall.ResponseStream.ReadAllAsync(cancellationTokenSource.Token)) { if (response != null) { switch (response.DataPackageCase) { case RobotFeedback.DataPackageOneofCase.None: break; case RobotFeedback.DataPackageOneofCase.HeartbeatData: //received heartbeat event Console.WriteLine("Received hearbeat: " + response.HeartbeatData.Beat); break; case RobotFeedback.DataPackageOneofCase.RobotStateData: //new robot state event string actPos = "A1: " + response.RobotStateData.ActualAxisPosition.AxisValues[0] + ", A2: " + response.RobotStateData.ActualAxisPosition.AxisValues[1] + ", A3: " + response.RobotStateData.ActualAxisPosition.AxisValues[2] + ", A4: " + response.RobotStateData.ActualAxisPosition.AxisValues[3] + ", A5: " + response.RobotStateData.ActualAxisPosition.AxisValues[4] + ", A6: " + response.RobotStateData.ActualAxisPosition.AxisValues[5]; Console.WriteLine("Robot is at: " + actPos); break; case RobotFeedback.DataPackageOneofCase.SettingsData: //Settings updated event Console.WriteLine("Received " + response.SettingsData.SettingsDictionary.Count + "settings objects."); break; case RobotFeedback.DataPackageOneofCase.PingData: //Ping event Console.WriteLine("Was pinged: " + response.PingData.Payload); break; default: break; } } } }, cancellationTokenSource.Token); } catch (Exception e) { PRC.Core.Data.Logging.WriteLine("Failed to subscribe to robot updates: " + e.Message, robotID); }
With the setup and feedback in place, you can now send tasks to the robot. The example below sends a PTP Motion Group with two Axis motions.
await client.AddRobotTaskAsync(req);
sends the request to the server.c#var ptpMotion1 = new MotionCommand() { AxisMotion = new AxisMotion() { Target = new JointTarget() { AxisValues = { -45, -90, 90, 0, 0, 0 }, Speed = { 0.15f } } } }; var ptpMotion2 = new MotionCommand() { AxisMotion = new AxisMotion() { Target = new JointTarget() { AxisValues = { 45, -90, 90, 0, 0, 0 }, Speed = { 0.15f } } } }; var ptpMotionGroup = new MotionGroup() { Commands = { ptpMotion1, ptpMotion2 }, Interpolation = "C_PTP", MotionGroupType = MotionGroupType.Ptp, RobotBase = new Base() { BaseFrame = new CartesianPosition() { Cs = new CoordinateSystem() { Origin = new Vector3 { X = 0, Y = 0, Z = 0 }, XAxis = new Vector3 { X = 1, Y = 0, Z = 0 }, YAxis = new Vector3 { X = 0, Y = 1, Z = 0 }, } }, BaseId = "0" } }; var req = new AddRobotTaskRequest { Id = robotID, RobotTask = new GRPC.Task() { Name = "InitTest", Payload = { new TaskPayload { MotionGroupTask = ptpMotionGroup } }, Type = TaskType.SimulateAndExecuteTask }, RobotSettings = new Settings() { SettingsDictionary = { setupData.RobotSettings.SettingsDictionary } } }; var toolpath = await client.AddRobotTaskAsync(req);
To change the simulation like you would do in Grasshopper with the simulation slider, use the GetSimulatedRobotState request. It is a unary request, so you can receive either a
RobotState
, or with AsyncStreamUpdate = true
get the new RobotState
via the feedback stream.c#var robotState = await client.GetSimulatedRobotStateAsync(new GetSimulatedRobotStateRequest { Id = robotID, NormalizedState = (float)i / 100, StreamUpdate = true });
The full code is available here:
PRC.Integrations
jbraumann • Updated Apr 29, 2025