Skip to content

02 Basic Software Architecture

Björn Giesler edited this page Mar 25, 2024 · 2 revisions

Timed execution vs setup() and loop()

Classic Arduino sketches usually do their bringup and execution in two toplevel functions: setup() (run once, expected to bring the sketch up), and loop() (run cyclically in a busy loop). This is a very simple concept that is easy to understand and that is applicable to many problems. It becomes very awkward and complex though as soon as the code needs to take care of more complex applications. For droids, our wishlist is:

  • timed and (close-to) realtime execution of code
  • cleartext console interaction on serial and/or Wifi links
  • setting / querying runtime parameters
  • persistent flash storage

While all of these are possible in a classic Arduino sketch, they lead to very complicated code that is hard to maintain. Therefore in BBDroids we move away from this classic structure.

Here is a simple example for a BBDroids sketch.

#include <LibBB.h>

using namespace bb;

class MyDroidSubsystem: virtual public Subsystem {
public:
    static MyDroidSubsystem droid; 
    virtual Result initialize(); 
    virtual Result start(); 
    virtual Result stop(); 
    virtual Result step(); 
protected:
    uint8_t iteration_;
};

Result MyDroidSubsystem::initialize() {
    name_ = "mydroid";
    description_ = "My Droid Subsystem";
    help_ = "Not much here yet";
    started_ = false;
    return Subsystem::initialize();
}

Result MyDroidSubsystem::start(ConsoleStream *stream) {
    iteration_ = 0;

    started_ = true;
    operationStatus_ = RES_OK;
    return operationStatus_;
}

Result MyDroidSubsystem::stop(ConsoleStream *stream) {
    started_ = false;
    operationStatus_ = RES_SUBSYS_NOT_STARTED;
    return RES_OK;
}

Result MyDroidSubsystem::step(ConsoleStream *stream) {
    Console::console.printfBroadcast("Iteration: %d\n", iteration_++;
    return RES_OK;
}

void setup(void) {
    Runloop::runloop.initialize();
    Console::console.initialize();
    MyDroidSubsystem::mydroid.initialize();

    MyDroidSubsystem::mydroid.start();
    Console::console.start();
    Runloop::runloop.start(); // never returns
}

void loop(void) {}

The remote and droid codes share quite a bit of code (console, runloop, XBee and Wifi comm, etc.). This joint code resides in the LibBB folder. This should go into your Arduino libraries folder. Where that is depends on your operating system; on Mac, it's ~/Documents/Arduino/libraries.

LibBB contains all the code required to setup and run a system you can communicate with, but it won't actually do anything because it does not contain subsystems for motion, sound, lights, or displays. Sketches using LibBB should provide these as their own subsystems.

Subsystem Concept

Subsystems are runnable entities that can be initialized, started, stopped, and periodically called by the runloop to perform work. Subsystems have names, descriptions and help texts. Since subsystems are identified by name, the names must be unique. Subsystems can expose parameters that can be read and modified from the command line, and that can be loaded from and stored into flash memory. Subsystems can also offer console commands.

Standard subsystems offered by LibBB are

  • console
  • runloop
  • wifi
  • xbee.

A sketch based on LibBB is expected to

In code, subsystems inherit from the virtual class bb::Subsystem, overwriting the start(), stop(), and step() methods, and optionally the initialize(), getParameterValue(), setParameterValue(), and handleConsoleCommand() methods. They are required to maintain the started_ (bool) and operationStatus_ (Result) members of their base class.

As a convention, subsystems are typically singletons objects (there can be only one wifi subsystem, for example). This is achieved by setting the constructor protected or private, and adding one static instance of the class as a class member variable, like so:

class MySubsystem: virtual public Subsystem { 
    public: 
        static MySubsystem mysub; 
        virtual Result initialize(); 
        virtual Result start(); 
        virtual Result stop(); 
        virtual Result step(); 
    protected: 
        MySubsystem(); 
};

Runloop

The runloop is a subsystem that cyclically calls all other subsystems' step() methods in an infinite loop. It tries to maintain a cycle time by measuring the sum of time all step() methods consume, and then sleeping for the rest. Please note that this is not guaranteed - if the sum of all step() methods takes too much time the cycle time will not be held. There is currently no mechanism to penalize individual subsystems for taking too much time.

The runloop's start() method contains an infinite loop. It is not stoppable by design. A way to stop it programmatically may be added in the future, but this cannot be possible from the console because the console itself depends on the runloop running, so stopping it would disable the console and remove any way of restarting it beyond a reset.

Console

LibBB provides code for using a command line to interact with the system. Some standard commands are provided to inquire system status, get help and status on individual subsystems, start / stop / restart individual subsystems, get and set parameter values and store them to flash. Several consoles can be opened at the same time, each getting its individual interaction stream, and broadcast messages can be sent to all of them.

The console expects line feeds as end of line characters (set your terminal to send LF or CRLF, CR alone will not work), and uses space to separate arguments. If string arguments contain spaces, use double quotes ("") to enclose them. Double quotes within strings can be escaped with a backslash. So if your Wifi key is 'My"Great Droid', specify it on the command line as '"My"Great Droid"' (remove single quotes for both).

In code, the console subsystem uses subclasses of the class for interaction over different communication channels. One serial console stream is always active and listening on , so it can be accessed via the Arduino's USB port. The wifi subsystem provides another console stream.

Wifi

The wifi subsystem uses the WiFiNINA library to interact with the Wifi module installed on the Arduino. It can be configured to connect to existing infrastructure networks or to create access points. For this purpose it offers parameters for SSID, WPA key, and a flag that states whether to connect to infrastructure or open an access point. The wifi subsystem offers a TCP server on a configurable port (3000 by default, can be set via parameter) that exposes a console.

The wifi subsystem has also been used in the past to send commands and state from the droid to the remote and back. Due to the very limited Wifi range of the Arduinos this is not currently supported, but it may come back in the future.

XBee

The xbee subsystem uses one of the Arduino's serial ports to communicate with an installed XBee module. Configuration is entirely done with XBee AT commands, no pre-configuration using XCTU or other tools is necessary (but can be done of course). It will auto-detect the connected XBee's bps rate and set channel, PAN, station and partner ID according to parameters.

The xbee subsystem is the main communication backbone. It exposes a delegate class that should be used as base class for all that want to receive packets in the system. At the moment, it operates in transparent mode, which is probably not optimal; it may be changed to API mode in the future. In its current implementation, all bytes in a packet are required to have their high bit set to 0, because a byte with a high bit is used to detect packet boundaries. The packet format is protected using a 7-bit CRC and has a well-defined length, both of which serve as protection against packet corruption.