This document is intended to provide a guide regarding the use of Dispatch.
Source code may be found on github.
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 happens, but - fortunately - you don't need to worry about it. Framing has been completely abstracted!
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.
Once the remote dispatch has received the message, it calls any subscribers to that data and allows them to execute.
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!
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.
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.
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.
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);
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);
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);
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)
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.
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.
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:
The dispatch message buffer is determined by:
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. |
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 |
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 |
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 |
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
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();
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.
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.
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) | - | - |
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 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".
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.
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 */
}
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 */
}
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 */
}
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.
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.
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.
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);
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 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.
/* Publishes data to a topic. This is the most generic publish function and has the most
flexibility. */
void DIS_publish(const char* topic, ...);
/* 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);
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);