# VectorHaptics-Core API Reference

Complete API documentation for the VectorHaptics-Core library. This reference covers all public classes, methods, macros, and types exposed to downstream consumers.

---

## Table of Contents

1. [VectorHaptics (Main API)](#1-vectorhaptics-main-api)
2. [Effect Macros](#2-effect-macros)
3. [Effect Classes](#3-effect-classes)
4. [Channel Management](#4-channel-management)
5. [Board Abstraction (HAL)](#5-board-abstraction-hal)
6. [Audio Streaming](#6-audio-streaming)
7. [DevTools](#7-devtools)
8. [EffectReceiver](#8-effectreceiver)
9. [I2S Channel](#9-i2s-channel)
10. [Custom Primitives](#10-custom-primitives)
11. [Utilities & Types](#11-utilities--types)

---

## 1. VectorHaptics (Main API)

**Header:** `VectorHaptics.h`

### Construction

```cpp
VectorHaptics vh;
```

Singleton. Access later via `VectorHaptics::getInstance()`.

### Initialization

#### `void init(BoardBasic *pBoard)`

Initialize in basic single-channel mode. Minimal HAL requirements.

| Parameter | Type | Description |
|---|---|---|
| `pBoard` | `BoardBasic*` | Pointer to basic board implementation |

#### `void init(BoardProfile *pBoard, std::vector<VHBaseType*> libraryObjects)`

Initialize with full board profile and optional extension modules.

| Parameter | Type | Description |
|---|---|---|
| `pBoard` | `BoardProfile*` | Pointer to full board profile implementation |
| `libraryObjects` | `std::vector<VHBaseType*>` | Extension module instances (e.g., DevTools, EffectReceiver, AudioStreaming, I2S) |

**Extension modules** are identified by their `IType` enum and registered automatically:
- `IType::idev_tools` → `IDevTools`
- `IType::ieffect_receiver` → `IEffectReceiver`
- `IType::ichannel_list` → `IVHChannels`
- `IType::ii2s` → `II2SoundBus`
- `IType::iprimitive_man` → `IPrimitiveManager`
- `IType::iaudio_streaming` → `IAudioStreaming`
- `IType::iaudio_streaming_mixer` → `IPcmMixer`

### Playing Effects (Blocking)

All `play()` methods block until the effect completes.

#### `int play(std::unique_ptr<IVhEffect> effect)`
Play a single effect on all available channels.

#### `int play(std::unique_ptr<IVhEffect> effect, int channelNo)`
Play a single effect on the given channel target. **Channel 0 = all channels**; 1, 2, 3... = specific channel IDs.

#### `int play(std::unique_ptr<IVhEffect> effect, const char *tags)`
Play a single effect on channels matching a tag expression.

#### `int play(std::unique_ptr<IVhEffect> effect, std::vector<unsigned char> chnls)`
Play a single effect on multiple specified channels. Use channel IDs 1, 2, 3... (0 means all channels when used as a target elsewhere).

#### `int play(std::initializer_list<std::unique_ptr<IVhEffect>> effectList)`
Play a sequence of effects on the default channel.

#### `int play(std::initializer_list<std::unique_ptr<IVhEffect>> effectList, int channelNo)`
Play a sequence of effects on a specific channel.

#### `int play(std::initializer_list<std::unique_ptr<IVhEffect>> effectList, const char *tags)`
Play a sequence of effects on channels matching a tag expression.

#### `int play(std::initializer_list<std::unique_ptr<IVhEffect>> effectList, std::vector<unsigned char> chnls)`
Play a sequence of effects on multiple specified channels.

**Returns:** `VH_SUCCESS` or `VH_ERROR`

**Usage:**
```cpp
vh.play(PULSE(100, 0.8, 0.5));
vh.play(VIBRATE(200, 0.7, 160.0, 0.3), 1);
vh.play({PULSE(100), PAUSE(50), VIBRATE(200)}, "left");
```

### Queueing Effects (Non-Blocking)

#### `int queue(std::initializer_list<std::unique_ptr<IVhEffect>> effectList, QueueOperType oper = APPEND_EFFECT)`
Queue effects on the default channel.

#### `int queue(std::initializer_list<std::unique_ptr<IVhEffect>> effectList, int channelNo, QueueOperType oper = APPEND_EFFECT)`
Queue effects on the given channel target. **Channel 0 = all channels**; 1, 2, 3... = specific channel IDs.

#### `int queue(std::initializer_list<std::unique_ptr<IVhEffect>> effectList, const char *tags, QueueOperType oper = APPEND_EFFECT)`
Queue effects on channels matching a tag expression.

#### `int queue(std::initializer_list<std::unique_ptr<IVhEffect>> effectList, std::vector<unsigned char> chnls, QueueOperType oper = APPEND_EFFECT)`
Queue effects on multiple specified channels.

| Parameter | Type | Description |
|---|---|---|
| `effectList` | initializer_list | One or more effects |
| `oper` | `QueueOperType` | `APPEND_EFFECT` (add to end) or `REPLACE_EFFECT` (clear and replace) |

#### `int renderQueue(int chnlNo = 0)`
Start rendering the queued effects. **Channel 0 = all channels**; 1, 2, 3... = specific channel. Call from a dedicated thread or loop.

#### `bool queueIsEmpty(int chnlNo)`
Check if a channel's render queue has been fully consumed. **Channel 0 = all channels**; 1, 2, 3... = specific channel.

**Usage:**
```cpp
vh.queue({VIBRATE(500), PAUSE(100), PULSE(50)}, 1, APPEND_EFFECT);
// In a render loop or RTOS task:
while (!vh.queueIsEmpty(1)) {
    vh.renderQueue(1);
}
```

### Playback Control

#### `void stop(unsigned int milliseconds = 0)`
Stop all playback. Optional ramp-down duration in ms.

#### `VHPlaybackState playbackStatus()`
Get global playback state.

#### `VHPlaybackState playbackStatus(uint8_t channelNo)`
Get playback state for a specific channel.

**Returns:** `VHPlaybackState::VH_IDLE`, `VHPlaybackState::VH_PLAYING`, `VHPlaybackState::VH_DISABLED`, or `VHPlaybackState::VH_UNKNOWN`

### Configuration

#### `void setVerbosity(int vals)`
Set which message types are eligible to be returned by `popMessage()`. Combine flags with `|`:
- `SILENT` (0) — no messages
- `ERROR_ONLY` (1) — errors only
- `INFO_ONLY` (2) — standard messages
- `WARNING_ONLY` (4) — warnings
- `VERBOSE` (7) — all messages

#### `void setDeviceInfo(string DeviceName, string Manufacture, string ModelNo, string SerialNo, string VersionNo)`
Set device metadata for identification over DevTools/EffectReceiver.

#### `void setPcmCallBack(PcmchunkCb cb)`
Set callback for PCM chunk requests. Signature: `void(std::string&)`

### Command Execution

#### `void executeStringCommand(std::string comdList)`
Execute a text command string (routes to DevTools if enabled).

#### `void executeCommand(const unsigned char *data, unsigned short len)`
Execute a binary command (routes to EffectReceiver if enabled).

### Instance Access

#### `static VectorHaptics* getInstance()`
Get the singleton VectorHaptics instance.

#### `BoardProfile* getBoardPtr()`
Get the active board profile pointer.

<!-- `getBoardUniqName()` was removed; use your own naming scheme or board APIs if needed. -->

### Message Queue

#### `int hasMessage()`
Check if there are pending messages (returns count).

#### `const char* popMessage()`
Pop and return the next message string. The string includes a log-type prefix: `[ERROR] `, `[INFO] `, `[WARNING] `, or `[RESPONSE] `. Returns an empty string when no messages are available.

---

## 2. Effect Macros

**Header:** `VectorHaptics.h`

Factory macros that create `std::unique_ptr<IVhEffect>` objects for use with `play()` and `queue()`.

### PULSE(...)

```cpp
PULSE()                                    // All defaults
PULSE(duration)                            // float ms
PULSE(duration, intensity)                 // float [0.0, 1.0]
PULSE(duration, intensity, sharpness)      // float [0.0, 1.0]
```

**Defaults:** duration=6.25ms (1000/RESONANT_FREQ), intensity=0.70, sharpness=0.50

### TICK(...)

```cpp
TICK()
TICK(duration)
TICK(duration, intensity)
TICK(duration, intensity, sharpness)
```

**Defaults:** duration=6.25ms, intensity=0.70, sharpness=1.00

### VIBRATE(...)

```cpp
VIBRATE()
VIBRATE(duration)
VIBRATE(duration, intensity)
VIBRATE(duration, intensity, frequency)
VIBRATE(duration, intensity, frequency, sharpness)
```

**Defaults:** duration=625us (FRQ_TO_DUR(160)), intensity=0.70, frequency=160Hz, sharpness=0.00

### PAUSE(...)

```cpp
PAUSE()              // Default 500ms
PAUSE(duration)      // float ms
```

### PCM(...)

```cpp
PCM(pcm_array, size)
```

| Parameter | Type | Description |
|---|---|---|
| `pcm_array` | `const uint8_t*` | Pointer to PCM data array (8-bit unsigned, 127=silence) |
| `size` | `uint16_t` | Number of samples |

### AUDIO_STREAM(...)

```cpp
AUDIO_STREAM()
AUDIO_STREAM(durationMs, intensity)
AUDIO_STREAM(durationMs, intensity, audioChannel)
```

| Parameter | Type | Description |
|---|---|---|
| `durationMs` | `float` | Duration in ms (-1.0 for infinite) |
| `intensity` | `float` | Volume scaling [0.0, 1.0] |
| `audioChannel` | `uint8_t` | Audio channel index (for multi-channel) |

### ERM(...)

```cpp
ERM()
ERM(duration)
ERM(duration, intensity)
```

**Defaults:** duration=1000ms, intensity=1.00. Intensity range: [-1.0, 1.0] (negative = reverse polarity on dual-pin channels).

### SWEEP(...)

```cpp
SWEEP(duration, startIntensity, startFrequency, startSharpness,
      endIntensity, endFrequency, endSharpness,
      transIntensity, transFrequency, transSharpness)
```

| Parameter | Type | Description |
|---|---|---|
| `duration` | `float` | Duration in ms |
| `startIntensity` / `endIntensity` | `float` | Intensity range [0.0, 1.0] |
| `startFrequency` / `endFrequency` | `float` | Frequency range in Hz |
| `startSharpness` / `endSharpness` | `float` | Sharpness range [0.0, 1.0] |
| `transIntensity` / `transFrequency` / `transSharpness` | `TransitionType` | Easing function for each parameter |

---

## 3. Effect Classes

**Header:** `VHEffects.h`

All effect classes support static configuration of defaults and min/max limits.

### VHPulse

```cpp
// Static configuration
VHPulse::setDefault(float duration, float intensity, float sharpness);
VHPulse::setMinMaxIntensity(float min, float max);
VHPulse::setMinMaxDuration(float min, float max);
VHPulse::setMinValues(float minD, float minI, float minS);
VHPulse::setMaxValues(float maxD, float maxI, float maxS);

// Getters
VHPulse::getDefault(float &dur, float &int, float &sharp);
VHPulse::getMinIntensity() → float;
VHPulse::getMaxIntensity() → float;
VHPulse::getMinDuration() → float;
VHPulse::getMaxDuration() → float;
VHPulse::getMinSharpness() → float;
VHPulse::getMaxSharpness() → float;
```

### VHTick

Same API as `VHPulse`. Default sharpness = 1.0.

### VHVibrate

Extends VHPulse API with frequency:

```cpp
// Overloads: parameters in order (Duration, Intensity, Frequency, sharpness), omit from right
VHVibrate::setDefault(float duration);                           // 1 param
VHVibrate::setDefault(float duration, float intensity);          // 2 params
VHVibrate::setDefault(float duration, float intensity, float freq);  // 3 params
VHVibrate::setDefault(float duration, float intensity, float freq, float sharpness);  // 4 params
VHVibrate::setMinFreq(float minFreq);
VHVibrate::setMaxFreq(float maxFreq);
VHVibrate::getMinFreq(float &minFreq);
VHVibrate::getMaxFreq(float &maxFreq);
```

### VHPause

```cpp
VHPause::setDefault(float duration);
VHPause::getDefault() → float;
VHPause::setMinMaxDuration(float min, float max);
```

### VHErm

```cpp
VHErm::setDefault(float duration, float intensity);
VHErm::setMinMaxIntensity(float min, float max);  // Range: [-1.0, 1.0]
```

### VHSweep

Sweep has its own static configuration API (separate from `VHVibrate`) with SWEEP-specific ranges and defaults:

```cpp
// Defaults (parameters in order: Duration, Intensity, Frequency, sharpness; omit from right)
VHSweep::setDefault(float duration);
VHSweep::setDefault(float duration, float intensity);
VHSweep::setDefault(float duration, float intensity, float frequency);
VHSweep::setDefault(float duration, float intensity, float frequency, float sharpness);
VHSweep::getDefault(float &duration, float &intensity, float &frequency, float &sharpness);
VHSweep::getDefault(float &duration, float &intensity, float &sharpness);

// Duration limits (used by checkDuration<VHSweep>())
VHSweep::setMinDuration(float minDuration);
VHSweep::setMaxDuration(float maxDuration);
VHSweep::setMinMaxDuration(float minDuration, float maxDuration);
VHSweep::getMinDuration() → float;
VHSweep::getMaxDuration() → float;

// Intensity limits
VHSweep::setMinIntensity(float minIntensity);
VHSweep::setMaxIntensity(float maxIntensity);
VHSweep::setMinValues(float minDuration, float minIntensity, float minSharpness);
VHSweep::setMaxValues(float maxDuration, float maxIntensity, float maxSharpness);
VHSweep::getMinValues(float &minDuration, float &minIntensity, float &minSharpness);
VHSweep::getMaxValues(float &maxDuration, float &maxIntensity, float &maxSharpness);

// Sharpness limits
VHSweep::setMinSharpness(float minSharpness);
VHSweep::setMaxSharpness(float maxSharpness);
VHSweep::getMinSharpness() → float;
VHSweep::getMaxSharpness() → float;

// Frequency limits
VHSweep::setMinFreq(float minFreq);
VHSweep::setMaxFreq(float maxFreq);
VHSweep::getMinFreq(float &minFreq);
VHSweep::getMaxFreq(float &maxFreq);

// Default TransitionType (easing) configuration
VHSweep::setDefaultTransitions(TransitionType tIntensity,
                               TransitionType tFrequency,
                               TransitionType tSharpness);
VHSweep::setDefaultIntensityTransition(TransitionType t);
VHSweep::setDefaultFrequencyTransition(TransitionType t);
VHSweep::setDefaultSharpnessTransition(TransitionType t);
VHSweep::getDefaultIntensityTransition() → TransitionType;
VHSweep::getDefaultFrequencyTransition() → TransitionType;
VHSweep::getDefaultSharpnessTransition() → TransitionType;
```

All of these operate on sweep-specific static fields backed by `SWEEP_*` macros in `VHUtilities.h`, independent from the `VIBRATE_*` limits. The default constructor `VHSweep()` uses the configured default transitions for intensity, frequency, and sharpness.

### VH_Pcm

```cpp
VH_Pcm::setSampleRate(int val);        // Static: set PCM playback rate
VH_Pcm::getSampleRate() → int;         // Static: get PCM playback rate
pcm.getFrame(unsigned long timeSinceStart) → uint8_t;  // Get sample at time offset
pcm.getDuration() → unsigned long;     // Total duration in microseconds
pcm.getPcmData() → const uint8_t*;     // Raw data pointer
```

---

## 4. Channel Management

**Headers:** `VHChannels/VHChannels.h`, `VHChannels/VHChannel.h`

### Channel Numbering

- **`channels.add(channelNo, ...)`**: Channel IDs start at **1**. There is no channel 0 when adding; each added channel uses a unique ID (1, 2, 3...).
- **Targeting (play, queue, renderQueue, queueIsEmpty)**: **Channel 0 = all channels**. Use 1, 2, 3... to target a specific channel.

### VHChannels (Channel Collection)

#### Adding Channels

```cpp
VHChannels channels;

// Add channel with tags (channel IDs: 1, 2, 3...)
channels.add(
    channelNo,                       // unsigned char: channel ID (1, 2, 3...)
    {pin1, pin2},                    // vector<unsigned char>: GPIO pins (1 or 2)
    {"tag1", "tag2"},                // initializer_list<string>: channel tags
    {PULSE | TICK | VIBRATE}         // vector<int>: supported primitive bitmask (-1 = all)
);

// Add channel without tags
channels.add(channelNo, {pin1}, {PULSE | TICK | VIBRATE | PCM});
```

#### Channel Selection

Channels can be targeted by:
- **Channel number**: `int channelNo` — 0 = all channels; 1, 2, 3... = specific channel IDs
- **Tag expression**: `const char* tags` — supports `&` (AND) and `|` (OR) operators
- **Channel vector**: `std::vector<unsigned char> chnls`

Tag expression examples:
- `"left"` — channels with tag "left"
- `"left & motor"` — channels with both "left" AND "motor" tags
- `"left | right"` — channels with "left" OR "right" tags

#### DSP Callbacks

```cpp
channels.applyDSP(channelNo, [](uint8_t val) -> uint8_t {
    // Process each output sample
    return processedVal;
});
```

#### Status Callbacks

```cpp
// Single-pin callback
channels.attachCallback(channelNo, CHANNEL_IDLE, [](uint8_t chnl) {
    // Channel went idle
});

channels.attachCallback(channelNo, CHANNEL_WAKE, [](uint8_t chnl) {
    // Channel started playing
});

// Dual-pin callback
channels.attachCallback(channelNo, CHANNEL_IDLE, [](uint8_t chnl, uint8_t pin) {
    // Dual-channel went idle
});
```

#### Idle Handler

```cpp
channels.enableIdleHandler(channelNo, true);   // Enable idle/wake detection (default)
channels.enableIdleHandler(channelNo, false);  // Disable for channel; use 0 for all channels
```

**Important:** Must be called **before** `vh.init()`. Only the pre-init state affects whether the idle handler task and polling are created. Calling after init has no effect.

#### PWM Frequency

```cpp
channels.changePwmFrequency(channelNo, 30000);  // Change PWM freq (default: 30kHz)
```

#### Playback Status

```cpp
VHPlaybackState state = channels.playbackStatus(channelNo);
// VHPlaybackState::VH_IDLE, VHPlaybackState::VH_PLAYING, VHPlaybackState::VH_DISABLED, VHPlaybackState::VH_UNKNOWN
```

### VHChannel (Individual Channel)

Per-channel configuration:

```cpp
channel.setMixingStrategy(MixingStrategy::ADDITIVE_MIX);
channel.setChnlMinMaxIntensity(0, 255);
channel.setSampleRate(8000);
channel.setI2sSndBusPtr(&i2sBus);  // Route output via I2S
```

---

## 5. Board Abstraction (HAL)

**Header:** `BoardProfile.h`

### BoardProfile (Abstract Base)

Downstream board-specific repos must implement this class.

#### Required Pure Virtual Methods

```cpp
// Initialization
void init(unsigned char pin) = 0;
void init(unsigned char pin1, unsigned char pin2, unsigned char ctrlPin) = 0;

// GPIO / PWM
void setOutput(unsigned char pin) = 0;
void write(unsigned char val) = 0;
void writeSinglePin(unsigned char pin, unsigned char value) = 0;
void writeDualPin(unsigned char pin1, unsigned char pin2, unsigned char val1, unsigned char val2) = 0;

// Timing
void delay(unsigned int ms) = 0;
void delayMicroseconds(unsigned int us) = 0;
void yield() = 0;  // Yield to other tasks (e.g. vTaskDelay(0) on FreeRTOS)
unsigned long getTimeMicroseconds() = 0;
unsigned long millis() = 0;

// RTOS Primitives
IBoardMutex* createMutexHandle() = 0;
IBoardQueue* createQueueHandle() = 0;
ITaskManager* createTask(TaskConfig* config) = 0;  // Single overload: use TaskConfig for all task creation
IBoardTimer* createTimerEvents(uint8_t timer, uint32_t freq, bool countup) = 0;

// NVS / Persistent Storage
void save(const char* key, const char* value) = 0;
std::string read(const char* key, const char* defaultVal) = 0;

// System
void restart() = 0;
```

#### Optional Virtual Methods (with defaults)

```cpp
virtual IBoardI2sMan* getI2sMan();                      // Returns mBoardI2sMan
virtual bool sendDataDMA(uint8_t* data, size_t len);    // Returns false by default
virtual void sendI2S(int channel, uint8_t* data, size_t len);  // No-op by default
virtual void changePwmFrequency(const unsigned char pin, const unsigned long freq);  // No-op
```

### BoardBasic (Minimal HAL)

For single-channel basic mode without RTOS.

```cpp
// Required methods
void init() = 0;
void setOutput(unsigned char pin) = 0;
void delay(unsigned int ms) = 0;
void write(unsigned char val) = 0;
unsigned long micros() = 0;
unsigned long millis() = 0;
```

### IBoardMutex

```cpp
void* createMutex() = 0;
bool take(uint32_t xBlockTime = 0) = 0;
void give() = 0;
```

### IBoardQueue

```cpp
void createQueue(const unsigned int queueSize, const unsigned int size) = 0;
int receive(void* pvBuffer, unsigned int xTicksToWait) = 0;
int send(const void* pvItemToQueue, unsigned int xTicksToWait) = 0;
int receiveISR(void* pvBuffer, int* pxTaskWoken) = 0;
int sendISR(const void* pvItemToQueue, int* pxTaskWoken) = 0;
int hasItems() = 0;
```

### IBoardTimer

```cpp
void setCallback(TimerCb cb, void* param = nullptr) = 0;
void startTimer() = 0;
void stopTimer() = 0;
```

### IBoardI2sMan

```cpp
void begin(I2sInfo& info) = 0;
void stop() = 0;
void start() = 0;
void write(void* data, size_t size) = 0;
void write(uint8_t val) = 0;
bool IsRunning() = 0;
```

### ITaskManager

```cpp
int createTask(TaskConfig* config) = 0;
void deleteTask() = 0;
```

### TaskConfig

```cpp
struct TaskConfig {
    const char* name = nullptr;
    int stackSize = 0;
    void* param = nullptr;
    int priority = 0;
    int8_t coreId = -1;
    void (*func)(void*) = nullptr;
};
```

### I2sInfo

```cpp
struct I2sInfo {
    int mck_io_num = -1;        // Master clock
    int bck_io_num = -1;        // Bit clock
    int ws_io_num = -1;         // Word select
    int data_out_num = -1;      // Data out
    int data_in_num = -1;       // Data in
    uint32_t samplerate = 8000;
    uint8_t bitpersample = 16;
};
```

---

## 6. Audio Streaming

**Header:** `VHAudioStreaming.h`

### AudioStreaming

#### Construction

```cpp
AudioStreaming stream(
    uint32_t sampleRate = 44100,    // Input sample rate
    uint16_t channelCount = 2,      // Input channel count (mono/stereo)
    uint8_t bitsPerSample = 16      // Input bit depth
);
```

#### Buffer Initialization

Single-buffer mode (all audio channels share one buffer):
```cpp
VH::RingBuffer<int16_t> buffer(bufferArray, bufferSize);
stream.init(&buffer);
```

Multi-buffer mode (separate buffer per audio channel):
```cpp
VH::RingBuffer<int16_t> buf0(buf0Storage, buf0Size);  // one RingBuffer per channel
VH::RingBuffer<int16_t> buf1(buf1Storage, buf1Size);
VH::RingBuffer<int16_t>* buffers[] = {&buf0, &buf1};
stream.init(buffers, 2);
```

Supported buffer element types: `int8_t`, `uint8_t`, `int16_t`, `uint16_t`, `int32_t`, `uint32_t`

#### Writing Audio Data

```cpp
stream.write(const int16_t* samples, size_t sampleCount);  // 16-bit input
stream.write(const uint8_t* samples, size_t sampleCount);   // 8-bit input
```

#### Playback Control

```cpp
// Start streaming to channels
stream.play(
    float intensity,                 // [0.0, 1.0]
    std::vector<uint8_t>& chnls,    // Target haptic channels
    float durationMs = -1.0f,        // Duration (-1 = infinite)
    uint8_t audioChannel = 0         // Audio channel index
);
```

#### DSP Processing

```cpp
stream.setAudioDSP([](uint8_t val) -> uint8_t {
    // Process each 8-bit sample before buffering
    return processedVal;
});
```

#### Mixing

```cpp
stream.enableMixing(channelNo, MixingStrategy::ADDITIVE_MIX);
```

#### Configuration

```cpp
stream.setMaxOutSampleRate(22050);  // Set maximum output sample rate
stream.setChannelPtr(&channels);     // Set channel manager reference
```

### VH::RingBuffer\<T\>

Lock-free SPSC ring buffer.

```cpp
T bufferArray[1024];
VH::RingBuffer<T> ring(bufferArray, 1024);

ring.push(item);          // Returns true on success
ring.pop(item);           // Returns true on success
ring.isEmpty();
ring.isFull();
ring.reset();
ring.size();              // Current element count
ring.capacity();          // Maximum elements (size - 1)

// DMA support
T* ptr = ring.dmaReadPtr(outLen);   // Contiguous read region
ring.dmaReadAdvance(count);
T* ptr = ring.dmaWritePtr(outLen);  // Contiguous write region
ring.dmaWriteAdvance(count);
```

---

## 7. DevTools

**Header:** `VHDevTools.h`

### VHDevTools

```cpp
VHDevTools devTools;
// Pass as extension module during init:
vh.init(board, {&devTools, ...});

// Parse commands (typically from serial input):
devTools.parseCommandList("PULSE 100 0.8 0.5");
devTools.parseCommandList("CHNL 1; VIBRATE 200 0.7 160 0.3");

// Or via VectorHaptics API:
vh.executeStringCommand("PULSE 100; PAUSE 50; TICK 50");
```

See [MODULES.md § VHDevTools](MODULES.md#4-vhdevtools) for the full command reference.

---

## 8. EffectReceiver

**Header:** `VHEffectReceiver.h`

### VHEffectReceiver

```cpp
VHEffectReceiver receiver;
// Pass as extension module during init:
vh.init(board, {&receiver, ...});

// Feed binary data (from serial, BLE, WiFi, etc.):
receiver.parseData(byteBuffer, length);

// Or via VectorHaptics API:
vh.executeCommand(byteBuffer, length);
```

### 8.1 Binary Command Format

EffectReceiver consumes binary packets with a 5-byte header, optional payload, and (for most PLAY/QUEUE commands) a 1-byte CRC.

#### 8.1.1 Packet Overview

All packets use a **5-byte header** followed by an optional payload.

| Field      | Size  | Description                                      |
|-----------|-------|--------------------------------------------------|
| `sync1`   | 1 B   | Sync byte 1: `0xAA`                              |
| `sync2`   | 1 B   | Sync byte 2: `0x55`                              |
| `cmd`     | 1 B   | Command type (`CONFIG`, `SYSTEM`, `PLAY`, `QUEUE`) |
| `subCmd`  | 1 B   | Sub-command (effect type or query type)          |
| `dataLen` | 1 B   | Number of payload bytes that follow              |

Payload and (optional) CRC follow the header:

- **Payload**: command-specific data (see below).
- **CRC (1 B)**: CRC-8 (polynomial 0x07) checksum for PLAY/QUEUE struct-style commands, PCM data, and custom primitives.

All multibyte fields (e.g., `float`, `uint16_t`) are **little-endian**.

#### 8.1.2 Command Types (`cmd`)

| Value | Name              | CRC | Description                        |
|-------|-------------------|-----|------------------------------------|
| `0`   | `CMD_TYPE_CONFIG` | No  | Configuration commands             |
| `1`   | `CMD_TYPE_SYSTEM` | No  | System queries (HDI)               |
| `2`   | `CMD_TYPE_PLAY`   | Yes | Play effects immediately           |
| `3`   | `CMD_TYPE_QUEUE`  | Yes | Queue effects for later playback   |

For PLAY/QUEUE:

- Struct-style effects (PULSE, TICK, VIBRATE, PAUSE, ERM, SWEEP, AUDIO_STREAM) use a CRC over the payload bytes only.
- PCM and CUSTOM_PRIM define their CRC scope separately (see below).

#### 8.1.3 CONFIG Sub-Commands (`cmd = 0`)

| `subCmd` | Name                 | Payload             | Description                                                       |
|----------|----------------------|---------------------|-------------------------------------------------------------------|
| `1`      | `VH_SETDEFAULT`      | 6 B (see below)     | Set default parameter value for a primitive                        |
| `2`      | `VH_SETMIN`          | 6 B (same as below) | Set minimum value for a primitive parameter                        |
| `3`      | `VH_SETMAX`          | 6 B (same as below) | Set maximum value for a primitive parameter                        |
| `4`      | `VH_SETPCMSAMPLERATE`| 4 B (see below)     | Set PCM playback sample rate (calls VH_Pcm::setSampleRate)         |
| `5`      | `VH_SETDEFMINMAXDUR` | 13 B (see below)    | Set custom primitive duration default/min/max (CustomPrim::setDefMinMaxDuration) |
| `6`      | `VH_SETDEFMINMAXPARAM` | 14 B (see below)  | Set custom primitive parameter default/min/max (CustomPrim::setDefMinMaxParameter) |
| `10`     | `VH_SETDEVICENAME`   | 1–32 B (string)    | Set device name (same as setDeviceInfo DeviceName)                                |
| `11`     | `VH_SETMANUFACTURER` | 1–32 B (string)    | Set manufacturer (same as setDeviceInfo Manufacture)                              |
| `12`     | `VH_SETMODELNUM`     | 1–32 B (string)    | Set model number (same as setDeviceInfo ModelNo)                                  |
| `13`     | `VH_SETSERIALNUM`    | 1–32 B (string)    | Set serial number (same as setDeviceInfo SerialNo)                                |
| `14`     | `VH_SETVERSIONNUM`   | 1–32 B (string)    | Set version (same as setDeviceInfo VersionNo)                                     |

**`VH_SETDEFAULT` / `VH_SETMIN` / `VH_SETMAX` payload (6 bytes, little-endian):**

| Offset | Size | Type  | Field        | Description                                                                 |
|--------|------|-------|--------------|-----------------------------------------------------------------------------|
| 0      | 1    | uint8 | primitiveId  | Primitive: 0=PULSE, 1=TICK, 2=PAUSE, 3=VIBRATE, 4=PCM, 5=ERM, 6=SWEEP       |
| 1      | 1    | uint8 | paramIndex   | Parameter index (primitive-specific, see table below)                       |
| 2      | 4    | float | value        | Value (float). For VH_SETDEFAULT + SWEEP 7–9: TransitionType id. PCM and SWEEP 7–9 not supported for VH_SETMIN/VH_SETMAX. |

**`paramIndex` by primitive:**

| Primitive | paramIndex | Parameter        |
|-----------|------------|------------------|
| PULSE, TICK | 0        | duration         |
|           | 1        | intensity        |
|           | 2        | sharpness        |
| PAUSE     | 0        | duration         |
| VIBRATE   | 0        | duration         |
|           | 1        | intensity        |
|           | 2        | frequency        |
|           | 3        | sharpness        |
| PCM       | 0        | sampling_rate    |
| ERM       | 0        | duration         |
|           | 1        | intensity        |
| SWEEP     | 0        | duration         |
|           | 1, 4     | intensity        |
|           | 2, 5     | frequency        |
|           | 3, 6     | sharpness        |
|           | 7        | transIntensity (TransitionType id) |
|           | 8        | transFrequency (TransitionType id) |
|           | 9        | transSharpness (TransitionType id) |

**`VH_SETPCMSAMPLERATE` payload (4 bytes, little-endian):**

| Offset | Size | Type   | Field        | Description                    |
|--------|------|--------|--------------|--------------------------------|
| 0      | 4    | uint32 | sampleRateHz | PCM sample rate in Hz (e.g. 8000, 16000) |

**`VH_SETDEFMINMAXDUR` payload (13 bytes, little-endian):**

| Offset | Size | Type  | Field   | Description                                 |
|--------|------|-------|---------|---------------------------------------------|
| 0      | 1    | uint8 | index   | Custom primitive index (same as VH_CUSTOM_PRIM) |
| 1      | 4    | float | default | Default duration value                      |
| 5      | 4    | float | min     | Minimum duration                            |
| 9      | 4    | float | max     | Maximum duration                            |

**`VH_SETDEFMINMAXPARAM` payload (14 bytes, little-endian):**

| Offset | Size | Type  | Field       | Description                                 |
|--------|------|-------|-------------|---------------------------------------------|
| 0      | 1    | uint8 | index       | Custom primitive index (same as VH_CUSTOM_PRIM) |
| 1      | 1    | uint8 | paramIndex  | Parameter index (matches HDI_D device info)  |
| 2      | 4    | float | default     | Default parameter value                     |
| 6      | 4    | float | min         | Minimum value                               |
| 10     | 4    | float | max         | Maximum value                               |

**`VH_SETDEVICENAME` / `VH_SETMANUFACTURER` / `VH_SETMODELNUM` / `VH_SETSERIALNUM` / `VH_SETVERSIONNUM` payload (1–32 bytes):**

Variable-length string; each byte is a character (e.g. ASCII/latin-1). `dataLength` in the header equals the number of bytes. Null terminator is optional; if present, only bytes up to the first null are used. These map to the five arguments of `setDeviceInfo(DeviceName, Manufacture, ModelNo, SerialNo, VersionNo)`.

#### 8.1.4 SYSTEM Sub-Commands (`cmd = 1`)

| `subCmd` | Name       | Payload         | Description                                                       |
|----------|------------|-----------------|-------------------------------------------------------------------|
| `1`      | `VH_HDI`   | 0 or 1 B        | Channel info: no payload = all channels; 1 B = channel number    |
| `2`      | `VH_HDI_D` | none            | **Device info**, including **custom primitives**                  |
| `3`      | `VH_HDI_C` | none            | All channels info                                                 |

HDI commands respond via the log/message system (`LOG_TYPE::RESPONSE_MSG`) rather than a binary response packet.

#### 8.1.5 PLAY/QUEUE Sub-Commands (`cmd = 2` or `3`)

| `subCmd` | Name              | Payload summary                                         | CRC scope                        |
|----------|-------------------|---------------------------------------------------------|----------------------------------|
| `0`      | `VH_PULSE`        | `PayLoadPulse`                                         | struct payload bytes only        |
| `1`      | `VH_TICK`         | `PayLoadTick` (= `PayLoadPulse`)                       | struct payload bytes only        |
| `2`      | `VH_VIBRATE`      | `PayLoadVibr`                                          | struct payload bytes only        |
| `3`      | `VH_PAUSE`        | `PayLoadPause`                                         | struct payload bytes only        |
| `4`      | `VH_PCM`          | channel byte + raw PCM data bytes + CRC                | channel + PCM bytes              |
| `5`      | `VH_ERM`          | `PayLoadErm`                                           | struct payload bytes only        |
| `6`      | `VH_SWEEP`        | `PayLoadSweep`                                         | struct payload bytes only        |
| `7`      | `VH_AUDIO_STREAM` | `PayLoadAudioStream`                                   | struct payload bytes only        |
| `8`      | `VH_CUSTOM_PRIM`  | channel byte + index byte + float payload + CRC       | channel + index + float payload  |

PLAY and QUEUE share the same `subCmd` values and payload layouts; only `cmd` differs.

#### 8.1.6 CRC Algorithm

All CRCs use CRC-8 with polynomial 0x07 (x^8 + x^2 + x + 1), initial value 0x00:

```text
crc = 0x00;
for each byte b in scope:
    crc ^= b;
    for bit = 0 to 7:
        if (crc & 0x80) then crc = (crc << 1) ^ 0x07;
        else crc = crc << 1;
```

Scope depends on command type (see table above).

### 8.2 Payload Layouts

Struct layouts mirror the packed payload structs in `datastructure.h`.

#### 8.2.1 `PayLoadPulse` / `PayLoadTick`

```text
offset  size  type    field       description
0       1     uint8   channel     Target channel
1       4     float   duration    Duration in milliseconds
5       1     uint8   intensity   0–255 (divide by 255 for 0.0–1.0)
6       1     uint8   sharpness   0–255 (divide by 255 for 0.0–1.0)
7       1     uint8   crc         CRC-8 over bytes 0–6
```

#### 8.2.2 `PayLoadVibr`

```text
offset  size  type    field       description
0       1     uint8   channel     Target channel
1       4     float   duration    Duration in ms
5       1     uint8   intensity   0–255
6       2     uint16  frequency   Frequency in Hz
8       1     uint8   sharpness   0–255
9       1     uint8   crc         CRC-8 over bytes 0–8
```

#### 8.2.3 `PayLoadPause`

```text
offset  size  type    field       description
0       1     uint8   channel     Target channel
1       4     float   duration    Duration in ms
5       1     uint8   crc         CRC-8 over bytes 0–4
```

#### 8.2.4 `PayLoadErm`

```text
offset  size  type    field       description
0       1     uint8   channel     Target channel
1       4     float   duration    Duration in ms
5       1     uint8   intensity   0–255 magnitude (divide by 255 for 0.0–1.0)
6       1     uint8   direction   0 = negative intensity, non-zero = positive
7       1     uint8   crc         CRC-8 over bytes 0–6
```

#### 8.2.5 `PayLoadSweep`

```text
offset  size  type    field            description
0       1     uint8   channel          Target channel
1       4     float   duration         Duration in ms
5       1     uint8   startIntensity   0–255
6       2     uint16  startFrequency   Start frequency (Hz)
8       1     uint8   startSharpness   0–255
9       1     uint8   endIntensity     0–255
10      2     uint16  endFrequency     End frequency (Hz)
12      1     uint8   endSharpness     0–255
13      1     uint8   transIntensity   `TransitionType` enum (see `startendparam.h`)
14      1     uint8   transFrequency   `TransitionType` enum
15      1     uint8   transSharpness   `TransitionType` enum
16      1     uint8   crc              CRC-8 over bytes 0–15
```

#### 8.2.6 `PayLoadAudioStream`

```text
offset  size  type    field        description
0       1     uint8   channel      Target channel
1       4     float   duration     ms (`-1` = infinite)
5       1     uint8   intensity    0–255
6       1     uint8   audioChannel 0 = mix all, 1 = left, 2 = right, etc.
7       1     uint8   crc          CRC-8 over bytes 0–6
```

#### 8.2.7 PCM (variable-length)

```text
[uint8 channel] [uint8 pcm_data...] [uint8 crc]
```

- `dataLen` = `1 + len(pcm_data) + 1`.
- CRC is CRC-8 over `channel` + `pcm_data` bytes.

#### 8.2.8 Custom Primitive (variable-length)

```text
[uint8 channel] [uint8 index] [float duration] [float param0] [float param1] ... [uint8 crc]
```

- `index` is the custom primitive index (0-based).
- Float payload = `duration` followed by one float per primitive parameter.
- CRC is CRC-8 over `channel` + `index` + the float bytes (duration + parameters).
- Total payload size must match `getCustomPrimPayloadSize(index)` on the device.

### 8.3 Custom Primitives, Index, and Parameter Order

Custom primitive indices and parameter lists are **device-defined**. To send `VH_CUSTOM_PRIM` packets correctly, query the device first via `HDI D`.

When you call `HDI D` (either as a text command via DevTools or as a binary `VH_HDI_D` command), the device responds with a \"Custom Primitives\" section in `getDeviceInfo()` like:

```text
 Custom Primitives :
    [0] MyPrim => param1 : 0.50, param2 : 1.00
    [1] AnotherPrim => freq : 100.00, intensity : 0.80
```

- The **index** used in binary `VH_CUSTOM_PRIM` packets is the bracketed value (`[0]`, `[1]`, …).
- The **parameter order** is the textual order after the arrow (`param1`, `param2`, or `freq`, `intensity`, etc.).

To build a `VH_CUSTOM_PRIM` payload:

1. Query `HDI D` and parse the custom primitive list.
2. For a given primitive index, build a float array: `[duration, param0, param1, ...]` in that exact order.
3. Compute the CRC over those float bytes.
4. Send header + `channel` + `index` + float bytes + CRC.

### 8.4 Resync Behavior

If EffectReceiver sees invalid sync bytes (`sync1 != 0xAA` or `sync2 != 0x55`), it shifts by one byte and retries until a valid header is found. This allows the parser to recover from dropped or extra bytes in the stream.

See also:

- `datastructure.h` (`Header`, `PayLoad*` structs, `CRC<T>` template)
- `VHEffectReceiver.cpp` (`processSystemCmd`, `processPlayCmd`, `processQueueCmd`)

---

## 9. I2S Channel

**Header:** `VHChannelsI2s.h`

### VHChannelsI2s

```cpp
VHChannelsI2s i2s;
// Pass as extension module during init:
vh.init(board, {&i2s, ...});

i2s.begin(bclkPin, lrcPin, dinPin, 8000, 16);
i2s.start();
i2s.write(sampleByte);
bool running = i2s.IsRunning();
i2s.stop();
```

---

## 10. Custom Primitives

**Header:** `VHPrimitives/VHPrimitives.h`

### Registering Custom Primitives

```cpp
VHPrimitives prims;
vh.init(board, {&prims, ...});

// Register a custom primitive with a callback
prims.registerCustom("MyEffect", [](VHPrimitiveParams* param, float startTime) -> unsigned char {
    float intensity = param->getParameter("intensity");
    float duration = param->getDur();
    // Compute and return waveform sample value [0, 255]
    return computedValue;
});
```

### Custom Primitive Callback Signature

```cpp
typedef unsigned char (*CustomPrimCb)(VHPrimitiveParams* param, float startTime);
```

| Parameter | Description |
|---|---|
| `param` | Pointer to primitive parameters (intensity, duration, custom params) |
| `startTime` | Elapsed time since primitive started (in microseconds) |
| **Returns** | `uint8_t` waveform sample value [0, 255] |

### VHPrimitiveParams

```cpp
VHPrimitiveParams params({{"intensity", 0.8}, {"frequency", 160.0}});

params.setParameter("intensity", 0.8);
params.setDefMinMaxParameter("frequency", 160.0, 20.0, 500.0);  // default, min, max
params.setDefMinMaxDuration(100.0, 10.0, 10000.0);              // default, min, max

float val = params.getParameter("intensity");
float dur = params.getDur();
params.setDur(500.0);
```

### Configuring Built-in Primitives

```cpp
VHPrimitives prims;

// Set defaults
prims.setDefault(VHPrimitives::PULSE, VHPrimitives::INTENSITY, 0.8);
prims.setDefault(VHPrimitives::VIBRATE, VHPrimitives::FREQUENCY, 200.0);
// Note: for VHSweep, setDefault only affects duration/intensity/frequency/sharpness.
// Default TransitionType values for sweeps are configured via VHSweep::setDefaultTransitions()
// (or the per-axis setters), not via the primitive manager API.

// Set min/max
prims.setMin(VHPrimitives::PULSE, VHPrimitives::DURATION, 10.0);
prims.setMax(VHPrimitives::PULSE, VHPrimitives::DURATION, 5000.0);
```

---

## 11. Utilities & Types

**Header:** `Utilities/VHUtilities.h`

### Enums

#### MixingStrategy
```cpp
enum MixingStrategy {
    NO_MIX = -1,          // No mixing
    INTERRUPT_MIX = 0,    // Primitive overrides PCM
    LARGESTVAL_MIX = 1,   // max(primitive, pcm)
    ADDITIVE_MIX = 2,     // Signed addition, clamped
    CUSTOM_MIX = 3        // PCM overrides primitive
};
```

#### QueueOperType
```cpp
enum QueueOperType {
    APPEND_EFFECT,   // Add to end of queue
    REPLACE_EFFECT   // Clear queue and add
};
```

#### VHPlaybackState
```cpp
enum VHPlaybackState {
    VH_IDLE,
    VH_PLAYING,
    VH_DISABLED,
    VH_UNKNOWN
};
```

#### TransitionType
```cpp
enum TransitionType {
    Linear = 0,
    EaseInSine, EaseOutSine, EaseInOutSine,
    EaseInQuad, EaseOutQuad, EaseInOutQuad,
    // ... (30 easing types total)
    EaseInBounce, EaseOutBounce, EaseInOutBounce
};
```

#### LOG_TYPE
```cpp
enum LOG_TYPE { ERROR_MSG, INFO_MSG, PCM_CHUNK_REQ, WARNING_MSG, RESPONSE_MSG };
```

#### Verbosity Flags
```cpp
#define SILENT       0
#define ERROR_ONLY   (1 << 0)
#define INFO_ONLY    (1 << 1)
#define WARNING_ONLY (1 << 2)
#define VERBOSE      (ERROR_ONLY | INFO_ONLY | WARNING_ONLY)
```

#### Primitive Type Bitmask
```cpp
#define PULSE        (1 << 0)
#define TICK         (1 << 1)
#define VIBRATE      (1 << 2)
#define PAUSE        (1 << 3)
#define PCM          (1 << 4)
#define AUDIO_STREAM (1 << 5)
#define ERM          (1 << 6)
#define SWEEP        (1 << 7)
#define CUSTOM       (1 << 8)
```

### Utility Functions (VH namespace)

```cpp
VH::to_string(float value, int decimalPoints) → std::string
VH::to_string(float value) → std::string
VH::to_string(int value) → std::string
VH::trim(std::string& s)
VH::ltrim(std::string& s)
VH::rtrim(std::string& s)
VH::to_Int(std::string& s) → int
VH::to_Float(std::string& s) → float
VH::isInteger(const std::string& str) → bool
VH::toLower(std::string s) → std::string
VH::equalsIgnoreCase(std::string s1, std::string s2) → bool
VH::map(long x, long in_min, long in_max, long out_min, long out_max) → long
```

### DataBufferManager

```cpp
DataBufferManager buf;
buf.write(data, length);          // Append bytes
int result = buf.read(data, len); // Read and consume (0=success, 1=insufficient)
buf.clear();
size_t sz = buf.getSize();
```

### CRC Utility

```cpp
uint8_t crc = CRC<PayLoadPulse>::calculateCRC(data, length);
bool valid = CRC<PayLoadPulse>::parse(byteArray, size, &payloadStruct);
```

### Macros

```cpp
#define vhconstrain(amt, low, high) ((amt) < (low) ? (low) : ((amt) > (high) ? (high) : (amt)))
#define FRQ_TO_DUR(freq) (100000.0f / (freq))
#define UNUSED(x) (void)(x)
```
