Extending Plaquette

This guide provides comprehensive instructions for developers who want to extend Plaquette by creating custom units. Whether you’re building a specialized sensor interface, a custom filter, or an advanced signal generator, this document covers everything you need to know.

Architecture Overview

Plaquette is built around a signal-centric programming model where data flows between components. The framework provides:

  • Automatic unit registration: Units register themselves with their Engine upon construction

  • Synchronized updates: All units are updated together in each execution step

  • Flow operators: The >> operator chains components for intuitive data routing

Core Components

Component

Description

Engine

Central orchestrator that manages all units, timing, and execution

Flowable

Base interface for all signal components. Provides get(), put(), >> operators, and conversion to float/bool so components can be used directly as values

Unit

Base class for engine-managed components with lifecycle methods

Note

All Flowables (including Units) can be used directly as values in expressions:

float v = sensor;          // get() called implicitly
if (button) { ... }        // true when isOn()
Serial.println(filter);    // prints current value

Execution Model

Plaquette replaces Arduino’s setup()/loop() with begin()/step():

Plaquette execution model diagram

The pq Namespace

All Plaquette classes and functions are defined within the pq namespace. This prevents naming conflicts with other libraries and the Arduino core. When writing extension code (header and implementation files), you should place your code inside the pq namespace:

// MyUnit.h
#include "PqCore.h"

namespace pq {

class MyUnit : public Unit {
  // ...
};

} // namespace pq

Note

If you’re writing a standalone library that extends Plaquette but don’t want to pollute the global namespace, you can use pq:: prefixes explicitly or add using namespace pq; only where needed.

Flowable vs Unit: Choosing Your Base Class

This is the most important decision when creating a custom component. Plaquette provides two fundamentally different base classes:

Flowable: Non-Engine-Managed Components

Subclass Flowable when:

  • Your component does NOT need to be managed by an engine

  • You do NOT need per-frame updates (step())

  • You do NOT need initialization hooks (begin())

  • You do NOT need access to engine timing (sample rate, elapsed time)

  • Your component can operate independently of the engine lifecycle

Characteristics of Flowable subclasses:

  • NOT registered with any Engine

  • NO begin() or step() methods

  • Implement get() and put() for flow operator compatibility

  • Compatible with Plaquette syntax

  • Lightweight with no engine management overhead

  • Examples: Float, Boolean, Integer (value wrappers), custom transformers

Example: Clamped Value

#include "PqCore.h"

namespace pq {

/// A value that stays within [min, max] bounds.
class ClampedValue : public Flowable {
public:
  ClampedValue(float min = 0.0f, float max = 1.0f)
    : _value(0), _min(min), _max(max) {}

  float get() override {
    return _value;
  }

  float put(float value) override {
    _value = constrain(value, _min, _max);
    return _value;
  }

private:
  float _value;
  float _min;
  float _max;
};

} // namespace pq

Usage:

ClampedValue clamped(0.0, 100.0);
sensor >> clamped >> output;  // Values automatically clamped

// Also works with Plaquette syntax:
float v = clamped;            // Uses operator float()
if (clamped) { ... }          // Uses operator bool()

Unit: Engine-Managed Components

Subclass Unit (or its descendants) when:

  • Your component needs to be managed by an engine

  • You need per-frame updates via step()

  • You need initialization via begin()

  • You need access to engine timing (seconds(), sampleRate(), samplePeriod())

  • You interact with hardware (sensors, actuators)

  • You implement time-based behavior (oscillators, timers, filters)

  • You need event callbacks

Characteristics of Unit subclasses:

  • Automatically registered with an Engine upon construction

  • MUST accept an Engine& parameter in ALL constructors (defaulting to primary engine)

  • Have begin() and step() lifecycle methods

  • Access to synchronized timing information via engine

  • Can register event callbacks

Decision Flowchart

