Skip to main content

MessageManager

Latest Version: 2.4.0

MessageManager is framework for asynchronous bidirectional agent to device communication. It is the successor to Bullwinkle.

The library uses ConnectionManager on the device side to receive notifications of connection and disconnection events, and to monitor connection status (ie. so that no attempt it made to send messages when the device is disconnected).

You can view the latest version of the library’s source code on GitHub. Click here to see information on other versions of this library.

To include this library in your project, add

#require "MessageManager.lib.nut:2.4.0"

at the top of your agent and device code.

Note MessageManager is designed to run over reliable (ie. TCP/TLS) connections. Retries only occur in the case of dropped connections or lost packets, or if called manually from beforeSend() or beforeRetry().

API Overview

MessageManager Usage

Constructor: MessageManager([options])

Calling the MessageManager constructor creates a new MessageManager instance. An optional table can be passed into the constructor (as options) to override default behaviours. options can contain any of the following keys:

Key Data Type Description
debug Boolean The flag that enables debug library mode, which turns on extended logging. Default: false
retryInterval Integer Changes the default timeout parameter passed to the retry method. Default: 0s
messageTimeout Integer Changes the default timeout required before a message is considered failed (to be acknowledged or replied to). Default: 10s
autoRetry Boolean If set to true, MessageManager will automatically continue to retry sending a message until maxAutoRetries has been reached when no onFail() callback is supplied. Please note that if maxAutoRetries is set to 0, autoRetry will have no limit to the number of times it will retry. Default: false
maxAutoRetries Integer Changes the default number of automatic retries to be performed by the library. After this number is reached the message will be dropped. Please note that the message will automatically be retried if there is no onFail() callback registered by the user. Default: 0
connectionManager ConnectionManager Optional instance of the ConnectionManager library that helps MessageManager track connectivity status. Default: null
nextIdGenerator Function User-defined callback that generates the next message ID. The function has no parameters. Default: null
onPartnerConnected Function Sets the callback to be triggered when the partner is known to be connected. The callback’s signature is: callbackName(reply), where reply(data) is a callback to respond to the “connected” event. Default: null
onConnectedReply Function Sets the callback to be triggered when the partner responds to the connected status. The callback’s signature is: callbackName(response), where response is the response data. Default: null
maxMessageRate Integer Maximum message send rate, which defines the maximum number of messages the library allows to send per second. If the application exceeds the limit, the onFail callback is triggered. Default: 10 messages/s
Note please don’t change the value unless absolutely necessary
firstMessageId Integer Initial value for the auto-incrementing message ID. Default: 0

Examples

// Initialize using default settings
local mm = MessageManager();
// Create ConnectionManager instance
local cm = ConnectionManager({
    "blinkupBehavior": CM_BLINK_ALWAYS,
    "stayConnected": true
});

imp.setsendbuffersize(8096);

// MessageManager options
local options = { "debug": true,
                  "retryInterval": 15,
                  "messageTimeout": 2,
                  "autoRetry": true,
                  "maxAutoRetries": 10,
                  "connectionManager": cm
};

local mm = MessageManager(options);

MessageManager.send(name[, data][, callbacks][, timeout][, metadata])

This method sends a named message to the partner side and returns the MessageManager.DataMessage object created. The data parameter can be a basic Squirrel type (1, true, "A String") or more complex data structures such as an array or table, but it must be a serializable Squirrel value.

mm.send("lights", true);   // Turn on the lights

callbacks is a table containing the local message event callback functions. The library uses the key name to determine which callback-registration method to use. For example, if the table contains the key onAck, the key’s value (which should be a function) will be passed into MessageManager.DataMessage.onAck() in order to register it as the acknowledgement callback.

Key Registration Method Description
onAck MessageManager.DataMessage.onAck Acknowledgement
onFail MessageManager.DataMessage.onFail Failure
onReply MessageManager.DataMessage.onReply Reply
onTimeout MessageManager.DataMessage.onTimeout Timeout

