Message handling
Why we need an updated scheme
Currently the main communication between UI and engine are on basis of CNode derived specialization classes, and this goes for
- Commands like hardware detect, config detect, checkinitialize
- Status changes
- Operational commands like resetting a persistence value
This design is not very flexible and overcrowds our Nodefactory. Even if we would still accept this for the major commands, then certainly for device specific commands it is hard to continue this, e.g. a laser tracker home command.
Apart from the CNode derivation, we also have codes in our CTCPGrams:
Here are all current TCP telegram types:
constexpr unsigned char TCPGRAM_CODE_DOUBLES = 0; // array of doubles
constexpr unsigned char TCPGRAM_CODE_COMMAND = 1;
constexpr unsigned char TCPGRAM_CODE_STATUS = 2;
constexpr unsigned char TCPGRAM_CODE_CONFIGURATION = 3;
constexpr unsigned char TCPGRAM_CODE_STRING = 4; // string
constexpr unsigned char TCPGRAM_CODE_EVENT = 5;
constexpr unsigned char TCPGRAM_CODE_INTERRUPT = 6; // interrupt
constexpr unsigned char TCPGRAM_CODE_ERROR = 7; // contains an error
constexpr unsigned char TCPGRAM_CODE_TEST_BIG = 10;
constexpr unsigned char TCPGRAM_CODE_INVALID = 100; // invalid return
constexpr unsigned char TCPGRAM_CODE_DONT_USE = UCHAR_MAX; // contains a warning
All fields in yellow are candidates to be converted into Message.
The ones in red, check if these are doing anything.
Message class
We can replace this with :
Message is a JSON encapsulation with a main interface for:
- ID : which identify the type of message
- Params : which is a JSON object, so it can contain anything
Furthermore it has Serialize and a static Deserialize to convert to/from std::string so we can ship it as text with a CTCPGram.
Classes
- Message: Represents a message with an ID and JSON parameters.
- Request: Encapsulates a pending request, holding a promise for the reply and an optional handler for the reply.
- MessageResponder: A core class that allows subscribing to message IDs and dispatching received messages to appropriate handlers. It also manages sending messages and handling requests with futures or callbacks.
- Subscriber: A base class to help manage subscriptions for other classes. Derive from this when a class has a lot of responders.
- Subscription: Represents an active subscription, allowing for automatic un-subscription when it goes out of scope (RAII).
MessageResponder
MessageResponder allows to subscribe to messages, so when these are given to RespondToMessage, the routines of the subscriber are executed when a message of given ID is received.
Subscription
When subscribing to a message a subscription object is returned, which the subscriber needs to store for its lifetime, or has to provide as parameter when unsubscribing. The subscription makes sure the MessageResponder is not calling any routines related to objects that no longer exits.
A subscription gets a weak pointer to the MessageResponder so in a similar way, if the MessageResponder is destroyed, it is no longer possible to unsubscribe.
Functions that are provided as callbacks have the next form
using Reply = std::unique_ptr<Message>;
using Handler = std::function<Reply(const Message &)>;
So we get the Message as parameter and we can optionally return a reply message, or a null pointer if we don't want to send a reply back.
Requests
A request is when we send a specific message and expect a reply back.
A request can be done in 2 forms
void SendRequest(Message &, Handler);
void SendRequest(Message &, std::future<Message> &);
With the first gives you can attach a callback function to the reply.
With the second we can get the result via a future. This is only useful if you need to wait for the result, providing you are not blocking the message receiving mechanism.
Here is an example of usage
std::future<CTrack::Message> futureRequest;
PrintCommand("Sending message {}",requestID);
Message message(requestID, {{"Param1", {"Value1"}}, {"Param2", {"Value2"}}});
UImessageResponder->SendRequest(message, futureRequest)
...... request is send and stored in UImessageResponder below is an excerpt of the loop that consumes messages
Message replyMessage;
UImessageResponder->RespondToMessage(replyMessage);
if (futureRequest.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready)
Message response = std::move(it->get());
In practice the callback form will be used most, the given routine will receive the reply message as parameter.
Chaining requests
If we let our request callback return a message, then this new message will be send to the server at the other side.
Basically we have a list of
struct RequestItem
{
Message message;
Handler handler;
RequestItem(Message msg, Handler hndlr) : message(std::move(msg)), handler(std::move(hndlr)) {}
};
Each message will be send as a request, when it returns then the optional handler is called, and the next request in the list is launched.
The main trick is in this function
void NextRequest(MessageResponder &responder, std::shared_ptr<std::deque<RequestItem>> queue)
{
if (queue->empty())
{
return;
}
RequestItem item = std::move(queue->front());
queue->pop_front();
// Recursive callback
Handler cb = [queue, item, &responder](const Message &reply) -> Reply
{
if (item.handler)
item.handler(reply); // call the handler for this item
NextRequest(responder, queue); // only now send the next
return nullptr; // no reply expected
};
responder.SendRequest(item.message, cb);
}
When a reply is received we need to combine 2 tasks:
- Execute the callback associated with the message
- Schedule the request for the next
And this is done by locally creating a new lambda that calls these two functions and providing this lambda as parameter for the request instead of the callback from the list.
Error handling
Errors are not directly throwing, instead they should be thrown in the callbacks, and this will happen based on the contents of the reply.