Dispatch Logo

Purpose

This document is intended to provide a guide regarding the use of Dispatch.

Source code may be found on github.

Concepts

Structure

Dispatch architecture

The Dispatch library consists of two primary components, the dispatch source and header and the framing source and header. Dispatch acts as the intermediary between your application and the remote application.

Your application publishes data to the subscribers using Dispatch and your subscribers consume data from Dispatch. Your application will never have to interact directly with the hardware or the framing libraries.

Dispatch is an event-driven framework which will execute your designated functions on receipt of certain messages.

Framing

Framing happens, but - fortunately - you don't need to worry about it. Framing has been completely abstracted!

Publishing Data Flow

A publish occurs when your application calls publish("topic", data). The data is then passed to Dispatch, through framing, and finally through the hardware drivers. The hardware drivers then pass the data through the communications channel where it is received by the hardware on the receiving end, de-framed, and then interpreted by the Dispatch library.

Publish flow

Once the remote dispatch has received the message, it calls any subscribers to that data and allows them to execute.

Subscribing

Subscribing is straightforward. Write your subscribing function and, then subscribe("topic", &mySubscriber);. Dispatch will execute your function every time that message is received.

Note that more than one function may be subscribed to a particular topic!

Retrieving Data

Data retrieval must occur inside the subscribing function. If the data is not retrieved within the function, then the data will be lost forever! Data retrieval is done within the subscribing function using the DIS_getElements() function.

Setting Up Dispatch

Hardware Interface

In the off-chance that a driver exists in /src/drivers/, then you are in luck!
In the likely scenario that it is not, then you will have to find or write the hardware driver yourself. There are four functions that need to be implemented: readable, writeable, read, and write. The function names do not matter since they will be assigned during initialization.

Examples can be found in the /src/drivers/ directory.

Buffers

The drivers should have some sort of internal memory buffer that functions as a circular buffer. These buffers are the ones accessed by the below functions. In this document, TX_BUF_LENGTH and RX_BUF_LENGTH are the defines that will be utilized to control the sizes of this buffer at compile time. You can call them what you wish.

Readable

The readable() function simply returns an unsigned 16-bit integer that indicates the amount of data than can currently be read from the RX circular buffer of the hardware interface.

uint16_t UART_readable(void);

Read

The read() function takes length amount of unsigned 8-bit data from the driver buffers and copies them to the given buffer.

void UART_read(void* data, uint16_t length);

Writeable

The writeable() function returns the unsigned 16-bit integer than indicates the amount of data that can safely be written to the circular buffer of the hardware interface.

uint16_t UART_writeable(void);

Write

The write() function will write length amount of unsigned 8-bit data from the given buffer to the driver buffer, which will be sent through the hardware interface.

void UART_write(void* data, uint16_t length)

Buffer Sizing

I considered not including defines for buffer sizing, but I quickly realized that some applications would be sending very modest amounts of data infrequenly and some could be sending significant amounts of data. As a result, you are required to set up the defines. There are default values that should get you started, but you should tune for your application.

Transmission Buffers

It is recommended that you write your UART TX driver to buffer at least 8 bytes. This gives enough room to send modest messages without stalling the processor to wait for the UART to send data. This is NOT a requirement! You can write your buffer to accept 1 byte at a time, but your processor will simply stall on a transmission waiting for bytes to move out of the TX queue.

Receive Buffers

Due to the issue of having to calculate the checksum and verifying before accepting the entire message, received messages MUST be buffered. There are three buffer levels that must be accounted for:

  1. RX Message Buffer
  2. Framing Buffer
  3. UART RX Driver Buffer

Dispatch RX Message Buffer

The dispatch message buffer is determined by:

  1. The topic string length
  2. The number of dimensions of the data
  3. The dimensional data width (width of data for all dimensions)
  4. The maximum length of all data to be received in a single reception

Use the provided buffer calculator to determine your memory footprint required for Dispatch to send and receive your data.

Description Value Unit Notes
Maximum topic string length characters Decrease this to decrease the recommended buffer size (1-to-1)
Number of dimensions of the data integer (1 to 15) Decrease this to decrease the recommended buffer size
Dimensional Width bytes This is the number of bytes/element transmitted. For instance, if you have two-dimensional data being sent, one of which is 1 byte wide and the other is 2 bytes wide, then this number should be 3. Decrease this to decrease the recommended buffer size.
Maximum data length unitless This is the number of elements that will be transmitted (or the size of the array)
Dispatch RX Buffer recommended length bytes This is a minimum and is non-optional. This should always be a power of 2.

