Commit 980c03ad authored by Niels Post's avatar Niels Post
Browse files

- Moved the parameter limits to the CommandCategoryController to make adding commands easier

- Added to the developer documentation: Adding communication method, creating your own documentation
- Testing GitLab CI to automatically generate documentation
parent aa097de1
Pipeline #8 failed with stage
image: alpine
stage: deploy
- apk update && apk add doxygen graphviz python3 python3-dev py3-pip
- cd docs/
- pip install -r requirements.txt
- doxygen Doxyfile
- sphinx-build -b html -Dbreathe_projects.MPFirm=xml "." "_build"
- rm -rf public && mv _build/ public/
- public
- development
......@@ -28,7 +28,8 @@ author = 'Niels Post'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
breathe_default_project = "MPFirm"
.. MP-Firm documentation master file, created by
sphinx-quickstart on Tue Oct 27 13:49:22 2020.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
......@@ -10,10 +5,29 @@ MP-Firm
:maxdepth: 3
:caption: Contents:
.. contents:: Table of Contents
:depth: 3
.. role:: bash(code)
:language: bash
This is the software documentation for the firmware for a moving platform robot built for the Warehouse PMSV project (link here).
This page is aimed at developers who want to extend the functionality of MP-Firm or adjust it to their own needs. For instructions on how to use the firmware as-is, please refer to this instructable (todo).
This documentation is aimed at developers who want to extend the functionality of MP-Firm or adjust it to their own needs.
For instructions on how to use the firmware as-is, please refer to this instructable (todo).
The base version of MP-firm allows the following:
- **Movement** MP-firm can move the Moving Platform using standard stepper motor drivers (Any driver module using a step and direction pin)
- **Communication** A robot using MP-firm can wirelessly receive commands and transmit measurement through RF, using an NRF24l01.
MP-Firm is built in an object-oriented way using the Entity-Controller-Boundary (ECB) architecture. This means that classes are divided in 3 categories:
......@@ -24,25 +38,93 @@ MP-Firm is built in an object-oriented way using the Entity-Controller-Boundary
Because of this modularity, expanding MP-Firm to add functionality is rather easy. In the following few sections,
some use cases for expansions are explained, along with some short implementation examples.
Extending MP-Firm
All the code for MP-Firm is in the same repository. For compiling, uploading and managing dependencies, PlatformIO is used.
This means that the codebase should work cross-platform (not tested), and also makes installation rather straightforward:
1. `Install PlatformIO core <>`_
2. Clone the MP-Firm repository anywhere you like:
.. code-block:: bash
git clone
3. Change into the code directory:
.. code-block:: bash
cd warehouse-pmsv-robot
4. Install dependencies
.. code-block:: bash
pio install
That's it!
When done modifying the code to your needs:
1. Plug in the robot through it's programming port (the USB port closes to the power jack)
2. Run the following to compile the code and upload it to the robot:
.. code-block:: bash
pio run -t upload
Adding Custom Commands
All commands in MP-firm are identified by the combination of a category ID (3 bits) and a command ID (5 bits).
The easiest way to add functionality to MP-firm is to create a new command category and add your custom commands to it.
All commands in MP-firm consist of a category ID (3 bits) and a command ID (5 bits).
When adding commands, it is easiest to define a custom category for it.
Adding a category ID
First, find a category ID that is not used yet. Category ID's are stored in ``include/entity/CommandData.hpp``.
First, find a category ID that is not used yet. Category ID's are stored in ``include/entity/CommandData.hpp`` in the ``CommandCategory`` enum class.
Add your Category ID to the CommandCategory enum. For this example we'll add a command category FOOBAR = 4.
The result might look as follows:
.. code-block:: c++
enum class CommandCategory : uint8_t {
While editing this file, it is recommended to add an enum for the commands in your custom category.
For the example, our category only accepts two commands, so the enum may look like:
.. code-block:: c++
enum FooBarCommand {
FOO = 0,
BAR = 1,
Creating a command handler
After this, you need a way to handle commands in the category. This can be done by extending CommandCategoryController.
A CommandCategoryController has the following methods:
.. doxygenclass:: CommandCategoryController
An example could be:
.. code-block:: c++
......@@ -51,7 +133,7 @@ An example could be:
class FooBarCommandController: public CommandCategoryController {
RobotMessage handle(const ControllerMessage &cmd) override {
switch(cmd.command_id) {
switch(static_cast<FooBarCommand>(cmd.command_id)) { // The
case 1: // Print Foo when command 1 was issued
......@@ -67,11 +149,23 @@ An example could be:
uint8_t getCategoryID() override {
return static_cast<uint8_t>(CommandCategory::FOOBAR);
// Both commands don't take data, but for the purpose of the instruction:
std::pair<uint8_t, uint8_t> getParameterLimits(uint8_t command_id) override {
switch(command_id) {
case FOO:
return {0,0};
case BAR:
return {0,0};
return {255,255};
Adding the command handler to MP-Firm
When the CommandCategoryController is implemented, you just need to make MP-firm aware of it.
This can be done in main.cpp, using the following steps:
......@@ -96,42 +190,152 @@ Add it to the CommandCategoryHandlers array:
All done! When you send a command with the category id 4, and the command id 1, the robot should send "Foo" over its USB interface.
All done! When you send a command with the category id 4, and the command id 1, the robot should print "Foo" over its USB interface.
Adding a communication method
By default, MP-Firm uses RF to communicate through an `NRF24l01 chip <>`_.
To make it easy to use different communication methods, MP-Firm has a boundary class called the MessageBoundary. This boundary is
responsible for all messages that are sent and received. To use a different communication method, you just need to create a MessageBoundary for it,
and make MP-Firm aware of it.
In the following example, we will be creating a MessageBoundary that uses the Arduino UART interface to communicate.
This could be used to receive and send commands over the USB interface.
Creating the MessageBoundary
The MessageBoundary has the following methods:
.. doxygenclass:: CommunicationController
.. doxygenclass:: MessageBoundary
An example of a USB MessageBoundary could be:
.. doxygenstruct:: ControllerMessage
.. doxygenstruct:: RobotMessage
.. code-block:: c++
class UartMessageBoundary: public MessageBoundary {
UARTClass &interface;
.. doxygenclass:: CommandCategoryController
UartMessageBoundary(UARTClass &anInterface);
.. doxygenclass:: GeneralCommandController
.. doxygenclass:: ActionCommandController
bool sendMessage(const RobotMessage &command) override{
// Write all command data
auto size_written = interface.write((const uint8_t *) (&command), command.size());
// Check if the bytes written matches the command size to determine success
return size_written == command.size();
.. doxygenclass:: MeasurementCommandController
bool isMessageAvailable() override() {
// Check if there are enough bytes available for a command header
return interface.available() >= 2;
ControllerMessage getNextMessage() override {
uint8_t data[ControllerMessage::maxMessageSize() + 1];
// Read the next command
// Note that in this boundary, we use a ~ to indicate the end of a command,
// This is because UART itself does not indicate ends of messages
// We add one to the maximum message size, since the ~ also needs to be read
size_t length = interface.readBytesUntil('~', data, ControllerMessage::maxMessageSize() + 1);
// Parse the data.
// If ill-formatted, parse() will automatically return an INVALID command.
return ControllerMessage::parse(data, length);
Adding the created MessageBoundary to MP-firm
When the MessageBoundary is done, you just need to tell MP-firm to use it when communicating.
This is done in ``main.cpp``.
First, create the boundary object. In this case we also pass Arduino's ``Serial`` Object, to tell the boundary to use the standard Serial interface (USB).
While doing this, you could remove the MessageBoundary used before, since it is no longer needed.
.. code-block:: c++
UartMessageBoundary uartBoundary{Serial};
Finally, pass the created boundary to the CommunicationController during its construction.
Look up the following line:
.. code-block:: c++
CommunicationController<20, array_size(commandCategoryHandlers)> communicationController{nrfBoundary, commandCategoryHandlers};
Change it to use your boundary instead
.. code-block:: c++
CommunicationController<20, array_size(commandCategoryHandlers)> communicationController{uartBoundary, commandCategoryHandlers};
Creating your own documentation
Generating this documentation page is done using Doxygen and Sphinx, using Breathe to combine the two.
Should you want to make your own version of this page, this chapter explains how.
Before starting, you need the following.
- `Python 3 and Pip <>`_
- `Doxygen <>`_
When both are installed, run the following to install the python packages required:
.. code-block:: bash
cd docs
pip install -r requirements.txt
After this, you should be ready to go
Changing the documentation
The main structure of the documentation is defined in ``docs/index.rst``. In here, multiple sections are included from ``docs/pages``.
All documentation is written in RestructuredText, with some extensions of it being added by sphinx and breathe.
While writing documentation, you can reference the following manuals:
- `Sphinx RST cheatsheet <>`_.
- `Breathe RST directives <>`_.
Building the documentation
To build the documentation, you need to execute two steps. Both of these steps need to be executed in the ``docs/`` folder.
These steps are automatically executed by ``docs/documentation.bat``
1. First, use doxygen to create the XML files Breathe uses to document code:
.. code-block:: bash
doxygen Doxyfile
2. Next, generate the HTML pages using sphinx:
.. code-block:: bash
sphinx-build -b html -Dbreathe_projects.MPFirm=xml "." "_build"
The documentation should now be generated in ``docs/_build``. To view the documentation, just open ``docs/_build/index.html``
Indices and tables
......@@ -10,7 +10,7 @@
* It is assumed that communication is two way between 1 controller and 1 robot (it is not a busline)
class CommandBoundary {
class MessageBoundary {
* Transmit a returncommand to the Controller
#pragma once
#include <boundary/communication/CommandBoundary.hpp>
#include <boundary/communication/MessageBoundary.hpp>
#include <RF24.h>
class NRFBoundary: public CommandBoundary {
class NRFBoundary: public MessageBoundary {
/// The radio used to communicate with the other party
RF24 nrfRadio;
#pragma once
#include <arduino.h>
#include <boundary/communication/MessageBoundary.hpp>
class UartMessageBoundary: public MessageBoundary {
UARTClass &interface;
UartMessageBoundary(UARTClass &anInterface);
bool sendMessage(const RobotMessage &command) override;
bool isMessageAvailable() override;
ControllerMessage getNextMessage() override;
\ No newline at end of file
......@@ -15,9 +15,16 @@ public:
virtual uint8_t getCategoryID() = 0;
* This method is called when a command is received with a category_id equal to getCategoryID().
* @param cmd Command to handle
* Process and execute a command in a message, and return a success code
* @param cmd Message to handle
* @return The result of the command. This should be sent back to the sender of the command
virtual RobotMessage handle(const ControllerMessage &cmd) = 0;
* Get the minimum and maximum size of each command in this category
* @param command_id The command id the bounds are requested for
* @return A pair of the minimum and maximum size
virtual std::pair<uint8_t, uint8_t> getParameterLimits(uint8_t command_id) = 0;
......@@ -27,5 +27,8 @@ public:
ActionCommandController(MovementController &movementController);
uint8_t getCategoryID() override;
std::pair<uint8_t, uint8_t> getParameterLimits(uint8_t command_id) override;
......@@ -18,4 +18,6 @@ public:
std::pair<uint8_t, uint8_t> getParameterLimits(uint8_t command_id) override;
......@@ -15,5 +15,7 @@ public:
RobotMessage handle(const ControllerMessage &cmd) override;
uint8_t getCategoryID() override;
std::pair<uint8_t, uint8_t> getParameterLimits(uint8_t command_id) override;
......@@ -3,7 +3,7 @@
#include <RF24.h>
#include <entity/StackQueue.hpp>
#include <boundary/communication/CommandBoundary.hpp>
#include <boundary/communication/MessageBoundary.hpp>
#include "CommandCategoryController.hpp"
class PMSVSettings;
......@@ -28,7 +28,7 @@ constexpr std::size_t array_size(const T (&array)[N]) noexcept
template<size_t commandQueueMaxLength, size_t handlerCount>
class CommunicationController {
CommandBoundary &commandBoundary;
MessageBoundary &commandBoundary;
/// Queue of commands that are still to be handled
StackQueue <ControllerMessage, commandQueueMaxLength> commandQueue;
......@@ -63,14 +63,31 @@ protected:
void parseNextCommand() {
if (commandBoundary.isMessageAvailable()) {
ControllerMessage cmd = commandBoundary.getNextMessage();
if (cmd.isValid()) {
if (isKnownAndValidMessage(cmd)) {
bool isKnownAndValidMessage(const ControllerMessage &msg) {
if(msg.isValid()) {
return false;
for (CommandCategoryController *handler: commandCategoryHandlers) {
if (msg.category_id == handler->getCategoryID()) {
auto limits = handler->getParameterLimits(msg.command_id);
if(msg.parameter_length >= limits.first && msg.parameter_length <= limits.second) {
return true;
return false;
CommunicationController(CommandBoundary &commandBoundary,
CommunicationController(MessageBoundary &commandBoundary,
CommandCategoryController *commandCatHandlers[handlerCount])
: commandBoundary(commandBoundary), commandQueue() {
for (size_t i = 0; i < handlerCount; i++) {
......@@ -12,14 +12,14 @@ enum class CommandCategory : uint8_t {
* Commands in the "General" category
enum class GeneralCommand : uint8_t {
enum GeneralCommand {
* Commands in the "Action" category
enum class ActionCommand : uint8_t {
enum ActionCommand {
......@@ -29,7 +29,7 @@ enum class ActionCommand : uint8_t {
* Commands in the "Measurement" category
enum class MeasurementCommand : uint8_t {
enum MeasurementCommand {
\ No newline at end of file
......@@ -18,11 +18,11 @@ struct ControllerMessage {
uint8_t command_id;
/// A value for identifying a specific message.
uint8_t message_id;
/// All parameter bytes given for this command
uint8_t parameters[COMMAND_MAX_PARAM_LENGTH];
/// The byte count of the parameters stored in "parameters"
size_t parameter_length;
/// All parameter bytes given for this command
uint8_t parameters[COMMAND_MAX_PARAM_LENGTH];
......@@ -40,27 +40,7 @@ struct ControllerMessage {
* @param len Length of the array
* @return
static ControllerMessage parse(uint8_t *data, const uint8_t &len);
* Check if an array of data is a properly formatted Command
* \details Checks if the size is correct for the given command.
* Checks if the command given is known.
* @param data
* @param len
* @return
static bool validate(const uint8_t *data, const uint8_t &len);
* Get the minimum and maximum parameter length for a given command.
* @param cat The Command Category id
* @param cmd The command ID
* @return A pair of sizes, where first is the minimum size, and second is the maximum size
static std::pair <uint8_t, uint8_t> getParameterLimits(CommandCategory cat, uint8_t cmd);
static ControllerMessage parse(uint8_t *data, const uint8_t &len = 0);
* Get a 16 bit unsigned integer from the parameters
......@@ -78,8 +58,10 @@ struct ControllerMessage {
uint32_t get_param32(const size_t &startIndex, bool msb_first = true) const;
bool isValid() {
return (category_id != 255 || command_id != 255 || message_id != 255);
bool isValid() const;
static size_t maxMessageSize() {
#pragma once
#undef min
#undef max
#include <algorithm>
template<typename T, size_t max_size>
class StackQueue {
size_t _front = 0;
......@@ -2,7 +2,7 @@
// Created by niels on 30/10/2020.
#include "boundary/communication/commandboundary/NRFBoundary.hpp"
#include "boundary/communication/messageboundary/NRFBoundary.hpp"