# VectorHaptics-Core — Reference Examples

> **For downstream board-specific repositories.**
> These reference examples define the canonical patterns that all board-specific example sketches should follow. They are **board-agnostic pseudo-code** — each downstream repo must substitute the placeholder `YourBoardProfile` with the concrete `BoardProfile` implementation for that MCU.
>
> When creating examples in a downstream repo, copy the relevant reference example, replace the board-specific placeholders, verify it compiles and runs, and ship it with that repo.
>
> **For step-by-step explanations** of each topic (initialization, primitives, channels, playback, etc.), see the [Developer Guide](guide/README.md).

---

## Table of Contents

1. [Basic Effects (Blocking Playback)](#1-basic-effects-blocking-playback)
2. [Non-Blocking Playback (Queue + Render)](#2-non-blocking-playback-queue--render)
3. [Channel Targeting (Number, Tags, Multi)](#3-channel-targeting)
4. [Effect Defaults and Limits](#4-effect-defaults-and-limits)
5. [Sweep with Easing](#5-sweep-with-easing)
6. [PCM Playback](#6-pcm-playback)
7. [Verbosity and Message Queue](#7-verbosity-and-message-queue)
8. [Audio Streaming (A2DP / External Source)](#8-audio-streaming)
9. [Audio Streaming with Mixing](#9-audio-streaming-with-mixing)
10. [DevTools Serial CLI](#10-devtools-serial-cli)
11. [Binary Effect Receiver](#11-binary-effect-receiver)
12. [Custom Primitives](#12-custom-primitives)
13. [DSP Callback](#13-dsp-callback)
14. [Channel Idle/Wake Callbacks](#14-channel-idlewake-callbacks)
15. [Playback Status Polling](#15-playback-status-polling)
16. [I2S Channel Output](#16-i2s-channel-output)
17. [Full-Featured Device Setup](#17-full-featured-device-setup)

---

## 1. Basic Effects (Blocking Playback)

Demonstrates all built-in effect types using blocking `play()`. Single-channel, simplest possible setup.

```cpp
// basic_effects.ino
// Plays each built-in effect type sequentially on a single channel.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"  // ← Replace with your board's header

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

void setup() {
    // Board-specific serial init (adjust baud as needed)
    Serial.begin(115200);

    // Configure a single channel on your output pin
    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});

    // Initialize
    vh.init(&board, {&channels});
    vh.setDeviceInfo("BasicEffectsDemo", "TitanHaptics");
}

void loop() {
    // --- Pulse ---
    // Short transient event. Good for button feedback, notifications.
    vh.play(PULSE(50, 0.8, 0.5));    // 50ms, 80% intensity, 50% sharpness
    delay(500);

    // --- Tick ---
    // Very sharp transient. Good for detent clicks, UI ticks.
    vh.play(TICK(30, 0.9, 1.0));     // 30ms, 90% intensity, max sharpness
    delay(500);

    // --- Vibrate ---
    // Continuous vibration at a frequency. Good for sustained feedback.
    vh.play(VIBRATE(300, 0.7, 160.0, 0.3));  // 300ms, 70% intensity, 160Hz, 30% sharpness
    delay(500);

    // --- Pause ---
    // Silent gap. Used in effect sequences.
    // (Standalone pause just waits — more useful in sequences below)

    // --- ERM ---
    // Constant-intensity vibration (like a traditional ERM motor).
    vh.play(ERM(500, 0.6));          // 500ms, 60% intensity
    delay(500);

    // --- Sweep ---
    // Smooth transition of intensity, frequency, and sharpness over time.
    vh.play(SWEEP(
        1000,                         // 1 second duration
        0.3, 80.0, 0.0,              // Start: intensity=0.3, freq=80Hz, sharpness=0.0
        1.0, 300.0, 0.8,             // End:   intensity=1.0, freq=300Hz, sharpness=0.8
        TransitionType::EaseInOutCubic,
        TransitionType::Linear,
        TransitionType::EaseOutQuad
    ));
    delay(1000);

    // --- Effect Sequence ---
    // Multiple effects played back-to-back (blocking).
    vh.play({
        PULSE(50, 0.8, 0.5),
        PAUSE(100),
        TICK(30, 1.0, 1.0),
        PAUSE(100),
        VIBRATE(200, 0.6, 160.0, 0.2)
    });
    delay(2000);
}
```

**For more info:** [01-setup.md](guide/01-setup.md) · [02-primitives.md](guide/02-primitives.md)

---

## 2. Non-Blocking Playback (Queue + Render)

Demonstrates queued effect playback, allowing the main loop to continue running while effects play in the background via `renderQueue()`.

```cpp
// non_blocking_playback.ino
// Queue effects and render them without blocking the main loop.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels});

    // Queue a sequence of effects (does NOT play yet)
    vh.queue({
        VIBRATE(500, 0.8, 160.0, 0.3),
        PAUSE(200),
        PULSE(50, 1.0, 0.7),
        PAUSE(100),
        VIBRATE(300, 0.5, 200.0, 0.0)
    }, 1, APPEND_EFFECT);  // 1 = channel 1; use 0 to target all channels
}

void loop() {
    // Render one step of the queue each iteration

    vh.renderQueue(1);


    // Main loop continues to run — do sensor reads, comms, etc.
    // myOtherWork();
}
```

**For more info:** [04-playback.md](guide/04-playback.md)

---

## 3. Channel Targeting

Demonstrates the three ways to target channels: by number, by tag expression, and by explicit channel vector.

```cpp
// channel_targeting.ino
// Route effects to specific channels using numbers, tags, or vectors.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

void setup() {
    Serial.begin(115200);

    // Channel 1: left actuator, tagged "left" and "motor"
    channels.add(1, {PIN_LEFT}, {"left", "motor"});

    // Channel 2: right actuator, tagged "right" and "motor"
    channels.add(2, {PIN_RIGHT}, {"right", "motor"});

    // Channel 3: center actuator (dual-pin H-bridge), tagged "center" and "lra"
    channels.add(3, {PIN_CENTER_A, PIN_CENTER_B}, {"center", "lra"});

    vh.init(&board, {&channels});

    // --- Target by channel number (0 = all channels) ---
    vh.play(PULSE(50, 0.8, 0.5), 0);   // All channels
    delay(300);

    vh.play(PULSE(50, 0.8, 0.5), 1);   // Channel 1 only (left)
    delay(300);

    vh.play(PULSE(50, 0.8, 0.5), 2);   // Channel 2 only (right)
    delay(300);

    // --- Target by tag expression ---
    vh.play(VIBRATE(200, 0.7, 160.0, 0.3), "motor");         // All channels tagged "motor" (1 and 2)
    delay(500);

    vh.play(PULSE(50, 1.0, 0.8), "left | center");            // Channels tagged "left" OR "center" (1 and 3)
    delay(500);

    vh.play(TICK(30, 0.9, 1.0), "left & motor");              // Channels tagged "left" AND "motor" (1 only)
    delay(500);

    // --- Target by explicit channel vector ---
    vh.play(ERM(300, 0.5), {1, 3}); // Channels 1 and 3 (left and center)
    delay(500);
}

void loop() {
    // Nothing here for this demo
}
```

**For more info:** [03-channels.md](guide/03-channels.md)

---

## 4. Effect Defaults and Limits

Demonstrates how to configure default values and min/max limits for effect parameters. Use either the direct effect-class API or the primitive manager API.

```cpp
// effect_defaults_and_limits.ino
// Configure default values and parameter limits for effects.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;
VHPrimitives primitives;

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels, &primitives});

    // --- Set default values ---
    // When a parameter is omitted in a macro, these defaults are used.
    // Option A: Direct effect-class API
    VHPulse::setDefault(50, 0.8, 0.5);       // duration=50ms, intensity=0.8, sharpness=0.5
    VHVibrate::setDefault(300, 0.5f, 160.0f, 0.2f);  // duration, intensity, frequency (Hz), sharpness

    // Option B: Primitive manager API (same result, useful for runtime configuration)
    primitives.setDefault(VHPrimitives::PrimitiveType::PULSE, VHPrimitives::ParamType::INTENSITY, 0.8);
    primitives.setDefault(VHPrimitives::PrimitiveType::VIBRATE, VHPrimitives::ParamType::FREQUENCY, 160.0f);

    // --- Set min/max limits ---
    // Parameters are clamped to these ranges automatically.
    // Option A: Direct effect-class API
    VHPulse::setMinMaxIntensity(0.1, 1.0);    // Pulse intensity always between 10%-100%
    VHPulse::setMinMaxDuration(10, 200);       // Pulse duration always between 10ms-200ms

    VHVibrate::setMinFreq(50.0);               // Minimum vibrate frequency: 50Hz
    VHVibrate::setMaxFreq(500.0);              // Maximum vibrate frequency: 500Hz

    VHPause::setMinMaxDuration(10, 5000);      // Pause between 10ms-5000ms

    VHErm::setMinMaxIntensity(-1.0, 1.0);     // ERM full range (negative = reverse on dual-pin)

    // Option B: Primitive manager API (per-parameter min/max)
    primitives.setMin(VHPrimitives::PrimitiveType::TICK, VHPrimitives::ParamType::INTENSITY, 0.2f);
    primitives.setMax(VHPrimitives::PrimitiveType::TICK, VHPrimitives::ParamType::INTENSITY, 0.9f);

    // --- Now play with defaults (no parameters needed) ---
    vh.play(PULSE());     // Uses configured defaults: 50ms, 0.8, 0.5
    delay(500);

    vh.play(VIBRATE());   // Uses defaults: 160Hz, etc.
    delay(500);

    // Parameters exceeding limits are clamped automatically:
    vh.play(PULSE(5000, 2.0, 0.5));  // Duration clamped to 200ms, intensity clamped to 1.0
    delay(500);
}

void loop() {}
```

**For more info:** [09-primitive-manager.md](guide/09-primitive-manager.md)

---

## 5. Sweep with Easing

Demonstrates sweep effects with various easing/transition functions.

```cpp
// sweep_easing.ino
// Sweep effects with different easing functions for smooth parameter transitions.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels});

    // --- Linear frequency sweep (constant ramp) ---
    vh.play(SWEEP(
        2000,                              // 2 seconds
        0.8, 50.0, 0.0,                   // Start: intensity=0.8, freq=50Hz, sharpness=0
        0.8, 400.0, 0.0,                  // End:   intensity=0.8, freq=400Hz, sharpness=0
        TransitionType::Linear,            // Intensity: constant
        TransitionType::Linear,            // Frequency: linear ramp
        TransitionType::Linear             // Sharpness: constant
    ));
    delay(2500);

    // --- Intensity fade-in with ease-in-cubic ---
    vh.play(SWEEP(
        1000,
        0.0, 160.0, 0.3,                  // Start: silent
        1.0, 160.0, 0.3,                  // End: full intensity, same freq/sharpness
        TransitionType::EaseInCubic,       // Slow start, accelerating
        TransitionType::Linear,
        TransitionType::Linear
    ));
    delay(1500);

    // --- Intensity fade-out with ease-out-quad ---
    vh.play(SWEEP(
        1000,
        1.0, 160.0, 0.3,
        0.0, 160.0, 0.3,
        TransitionType::EaseOutQuad,       // Fast start, decelerating to silence
        TransitionType::Linear,
        TransitionType::Linear
    ));
    delay(1500);

    // --- Combined: intensity ramps up while frequency sweeps with bounce ---
    vh.play(SWEEP(
        3000,
        0.2, 80.0, 0.0,
        1.0, 300.0, 1.0,
        TransitionType::EaseInOutSine,     // Smooth S-curve for intensity
        TransitionType::EaseOutBounce,     // Bouncy frequency transition
        TransitionType::EaseInExpo         // Exponential sharpness build
    ));
    delay(4000);
}

void loop() {}
```

**For more info:** [02-primitives.md](guide/02-primitives.md) (SWEEP)

---

## 6. PCM Playback

Demonstrates playing raw PCM waveform data as a haptic effect.

```cpp
// pcm_playback.ino
// Play raw PCM data as a haptic effect.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

// 8-bit unsigned PCM data. 127 = silence, 0 = min, 255 = max.
// This is a simple ramp-up pattern. Replace with your actual waveform.
const uint8_t myPcmData[] = {
    127, 140, 153, 166, 179, 191, 204, 217, 230, 242, 255,
    242, 230, 217, 204, 191, 179, 166, 153, 140, 127,
    114, 101,  88,  75,  63,  50,  37,  25,  12,   0,
     12,  25,  37,  50,  63,  75,  88, 101, 114, 127
};

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels});

    // Optionally set the PCM sample rate (default: 8000 Hz)
    VH_Pcm::setSampleRate(8000);

    // Play PCM data on default channel
    vh.play(PCM(myPcmData, sizeof(myPcmData)));
    delay(500);

    // Play PCM on a specific channel
    vh.play(PCM(myPcmData, sizeof(myPcmData)), 0);
    delay(500);

    // PCM in a sequence with other effects
    vh.play({
        PULSE(30, 1.0, 0.8),
        PAUSE(50),
        PCM(myPcmData, sizeof(myPcmData)),
        PAUSE(50),
        TICK(30, 0.9, 1.0)
    });
}

void loop() {}
```

**For more info:** [05-audio.md](guide/05-audio.md)

---

## 7. Verbosity and Message Queue

Demonstrates how to configure verbosity and read back messages from the internal message queue using `hasMessage()` / `popMessage()`.

```cpp
// verbosity_and_messages.ino
// Print VectorHaptics log messages over serial based on verbosity flags.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});

    vh.init(&board, {&channels});

    // Enable all log types (ERROR_ONLY, WARNING_ONLY, INFO_ONLY, RESPONSE_ONLY).
    // In production you would typically use ERROR_ONLY or ERROR_ONLY | WARNING_ONLY.
    vh.setVerbosity(VERBOSE);

    Serial.println("Verbosity demo starting...");

    // Play a few effects to generate INFO messages when they complete.
    vh.queue({
        PULSE(50, 0.8f, 0.5f),
        PAUSE(100),
        VIBRATE(200, 0.6f, 160.0f, 0.3f)
    }, 1);  // 1 = channel 1; use 0 to target all channels
}

void loop() {
    // Drive the queued effects (non-blocking). This will eventually generate log messages.

    vh.renderQueue(1);


    // Drain the internal message queue and print everything to serial.
    // Messages are prefixed (e.g., [ERROR], [INFO], [WARNING], [RESPONSE]) based on verbosity.
    while (vh.hasMessage()) {
        Serial.println(vh.popMessage());
    }
}
```

**For more info:** [08-devtools.md](guide/08-devtools.md)

---

## 8. Audio Streaming

Demonstrates routing live audio (e.g., from A2DP Bluetooth) to haptic channels.

```cpp
// audio_streaming.ino
// Stream live audio to haptic output channels.
// The audio source (A2DP, I2S input, etc.) is board-specific.

#include <VectorHaptics.h>
#include <VHAudioStreaming.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

// Ring buffer storage (allocated statically for real-time safety)
int16_t ringBufStorage[4096];
VH::RingBuffer<int16_t> ringBuffer(ringBufStorage, 4096);

// Audio streaming engine: 44100 Hz input, stereo, 16-bit
AudioStreaming audioStream(44100, 2, 16);

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});

    // Initialize with audio streaming module
    vh.init(&board, {
        &channels,
        &audioStream
    });

    // Bind ring buffer to audio stream
    audioStream.init(&ringBuffer);

    // Start streaming via the AUDIO_STREAM macro:
    // - VH_AUDIO_STREAM_INFINITY: keep streaming until explicitly stopped
    // - 1.0f: full intensity
    // - vh.play(..., 0): target all channels (channel 0 = all)
    vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 1.0f), 0);
}

// This function is called by your board-specific audio source
// (e.g., A2DP callback, I2S read callback, etc.)
void onAudioDataReceived(const int16_t* samples, size_t count) {
    // 'count' is the total number of samples (all channels interleaved)
    audioStream.write(samples, count);
}

void loop() {
    // Audio is processed via timer ISR — nothing to do here
    // Your board-specific audio source feeds onAudioDataReceived()
}
```

**For more info:** [05-audio.md](guide/05-audio.md)

### 8.1. Audio Streaming with Multiple Ring Buffers (Per-Channel)

The following example shows how to use **separate ring buffers per audio input channel**
and select which audio channel drives which haptic output channel using `AUDIO_STREAM`.

```cpp
// audio_streaming_multibuffer.ino
// Use separate ring buffers per audio channel and route them to different actuators.
// The audio source (A2DP, I2S input, etc.) is board-specific.

#include <VectorHaptics.h>
#include <VHAudioStreaming.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

// Stereo input: 44100 Hz, 2 channels, 16-bit
AudioStreaming audioStream(44100, 2, 16);

// Two independent ring buffers, one per audio input channel
int16_t ringBufLStorage[4096];
int16_t ringBufRStorage[4096];
VH::RingBuffer<int16_t> ringBufL(ringBufLStorage, 4096);
VH::RingBuffer<int16_t> ringBufR(ringBufRStorage, 4096);
VH::RingBuffer<int16_t>* stereoBuffers[2] = { &ringBufL, &ringBufR };

void setup() {
    Serial.begin(115200);

    // Channel 1: left actuator, Channel 2: right actuator
    channels.add(1, {PIN_LEFT}, {"left", "motor"});
    channels.add(2, {PIN_RIGHT}, {"right", "motor"});

    // Initialize VectorHaptics with audio streaming module
    vh.init(&board, {
        &channels,
        &audioStream
    });

    // Bind per-channel ring buffers (L/R) to the audio stream
    // Number of buffers must match the audioStream's input channel count (2 here).
    audioStream.init(stereoBuffers, 2);

    // Example 1: route mixed audio (L+R) to actuators at 80% intensity
    vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f, 0), 0);  // mix all audio → target all channels
    vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f, 0), 1);  // mix all audio → target channel 1

    // Example 2: later, you can route specific audio channels independently:
    // vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f, 1), 0); // left input only  → all channels
    // vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f, 2), 1); // right input only → channel 1
}

// Board-specific audio callback providing interleaved stereo samples:
// samples: [L0, R0, L1, R1, ...], frameCount: number of stereo frames
void onAudioDataReceived(const int16_t* samples, size_t frameCount) {
    // Write all samples (L+R) into the streaming engine.
    // Internally, in multi-channel mode, samples are fanned out to the per-channel buffers.
    audioStream.write(samples, frameCount * 2);  // total sample count = frames * channelCount
}

void loop() {
    // Audio processing runs in the background via timer ISR
}
```

**For more info:** [05-audio.md](guide/05-audio.md)

---

## 9. Audio Streaming with Mixing

Demonstrates playing haptic effects on top of an active audio stream using mixing strategies.

```cpp
// audio_mixing.ino
// Mix haptic primitives with a live audio stream on the same channel.

#include <VectorHaptics.h>
#include <VHAudioStreaming.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

int16_t ringBufStorage[4096];
VH::RingBuffer<int16_t> ringBuffer(ringBufStorage, 4096);
AudioStreaming audioStream(44100, 2, 16);

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});

    vh.init(&board, {
        &channels,
        &audioStream
    });

    audioStream.init(&ringBuffer);

    // Enable additive mixing: audio + haptic primitives are summed
    // Other options: INTERRUPT_MIX, LARGESTVAL_MIX, CUSTOM_MIX
    audioStream.enableMixing(0, MixingStrategy::ADDITIVE_MIX);

    // Start audio stream targeting all channels (0 = all) using the AUDIO_STREAM macro
    // VH_AUDIO_STREAM_INFINITY: keep streaming, 0.8f: 80% intensity
    vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f), 0);
}

void onAudioDataReceived(const int16_t* samples, size_t count) {
    audioStream.write(samples, count);
}

void loop() {
    // Overlay a pulse on top of the running audio stream.
    // The mixer combines both signals according to the mixing strategy.
    vh.play(PULSE(50, 1.0, 0.8), 0);
    delay(2000);

    vh.play(VIBRATE(300, 0.5, 160.0, 0.3), 0);
    delay(2000);
}
```

**For more info:** [05-audio.md](guide/05-audio.md)

---

## 10. DevTools Serial CLI

Demonstrates the text-based debug interface for testing effects via serial input.

```cpp
// devtools_serial.ino
// Interactive serial CLI for testing haptic effects.

#include <VectorHaptics.h>
#include <VHDevTools.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;
VHDevTools devTools;

void setup() {
    Serial.begin(115200);

    channels.add(1, {PIN_LEFT}, {"left", "motor"});
    channels.add(2, {PIN_RIGHT}, {"right", "motor"});

    vh.init(&board, {
        &channels,
        &devTools
    });

    vh.setVerbosity(VERBOSE);

    Serial.println("VectorHaptics DevTools ready. Type commands:");
    Serial.println("  PULSE 50 0.8 0.5");
    Serial.println("  VIBRATE 300 0.7 160 0.3");
    Serial.println("  CHNL 1; TICK 30 1.0 1.0");
    Serial.println("  CHNL 'left'; PULSE 50");
    Serial.println("  HDI D   (device info)");
    Serial.println("  HDI C   (all channel info)");
}

void loop() {
    if (Serial.available()) {
        String input = Serial.readStringUntil('\n');
        input.trim();
        if (input.length() > 0) {
            vh.executeStringCommand(std::string(input.c_str()));

            // Print any response messages
            while (vh.hasMessage()) {
                Serial.println(vh.popMessage());
            }
        }
    }
}
```

**For more info:** [08-devtools.md](guide/08-devtools.md)

---

## 11. Binary Effect Receiver

Demonstrates receiving haptic commands over a binary protocol (e.g., from BLE or serial).

```cpp
// binary_receiver.ino
// Receive and execute haptic effects via binary protocol.

#include <VectorHaptics.h>
#include <VHEffectReceiver.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;
VHEffectReceiver receiver;

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});

    vh.init(&board, {
        &channels,
        &receiver
    });

    // The receiver spawns a background thread that continuously
    // parses incoming binary data and triggers effects.
    Serial.println("Binary effect receiver ready.");
}

void loop() {
    // Feed incoming serial bytes to the receiver.
    // In practice, this data may come from BLE, WiFi, or another interface.
    if (Serial.available()) {
        uint8_t buffer[256];
        size_t len = Serial.readBytes(buffer, sizeof(buffer));
        vh.executeCommand(buffer, len);
    }
}

```

**For more info:** [07-effect-receiver.md](guide/07-effect-receiver.md)

---

## 12. Custom Primitives

Demonstrates registering and playing user-defined haptic primitives with custom waveform callbacks.

```cpp
#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;
VHPrimitives primitives;

unsigned char sawtoothCallback(VHPrimitiveParams* param, float startTime) {
    float durationMs = param->getDur();
    float intensity  = param->getParameter("intensity", 0.8f);
    float frequency  = param->getParameter("frequency", 120.0f);

    unsigned long currentTime = BoardProfile::getInstance()->getTimeMicroseconds();
    unsigned long timeElapsed = currentTime - (unsigned long)(startTime * 1000.0f);

    unsigned long timePeriod = 1000000.0f / frequency;
    unsigned long cyclePosition = timeElapsed % timePeriod;
    float cycleFraction = (float)cyclePosition / (float)timePeriod;

    float value01 = cycleFraction * intensity;
    return (uint8_t)(127 + value01 * 128.0f);
}

void setup() {
    Serial.begin(115200);

    int SAWTOOTH = primitives.registerCustom("SAWTOOTH", sawtoothCallback);

    auto prim = primitives.getPrimitive("SAWTOOTH");
    if (prim) {
        prim->setDefMinMaxDuration(1000.0f, 1.0f, 10000.0f);
        prim->setDefMinMaxParameter("intensity", 0.8f, 0.2f, 1.0f);
        prim->setDefMinMaxParameter("frequency", 120.0f, 60.0f, 240.0f);
    }

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"}, {SAWTOOTH});
    vh.init(&board, {&channels, &primitives});
}

#define SAWTOOTH(dur, inten, freq) \
    (primitives.getPrimitive("SAWTOOTH", dur, {{"frequency", freq}, {"intensity", inten}}))

void loop() {
    vh.play(SAWTOOTH(500, 0.8f, 120.0f));
    delay(2000);
}
```

**For more info:** [09-primitive-manager.md](guide/09-primitive-manager.md)

---

## 13. DSP Callback

Demonstrates per-channel DSP processing of the output signal.

```cpp
// dsp_callback.ino
// Apply custom signal processing to channel output.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels});

    // Apply a DSP callback to channel 1.
    // This function is called for EVERY output sample before it's written to the pin.
    channels.applyDSP(1, [](uint8_t val) -> uint8_t {
        // Example: soft clipping — reduce values above 200
        if (val > 200) {
            val = 200 + (val - 200) / 4;  // Compress peaks
        }
        // Example: dead-zone removal — boost low values
        if (val > 127 && val < 140) {
            val = 140;  // Minimum active threshold
        }
        return val;
    });
}

void loop() {
    vh.play(VIBRATE(500, 1.0, 160.0, 0.3));
    delay(1000);
}
```

**For more info:** [06-dsp.md](guide/06-dsp.md)

---

## 14. Channel Idle/Wake Callbacks

Demonstrates monitoring channel activity with idle and wake event callbacks.

```cpp
// channel_callbacks.ino
// Get notified when channels become idle or start playing.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

void setup() {
    Serial.begin(115200);

    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    channels.enableIdleHandler(1, true);  // Must be before init
    vh.init(&board, {&channels});

    // Callback when channel goes idle (no effects playing)
    channels.attachCallback(1, CHANNEL_IDLE, [](uint8_t channelNo) {
        Serial.printf("Channel %d is now IDLE\n", channelNo);
        // Good place to enter low-power mode, turn off amplifier, etc.
    });

    // Callback when channel wakes up (effect starts playing)
    channels.attachCallback(1, CHANNEL_WAKE, [](uint8_t channelNo) {
        Serial.printf("Channel %d is now ACTIVE\n", channelNo);
        // Good place to enable amplifier, wake from sleep, etc.
    });
}

void loop() {
    vh.play(VIBRATE(300, 0.7, 160.0, 0.3));
    delay(2000);  // Idle callback fires during this gap
}
```

**For more info:** [11-playback-status-and-callbacks.md](guide/11-playback-status-and-callbacks.md)

---

## 15. Playback Status Polling

Demonstrates checking playback state programmatically.

```cpp
// playback_status.ino
// Poll playback status to coordinate application logic.

#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

void setup() {
    Serial.begin(115200);

    channels.add(1, {PIN_A}, {"left"});
    channels.add(2, {PIN_B}, {"right"});
    channels.enableIdleHandler(1, true);  // Must be before init
    channels.enableIdleHandler(2, true);
    vh.init(&board, {&channels});

    // Queue a long effect on all channels (0 = all)
    vh.queue({VIBRATE(2000, 0.8, 160.0, 0.3)}, 0);
}

void loop() {
    // Render queued effects (0 = all channels)

    vh.renderQueue(0);


    // Check per-channel status
    VHPlaybackState ch1 = vh.playbackStatus(1);
    VHPlaybackState ch2 = vh.playbackStatus(2);

    if (ch1 == VHPlaybackState::VH_PLAYING) {
        Serial.println("Channel 1: playing");
    } else if (ch1 == VHPlaybackState::VH_IDLE) {
        Serial.println("Channel 1: idle");
    }
    if (ch2 == VHPlaybackState::VH_PLAYING) {
        Serial.println("Channel 2: playing");
    } else if (ch2 == VHPlaybackState::VH_IDLE) {
        Serial.println("Channel 2: idle");
    }

    // Check overall device status
    VHPlaybackState overall = vh.playbackStatus();
    if (overall == VHPlaybackState::VH_IDLE) {
        Serial.println("All channels idle — ready for next effect");
    }

    delay(100);
}
```

**For more info:** [11-playback-status-and-callbacks.md](guide/11-playback-status-and-callbacks.md)

---

## 16. I2S Channel Output

Demonstrates routing haptic/audio output through I2S instead of PWM.

```cpp
// i2s_output.ino
// Output haptic signals via I2S for audio-quality actuator driving.

#include <VectorHaptics.h>
#include <VHChannelsI2s.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;
VHChannelsI2s i2s;

void setup() {
    Serial.begin(115200);

    // Channel 1 uses I2S output instead of PWM.
    // The three pins are: BCLK (bit clock), LRC (word select), DOUT (data out).
    channels.add(1, {BCLK_PIN, LRC_PIN, DOUT_PIN}, {"i2s_out"});

    // Initialize VectorHaptics with both the channel list and the I2S sound bus.
    vh.init(&board, {
        &channels,
        &i2s
    });

    // Optional: configure I2S with an explicit sample rate and bit depth.
    // If you skip this, vh.init() has already called i2s.begin(...) with the same pins
    // and a default sample rate.
    i2s.begin(BCLK_PIN, LRC_PIN, DOUT_PIN, 16000, 16);

    // Start the I2S peripheral so it can actually stream samples.
    i2s.start();
}

void loop() {
    vh.play(VIBRATE(500, 0.8, 160.0, 0.3), 0);
    delay(1000);
}
```

**For more info:** [05-audio.md](guide/05-audio.md)

---

## 17. Full-Featured Device Setup

Demonstrates a production-ready setup with all modules enabled: multi-channel, DevTools, binary receiver, audio streaming, and callbacks.

```cpp
// full_device.ino
// Complete device setup with all VectorHaptics modules.

#include <VectorHaptics.h>
#include <VHDevTools.h>
#include <VHEffectReceiver.h>
#include <VHAudioStreaming.h>
#include "YourBoardProfile.h"

// --- Core instances ---
VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

// --- Extension modules ---
VHDevTools devTools;
VHEffectReceiver receiver;
VHPrimitives primitives;

// --- Audio streaming ---
int16_t ringBufStorage[4096];
VH::RingBuffer<int16_t> ringBuffer(ringBufStorage, 4096);
AudioStreaming audioStream(44100, 2, 16);

void setup() {
    Serial.begin(115200);

    // --- Channel configuration ---
    channels.add(1, {PIN_LEFT},  {"left",  "motor"});
    channels.add(2, {PIN_RIGHT}, {"right", "motor"});
    channels.enableIdleHandler(0, true);  // Must be before init
    channels.enableIdleHandler(1, true);

    // --- Initialize all modules ---
    vh.init(&board, {
        &channels,
        &devTools,
        &receiver,
        &primitives,
        &audioStream
    });

    // --- Device info ---
    vh.setDeviceInfo("MyHapticDevice", "TitanHaptics", "MODEL_X", "SN00001");
    vh.setVerbosity(ERROR_ONLY);  // Production: minimal logging

    // --- Effect defaults (direct API or primitive manager API) ---
    VHPulse::setDefault(50, 0.8, 0.5);
    VHVibrate::setDefault(300, 0.5f, 160.0f, 0.2f);
    // Or: primitives.setDefault(VHPrimitives::PrimitiveType::PULSE, VHPrimitives::ParamType::INTENSITY, 0.8);

    // --- Audio streaming ---
    audioStream.init(&ringBuffer);
    audioStream.enableMixing(0, MixingStrategy::ADDITIVE_MIX);
    audioStream.enableMixing(1, MixingStrategy::ADDITIVE_MIX);

    // --- Idle callbacks (e.g., for power management) ---
    channels.attachCallback(0, CHANNEL_IDLE, [](uint8_t ch) {
        // Enter low-power mode for left actuator
    });
    channels.attachCallback(0, CHANNEL_WAKE, [](uint8_t ch) {
        // Wake up left actuator amplifier
    });

    Serial.println("VectorHaptics device ready.");
}

void loop() {
    // --- Serial DevTools input ---
    if (Serial.available()) {
        String input = Serial.readStringUntil('\n');
        input.trim();
        if (input.length() > 0) {
            vh.executeStringCommand(std::string(input.c_str()));
            while (vh.hasMessage()) {
                Serial.println(vh.popMessage());
            }
        }
    }

    // --- Binary commands (from BLE, WiFi, etc.) ---
    // In a real device, feed binary data from your communication layer:
    // vh.executeCommand(bleData, bleDataLen);
}

// --- Audio source callback (board-specific) ---
void onAudioData(const int16_t* samples, size_t count) {
    audioStream.write(samples, count);
}
```

**For more info:** [01-setup.md](guide/01-setup.md) · [10-board-profiles.md](guide/10-board-profiles.md) · [11-playback-status-and-callbacks.md](guide/11-playback-status-and-callbacks.md)

---

## Downstream Implementation Checklist

When creating examples in a board-specific repo:

- Replace `YourBoardProfile` with your concrete class (e.g., `ESP32Board`)
- Replace `YOUR_OUTPUT_PIN`, `PIN_LEFT`, etc. with actual GPIO numbers
- Add board-specific serial/logging initialization
- Add board-specific audio source setup (A2DP, I2S in, etc.) for streaming examples
- Verify each example compiles and runs on your target board
- Add board-specific comments explaining pin choices and hardware setup
- Place examples in the standard `examples/` directory for Arduino/PlatformIO discovery