Note Message-local callbacks override the global ones, ie. if a message-local callback is triggered, the global one, if set, will not be executed.

MessageManager.on(messageName, callback)

If the messageName parameter’s argument is not null, this method sets the name-specific message callback. Otherwise it uses the generic message callback. The generic message callback captures any messages not handled by any name-specific callback. The callback function takes two parameters: message (the message) and reply (a function that can be called to reply to the message).

Example

// Set a name-specific message callback
mm.on("lights", function(message, reply) {
    led.write(message.data);
    reply("Got it!");
});

// Set a generic message callback
mm.on(null, function(message, reply) {
    log(message.data);
    reply("Got it!");
});

MessageManager.defaultOn(callback)

This method sets a callback which is executed only for those messages that are not matched and handled by any name-specific callbacks set via MessageManager.on. The callback function takes two parameters: message (the message) and reply (a function that can be called to reply to the message).

Example

// Set a generic message callback
mm.defaultOn(function(message, reply) {
    log(message.data);
    reply("Got it!");
});

MessageManager.beforeSend(callback)

This method sets the callback which will be called before a message is sent. The callback has the following parameters:

Parameter Description
message An instance of DataMessage to be sent
enqueue A function with no parameters which appends the message to the retry queue for later processing
drop A function which disposes of the message. It takes two optional parameters: silently, which defaults to true and which governs whether the disposal takes place silently or through the onFail callbacks, and error which if silently is false, specifies the error message to be provided to the onFail callback

The enqueue and drop functions must be called synchronously, if they are called at all.

Example

mm.beforeSend(
    function(msg, enqueue, drop) {
        if (runningOutOfMemory()) {
            drop();
        }

        if (needToPreserveMessageOrder() && previousMessagesFailed()) {
            enqueue();
        }
    }
);

MessageManager.beforeRetry(callback)

This method sets the callback for retry operations. It will be called before the library attempts to re-send the message and has the following parameters:

Parameter Description
message An instance of DataMessage to be re-sent
skip A function with a single parameter, duration, which postpones the retry attempt and leaves the message in the retry queue for the specified amount of time. If duration is not specified, it defaults to the retryInterval provided for MessageManager constructor
drop A function which disposes of the message. It takes two optional parameters: silently, which defaults to true and which governs whether the disposal takes place silently or through the onFail callbacks, and error which if silently is false, specifies the error message to be provided to the onFail callback

The skip and drop functions must be called synchronously, if they are called at all.

Example

mm.beforeRetry(
    function(msg, skip, drop) {
        if (runningOutOfMemory()) {
            drop();
        }

        if (needToWaitForSomeReasonBeforeRetry()) {
            skip(duration);
        }
    }
);

MessageManager.onFail(callback)

This method sets the function to be called when a message error occurs. The callback has the following parameters:

Parameter Description
message An instance of DataMessage that caused the error
reason The error description string
retry A function that can be invoked to retry sending the message in a specified period of time. This function must be called synchronously, if it is called at all. It takes one parameter, interval. If there is no interval parameter specified, the retryInterval value provided for MessageManager constructor is used. If the function is not called, the message will expire

Example

mm.onFail(
    function(msg, error, retry) {
        // Always retry to send the message
        retry();
    }
);

MessageManager.onTimeout(callback)

This method sets the callback to be executed when a message timeout occurs. The callback has the following parameters:

Parameter Description
message An instance of DataMessage that caused the timeout
wait A function which resets the acknowledgement timeout for the message, which means the message will not raise a timeout error for the interval of time specified by the function’s interval parameter. This function must be called synchronously
fail A function which makes the message fall through the onFail callbacks

If neither wait nor fail are called, the message will expire.

Example

mm.onTimeout(
    function(msg, wait, fail) {
        if (isStillValid(msg)) {
            wait(10);
        } else {
            // Fail otherwise
            fail();
        }
    }
);

MessageManager.onAck(callback)