Does your component need...
    │
    ├── Per-frame updates (step)?
    │   └── YES → Use Unit
    │
    ├── Initialization (begin)?
    │   └── YES → Use Unit
    │
    ├── Access to synchronized time (seconds, sampleRate)?
    │   └── YES → Use Unit
    │
    ├── Event callbacks?
    │   └── YES → Use Unit
    │
    └── None of the above, just flow compatibility?
        └── YES → Use Flowable

Comparison Table

Feature

Flowable

Unit

Engine registration

×

✓ (automatic)

begin() method

×

step() method

×

Synchronized timing

×

Event callbacks

×

Flow operators (>>)

operator float()

operator bool()

Memory overhead

Minimal

Engine tracking

Use case

Standalone components

Engine-managed components

Class Hierarchy

Complete Hierarchy

Flowable (interface - NOT engine-managed)
    │
    ├── Value<T> (Float, Boolean, Integer - NOT engine-managed)
    │
    └── Unit (base class - engine-managed)
            │
            ├── DigitalUnit (boolean on/off units)
            │       │
            │       └── DigitalSource (with change detection)
            │
            └── AnalogSource (values in [0, 1] range)

Flowable

The foundational interface for all chainable components:

class Flowable {
public:
  virtual float get();                    // Read current value
  virtual float put(float value);         // Write value (default: read-only)
  virtual float mapTo(float toLow, float toHigh);  // Map value to range

  operator float();                       // Implicit conversion to float
  explicit operator bool();               // Explicit conversion to bool
};

Unit

The base class for all engine-managed components:

class Unit : public Flowable {
protected:
  virtual void begin();   // Called once after all units are registered
  virtual void step();    // Called every frame in the main loop

public:
  // IMPORTANT: Constructor MUST accept Engine reference
  Unit(Engine& engine = Engine::primary());

  // Access engine-synchronized timing
  float seconds() const;
  uint32_t milliSeconds() const;
  uint64_t microSeconds() const;
  unsigned long nSteps() const;
  float sampleRate() const;
  float samplePeriod() const;

  // Event support
  virtual void onEvent(EventCallback callback, EventType eventType);
};

DigitalUnit

Base for boolean (on/off) units:

class DigitalUnit : public Unit {
public:
  DigitalUnit(Engine& engine = Engine::primary());

  virtual bool isOn();
  virtual bool isOff();
  virtual int getInt();           // Returns 0 or 1
  virtual float get();            // Returns 0.0 or 1.0

  virtual bool on();              // Turn on
  virtual bool off();             // Turn off
  virtual bool putOn(bool value); // Set state (override for writable units)
};

DigitalSource

Digital unit with change detection and events:

class DigitalSource : public DigitalUnit {
public:
  DigitalSource(Engine& engine = Engine::primary());

  virtual bool rose();      // True if just changed from off to on
  virtual bool fell();      // True if just changed from on to off
  virtual bool changed();   // True if state changed this step

  // Event callbacks
  virtual void onRise(EventCallback callback);
  virtual void onFall(EventCallback callback);
  virtual void onChange(EventCallback callback);
};

AnalogSource

Base for units outputting normalized values in [0, 1]:

class AnalogSource : public Unit {
public:
  AnalogSource(Engine& engine = Engine::primary());
  AnalogSource(float initialValue, Engine& engine = Engine::primary());

  virtual float get();  // Returns _value (always in [0, 1])

protected:
  float _value;         // Must stay in [0, 1] range
};

Unit Lifecycle

Every unit follows a predictable lifecycle managed by the Engine.

Construction and Engine Registration

Warning

All Unit subclasses MUST accept an Engine& parameter in every constructor, defaulting to the primary engine.

Header file (.h): Declare the default value here:

class MyUnit : public AnalogSource {
public:
  MyUnit(Engine& engine = Engine::primary());
  MyUnit(float param, Engine& engine = Engine::primary());
  MyUnit(float param1, float param2, Engine& engine = Engine::primary());
  // ...
};

Implementation file (.cpp):

MyUnit::MyUnit(Engine& engine)
  : AnalogSource(engine)
{ }

