Skip to content

Extending OttoFlow

Ahmed Dabak edited this page Jul 24, 2026 · 2 revisions

Extending OttoFlow

Introduction

Adding a new capability -- a sensor, an actuator, an add-on board -- follows the same shape every built-in part follows. Do it right and your part feels native: same naming, same guard behaviour, same zero-config defaults. This is also the best way to contribute to OttoFlow.

Read Architecture first; this page assumes the three-layer model and the naming rules.

Anatomy of a part

A part is up to four small pieces:

  1. A module in src/modules/ -- the hardware-truth logic. Required.
  2. A facade in src/facades/ -- friendly sugar. Optional, add it when a nicer vocabulary helps.
  3. Configuration in core/OttoConfig.h -- pins and an enable flag, with kit-accurate defaults.
  4. Initialisation wired into OttoFlow::start().

We will add a fictional RGB LED part as the running example.

Step 1: the module

Create the header and source under src/modules/. Names state meaning and unit; reach the shared driver and config through core/Internal.h if you need them.

src/modules/RgbLed.h

//================================================================
// OttoFlow - modules/RgbLed.h
// A common-cathode RGB LED on three PWM pins.
//================================================================
#pragma once
#include <Arduino.h>

namespace RgbLed {
  void begin();                                   // called by start() when enabled
  void setColor(uint8_t red, uint8_t green, uint8_t blue);
  void off();
}

src/modules/RgbLed.cpp

#include "RgbLed.h"
#include "../core/Internal.h"

using ottoflow_internal::config;

namespace RgbLed {

static bool s_ready = false;

void begin() {
  if (!config().rgbLed.enabled) return;
  pinMode(config().rgbLed.redPin,   OUTPUT);
  pinMode(config().rgbLed.greenPin, OUTPUT);
  pinMode(config().rgbLed.bluePin,  OUTPUT);
  s_ready = true;
}

void setColor(uint8_t red, uint8_t green, uint8_t blue) {
  if (!s_ready) return;                           // guard: silent no-op when off
  analogWrite(config().rgbLed.redPin,   red);
  analogWrite(config().rgbLed.greenPin, green);
  analogWrite(config().rgbLed.bluePin,  blue);
}

void off() { setColor(0, 0, 0); }

}  // namespace RgbLed

The guard pattern is the important part: when the part is disabled or not yet initialised, its calls do nothing rather than crash. Every built-in module behaves this way -- it is what makes disabling and optional hardware painless. See Architecture.

Step 2: the facade

If a friendlier vocabulary helps, add an inline facade in src/facades/. It contains no logic -- only forwarding -- so it stays in lockstep with the module and costs nothing.

src/facades/Mood.h

//================================================================
// OttoFlow - facades/Mood.h
// Express a mood as a colour on the RGB LED.
//================================================================
#pragma once
#include "../modules/RgbLed.h"

namespace Mood {
  inline void happy()   { RgbLed::setColor(0, 255, 0); }
  inline void angry()   { RgbLed::setColor(255, 0, 0); }
  inline void calm()    { RgbLed::setColor(0, 0, 255); }
  inline void off()     { RgbLed::off(); }
}

Not every module needs a facade. LightSensor and Bluetooth ship without one because their module names are already plain.

Step 3: configuration

Add a config struct with kit-accurate defaults and an enabled flag, then include it in OttoConfig.

In core/OttoConfig.h:

struct RgbLedConfig {
  bool    enabled  = false;    // off by default
  uint8_t redPin   = A0;
  uint8_t greenPin = A1;
  uint8_t bluePin  = A2;
};

struct OttoConfig {
  // ... existing members ...
  RgbLedConfig rgbLed;
};

Optional hardware is enabled = false by default; a part every kit has can default to on.

Step 4: wire it into start()

In core/OttoCore.cpp, include the header and initialise the part where the other optional modules are brought up:

#include "../modules/RgbLed.h"

// inside start(const OttoConfig& config), after s_config = config;
if (s_config.rgbLed.enabled) RgbLed::begin();

Your part now comes up with the rest of the robot whenever it is enabled.

Step 5: an example

Ship a short sketch so people can see it work, and add it to the single top-level include if it is a core part. The Roadmap describes the planned examples/ folder.

#include <OttoFlow.h>

void setup() {
  OttoConfig cfg;
  cfg.rgbLed.enabled = true;
  OttoFlow::start(cfg);
  Mood::happy();
}

void loop() {
  Mood::angry();  delay(500);
  Mood::calm();   delay(500);
}

Finally, add the includes to src/OttoFlow.h alongside the other modules and facades so a sketch gets your part from the one include.

Checklist

A part that does all of this will feel like it was always there:

  • Module in src/modules/, unit-explicit names, guard pattern for the disabled state.
  • Optional facade in src/facades/, inline forwarding only, no logic.
  • Config struct in OttoConfig with kit-accurate defaults and an enabled flag.
  • Initialisation wired into OttoFlow::start().
  • Headers added to src/OttoFlow.h.
  • An example sketch.
  • .cpp files include <Otto.h> only if they truly need the driver -- public headers never do.

Parts that follow this pattern are welcome as contributions. Several are already sketched out on the Roadmap.

Clone this wiki locally