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.

Note

The serial IO protocol carries RPCs only. Streams and events are not available, and the stream procedures, such as KRPC.AddStream and KRPC.RemoveStream, return an error.

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;
}

The server leaves the stream_update field of a MultiplexedResponse unset.

Connecting to the Server#

There is a single serial port connection for RPC calls. 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;
      uint32 protocol_version = 4;
    
      enum Type {
        RPC = 0;
        STREAM = 1;
      };
    }
    

    The type field should be set to ConnectionRequest.RPC and the protocol_version field to 1. 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;
        WRONG_PROTOCOL_VERSION = 4;
      }
      string message = 2;
      bytes client_identifier = 3;
      uint32 protocol_version = 4;
    }
    

    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.

    The protocol_version field contains the version of the protocol the server accepted. A server from before kRPC 0.7 leaves it as 0. See Protocol Version.

    The server waits up to 3 seconds for the complete connection request message to arrive after its first bytes are received. If it does not arrive in time, the server responds with the TIMEOUT status.

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.

Buffering and Data Rates#

A serial port carries data far more slowly than the game produces it. At the default 9600 baud it carries roughly a thousand bytes per second. The server therefore buffers up to a megabyte of data in each direction, and the two directions behave differently when their buffer fills:

  • Data received from the port is buffered until the game reads it. When the buffer fills, the server stops reading from the port until the game catches up. The data is kept, and the port carries it more slowly.

  • Data written by the game is buffered until the port has sent it. The game writes only responses, so this buffer fills when a client sends requests faster than it reads their responses. The server then drops the connection and stops. It can be started again from the in-game configuration window.

To keep the write buffer from filling, read each response before sending the next request. A higher baud rate moves more data through the port.

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.decoder import _DecodeVarint
from google.protobuf.internal.encoder import _VarintEncoder

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.protocol_version = 1
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)
if response.protocol_version != 1:
    raise RuntimeError("The server does not support protocol version 1")
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)