MyUnit::MyUnit(float param, Engine& engine)
  : AnalogSource(engine), _param(param)
{ }

MyUnit::MyUnit(float param1, float param2, Engine& engine)
  : AnalogSource(engine), _param1(param1), _param2(param2)
{ }

Caution

Never provide constructors without an Engine& parameter.

In header:

// WRONG: Missing engine parameter
MyUnit(float param);

Core abstract classes Unit, DigitalUnit, DigitalSource and AnalogSource deliberately do NOT provide a default engine at construction. This causes a compilation error if you forget the engine parameter - a safety feature that catches mistakes early.

Caution

Always pass the engine to the parent class.

When subclassing units whose constructors provide a default engine, forgetting to pass the engine breaks multi-engine setups silently:

// WRONG: Missing parent initializer - defaults to primary engine, ignoring parameter
MyUnitSubClass::MyUnitSubClass(Engine& engine)
{ }

// WRONG: Same problem - engine parameter is ignored
MyUnitSubClass::MyUnitSubClass(float param, Engine& engine)
  : MyUnit(param)
{ }

// CORRECT: Always pass the engine to the parent class
MyUnitSubClass::MyUnitSubClass(float param, Engine& engine)
  : MyUnit(param, engine)
{ }

Why is this important?

  • Users may want to assign units to secondary engines

  • The engine parameter must ALWAYS be the LAST parameter

  • Forgetting to pass the engine breaks multi-engine setups silently

// User can assign to different engines:
Engine slowEngine;
MyUnit unit1;                    // Uses primary engine (default)
MyUnit unit2(0.5);               // Uses primary engine (default)
MyUnit unit3(0.5, slowEngine);   // Uses secondary engine

begin()

Called once after all units are registered, before the main loop:

void MyUnit::begin() {
  // Initialize state
  // Read initial sensor values
  // Set up hardware
}

step()

Called once per frame in the main loop:

void MyUnit::step() {
  // Update internal state
  // Read sensors / write actuators
  // Cache computed values for get()
}

Timing Access

Inside any unit method, you have access to engine-synchronized timing information.

Warning

Always use the unit’s timing methods instead of global Arduino functions like millis() or micros(). The unit’s timing methods are synchronized with its engine, ensuring consistent behavior especially when using multiple engines with different sample rates.

void MyUnit::step() {
  // CORRECT: Use engine-synchronized timing
  float t = seconds();           // Time since engine start (seconds)
  float dt = samplePeriod();     // Time since last step (seconds)
  float rate = sampleRate();     // Steps per second
  unsigned long n = nSteps();    // Number of steps executed

  // WRONG: Don't use global Arduino timing functions
  // unsigned long t = millis();   // Not synchronized with engine!
  // unsigned long t = micros();   // Not synchronized with engine!
}

Why use engine timing?

  • Engine timing is synchronized with the unit’s step cycle

  • Different engines can run at different sample rates

  • Consistent timing even when using secondary engines

  • Proper handling of sample rate changes

Step-Invariance Principle

The step-invariance principle applies to source units (units whose put(float) is inactive or not overridden). For these units, get() should return a constant value throughout a step.

Sink and filter units (units that accept input via put(float)) are different: their value can change during a step when put() is called. This is expected and correct behavior.

When Step-Invariance Applies

Unit Type

put() Behavior

Step-Invariance

Source (sensor, generator)

Inactive / not overridden

get() constant throughout step

Sink (output, actuator)

Accepts input, stores for step()

get() may change when put() called

Filter (transformer)

Accepts input, transforms immediately

get() changes when put() called

For source units, the principle ensures:

  1. get() returns a constant value throughout a step

  2. Hardware reads only occur in step() (never in get())

  3. Values are cached and returned consistently until next step

Flow Operators

The >> operator enables intuitive signal chaining:

sensor >> filter >> output;
// Equivalent to: output.put(filter.put(sensor.get()));

How It Works

