Dispatch Logo

Purpose

This document is intended to provide a guide regarding the use of Dispatch. Move over to the Dispatch Function Reference for a complete list of the functions available guide for using 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.

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!

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

Initializing Dispatch

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".



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