-
Notifications
You must be signed in to change notification settings - Fork 1
Getting Started Guide
The gRPC JSON Client is a library that allows languages without native gRPC support (such as MATLAB) to communicate with gRPC services using JSON. The use cases for the library are broad, but the main purpose is to enable remote control of NI instrumentation by pairing it with grpc-device, NI's gRPC Device Server. Since strings are the primary communications mechanism, the library offers a familiar look and feel to those already comfortable with traditional message-based communications standards like SCPI.
In this Getting Started Guide, we'll cover how to formulate and interpret gRPC JSON messages with an eye towards NI's driver implementation. First, we'll examine some of the basic concepts of gRPC and Protocol Buffers, the underlying technologies from which the client and server are built on. Then, we'll establish a methodology for composing messages by mapping NI instrument driver C API calls to their corresponding gRPC calls. Finally, we'll end on how to build JSON strings from message contents. By the end of this guide, you will become familiar with the relevant concepts, procedures, and documentation required to develop gRPC applications with NI devices.
gRPC is an open source remote procedure call (RPC) framework built and maintained by Google. The framework internally uses Google's Protocol Buffers to structure and serialize data into messages that are sent between a client and a server during the course of a RPC. If you're starting to worry this might get complicated, don't. A deep understanding of these technologies isn't required to be successful here. However, it is important to know that the topics we'll discuss carry a dependency on gRPC, so there are still a few important pieces we need to cover.
gRPC is rooted in services. Services are entities which perform some action on our behalf and return a result. We typically don't know or care how the result is obtained, just that the service returns it to us in a reasonable time.
Think of going to a restaurant and ordering a glass of water. The waiter or waitress serving the request provides a beverage service that includes ordering water. How they obtain it typically isn't your concern. All you care about is that you ordered a drink and expect to receive it in a timely manner. Your request may also include a few parameters, such as the size, the amount of ice, and an option to include a garnish.
Whether you decide to use them or not, the waiter likely provides other services too, such as a food service or a bottle service.
The conclusion to draw from this analogy is that a server (the waiter) is capable of providing multiple services, each with its own set of actions. Those actions in turn have their own sets of input and output parameters.
At the heart of gRPC is the .proto
file. These files define services, methods, and messages using the Protocol Buffer syntax. Their primary purpose is for generating interface code and stubs, but we won't be generating any source code here. Our primary use for them will be as a reference since they house all of the information required for properly invoking a RPC.
Here is a subset of the nirfsa.proto
file in the grpc-device
project. We will reference it several times in the following sections.
package nirfsa_grpc;
import "nidevice.proto";
import "session.proto";
service NiRFSA {
rpc Init(InitRequest) returns (InitResponse);
}
Note: Several lines from the original file have been intentionally omitted for simplicity.
Skipping the first few lines for a moment, we can see that a service named NiRFSA
is defined. The service in turn defines a single method called Init
(wrapped in curly braces) which accepts an InitRequest
and returns an InitResponse
. These are message types and their definitions are further down in the file. We will cover message definitions soon. For now, let's examine what the other lines are doing too.
The package
specifier creates a namespace for all definitions in the .proto
file. Namespace elements are accessed via dot notation. Therefore, the full name of the service defined above is nirfsa_grpc.NiRFSA
and the full name of the method is nirfsa_grpc.NiRFSA.Init
. This helps prevent name collisions when importing other .proto
files.
The import
statement allows definitions from other .proto
files to be referenced by the current file. Imports allow shared components to be placed in their own .proto
files rather than requiring redefinition wherever they are needed. All of the NI instrument driver services carry a dependency on session.proto
. More on this dependency will be covered later in the The Session Message section.
Further down the .proto
file we will find the definitions for the InitRequest
and InitResponse
messages.
message InitRequest {
string session_name = 1;
string resource_name = 2;
bool id_query = 3;
bool reset = 4;
}
message InitResponse {
int32 status = 1;
nidevice_grpc.Session vi = 2;
string error_message = 3;
}
Each message defines a set of fields (wrapped in curly braces) and each field has a name, a datatype, and a unique number. The unique number is a low level implementation detail that can be safely ignored for our purposes. In this case, the resource_name
field of the InitRequest
message is a string
.
Protocol Buffers also support nested messages. Notice the InitResponse
message's vi
field is a nidevice_grpc.Session
message type. This type is defined in session.proto
and was imported via the import "session.proto"
statement discussed earlier.
NI's instrument drivers require sessions to identify resources and manage memory. Sessions are the mechanism that allows the same driver software to communicate with multiple devices simultaneously. Most if not all methods will require a session to identify which resource (or set of resources) an action should be performed on. Sessions are encapsulated into their own message type to provide consistency across drivers and to enable a common set of utilities. Here is the Session
message definition from the session.proto
file.
message Session {
oneof session {
string name = 1;
uint32 id = 2;
}
}
The oneof
field named session
specifies that at most one of the enclosing types can be used. For the Session
message above, the session
field can either contain the name of the session as a string
or the ID of the session as an uint32
. As we saw previously, the InitResponse
message returns a nested Session
message in its vi
field. The value of this field can be used by subsequent RPCs to control the initialized device.
The semantics of using either the name
or id
fields will be covered later in the Session Names section.
Every instrument driver available in NI's Device Server is exposed as a separate service. As we've already seen, NI-RFSA is defined in the nirfsa_grpc.NiRFSA
service. Other drivers, like NI-RFSG and NI-DAQmx, are defined in the nirfsg_grpc.NiRFSG
and nidaqmx_grpc.NiDAQmx
services respectively. For a full list of supported drivers, see the table here.
Every driver service is derived from the driver's C API. The Converting C API Calls to gRPC topic in the grpc-device
wiki covers mapping the driver C functions to gRPC service methods in great detail. Here are the important points:
- Every C function call has a corresponding service method.
- Every method accepts a
{FUNCTION_NAME}Request
message that encapsulates the function's input parameters. - Every method returns a
{FUNCTION_NAME}Response
message that encapsulates the function's output parameters. - Constants have been grouped into Enumerations.
Let's examine the C prototype of the NiRFSA.Init
method next to the nirfsa.proto
definitions.
ViStatus niRFSA_init(ViRsrc resourceName, ViBoolean IDQuery, ViBoolean reset, ViSession* vi);
service NiRFSA {
rpc Init(InitRequest) returns (InitResponse);
}
message InitRequest {
string session_name = 1;
string resource_name = 2;
bool id_query = 3;
bool reset = 4;
}
message InitResponse {
int32 status = 1;
nidevice_grpc.Session vi = 2;
string error_message = 3;
}
As we expect, each input parameter is defined as a field in the InitRequest
message and each output parameter is defined as a field in the InitResponse
message. The parameter names have changed slightly to meet the proto3
style guidelines, but their semantics haven't.
Now for the bigger differences. The vi
output parameter is defined as the nidevice_grpc.Session
message we discussed earlier. The remaining fields, session_name
and error_message
, are extra fields. As a rule of thumb, fields that map directly to the C prototype offer core functionality of the driver while the extra fields are accessories for features specific to NI's Device Server. If you aren't yet aware of how to use them, that's okay, you can still expect correct behavior by using their default values. In this situation, the error_message
field returns the corresponding error message for a non-zero status
field if an error occurs while initializing the session. The General API Differences section of the grpc-device
wiki describes other subtle differences that exist due to the languages' inherent differences.
Now that we've made the connection between the instrument driver C APIs and related services, we can start building JSON messages. JSON defines a succinct set of data types, each with a mapping to Protocol Buffers types. A message maps to a JSON Object Literal, so your JSON strings will always start and end with a curly brace. Each field is represented as a key/value pair in the JSON object. The key is the name of the field and the value is the JSON string representation of the field's value. Here is the InitRequest
message in JSON with the resource_name
, id_query
, and reset
fields set. Fields that are not explicitly set will assume the default value for their type.
{
"resource_name": "VST_01",
"id_query": true,
"reset": false
}
Here is a response we might expect to see after passing this message to the NiRFSA.Init
method.
{
"status": 0,
"vi": {
"id": 1
},
"error_message": ""
}
That's all there is to it! JSON is everywhere, so don't forget to research whether the language you are using offers any neat tools for working with it.
Earlier in this guide, we introduced the nidevice_grpc.Session
message defined in session.proto
. This message is nested in requests for every method that acts on a session. When a session is initialized, the response will include a Session
instance (typically named vi
) with the id
field set. In most situations, it is sufficient to store this value in a variable for use in proceeding calls. However, occasionally it may be more desirable to specify a unique name for a session instead of an ID. This can facilitate reserving sessions by using the session name as the reservation_id
.
NI's Device Server maintains a mapping of session names to session IDs. If a name is supplied instead of an ID, the server will lookup the ID for the named session. For this to work properly though, a unique name needs to be supplied to the server when the session is initialized. To do so, supply an argument to the session_name
parameter of the request when initializing the session.