This method sets the callback to be executed when the message’s receipt is acknowledged. The callback has the following parameters:

Parameter Description
message An instance of DataMessage that was acknowledged

Example

mm.onAck(
    function(msg) {
        // Just log the ACK event
        server.log("ACK received for " + msg.payload.data);
    }
);

Note A message is only acknowledged if the receiving party has defined a callback for this message.

MessageManager.onReply(callback)

This method sets the callback to be executed when the message is replied to. The callback has the following parameters:

Parameter Description
message An instance of DataMessage that was replied to
response The response from the partner

Example

mm.onReply(
    function(msg, response) {
        processResponseFor(msg.payload.data, response);
    }
);

Note Setting the callback doesn’t necessarily imply that the other side must reply to the message. It just registers a function which is executed if and when the message is replied to.

MessageManager.getPendingCount()

This method returns the overall number of pending messages (either waiting for acknowledgement or waiting in the retry queue).

Example

if (mm.getPendingCount() < SOME_MAX_PENDING_COUNT) {
    mm.send("temp", temp);
} else {
    // Do something else
}

MessageManager.DataMessage

MessageManager.DataMessage instances are not intended to be created by users manually — they are always returned from the MessageManager.send() method.

MessageManager.DataMessage.onFail()

This method sets a message-local version of the MessageManager.onFail() callback.

MessageManager.DataMessage.onTimeout()

This method sets a message-local version of the MessageManager.onTimeout() callback.

MessageManager.DataMessage.onAck()

This method sets a message-local version of the MessageManager.onAck() callback.

MessageManager.DataMessage.onReply()

This method sets a message-local version of the MessageManager.onReply() callback.

Other Usage Examples

Integration with ConnectionManager

// Device code

#require "ConnectionManager.lib.nut:3.0.0"
#require "MessageManager.lib.nut:2.4.0"

local cm = ConnectionManager({
    "blinkupBehavior": CM_BLINK_ALWAYS,
    "stayConnected": true
});

// Set the recommended buffer size
// (see https://github.com/electricimp/ConnectionManager for details)
imp.setsendbuffersize(8096);

local config = {
    "messageTimeout": 2,
    "connectionManager": cm
};

local counter = 0;
local mm = MessageManager(config);

mm.onFail(
    function(msg, error, retry) {
        server.log("Error occurred: " + error);
        retry();
    }
);

mm.onReply(
    function(msg, response) {
        server.log("Response for " + msg.payload.data + " received: " + response);
    }
);

function sendData() {
    mm.send("name", counter++);
    imp.wakeup(1, sendData);
}

sendData();
// Agent code

#require "MessageManager.lib.nut:2.4.0"

local mm = MessageManager();

mm.on("name", function(message, reply) {
    server.log("Message received: " + message.data);
    reply("Got it!");
});

Release History

The Electric Imp Dev Center documents the latest version of the library. For past versions, please see the Electric Imp public GitHub repos listed below.

Version Source Code Notes
0.9.0 GitHub Initial release
1.0.0 GitHub Add firstMessageId key to constructor options table; add error parameter to beforeRetry() and beforeSend() callback’s drop() parameter list
1.0.1 GitHub Bug fix for the “end of statement expected (; or lf) in file 'messagemanager.class.nut:1.0.0' row 299 col 120” issue
1.0.2 GitHub Fixed maxMessageRate issue
2.0.0 GitHub Bug fixes; Updated library name to meet requirements of new naming scheme
2.1.0 GitHub Message-local handlers overwrite the library-global ones
2.2.0 GitHub Send ACK only if there is a handler for the message registered (send NACK otherwise)
2.2.1 GitHub Fixed the "undefined handler" issue on startup
2.3.0 GitHub Add default message handler; code clean-up
2.4.0 GitHub Requires ConnectionManager v3.1.0 or newer; use named connect/disconnect handlers; don't connect on MessageManager instantiation when working with ConnectionManager

License

This library is licensed under the MIT License.