In the last few projects I completed, I needed to find a way to process user commands arising from multiple input sources. A simple example would be a clock with tact switches and a Bluetooth interface providing identical functionality from either user input source.
For these applications I developed a simple modular and scalable approach that can be applied in other projects.
Continuing with the clock example, one of the aims of processing multiple input streams is to provide seamless operation between the two modes of user input. The user should be able to start a command on in one mode (a switch at the back of the clock) and complete it from another (an App on a phone or tablet). There may also be commands only available through one or more input methods.
From a programming perspective I also wanted to decouple the command interfaces from the application so that, for example, the clock code can be compiled without the Bluetooth interface enabled and it would still work with the full functionality of the remaining modes. Similarly, adding a new command interface, like an infrared remote control, should also be straightforward and not interfere with the existing interfaces.
Structuring the Application
The application is split into a front and a back end. A command queue is filled by ‘producers’ at the front end and read by the a ‘consumer’, which processes the commands, at the back end. Broadly, the front end deals with the user interfaces that interact with the outside world and the back end is where the application does its work.
This design scales to allow multiple producers to feed the queue, seamlessly mixing the different possible sources and eliminating multi-master contention by simple sequencing within the queue. Properly implemented it enables the programmer to add or remove producers without affecting the consumer or other any producers.
One common data structure that works well as the queuing mechanism is a circular buffer.
The useful property of a circular buffer is that it does not need to have its elements shuffled around when one is consumed. In other words, the circular buffer is well-suited as a FIFO buffer, which is exactly what is needed to maintain the sequencing of user commands.
Circular buffering makes a good implementation strategy for a queue that has fixed maximum size. In this application, and on a limited memory system like the Arduino, this is implemented as a short fixed length array of a few (less than 10) elements. For arbitrarily expanding queues, a linked list approach is usually a better option.
The simple way to understand the circular buffer queuing mechanism applied to this application is through a brief explanation with reference to the diagram below.
Each Producer puts the user command it receives in the queue (array) at the PUT position. If PUT is the same as GET, in this application, the command is discarded (that is, we do not overwrite old commands). Once the command is stored in the queue, PUT is incremented by 1. If PUT is greater than the size of the array it is reset to the first element of the array.
The Consumer takes its next command from the GET position. If GET is the same as PUT, the queue is empty and no commands are available for processing. Once the command is taken from the queue, GET is incremented by 1. If GET is greater than the size of the array it is reset to the first element of the array.
In this way, the GET and PUT indices manage the data in the circular buffer in FIFO order. It is convenient to encapsulate the circular buffer in a class, such as the MD_CirQueue Arduino library that I now use in my applications.
By independently placing their data in the queue, Producers are decoupled from each other. On the other side of the queue, the Consumer only needs to process the next command in the queue, without needing to know its source, and is decoupled from all the Producers. This is the key to the modularity and scalability of this approach.