// Float to flowable
inline float operator>>(float value, Flowable& unit) {
  return unit.put(value);
}

// Flowable to flowable
inline float operator>>(Flowable& source, Flowable& sink) {
  return sink.put(source.get());
}

Making Your Component Flow-Compatible

Both Flowable and Unit subclasses are automatically flow-compatible if they implement get() and/or put():

// Flowable subclass (no engine)
class MyTransformer : public Flowable {
public:
  float get() override { return _value; }
  float put(float value) override {
    _value = transform(value);
    return _value;
  }
private:
  float _value;
  float transform(float v) { return v * v; }
};

// Unit subclass (with engine)
class MyUnit : public AnalogSource {
public:
  MyUnit(Engine& engine = Engine::primary()) : AnalogSource(engine) {}

  float get() override { return _value; }
  float put(float value) override {
    _value = constrain01(value);
    return _value;
  }
};

// Both work with flow operators:
sensor >> myTransformer >> myUnit >> output;

Creating Your First Unit

Let’s create a simple unit step by step.

Step 1: Choose Your Base Class

If your component…

Inherit from

Doesn’t need engine management (no timing, no step)

Flowable

Outputs values in [0, 1] (sensors, oscillators, filters)

AnalogSource

Outputs boolean on/off states

DigitalUnit

Outputs boolean with change detection (buttons, triggers)

DigitalSource

Has custom value range or complex behavior

Unit

Step 2: Create the Header File

// RandomWalk.h
#ifndef RANDOM_WALK_H_
#define RANDOM_WALK_H_

#include "PqCore.h"

namespace pq {

/**
 * Generates a smooth random walk in [0, 1].
 *
 * The value drifts randomly at each step, scaled by the rate
 * parameter and the engine's sample period. Use put() to hard
 * reset to a specific value.
 */
class RandomWalk : public AnalogSource {
public:
  /**
   * Constructor with default rate.
   * @param engine the engine running this unit
   */
  RandomWalk(Engine& engine = Engine::primary());

  /**
   * Constructor with specified rate.
   * @param rate maximum change per second (default: 0.5)
   * @param engine the engine running this unit
   */
  RandomWalk(float rate, Engine& engine = Engine::primary());

  virtual ~RandomWalk() {}

  /// Sets the rate of change per second.
  void rate(float rate);

  /// Returns the current rate.
  float rate() const { return _rate; }

  /// Returns a ParameterSlot for dynamic rate modulation.
  ParameterSlot<RandomWalk> Rate() {
    return ParameterSlot<RandomWalk>(this, &RandomWalk::rate, &RandomWalk::rate);
  }

  /**
   * Hard resets the random walk to a specific value.
   * @param value the value to reset to (clamped to [0, 1])
   * @return the new value
   */
  virtual float put(float value) override;

protected:
  virtual void begin() override;
  virtual void step() override;

private:
  float _rate;
};

} // namespace pq

#endif // RANDOM_WALK_H_

Step 3: Create the Implementation File

// RandomWalk.cpp
#include "RandomWalk.h"

namespace pq {

RandomWalk::RandomWalk(Engine& engine)
  : AnalogSource(engine),  // Pass engine to parent
    _rate(0.5f)            // Default rate: 0.5 units/second
{
  // Unit is automatically registered with engine
}

RandomWalk::RandomWalk(float rate, Engine& engine)
  : AnalogSource(engine),  // Pass engine to parent
    _rate(rate)
{
  // Unit is automatically registered with engine
}

void RandomWalk::begin() {
  // Initialize to center position
  _value = 0.5f;
}

void RandomWalk::step() {
  // Compute random delta scaled by rate and sample period.
  // randomFloat(-1, 1) gives uniform random in [-1, 1].
  // Multiplying by rate and samplePeriod() makes the walk
  // time-consistent regardless of sample rate.
  float delta = randomFloat(-1.0f, 1.0f) * _rate * samplePeriod();

  // Apply delta and constrain to [0, 1]
  _value = constrain01(_value + delta);
}

void RandomWalk::rate(float rate) {
  _rate = max(rate, 0.0f);  // Rate must be non-negative
}

float RandomWalk::put(float value) {
  // Hard reset: immediately set value (bypass random walk)
  _value = constrain01(value);
  return _value;
}

} // namespace pq

