ThingSet framework in C++.
ThingSet is a transport-agnostic data accessibility framework for embedded devices. For more information on ThingSet, please visit the ThingSet web site.
There are four transports in various stages of completeness and maturity:
- SocketCAN - client and server (Linux)
- Zephyr CAN - client and server (Zephyr RTOS)
- TCP/UDP Sockets - client and server (Linux, macOS (client only), FreeBSD, OpenBSD, Zephyr RTOS)
- TCP/UDP asio Sockets - client and server (Linux, macOS, FreeBSD, OpenBSD)
Both text (JSON) and binary (CBOR) encodings are supported.
The basic building blocks are properties, functions and groups. Each has an ID, a parent, a name and a value of a certain type.
A basic property is declared thus:
ThingSetReadWriteProperty<float> voltage { 0x300, 0, "voltage" };The type will be inferred if the value is specified at initialisation:
ThingSetReadWriteProperty voltage { 0x300, 0, "voltage", 24.0f };A property so declared provides its own storage for the given value. Alternatively, a property may be declared with a pointer:
float voltage;
ThingSetReadWriteProperty voltageProperty { 0x300, 0, "voltage", &voltage };The type will be inferred from the pointer type. Assignment to the property will update the underlying value:
voltageProperty = 25.0;
std::cout << voltage << std::endl; // prints 25All the basic primitive types are supported. Properties may also be structures*, or arrays of primitives or structures (known in C ThingSet as 'records').
Structures should be declared thus:
struct ModuleRecord
{
ThingSetReadWriteRecordMember<0x601, 0x600, "voltage", float> voltage;
ThingSetReadWriteRecordMember<0x602, 0x600, "current", float> current;
ThingSetReadWriteRecordMember<0x603, 0x600, "error", uint64_t> error;
ThingSetReadWriteRecordMember<0x604, 0x600, "cellVoltages", std::array<float, 6>> cellVoltages;
};As in C ThingSet, the above will be serialised as a map, with either integer ID or string name keys.
(* There is no direct equivalent of a single structure being a value in C ThingSet, which may cause compatibility problems.)
Functions are declared thus:
// inline lambda function
ThingSetUserFunction<0x400, 0x0, float, float, float> xAdd([](float x, float y) { return x + y});
int getStringLength(std::string &value)
{
return value.size();
}
// call existing function
ThingSetUserFunction<0x410, 0x0, int, std::string &> xStringLength(getStringLength);Note that, by default, parameters' IDs will be generated by incrementing the function ID. It is possible to override this.
Once you have declared your properties and functions, instantiate a server with an appropriate transport to expose them to clients.
ThingSetSocketServerTransport transport;
auto server = ThingSetServerBuilder::build(transport);
server.listen();The client is instantiated along similar lines:
std::array<uint8_t, 1024> rxBuffer;
std::array<uint8_t, 1024> txBuffer;
ThingSetSocketClientTransport clientTransport("127.0.0.1");
ThingSetClient client(clientTransport, rxBuffer, txBuffer);
client.connect();Values can then be retrieved:
float voltage;
if (client.get(0x300, voltage)) {
...
}...or updated:
client.update("voltage", 25.0f);...and functions invoked:
int result;
if (client.exec(0x1000, &result, 2, 3)) {
...
}See the Buffer class for an example of how serialisation and deserialisation support can be added to custom types.
See the examples folder for some samples of the code in action. The unit tests also give an idea of what can be done.