A tiny message queue library for processing requests and broadcasting evens. Using this library you can build an IoT application using low coupled components.
The library contains a very basic implementation of a message queue and serialization abstractions. Exceptions and RTTI are not used intentionally since they might not be supported by the compilers on IOT devices.
Clone the library
$ git clone https://github.com/anisimovsergey/gluino.git
$ cd gluino
Build using cmake
$ mkdir build
$ cd build
$ cmake ..
$ make
Run the tests
$ ./test/gluino_test
Reset the counters
$ lcov --directory . --zerocounters
Even though RTTI is not used in this library, it is still possible to do the dynamic casting using a very basic type identification system. In order to participate in this system, your class should inherit from IEntity and use TYPE_INFO macro.
class Color : public Core::IEntity {
TYPE_INFO(Color, Core::IEntity, "color")
public:
Color(uint8_t h, uint8_t s, uint8_t v);
...
};
Status class is used for propagating an operation result and contains a status code, a message and an optional nested status. In order to return the result of an operation with some payload you can construct a tuple.
std::tuple<Core::Status, std::unique_ptr<Color>>
RequestSerializer::deserializeImpl(const IDeserializationContext& context) const {
...
auto color = std::make_unique<Color>(h, s, v);
return std::make_tuple(Status::OK, std::move(color));
}
All the messages in the system are divided into three types: requests, responses and events. Requests are added to the message queue and processed in FIFO order.
There are two major scenarios you can implement using the message queue.
- Reading a resource.
A resource client is sending a read request to the message queue. The resource controller, which is responsible for managing the specified resource, receives the request and responds with a response message. Normally the response contains a resource representation as a payload but in a case of an error the payload contains a status with the error description. The read operation supposed to be idempotent and should not modify the resource.
- Modifying a resource.
Resource modification can be initiated by a resource client. A resource modification request gets added to the message queue and received by a corresponding resource controller. The resource controller modifies the resource and responds to the client with a response containing an operation result. When the resource is successfully modified the event describing the modified resource is also broadcasted to all the clients.
The diagram displays an event added to the message queue before a response but because the events have lower priority it gets propagated to the client(s) after the response.
The request message (Request class) is send by resource clients (QueueResourceClient class) and handled by resource controllers (QueueResourceController class).
auto connection = std::make_unique<ConnectionParams>("WIFI_NAME", "PASSWORD");
auto request = std::make_unique<Request>("requestId", "clientId", RequestType::Create, "connection", std::move(connection));
The response message (Response class) is sent by queue resource controllers (QueueResourceController class) in the result of handling a request sent by a resource client (QueueResourceClient class).
auto status = std::make_unique<Status>(StatusCode::Created, "The connection was created.");
auto response = std::make_unique<Response>("requestId", "clientId", RequestType::Create, "connection", std::move(status));
The event message (Event class) is sent by queue resource controllers (QueueResourceController class) in order to notify all the queue resource clients about some changes in the resource state. Normally an event is sent in the result of some successfully performed modification request.
auto connection = std::make_unique<Connection>("WIFI_NAME", isConnected);
auto event = std::make_unique<Event>(EventType::Created, "connection", std::move(connection));
The queue resource client can be used to get access to a resource and perform operations with it. A resource client is created using IMessageQueue.createClient. During its creation the client gets added to the resource clients list of the queue and automatically removed when it goes out of scope.
colorClient = messageQueue.createClient("SenderId", Color::TypeId());
colorClient->addOnResponse(RequestType::Read, [=](const Color& color) {
onColorReadResponse(color);
});
colorClient->addOnEvent(EventType::Updated, [=](const Color& color) {
onColorUpdatedEvent(color);
});
The queue resource client simplifies the request sending by automatically including the client id and the resource type to the requests.
auto newColor = std::make_unique<Color>(h, s, v);
colorClient->sendRequest(RequestType::Update, std::move(newColor));
The queue resource controller is handling requests to a particular resource. The requests can involve a modification of the resource or can be just a simple read. In the former case the response should contain the operation status only and the new (modified) resource representation can be sent in the correspondent notification or explicitly retrieved by an idempotent read request.
colorController = messageQueue.createController(Color::TypeId());
colorController->addOnRequest(RequestType::Update, [=](const Color& color){
setColor(color);
colorController->sendEvent(EventType::Updated, std::make_unique<Color>(color));
return std::make_unique<Status>(Status::OK);
});
In case of a read request the response should contain the resource representation if the request was successful or a status otherwise.
connectionController = messageQueue.createController(Connection::TypeId());
connectionController->addOnRequest(RequestType::Read, [=](){
if (hasConnection()) {
return createConnectionObject();
} else {
return std::make_unique<Status>(StatusCode::NotFound, "The connection doesn't exist.");
}
});
Gluino library provides a very basic abstractions for serialization and deserialization. In order to use those abstractions you need to implement the following interfaces for your platform:
- IContextFactory - a factory responsible for creating the serialization and deserialization contexts
- ISerializationContext - the context needed for serialization and contains methods like
setString
,setInt
etc. - IDeserializationContext - the context needed for deserialization and contains methods like
getString
,getInt
etc.
In order to create a serializer for your object you need to inherit from Seializer<T>
and implement serializeImpl
and/or deserializeImpl
.
Core::Status
ColorSerializer::serializeImpl(ISerializationContext& context, const Color& status) const {
auto result = context.setByte("hue", color.getR());
if (!result.isOk())
return result;
... // Repeat for the saturation and volume
return Status::OK;
}
std::tuple<Core::Status, std::unique_ptr<Color>>
RequestSerializer::deserializeImpl(const IDeserializationContext& context) const {
Status result;
uint8_t h, s, v;
std::tie(result, h) = context.getByte("hue");
if (!result.isOk())
return std::make_tuple(result, nullptr);
... // Repeat for the saturation and volume
auto color = std::make_unique<Color>(h, s, v);
return std::make_tuple(Status::OK, std::move(color));
}