Step 4: Use Your Unit

#include <Plaquette.h>
#include "RandomWalk.h"

using namespace pq;

RandomWalk wanderer(0.3);  // Slow drift: 0.3 units/second max
AnalogOut led(LED_BUILTIN);
DigitalIn button(2);
SineWave lfo(20.0);  // Slow LFO for rate modulation

void step() {
  // Modulate rate dynamically via ParameterSlot
  lfo >> wanderer.Rate();

  // LED brightness follows random walk
  wanderer >> led;

  // Button press resets walk to minimum
  if (button.rose())
    0 >> wanderer;
}

// Or with a secondary engine:
Engine slowEngine;
RandomWalk slowWanderer(0.1, slowEngine);  // Assigned to secondary engine

Unit Templates

The following templates show common patterns. All Unit subclasses must include Engine& engine = Engine::primary() as the last parameter in every constructor.

Source Unit (Generator)

For units that generate values (step-invariant):

class MyGenerator : public AnalogSource {
public:
  MyGenerator(float frequency, Engine& engine = Engine::primary())
    : AnalogSource(engine), _frequency(frequency) {}

protected:
  void step() override {
    float t = seconds();  // Use engine timing, not millis()!
    _value = /* compute value in [0, 1] */;
  }

private:
  float _frequency;
};

Filter Unit

For units that transform incoming signals:

class MyFilter : public AnalogSource {
public:
  MyFilter(float strength, Engine& engine = Engine::primary())
    : AnalogSource(engine), _strength(strength) {}

  float put(float value) override {
    _value = constrain01(applyFilter(value));
    return _value;
  }

private:
  float _strength;
  float applyFilter(float v) { return v * _strength; }
};

Digital Trigger Unit

For units that detect events (inherits rose(), fell(), changed()):

class MyTrigger : public DigitalSource {
public:
  MyTrigger(float threshold, Engine& engine = Engine::primary())
    : DigitalSource(engine), _threshold(threshold) {}

  float put(float value) override {
    _currentValue = value;
    return get();
  }

protected:
  void step() override {
    _onValue = (_currentValue >= _threshold);
    _updateChangeState();  // Updates rose/fell/changed
  }

private:
  float _threshold;
  float _currentValue;
};

Parameter Slots

Parameter slots allow dynamic control of unit properties via flow operators:

Wave lfo(10.0);
Wave carrier(0.5);

void step() {
  lfo >> carrier.Frequency();  // LFO modulates carrier frequency
}

Implementing Parameter Slots

Use the ParameterSlot template to expose parameters:

// In your header
#include "PqCore.h"

class MyOscillator : public AnalogSource {
public:
  MyOscillator(Engine& engine = Engine::primary())
    : AnalogSource(engine), _frequency(1.0f) {}

  // Getter and setter for frequency
  void frequency(float freq) { _frequency = max(0.0f, freq); }
  float frequency() const { return _frequency; }

  // Parameter slot accessor
  ParameterSlot<MyOscillator> Frequency() {
    return ParameterSlot<MyOscillator>(
      this,
      &MyOscillator::frequency,  // setter
      &MyOscillator::frequency   // getter (same name, different signature)
    );
  }

private:
  float _frequency;
};

Event System

Units can trigger callbacks when specific events occur. Plaquette provides several event types for different use cases.

Event Types Reference

Event Type

When to Use

Boolean Check

Callback Registration

EVENT_BANG

Trigger/pulse units where get() returns 1 momentarily during step() (e.g., metronome tick, peak detected) and 0 otherwise

n/a

onBang()

EVENT_RISE

Digital units that transition from off to on

rose()

