Protocol Buffers over Serial IO#

This communication protocol allows microcontrollers and other devices that communicate over a serial port to interact with a kRPC server, for example an Arduino.

Note

If a client library is available for your language, you do not need to implement this protocol.

Configuring the Server#

Unlike the TCP/IP and WebSockets protocols, the serial IO protocol is not enabled by default. To use it, add a new server from the in-game server configuration window and select Protobuf over SerialIO as the protocol. The configuration window allows the serial port name and baud rate (which defaults to 9600) to be set.

Sending and Receiving Messages#

Communication with the server is performed via Protocol Buffer messages, encoded according to the protobuf binary format. When sending messages to and from the server, they are prefixed with their size, in bytes, encoded as a Protocol Buffers varint.

To send a message to the server:

  1. Encode the message using the Protocol Buffers format.

  2. Get the length of the encoded message data (in bytes) and encode that as a Protocol Buffers varint.

  3. Write the encoded length followed by the encoded message data to the serial port.

To receive a message from the server, do the reverse:

  1. Read the size of the message as a Protocol Buffers encoded varint from the serial port.

  2. Decode the message size.

  3. Read message size bytes of message data from the serial port.

  4. Decode the message data.

Unlike the TCP/IP and WebSockets protocols, all messages sent to the server after the initial connection handshake are wrapped in a MultiplexedRequest message, and all messages received from the server after the handshake are wrapped in a MultiplexedResponse message:

message MultiplexedRequest {
  ConnectionRequest connection_request = 1;
  Request request = 2;
}

message MultiplexedResponse {
  Response response = 1;
  StreamUpdate stream_update = 2;
}

Connecting to the Server#

There is a single serial port connection for both RPC calls and stream updates. To establish a connection, a client must do the following:

  1. Open a connection to the serial port.

  2. Send a MultiplexedRequest message to the server with its connection_request field populated. The ConnectionRequest message is defined as:

    message ConnectionRequest {
      Type type = 1;
      string client_name = 2;
      bytes client_identifier = 3;
      enum Type {
        RPC = 0;
        STREAM = 1;
      };
    }
    

    The type field should be set to ConnectionRequest.RPC and the client_name field can be set to the name of the client to display on the in-game UI. The client_identifier should be left blank.

  3. Receive a ConnectionResponse message from the server. This message is defined as:

    message ConnectionResponse {
      Status status = 1;
      enum Status {
        OK = 0;
        MALFORMED_MESSAGE = 1;
        TIMEOUT = 2;
        WRONG_TYPE = 3;
      }
      string message = 2;
      bytes client_identifier = 3;
    }
    

    If the status field is set to ConnectionResponse.OK then the connection was successful. If not, the message field contains a description of what went wrong.

Receiving Stream Updates#

Clients can receive Streams over the same serial port connection used for RPC calls. Stream updates arrive as MultiplexedResponse messages with the stream_update field populated. After sending an RPC request, the client should inspect each received MultiplexedResponse to determine whether it contains a response (the result of an RPC call) or a stream_update (a stream result).

Invoking Remote Procedures#

See Messaging Protocol.

To send a procedure call, wrap the Request message in a MultiplexedRequest and set its request field. Receive the result as a MultiplexedResponse and read the Response from its response field.

Examples#

The following Python code connects to a server on serial port /dev/ttyUSB0 using the name “Jeb”. It then invokes the KRPC.GetStatus RPC, receives and decodes the result, and prints out the server version number from the response.

import serial
from google.protobuf.internal.encoder import _VarintEncoder
from google.protobuf.internal.decoder import _DecodeVarint
from krpc.schema import KRPC_pb2 as KRPC


def encode_varint(value):
    """Encode an int as a protobuf varint"""
    data = []
    _VarintEncoder()(data.append, value, False)
    return b"".join(data)


def decode_varint(data):
    """Decode a protobuf varint to an int"""
    return _DecodeVarint(data, 0)[0]


def send_message(port, msg):
    """Send a message, prefixed with its size, to a serial port"""
    data = msg.SerializeToString()
    size = encode_varint(len(data))
    port.write(size + data)


def recv_message(port, msg_type):
    """Receive a message, prefixed with its size, from a serial port"""
    # Receive the size of the message data
    data = b""
    while True:
        try:
            data += port.read(1)
            size = decode_varint(data)
            break
        except IndexError:
            pass
    # Receive the message data
    data = port.read(size)
    # Decode the message
    msg = msg_type()
    msg.ParseFromString(data)
    return msg


# Open a connection to the serial port
conn = serial.Serial("/dev/ttyUSB0")

# Send a connection request wrapped in a MultiplexedRequest
request = KRPC.MultiplexedRequest()
request.connection_request.type = KRPC.ConnectionRequest.RPC
request.connection_request.client_name = "Jeb"
send_message(conn, request)

# Receive the connection response
response = recv_message(conn, KRPC.ConnectionResponse)

# Check the connection was successful
if response.status != KRPC.ConnectionResponse.OK:
    raise RuntimeError("Connection failed: " + response.message)
print("Connected to server")

# Invoke the KRPC.GetStatus RPC, wrapped in a MultiplexedRequest
call = KRPC.ProcedureCall()
call.service = "KRPC"
call.procedure = "GetStatus"
request = KRPC.MultiplexedRequest()
request.request.calls.extend([call])
send_message(conn, request)

# Receive the MultiplexedResponse
multiplexed_response = recv_message(conn, KRPC.MultiplexedResponse)
response = multiplexed_response.response

# Check for an error in the response
if response.HasField("error"):
    raise RuntimeError("ERROR: " + str(response.error))

# Check for an error in the results
assert len(response.results) == 1
if response.results[0].HasField("error"):
    raise RuntimeError("ERROR: " + str(response.results[0].error))

# Decode the return value as a Status message
status = KRPC.Status()
status.ParseFromString(response.results[0].value)

# Print out the Status message
print(status)