A C++ library for Arduino and Teensy microcontrollers that provides the framework for integrating custom hardware modules with a centralized PC control interface.
This library allows integrating custom hardware modules of any complexity managed by the Arduino or Teensy microcontrollers with the centralized PC interface implemented in Python. To do so, the library defines a shared API that can be integrated into user-defined modules by subclassing the (base) Module class. It also provides the Kernel class that manages runtime task scheduling, and the Communication class, which handles high-throughput bidirectional communication with the PC.
- Supports all recent Arduino and Teensy architectures and platforms.
- Provides an easy-to-implement API that integrates any hardware with the centralized host-computer (PC) interface written in Python.
- Abstracts communication and runtime task scheduling, allowing end users to focus on implementing the logic of their custom hardware modules.
- Supports concurrent command execution for multiple module instances.
- Contains many sanity checks performed at compile time and initialization to minimize the potential for unexpected behavior and data corruption.
- GPL 3 License.
- Dependencies
- Installation
- Usage
- API Documentation
- Developers
- Versioning
- Authors
- License
- Acknowledgements
- An IDE or Framework capable of uploading microcontroller software that supports Platformio. This library is explicitly designed to be uploaded via Platformio and will likely not work with any other IDE or Framework.
Note! Developers should see the Developers section for information on installing additional development dependencies.
Note, installation from source is highly discouraged for anyone who is not an active project developer.
- Download this repository to the local machine using the preferred method, such as git-cloning. Use one of the stable releases.
- Unpack the downloaded tarball and move all 'src' contents into the appropriate destination ('include,' 'src,' or 'libs') directory of the project that needs to use this library.
- Add
include <kernel.h>,include <communication.h>, andinclude <module.h>at the top of the main.cpp file andinclude <module.h>at the top of each custom hardware module header file.
- Navigate to the project’s platformio.ini file and add the following line to the target environment specification:
lib_deps = inkaros/ataraxis-micro-controller@^2.0.0. - Add
include <kernel.h>,include <communication.h>, andinclude <module.h>at the top of the main.cpp file andinclude <module.h>at the top of each custom hardware module header file.
This section demonstrates how to use custom hardware modules compatible with this library. See this section for instructions on how to implement custom hardware module classes. Note, the example below should be run together with the companion python interface example. See the module_integration.cpp for the .cpp implementation of this example:
// Dependencies
#include "../examples/example_module.h" // Since there is an overlap with the general 'examples', uses the local path.
#include "Arduino.h"
#include "communication.h"
#include "kernel.h"
#include "module.h"
// Specifies the unique identifier for the test microcontroller
constexpr uint8_t kControllerID = 222;
// Keepalive interval in milliseconds. If the keepalive interval is greater than 0, the Kernel expects the PC to send
// keepalive messages at that interval. If the Kernel does not receive a keepalive message in time, it assumes that the
// microcontroller-PC communication has been lost and resets the microcontroller, aborting the runtime.
constexpr uint32_t kKeepaliveInterval = 5000; // Sets the keepalive interval to 5 seconds.
// Initializes the Communication class. This class instance is shared by all other classes and manages incoming and
// outgoing communication with the companion host-computer (PC). The Communication has to be instantiated first.
// NOLINTNEXTLINE(cppcoreguidelines-interfaces-global-init)
Communication axmc_communication(Serial);
// Creates two instances of the TestModule class. The first argument is the module type (family), which is the same (1)
// for both, the second argument is the module ID (instance), which is different. The type and id codes do not have
// any inherent meaning, they are defined by the user and are only used to ensure specific module instances can be
// uniquely addressed during runtime.
TestModule<> test_module_1(1, 1, axmc_communication);
// Also uses the template to override the digital pin controlled by the module instance from the default (5) to 6.
TestModule<6> test_module_2(1, 2, axmc_communication);
// Packages all module instances into an array to be managed by the Kernel class.
Module* modules[] = {&test_module_1, &test_module_2};
// Instantiates the Kernel class. The Kernel has to be instantiated last.
Kernel axmc_kernel(kControllerID, axmc_communication, modules, kKeepaliveInterval);
// This function is only executed once. Since Kernel manages the setup for each module, there is no need to set up each
// module's hardware individually.
void setup()
{
// Initializes the serial communication.
Serial.begin(115200);
// Sets up the hardware and software for the Kernel and all managed modules.
axmc_kernel.Setup();
}
// This function is executed repeatedly while the microcontroller is powered.
void loop()
{
// Since the Kernel instance manages the runtime of all modules, the only method that needs to be called
// here is the RuntimeCycle method.
axmc_kernel.RuntimeCycle();
}
This library is designed to flexibly support many different use patterns. To do so, it intentionally avoids hardcoding certain metadata variables that allow the PC interface to individuate and address the managed microcontroller and specific hardware module instances. Each end user has to manually define these values both for the microcontroller and the PC.
-
Controller ID. This is a unique code from 1 to 255 that identifies the microcontroller. This ID code is used when communicating with the microcontroller and logging the data received from the microcontroller, so it has to be unique for all microcontrollers and other Ataraxis assets used at the same time. For example, Video System classes also use the ID code system to identify themselves during communication and logging and clash with microcontroller IDs if both are used at the same time. -
Module Typefor each hardware module instance. This is a unique code from 1 to 255 that identifies the family (class) of each module instance. For example, all solenoid valves may use the type-code '1,' while all voltage sensors may use the type-code '2.' The type-codes do not have any inherent meaning. Their interpretation depends entirely on the end-user’s preference when implementing the hardware module and its PC interface. -
Module IDfor each hardware module instance. This code has to be unique within the module type (family) and is used to identify specific module instances. For example, if two voltage sensors (type code '2') are used at the same time, the first voltage sensor should use ID code '1,' while the second sensor should use ID code '2.'
A major runtime safety feature of this library is the support for keepalive messaging. When enabled, the Kernel instance
expects the PC to send a 'keepalive' command at regular intervals, specified by the keepalive_interval Kernel
constructor argument. If the Kernel does not receive the keepalive message for two consecutive interval windows,
it aborts the runtime by resetting the microcontroller’s hardware and software to the default state and sends an error
message to the PC.
The keepalive functionality is disabled (set to 0) by default, but it is recommended to enable it for most use cases. See the API documentation for the Kernel class for more details on configuring the keepalive messaging.
Note! The appropriate keepalive interval depends on the communication speed and the CPU frequency of the microcontroller. For a fast microcontroller (teensy4.1) that uses the USB communication interface, an appropriate keepalive interval is typically measured in milliseconds (100 to 500). For a slower microcontroller (arduino mega) with a UART communication interface using the baudrate of 115200, the appropriate keepalive interval is typically measured in seconds (2 to 5).
For this library, any external hardware that communicates with Arduino or Teensy microcontroller pins is a hardware module. For example, a 3d-party voltage sensor that emits an analog signal detected by an Arduino microcontroller is a module. A rotary encoder that sends digital interrupt signals to 3 digital pins of a Teensy microcontroller is a module. A solenoid valve gated by HIGH signal sent from an Arduino microcontroller’s digital pin is a module.
The library expects that the logic that governs how the microcontroller interacts with these modules is provided by a C++ class, the 'software' portion of the hardware module. Typically, this class contains the methods for manipulating the hardware module or collecting the data from the hardware module. The central purpose of this library is to enable the centralized PC interface, implemented in Python, to work with a wide range of custom hardware modules in a standardized fashion. To achieve this, all custom hardware modules have to subclass the base Module class, provided by this library. See the section below for details on how to implement compatible hardware modules.
All modules intended to be accessible through this library have to follow the implementation guidelines described in the example module header file. Specifically, all custom modules have to subclass the Module class from this library and overload all pure virtual methods. Additionally, it is highly advised to implement the module’s custom command logic using the stage-based design pattern shown in the example. Note, all examples featured in this guide are taken directly from the example_module.h and the module_integration.cpp.
The library is intended to be used together with the companion PC interface. Each custom hardware module class implemented using this library must have a companion ModuleInterface class implemented in Python. These two classes act as the endpoints of the PC-Microcontroller interface, while other library assets abstract the intermediate steps that connect the PC interface class with the microcontroller hardware logic class.
Do not directly access the Kernel or Communication classes when implementing custom hardware modules. The base Module class allows accessing all necessary library assets through the inherited utility methods. See the 'protected static functions' section of the Module class API documentation for more details about the available utility methods
A major feature of the library is that it allows maximizing the microcontroller’s throughput by partially overlapping the execution of multiple commands under certain conditions. Specifically, it allows executing other commands while waiting for a time-based delay in the currently executed command. This feature is especially relevant for higher-end microcontrollers, such as Teensy 4.0+, that can execute many instructions during a multi-millisecond delay interval.
During each cycle of the microcontroller’s main loop() function, the Kernel sequentially instructs each managed module
instance to execute its active command. Typically, the module runs through the command, delaying code execution as
necessary, and resulting in the microcontroller doing nothing during the delay. With this library, commands can use the
WaitForMicros utility method together with the stage-based design pattern showcased by the
TestModule’s Pulse command to allow other modules to run their commands while the module
waits for the delay to expire.
Warning! The non-blocking mode is most effective when used with delays that tolerate a degree of imprecision or on microcontrollers that have a very high CPU clock speed. Additionally, to support non-blocking runtimes, all modules used at the same time must support non-blocking execution for all commands. Overall, the decision of whether to use the non-blocking mode often requires practical testing under the intended runtime conditions and may not be suitable for all use cases.
Note! While this library only supports non-blocking execution for time-based delays natively, advanced users can follow the same design principles to implement non-blocking sensor-based delays when implementing custom command logic.
These methods provide the inherited API that integrates any custom hardware module with the centralized control interface running on the companion host-computer (PC). Specifically, the Kernel calls these methods during runtime to interface with each managed module instance.
This method enables the Kernel to unpack and save the module’s runtime parameters, when updated parameter values are received from the PC. The primary purpose of this virtual method is to tell the Kernel where to unpack the module’s parameter data.
For most use cases, the method can be implemented with a single call to the ExtractParameters() utility method inherited from the base Module class:
bool SetCustomParameters() override
{
return ExtractParameters(parameters); // Unpacks the received parameter data into the storage object
}
The parameters object is typically a structure that stores the instance’s PC-addressable runtime parameters.
The ExtractParameters() utility method, reads the data received from the PC and uses it to overwrite the memory of
the provided object.
This method enables the Kernel to execute the managed module’s logic in response to receiving module-addressed commands from the PC. Specifically, the Kernel receives and queues the commands to be executed and then calls this method for each managed module to run the queued command’s logic. The primary purpose of this method is to translate the active command code into the call to the command’s logic method.
For most use cases, this method can be implemented with a simple switch statement:
switch (static_cast<kCommands>(GetActiveCommand()))
{
// Active command matches the code for the Pulse command
case kCommands::kPulse:
Pulse(); // Executes the command logic
return true; // Notifies the Kernel that command was recognized and executed
// Active command matches the code for the Echo command
case kCommands::kEcho: Echo(); return true;
// Active command does not match any valid command code
default: return false; // Notifies the Kernel that the command was not recognized
}
The switch uses the GetActiveCommand() method, inherited from the base Module class, to retrieve the code of the
currently active command. It is recommended to use an enumeration to map valid command codes to meaningful names
addressable in code, like it is done with the kCommands enumeration in the demonstration above.
Note! The method has to return true if it recognizes the command and return false if it does not. The
returned value of this method only communicates whether the command was recognized. It should not be used
to track the runtime status (success / failure) of the called command’s method.
This method enables the Kernel to set up the hardware and software assets for each managed module instance. This is
done from the global setup() function, which is executed by the Arduino and Teensy microcontrollers after firmware
reupload. This is also done in response to the PC requesting the controller to be reset to the default state. The
primary purpose of this method is to initialize the hardware and software of each module instance to the state that
supports the runtime.
The implementation of this method should follow the same guidelines as the general microcontroller setup() function:
bool SetupModule() override
{
// Configures class hardware
pinMode(kPin, OUTPUT);
digitalWrite(kPin, LOW);
// Configures class software
parameters.on_duration = 2000000;
parameters.off_duration = 2000000;
parameters.echo_value = 123;
// Notifies the Kernel that the setup is complete
return true;
}
It is recommended to implement the method in a way that always returns true and does not fail. However, to support
the runtimes that need to be able to fail, the method supports returning false to notify the Kernel that the setup has
failed. If this method returns false, the Kernel deadlocks the microcontroller in the error state until the
microcontroller firmware is reuploaded to fix the setup error.
To further simplify implementing custom hardware modules, the base Module class exposes a collection of utility methods. These methods provide an easy-to-use API for safely accessing internal attributes and properties of the superclass, simplifying the interaction between the superclass (Module) and the custom logic of each hardware module that inherits from the base class.
See the API Documentation of the base Module class for the list of available (protected) Utility methods. Also, see the TestModule for the demonstration on how to use some of these methods when implementing custom hardware module, most notably those relating to sending the data to the PC and using the stage-based command design pattern.
See the API documentation for the detailed description of the methods and classes exposed by components of this library.
This section provides installation, dependency, and build-system instructions for project developers.
- Install Platformio either as a standalone IDE or as an IDE plugin.
- Download this repository to the local machine using the preferred method, such as git-cloning.
- If the downloaded distribution is stored as a compressed archive, unpack it using the appropriate decompression tool.
cdto the root directory of the prepared project distribution.- Run
pio project initto initialize the project on the local machine. See Platformio API documentation for more details on initializing and configuring projects with platformio. - If using an IDE that does not natively support platformio integration, call the
pio project metadatacommand to generate the metadata to integrate the project with the IDE. Note; most mainstream IDEs do not require or benefit from this step.
Warning! To build this library for a platform or architecture that is not explicitly supported, edit the
platformio.ini file to include the desired configuration as a separate environment. This project comes preconfigured
with support for teensy 4.1, arduino due, and arduino mega (R3) platforms.
In addition to installing Platformio and main project dependencies, install the following dependencies:
- Tox and Doxygen to build the API documentation for the project. Note; both dependencies have to be available on the local system’s path.
Unlike other Ataraxis libraries, the automation for this library is primarily provided via the
Platformio’s command line interface.
Additionally, this project uses tox for certain automation tasks not
directly covered by platformio, such as API documentation generation. Check the tox.ini file for details
about the available pipelines and their implementation. Alternatively, call tox list from the root directory of
the project to see the list of available tasks.
Note! All pull requests for this project have to successfully complete the tox, pio check, and pio test tasks
before being submitted.
This project uses semantic versioning. See the tags on this repository for the available project releases.
- Ivan Kondratyev (Inkaros)
- Jasmine Si
This project is licensed under the GPL3 License: see the LICENSE file for details.
- All Sun lab members for providing the inspiration and comments during the development of this library.
- The creators of all other dependencies and projects listed in the platformio.ini file.