onRise()

EVENT_FALL

Digital units that transition from on to off

fell()

onFall()

EVENT_CHANGE

Digital units that change state (either direction)

changed()

onChange()

EVENT_FINISH

Timers or processes that complete (e.g., Ramp, Alarm)

finished()

onFinish()

EVENT_UPDATE

Units that get updated (eg. received data)

updated()

onUpdate()

EVENT_CUSTOM_1 to EVENT_CUSTOM_4

Custom events for specialized units

(user-defined)

(user-defined)

Event Naming Convention

Important

Plaquette follows a consistent naming convention for events:

  • Boolean check: Use past tense (e.g., rose(), fell(), finished())

  • Callback registration: Use present tense with on prefix (e.g., onRise(), onFall(), onFinish())

This convention makes code read naturally:

if (button.rose()) { ... }        // "if button rose" (past tense check)
button.onRise(myCallback);        // "on rise, call myCallback" (present tense action)

Adding Event Support

class MyDetector : public DigitalUnit {
public:
  MyDetector(Engine& engine = Engine::primary())
    : DigitalUnit(engine), _detected(false) {}

  /// Returns true if detection occurred this step (past tense).
  bool detected() const { return _detected; }

  /// Register callback for detection events (present tense with "on" prefix).
  void onDetect(EventCallback callback) {
    onEvent(callback, EVENT_BANG);
  }

protected:
  bool eventTriggered(EventType eventType) override {
    switch (eventType) {
      case EVENT_BANG: return _detected;
      default: return DigitalUnit::eventTriggered(eventType);
    }
  }

  void step() override {
    _detected = checkCondition();
    // Events are dispatched automatically after step()
  }

private:
  bool _detected;

  bool checkCondition() {
    // Your detection logic
    return false;
  }
};

Usage

MyDetector detector;

void begin() {
  detector.onDetect([]() {
    Serial.println("Detected!");
  });
}

void step() {
  // Can also check the boolean directly
  if (detector.detected()) {
    // Do something
  }
}

Testing Your Unit

Plaquette uses AUnit for testing, running natively on Linux/macOS via EpoxyDuino.

Test File Structure

Create a test directory and .ino file:

tests/
  myunit/
    myunit.ino
    Makefile

Example Test File

// tests/myunit/myunit.ino
#include <Arduino.h>
#include <PlaquetteLib.h>
#include <AUnit.h>

using namespace pq;

MyGain gain(2.0);

test(gain_doubles_value) {
  Plaquette.step();

  gain.put(0.25);
  assertNear(gain.get(), 0.5f, 0.001f);

  Plaquette.step();
}

test(gain_constrains_to_01) {
  Plaquette.step();

  gain.put(0.75);  // 0.75 * 2 = 1.5, should clamp to 1.0
  assertEqual(gain.get(), 1.0f);

  Plaquette.step();
}

test(gain_flow_operator) {
  Plaquette.step();

  0.3f >> gain;
  assertNear(gain.get(), 0.6f, 0.001f);

  Plaquette.step();
}

test(gain_with_secondary_engine) {
  Engine secondaryEngine;
  MyGain secondaryGain(3.0, secondaryEngine);

  secondaryEngine.begin();
  secondaryEngine.step();

  secondaryGain.put(0.2);
  assertNear(secondaryGain.get(), 0.6f, 0.001f);

  secondaryEngine.step();
}

void setup() {
  Plaquette.begin();
}

void loop() {
  aunit::TestRunner::run();
}

Makefile

APP_NAME := myunit
ARDUINO_LIBS := AUnit Plaquette

include ../../../libraries/EpoxyDuino/EpoxyDuino.mk

Running Tests

cd tests/myunit
make clean
make
./myunit.out

Testing Tips

  1. Always call Plaquette.step() between test operations to simulate the main loop

  2. Use assertNear() for floating-point comparisons

  3. Test edge cases: zero values, maximum values, negative inputs

  4. Test flow operators: ensure >> works correctly

  5. Test step-invariance: verify get() returns the same value within a step

  6. Test with secondary engines: verify the engine parameter works correctly

