Skip to content

Architecture

Ahmed Dabak edited this page Jul 24, 2026 · 1 revision

Architecture

Introduction

Three goals shape every decision in OttoFlow:

  1. Rapid development. A working behaviour in a handful of readable lines.
  2. A layered audience. Beginners never need to leave the top layer; power users never hit a locked door.
  3. A clear, consistent API. Every identifier states what it does, and in which unit.

The three layers

Facades   Mouth, Eyes, Voice, Legs, Arms, Gestures,        src/facades/
          Motion, Touch, Ears, Balance
          Expressive sugar. Inline, zero overhead.

Modules   Matrix, Ultrasonic, Buzzer, Melody, LegServos,   src/modules/
          ArmServos, TouchSensor, SoundSensor, LightSensor,
          Bluetooth, Mpu6050, AppLink
          Hardware-truth names. All real logic lives here.

Driver    OttoDIYLib                                       dependency
          Reached by module .cpp files, and by you through
          the official escape hatch, OttoFlowDriver.h.

The same action at each layer:

Mouth::show(Icon::Heart);                 // facade
Matrix::drawIcon(Icon::Heart);            // module
OttoFlow::driver().putMouth(heart);       // driver

Facades are inline forwarders, so the first line compiles to exactly the same machine code as the second. Expressiveness costs nothing on an ATmega328.

Layer rules

  • Each layer talks only to the layer directly below it.
  • Facades contain no logic -- only inline forwarding. This keeps the friendly vocabulary and the technical vocabulary in lockstep with zero duplication.
  • Public headers never include OttoDIYLib headers. OttoDIYLib defines lowercase macros such as heart and smile; keeping them out of <OttoFlow.h> prevents them from colliding with names in your sketch. Modules include <Otto.h> in their .cpp files only.
  • Modules reach the shared driver instance and the active configuration through core/Internal.h, which is framework-internal and never included by a sketch.

The vocabulary problem

What one person calls a face, another calls a mouth, and a third calls a display. OttoFlow does not pick a winner -- it layers:

  • Underneath, unambiguous technical names. Nobody argues about what Matrix means.
  • On top, expressive names as documented sugar: Mouth.

Both are official API. The technical layer is the truth.

For the sugar word itself OttoFlow deliberately adopts the upstream OttoDIYLib and community vocabulary: the matrix is Otto's mouth (putMouth in the driver, "mouth" in every Otto tutorial). Ecosystem consistency beats inventing a prettier word.

Naming rules

  • Units live in the name. distanceCm(), playToneHz(), periodMs, positionLeftDegrees(). Never a bare number with an implicit unit.
  • Directions are words, not signs. walkForward(2) and walkBackward(2), never walk(2, 1000, -1) in user-facing code.
  • Conditions read as sentences. Eyes::closerThanCm(15).
  • Enums instead of magic numbers. Icon::Heart, Sound::Happy, Gesture::Victory.
  • PascalCase for namespaces and enum values, camelCase for functions.
  • Plain hardware names over domain jargon. The leg module is LegServos, symmetric with ArmServos, not Gait. A beginner should never need a robotics glossary to guess what a module controls.

Why the namespace is OttoFlow

OttoDIYLib defines a C++ class named Otto. A namespace with the same name cannot coexist with it in one translation unit -- and the escape hatch requires both to be visible at once. So the entry point is OttoFlow:: (on brand anyway) and the driver class keeps its original name.

Blocking today, async ready

Every movement call blocks: Legs::walkForward(2) returns when the walk is finished. The underlying OttoDIYLib is blocking, and sequential code is what beginners reason about most easily.

The names were chosen so a non-blocking engine can be added later without renaming or breaking anything: startWalkingForward(), isMoving(), and stop() will sit next to the current calls. See the Roadmap.

Guards: disabled means silent

Every module that can be switched off follows the same pattern: while it is disabled, its calls are silent no-ops. Not an error, not a warning, not a compile-time flag.

Motion::disable();
Legs::walkForward(2);   // does nothing, and that is fine

This is what makes Bench Testing work without #ifdefs scattered through a sketch, and what lets one sketch run on builds with and without arms.

Source layout

src/
  OttoFlow.h            the single include a sketch needs
  OttoFlowDriver.h      opt-in escape hatch to raw OttoDIYLib
  core/
    OttoCore.h/.cpp     start(), isStarted(), version(), the driver instance
    OttoConfig.h        OttoConfig, Preset, per-module settings
    Icons.h             the Icon enum
    Sounds.h            the Sound enum
    Gestures.h          the Gesture enum
    Internal.h          framework-internal access to driver and config
  modules/              hardware-truth layer
  facades/              expressive layer
  console/              the serial test console

To add your own part, see Extending OttoFlow.

Clone this wiki locally