RPC framework for embedded systems based on ETL. Generates C++ code with no dynamic memory allocations, no exceptions, no RTTI, etc.
WARNING: This project is work in progress
uint8_t
uint16_t
uint32_t
uint64_t
int8_t
int16_t
int32_t
int64_t
float
double
bool
- string
- Fixed size: e.g.,
string_10
for a maximum size of 10 characters (excluding termination character) - Automatic size:
string
. For this type the number of bytes that is transferred is determined by how long the actual string is. This can be different for every RPC function call, contrary to the fixed size string.
- Fixed size: e.g.,
- array
- Array can be of any type except
string
- Enabled by using the
count
setting in the definition file with any value larger than 1
- Array can be of any type except
- optional
- Can be used with any type
- Translated to
etl::optional
in the generated C++ code - Enabled by using the
count
setting in the definition file with a value?
- custom
- struct
- A custom struct can have any number of fields of any type except
string
, even other custom types - A custom struct can be used as a function argument or function return type by referencing it with a
@
-prefix
- A custom struct can have any number of fields of any type except
- enum
- A custom enum can have any number of fields
- Translated to an
enum class
in the generated C++ code
- struct
Multiple return values for a function are supported. In the generated C++ code this feature is translated to a function returning a std::tuple
The interface definition file is in YAML format. This makes it easy to parse in many languages and use it for any other purpose that you may need. There is a schema for the interface definition file to provide code completion and error checking in editors that support JSON schema.
- Install the YAML extension
- In the global or local settings.json, add the following entry:
"yaml.schemas": { "/path/to/LotusRpc/generator/lotusrpc-schema.json": "*.lrpc.yaml"}
- Now every file with
.lrpc.yaml
extension will get code completion and validation in VS Code
All code is generated in the namespace specified in the interface definition file. If no namespace is specified, all code is generated in the global namespace
Receive and transmit buffer sizes can be configured in the interface definition file. If not specified, both take the value of 256 bytes
Every LRPC service has an ID with a value between 0 and 254 (ID 255 is reserved for). This means that LRPC supports a maximum of 255 services. Duplicate service IDs are not allowed. The service ID can optionally be specified in the definition file. If it is not specified an ID is generated, starting with 0 for the first service, 1 for the second service, etc. It is possible to specify only some service IDs and let the rest be generated automatically. For example, if a definition contains 4 services, but only the third service has a specified ID of 17, then the resulting service IDs are [0, 1, 17, 18]
The same applies to LRPC functions inside a service.
LRPC uses Python to generate code and can therefore be used on all platforms that support Python. The generated C++ code is C++11 compatible and can be compiled for any platform with a suitable compiler.
DISCLAIMER: All development is done on Windows. Continuous integration is done with Github Actions on Ubuntu.
lrpcc
is the LRPC client tool. A common use case of LRPC is to control a device running an LRPC server from a PC. This means the user must implement an LRPC client on the PC. lrpcc
makes this very easy by constructing a client at runtime from the definition file and present itself as a CLI tool. lrpcc
is installed as a command line tool with LRPC and can be called from any location.
Example: Given an LRPC definition with a function add inside the service math, taking two integers and returning one integer, the function can be called from the PC with the following command
lrpcc math add 3 7
and the result will printed to the console.
In order to work, lrpcc
needs the following information
- Location of the LRPC definition file
- Information about the transport layer between PC and device
Since the command line arguments for lrpcc
are reserved for the communication with the device, this information must be supplied by the user in the form of a configuration file with name lrpcc.config.yaml. lrpcc
looks for this file in the current working directory and all subdirectories recursively and uses the first one it encounters. This means that you can put the lrpcc
configuration file anywhere in your project structure and call lrpcc
from your project root. Alternatively, you can have various different configurations and possibly different LRPC definitions in various subdirectories of your project. You can use any of them by simply navigating to the directory and call lrpcc
from there.
lrpcc
constructs a command line application with Click. This means that you can easily retrieve help about all available commands by just adding the --help
flag.
If the file lrpcc.config.yaml is not found, lrpcc
uses the file specified in the LRPCC_CONFIG environment variable.
Here is an example lrpcc.config.yaml file
definition_url: '../example.lrpc.yaml'
transport_type: 'serial'
transport_params:
port: 'COM30'
baudrate: 16384
timeout: 2
The fields definition_url
, transport_type
and transport_params
are required. definition_url
is the path of the LRPC definition file and can be relative to lrpcc.config.yaml or an absolute path. The subfields of transport_params
are passed as keyword arguments to the transport class. lrpcc
uses pyserial for serial communication, so the transport_params
can be any of the constructor parameters of the serial.Serial class. lrpcc
currently only supports the serial transport type.
File name: example.lrpc.yaml
namespace: "ex"
rx_buffer_size: 200
tx_buffer_size: 300
services:
- name: "battery"
id: 0
functions:
- name: "get"
id: 0
params:
- name: "option"
type: "@VoltageScales"
returns:
- name: "voltage"
type: double
enums:
- name: "VoltageScales"
fields:
- id: 1
name: "microvolts"
- id: 55
name: "millivolts"
- id: 59
name: "volts"
- code-generation:
pip install code-generation
- click:
pip install click
Basic usage: python lotusrpc.py example.lrpc.yaml -o output-dir
For more info type python lotusrpc.py --help
Include the file <out-dir>/example/battery_ServiceShim.hpp
in your project. Derive you own service class from ex::batteryServiceShim
and implement all pure virtual functions. These are the remote procedures that are called when issuing a function call on the client. Implement these functions as desired.
Include the file <out-dir>/example/example.hpp
in your project. This file gives access to the LRPC server class called ex::example
. Instantiate your service class(es) and register them to the server with ex::example::registerService
. Feed incoming bytes to the server by calling the ex::example::decode
function. You are responsible for making sure this data is correct.
The LRPC definition is written in YAML and therefore benefits from all the features and tooling that are available for YAML. Think about editor support, easy parsing in various programming languages. The fact that there is a schema available, makes it possible to have code completion and documentation in supporting editors.
The LRPC definition file has the following properties:
Required | Optional |
---|---|
name | structs |
services | enums |
constants | |
rx_buffer_size | |
tx_buffer_size | |
namespace |
At the top level it is also allowed to use additional properties. These properties are ignored by the LRPC tool, but may be useful creating anchors or for any other purpose that you may have. Remember that it's very easy to parse the definition file, so everyone is free to extend the functionality of LRPC.
This is the name of the RPC engine. It is used in generated files and directories, as well as generated code. Therefore, the name must be a valid C++ identifier
A single RPC engine can contain up to 256 different services. A service can be considered a group of related functions. In the generated code a service with functions corresponds to a class with methods. A RPC engine must have at least one service.
A service has the following properties
Required | Optional |
---|---|
name | id |
functions |
name
is the name of the service. It must be a valid C++ identifier.
Every LRPC service has an identifier that is needed for proper transfer of information between two endpoints. If the service identifier is not specified, LRPC will generate one. When starting out with a fresh RPC, it's usually not necessary to specify service IDs. Later on it may be useful to explicitly specify a service ID for backwards compatibility.
NOTE: The most efficient code is generated when service IDs are contiguous and start at 0. This is the default when no IDs are specified
A single LRPC service can contain up to 256 functions (but at least 1). A function can have any number of arguments and return values.
A function has the following properties:
Required | Optional |
---|---|
name | id |
params | |
returns |
name
is the name of the function. It must be a valid C++ identifier. id
is the function identifier, similar to the service ID. params
is a list of parameters and returns
is a list of return values. Every item in params
and returns
is a LrpcType.
LRPC supports defining custom aggregate data types in the structs
property. structs
contains a list of custom struct definitions, where every item has the following properties:
Required | Optional |
---|---|
name | |
fields |
name
is the name of the struct. It must be a valid C++ identifier. fields
is a list of data members, every member being a LrpcType. Custom structs can be referenced inside the LRPC definition file by prepending the name with the @
sign.
LRPC supports defining custom enum types in the enums
property. enums
contains a list of custom enum definitions, where every item has the following properties:
Required | Optional |
---|---|
name | external |
fields | external_namespace |
name
is the name of the enum. It must be a valid C++ identifier. fields
is a list of enum fields, every field having a required name
property and an optional id
property. name
is the enum label and id
is the value of the label. fields
can also simply be a list of strings, every string being the name of a field. The field IDs are determined automatically in that case. This allows for shorter notation in case the specific value of the IDs is not important.
It is possible to use an enum in LRPC that already exists in your codebase. In this case, the external
property must be used to specify the file that contains the definition of the enum. If the external enum lives inside a namespace, that namespace must be specified with the external_namespace
property. LRPC does not generate any functional code for external enums, but it does generate some checks to verify that the external enum corresponds to the LRPC definition file. LRPC also creates an alias for the external type (unless code is generated in the global namespace and the external enum lives in the global namespace). The alias is used internally in code generated by LRPC
Example:
...
enums:
# short notation
- { name: "MyEnum", fields: [V0, V1, V2, V3]}
# short notation, external enum in namespace
- { name: "MyEnum2", fields: [V0, V1, V2, V3], external: ext_files/MyEnum2.hpp, external_namespace: "a::b::c"}
# full notation, external enum in global namespace
- name: "MyEnum3"
external: ext_files/MyEnum3.hpp
fields:
- {name: V0} # id omitted, defaults to 0
- {name: V1} # id omitted, defaults to 1
- {name: V55, id: 55}
- {name: V200, id: 200 }
- {name: V201} # id omitted, defaults to 201
...
The LRPC definition file uses LrpcType to describe function arguments, function return values and struct fields.
A LrpcType has the following properties:
Required | Optional |
---|---|
name | count |
type |
name
is the name of the LrpcType.
See section data types
Specifying count
as a number (at least 2) turns the LRPC type into an array of that size. Specifying count
as ?
turns the LRPC type into an optional. If count
is omitted, the type specified by type
is used without any modifications