Coding Conventions

Follow these conventions for consistency with the Plaquette codebase.

Naming

Element

Convention

Example

Classes

PascalCase

PeakDetector

Public methods

camelCase()

frequency()

Parameter methods

PascalCase()

Frequency()

Public variables

camelCase

value

Private/protected methods

_camelCase()

_step()

Private/protected variables

_camelCase

_value

Enums/constants

UPPER_SNAKE

PEAK_MAX

Event boolean check

past tense

rose(), fell(), sampled()

Event callback registration

on + present tense

onRise(), onFall(), onSample()

Constructor Pattern

Important

All Unit subclasses must follow this pattern:

class MyUnit : public AnalogSource {
public:
  // Constructor with just engine (always provide this one)
  MyUnit(Engine& engine = Engine::primary());

  // Constructor with parameters - engine is ALWAYS LAST with default
  MyUnit(float param1, Engine& engine = Engine::primary());

  // Multiple parameters - engine is ALWAYS LAST with default
  MyUnit(float param1, float param2, Engine& engine = Engine::primary());
};

Getter/Setter Pattern

Use the same name for getter and setter:

// Getter
float frequency() const { return _frequency; }

// Setter
void frequency(float freq) { _frequency = freq; }

Formatting

  • Indentation: 2 spaces (no tabs)

  • Braces: Opening on same line, closing on own line

  • Line length: 100 characters or fewer

void MyUnit::step() {
  if (condition) {
    doSomething();
  } else {
    doSomethingElse();
  }
}

Documentation

Use Doxygen-style comments:

/**
 * Sets the frequency of oscillation.
 * @param frequency the frequency in Hz (must be >= 0)
 */
void frequency(float frequency);

Include Guards

Use #define style (not #pragma once):

#ifndef MY_UNIT_H_
#define MY_UNIT_H_

// ... content ...

#endif // MY_UNIT_H_

License Header

All files must include the GPLv3+ header:

/*
 * MyUnit.h
 *
 * (c) 2025 Your Name :: your@email.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

Memory Considerations

  • Avoid heap allocations - use stack or pre-allocated buffers

  • Use bit-packing for boolean flags: bool _flag : 1;

  • Prefer const references to avoid copies

  • Target Arduino Uno/Nano memory constraints

Summary

Key Decisions

  1. Flowable vs Unit: Use Flowable for components that don’t need engine management; use Unit for components requiring begin()/step() lifecycle and synchronized timing

  2. Engine Parameter: ALL Unit constructors MUST accept Engine& engine = Engine::primary() as the LAST parameter

  3. Timing Functions: Always use the unit’s timing methods (seconds(), samplePeriod(), etc.) instead of global Arduino functions (millis(), micros()) to ensure synchronization with the engine

  4. Event Naming: Use past tense for boolean checks (sampled()) and present tense with on prefix for callbacks (onSample())

  5. Base Class Selection:

    • Flowable - Non-engine-managed components

    • AnalogSource - Values in [0, 1]

    • DigitalUnit - Boolean on/off

    • DigitalSource - Boolean with change detection

    • Unit - Custom behavior

Implementation Checklist

  • Choose appropriate base class (Flowable or Unit subclass)

  • If Unit: Add Engine& parameter to ALL constructors (last, with default)

  • If Unit: Use engine timing methods, not global Arduino functions

  • Implement begin() for initialization (Unit only)

  • Implement step() for per-frame updates (Unit only)

  • Implement get() to return cached values

  • Implement put() to accept input values

  • Follow step-invariance principle (optional but recomended)

  • For events: past tense boolean, present tense callback

  • Add parameter slots for dynamic control (optional)

  • Write tests using AUnit

  • Follow coding conventions

  • Add GPLv3+ license header

For questions or contributions, see the CONTRIBUTING.md file or open an issue on GitHub.