Frame RX Buffer

The simplest way to determine the framing RX buffer is to simply double the size of the Dispatch message buffer. This will give a worst-case estimate that will ALWAYS be adequate under all circumstances. The reason for this is that the framing process adds escape characters when certain numbers are encountered. If the message consists of nearly all escape characters, then the message simply doubles in size. This is an unlikey event in some applications, but entirely plausible in others. For instance, it is not uncommon for sensor data to dwell on a particular number for some time. Ultimately, you must make the decision regarding this.

Description Value Unit
Recommended Frame Buffer bytes

UART RX Buffer

The UART RX Buffer is where the hardware interfaces with the software. In some cases, there will be hardware buffers on-die that will be adequate. On many microcontrollers, this will not be the case and software buffers will need to be implemented. The size of this buffer is determined by two factors: baud rate and the frequency that DIS_process() is called.

Description Value Unit Notes
Interface Data Rate bits/s Decrease this to decrease the recommended buffer size
Frequency of `DIS_process()` calls/s Increase this to decrease the recommended buffer size
Recommended Driver Buffer Size bytes

Summary

In the examples there is a file called 'dispatch_config.h' which contains some defines. Here, we will boil the above entries into those defines just to go the extra mile to make your experience a bit easier.

Define Value
MAX_NUM_OF_FORMAT_SPECIFIERS
MAX_TOPIC_STR_LEN
MAX_RECEIVE_MESSAGE_LEN
RX_FRAME_LENGTH

Initialization

Include the Source Files

In our case, we are including our hardware source file, uart.h, along with the dispatch header file, dispatch.h:

#include "uart.h"
#include "dispatch.h

Initializing the Hardware

It is usually necessary to initialize the hardware independently of Dispatch. For a serial module, this configures the registers to communicate on the desired channel at a particular baud rate. We have called our pin hardware initializer DIO_init() and the channel initializer UART_init().

DIO_init();
UART_init();

Initializing Dispatch

There are two items that need to be tended to, initializing Dispatch itself and assigning the function that Dispatch will utilize for communication.

/* Assign the four necessary channel functions to Dispatch. */
DIS_assignChannelReadable(&UART_readable);
DIS_assignChannelWriteable(&UART_writeable);
DIS_assignChannelRead(&UART_read);
DIS_assignChannelWrite(&UART_write);

/* Initialize Dispatch */
DIS_init();

As you can see, our communication hardware functions were very similary named to the Dispatch functions so as to make the assignments easier to identify.

Processing

After initialization, DIS_process() must be called periodically in order to process incoming data and subscriptions. Generally, this is placed into the infinite while(1) loop, but it can be assigned to a task or other periodic function.

while(1){
    DIS_process();

    /* ... other continually executing functions ... */
}

It is not recommended to place DIS_process() within a timer interrupt as it may block all of your other interrupts, depending on architecture and configuration of your device.

Using Dispatch

Publishing

As described in the initial post, publishing to Dispatch is easy. We have renamed some of the functions to keep them from potentially colliding with other functions, but the functionality has not changed. A quick summary:

/* send "my string" to subscribers of "foo" */
DIS_publish("foo", "my string");

/* send first element of bar[] to subscribers of "foo" */
DIS_publish("foo,u8", bar);

/* send 10 elements of bar[] to subscribers of "foo" */
DIS_publish("foo:10,u8", bar);

/* send 10 elements of bar[] and baz[] to subscribers of "foo" */
DIS_publish("foo:10,s16,s32", bar, baz);
//                             ^    ^ data sources
//                   ^   ^ format specifiers
//                ^ number of elements to send
//            ^ topic

When not sending a string, the format specifiers must be in place for each array of data to be sent. Use the format specifiers shown here:

Format Specifier Signed/Unsigned Width (bytes)
u8 unsigned 1
s8 signed 1
u16 unsigned 2
s16 signed 2
u32 unsigned 4
s32 signed 4
(str) - -

Alternate Publishing Methods

In order to reduce the program memory footprint, we have introduced less dynamic publishing functions which perform the same function as DIS_publish(), but simply use less memory by making the function less generic.

