📨 Message handling
📨

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. 

💡

The callback of a request will be executed only once, in contrast to regular callback sed with RespondToMessage

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.

A request of a specific ID can only be send once, sending a second request with the same ID will throw an exception. This could be changed in the future if there is a need for this, but then we need to implement it in a similar way as a subscription, returning a request subscription.

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.