For instance, the two publish functions will result in sending the same data:

uint8_t data[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
DIS_publish("foo:10,u8", data);
DIS_publish_u8("foo:10", data);

We have reduce the amount of processing and program memory footprint necessary to send a transmission. This approach is only recommended for situations in which the program memory is limited.

Subscribing

Subscribing usually happens before the infinite while(1), but can happen at any time, even in response to other events.

First, we must write the subscribing function. Our subscribing function is going to increment a local counter and then publish that counter back to topic "i":

void mySubscriberFunction(void){
    static uint16_t i = 0;

    i++;

    /* publish i back to the sender to 'close the loop' */
    DIS_publish("i,u16", &i);
}

At some point, we must actually subscribe the function to the topic to create the association within Dispatch:

DIS_subscribe("foo", &mySubscriberFunction);

Now, every time that the topic "foo" is received, Dispatch calls mySubscriberFunction() which increments i and publishes it to topic "i".

Retrieving Topical Data

There is usually some payload sent with the topic. This can be string data or numeric, of 8, 16, or 32 bits. It may be a single data point or consist of an array or multiple arrays of data. How do we get the approprate data? In this, we will use the DIS_getElements() function.

Topical data should always have the same format. It is possible to send different data formats to the same topic, but there is no way to distinguish one format from another. Best to stick with one format per topic.

The DIS_getElements(uint16_t element, void* destArray) function takes two arguments, element and destArray. The element is the element number that is expected. In single dimensional data - such as strings, single numbers, and singe arrays - this number will be 0. In multidimensional data, this may be a different number in order to retrieve different values. A few examples should clear it up.

Retrieving a String

For instance, if the topic is known to receive a string, then allocate string storage within the subscribing function and use DIS_getElements to retrieve it:

void mySubscriberFunction(void){
    char str[32] = {0};  // allocate your string array
    DIS_getElements(0, str);    // copy the received data into str

    /* now use the data here */
}

Retrieving a Number

To retrieve a single number, we use a similar notation. When mySubscriberFunction is expecting a single unsigned integer:

void mySubscriberFunction(void){
    uint16_t v;  // allocate memory
    DIS_getElements(0, &v);    // copy the received data into local variable

    /* now use the data here */
}

Multiple Dimensions

To transmit more than one variable, multiple calls to DIS_getElements() will be necessary. Note that the different calls will specify different element numbers. In the below function, we are mixing different integer widths and signs and still receiving them appropriately, so long as the correct element is retrieved:

void mySubscriberFunction(void){
    uint16_t a;  // allocate memory
    int16_t b;
    int32_t c;

    DIS_getElements(0, &a);    // copy the received data into local variable
    DIS_getElements(1, &b);
    DIS_getElements(2, &c);

    /* now use the data here */
}

Arrays

It is also possible to retrieve arrays of data, so long as the array length is pre-determined. In fact, transmitting data within an array is much more bandwidth-efficient and has a very similar syntactical complexity:

void mySubscriberFunction(void){
    uint16_t v[10];  // allocate memory

    DIS_getElements(0, v);    // copy the received data into local array

    /* now use the data here */
}

One may even retrieve a multi-dimensional array with similar effort, even with different data widths:

void mySubscriberFunction(void){
    uint16_t x[10];  // allocate memory
    int8_t y[10];

    DIS_getElements(0, x);    // copy the received data into local array
    DIS_getElements(1, y);

    /* now use the data here */
}

And, just like that, 10 elements of x and y are received.

Function Reference

This document is intended to list all of the available functions of Dispatch. Move over to the Dispatch How-To for a comprehensive guide for using Dispatch.

Source code may be found on github.

Basic Functions

This table contains the basic functions which make up the Dispatch library. Most application will use all of these functions in one form or another.

Channel Assignment and Initialization

These functions should only be called at the beginning of program execution.

/* Assigns the function which is used to determine how many bytes are currently 
    readable from the UART RX buffer. */
void DIS_assignChannelReadable(uint16_t (*functPtr)());

/* Assigns the function which is used to determine how many bytes may be written to
    the UART TX buffer. */
void DIS_assignChannelWriteable(uint16_t (*functPtr)());

/* Assigns the function which is used to read from the UART RX buffer. */
void DIS_assignChannelRead(void (*functPtr)(uint8_t* data, uint16_t length));

/* Assigns the function which is used to write to the UART TX buffer. */
void DIS_assignChannelWrite(void (*functPtr)(uint8_t* data, uint16_t length));

/* Initializes Dispatch.  Must be called after the `DIS_assignChannel` functions. */
void DIS_init(void);

Normal Use

Subscribing/Unsubscribing

Subscribing to a topic will likely only occur once during initialization, but it is possible to dynamically subscribe and unsubscribe to topics. The DIS_getElements function is utilized to retrieve data within the subscriber.

/* Subscribes to a topic.  Normally called one time at initialization, but
    may be called at any time during program execution */
void DIS_subscribe(const char* topic, void (*functPtr)());

/* Removes a subscriber from the subscription list.  If the subscriber is not active, then
    there is no action. */
void DIS_unsubscribe(void (*functPtr)());

Retrieving Data

Retrieving data is - hopefully - as simple as sending it. Note that it must be completed within the subscribing function.

/* Retrieve data received on the RX. */
uint16_t DIS_getElements(uint16_t element, void* destArray);

The element is the element number which is to be retrieved. Generally, if it warrants a new variable, then it will have its own element number within a particular topic.

The destArray is the address of a variable to which the data is to be stored.

Publishing Data

/* Publishes data to a topic.  This is the most generic publish function and has the most
    flexibility. */
void DIS_publish(const char* topic, ...);

Processing Dispatch

/* Processes incoming messages and calls the appropriate subscribers.  Must be called
    periodically.  If this is called infrequently, then any subscriber functions are also called
    infrequently and may be missed. */
void DIS_process(void);

Reduced Memory Publishing

These functions are intended to replace the DIS_publish() function above with a specific variant in order to reduce the program memory footprint of Dispatch in some applications. It is not necessary to utilize any of these functions. All of these could be replaced by an appropriate call to DIS_publish().

These functions are available in releases of Dispatch that are greater than v1.0.

/* Publishes a string to a topic without having to use `stdarg.h`.  If the user only needs to send a
    string using Dispatch, then this will be smaller and faster than `DIS_publish()` */
void DIS_publish_str(const char* topic, char* str);

/* Publishes a single unsigned 8-bit array to the topic without using `stdarg.h`.  If this user only
    needs to send an 8-bit array using Dispatch, then this will be smaller and faster than `DIS_publish()`. */
void DIS_publish_u8(const char* topic, uint8_t* data);

/* Publishes a single signed 8-bit array to the topic without using `stdarg.h`.  If this user only
    needs to send an 8-bit array using Dispatch, then this will be smaller and faster than `DIS_publish()`. */
void DIS_publish_s8(const char* topic, int8_t* data);

/* Publishes two unsigned 8-bit array to the topic without using `stdarg.h`.  If this user only
    needs to send an 8-bit array using Dispatch, then this will be smaller and faster than `DIS_publish()`. */
void DIS_publish_2u8(const char* topic, uint8_t* data0, uint8_t* data1);

/* Publishes two signed 8-bit array to the topic without using `stdarg.h`.  If this user only
    needs to send an 8-bit array using Dispatch, then this will be smaller and faster than `DIS_publish()`. */
void DIS_publish_2s8(const char* topic, int8_t* data0, int8_t* data1);

/* Publishes a single unsigned 16-bit array to the topic without using `stdarg.h`.  If this user only
    needs to send an 8-bit array using Dispatch, then this will be smaller and faster than `DIS_publish()`. */
void DIS_publish_u16(const char* topic, uint16_t* data);

/* Publishes a single signed 16-bit array to the topic without using `stdarg.h`.  If this user only
    needs to send an 8-bit array using Dispatch, then this will be smaller and faster than `DIS_publish()`. */
void DIS_publish_s16(const char* topic, int16_t* data);

/* Publishes a single unsigned 32-bit array to the topic without using `stdarg.h`.  If this user only
    needs to send an 8-bit array using Dispatch, then this will be smaller and faster than `DIS_publish()`. */
void DIS_publish_u32(const char* topic, uint32_t* data);

/* Publishes a single signed 32-bit array to the topic without using `stdarg.h`.  If this user only
    needs to send an 8-bit array using Dispatch, then this will be smaller and faster than `DIS_publish()`. */
void DIS_publish_s32(const char* topic, int32_t* data);


© by Jason R. Jones 2016
My thanks to the Pelican and Python Communities.