# 9. Primitive Manager

VHPrimitives is the primitive manager. It lets you change default values and min/max limits for built-in primitives (PULSE, TICK, VIBRATE, etc.), and register **custom primitives** with user-defined waveform callbacks. Use the primitive manager when you want to tweak effect defaults, clamp parameters for safety, or create entirely new haptic effects that the built-in primitives do not provide.

---

## 9.1 Setup

Instantiate `VHPrimitives` and pass it to `vh.init()` along with the channels:

```cpp
VHPrimitives primitives;

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

Without the primitive manager, you can still use built-in primitives with their default values. The primitive manager gives you control over defaults and limits, and enables custom primitives.

---

## 9.2 Changing Default / Min / Max Parameters

Use static methods on effect classes to set defaults and limits. Defaults are used when you call macros without arguments (e.g. `PULSE()` instead of `PULSE(50, 0.8, 0.5)`). Limits clamp any value you provide so it stays within range.

### PULSE and TICK

```cpp
VHPulse::setDefault(50.0f, 0.8f, 0.5f);  // duration (ms), intensity, sharpness
VHPulse::setMinMaxIntensity(0.1, 1.0);  // Clamp intensity to 10%–100%
VHPulse::setMinMaxDuration(10.0f, 200.0f);  // Clamp duration to 10–200 ms
```

If you later call `vh.play(PULSE())`, it uses duration=50.0, intensity=0.8, sharpness=0.5. If you call `vh.play(PULSE(5000.0f, 2.0f, 0.5f))`, duration is clamped to 200.0 ms and intensity to 1.0.

### VIBRATE

```cpp
VHVibrate::setDefault(300, 0.5f, 160.0f, 0.2f);  // duration (ms), intensity, frequency (Hz), sharpness
VHVibrate::setMinFreq(50.0);    // Minimum allowed frequency
VHVibrate::setMaxFreq(500.0);   // Maximum allowed frequency
```

### PAUSE

```cpp
VHPause::setDefault(500.0f);              // Default pause duration in ms
VHPause::setMinMaxDuration(10.0f, 5000.0f); // Clamp pause to 10–5000 ms
```

### ERM

```cpp
VHErm::setDefault(1000.0f, 0.8f);        // duration (ms), intensity
VHErm::setMinMaxIntensity(-1.0, 1.0);  // -1 to 1 (negative = reverse on dual-pin)
```

### Using the primitive manager API

You can also use the generic primitive manager API with type enums instead of effect-specific classes:

```cpp
primitives.setDefault(VHPrimitives::PrimitiveType::VIBRATE, VHPrimitives::ParamType::FREQUENCY, 300.0f);
primitives.setMin(VHPrimitives::PrimitiveType::TICK, VHPrimitives::ParamType::INTENSITY, 0.2f);
primitives.setMax(VHPrimitives::PrimitiveType::TICK, VHPrimitives::ParamType::INTENSITY, 0.8f);
```

This is useful when you need to configure primitives by type at runtime.

### Applying defaults

After setting defaults, use macros without arguments to apply them:

```cpp
vh.play(PULSE());     // Uses configured defaults (e.g. 50 ms, 0.8, 0.5)
vh.play(VIBRATE());   // Uses configured defaults (e.g. 160 Hz)
```

Parameters you explicitly pass are still clamped to min/max.

---

## 9.3 Custom Primitives

Custom primitives let you define entirely new haptic effects by writing a **waveform callback**. The engine calls your callback repeatedly at the render rate (typically thousands of times per second). Each call, you return one 8-bit sample (0–255). The engine assembles those samples into a continuous waveform and drives the actuator. This gives you full control over the shape of the effect—sawtooth, triangle, custom envelopes, or anything you can compute.

### How the callback works

The engine calls your callback once per output sample. Each call receives:

- **params** — A pointer to `VHPrimitiveParams`, which holds the primitive’s parameters (duration, intensity, frequency, or any custom parameters you define). You read from it with `params->getParameter("name", default)` or `params->get(ParamType, default)`.
- **startTime** — The **absolute wall-clock time** when the primitive started playing, in **milliseconds**. This is *not* elapsed time. To get elapsed time, you must call `BoardProfile::getInstance()->getTimeMicroseconds()` to get the current time in microseconds, then compute `timeElapsed = currentTime - (startTime * 1000)` (startTime is ms, currentTime is µs, so multiply startTime by 1000 to align units).

You must return an 8-bit unsigned value: **127** = silence (neutral), **0** = minimum deflection one way, **255** = maximum the other. The engine uses your return value directly as the output sample for that moment in time.

### Callback signature

```cpp
unsigned char callback(VHPrimitiveParams* params, float startTime);
```

`params` holds the duration and any user-defined parameters. `startTime` is the absolute start time in milliseconds (when the primitive began). Return a `uint8_t` in the range 0–255.

### Writing a callback: step by step

A typical callback does four things: read parameters, compute elapsed time from the current time and start time, compute the current phase or position on the waveform, and return the corresponding 8-bit value.

**1. Read parameters.** Use `params->getParameter("name", default)` for custom parameters and `params->getDur()` or `params->get(VHPrimitives::ParamType::DURATION, default)` for duration. Duration is always required.

```cpp
float durationMs = param->getDur();
float intensity  = param->getParameter("intensity", 0.8f);
float frequency  = param->getParameter("frequency", 120.0f);
```

**2. Compute elapsed time.** `startTime` is the absolute start time in milliseconds. Get the current time in microseconds and subtract the start time (convert startTime to µs by multiplying by 1000):

```cpp
unsigned long currentTime = BoardProfile::getInstance()->getTimeMicroseconds();
unsigned long timeElapsed = currentTime - (unsigned long)(startTime * 1000.0f);  // µs
```

**3. Convert elapsed time to phase.** Use the elapsed time and frequency to compute where you are in the waveform cycle (0 to 1). For a sawtooth, phase ramps from 0 to 1 and wraps:

```cpp
unsigned long timePeriod = 1000000.0f / frequency;  // µs per cycle
unsigned long cyclePosition = timeElapsed % timePeriod;
float cycleFraction = (float)cyclePosition / (float)timePeriod;  // 0 → 1
```

**4. Combine and convert to 8-bit.** Scale phase by intensity and envelope, then map from [0, 1] to [127, 255]. 127 is silence:

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

### Sawtooth example

Here is a complete sawtooth callback. A sawtooth ramps from 0 to 1 over each cycle, then resets. The key is computing elapsed time from the current time and start time:

```cpp
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);
}
```

`startTime` is the absolute time when the primitive started (ms). `currentTime` is the current wall-clock time (µs). `timeElapsed` is how long the primitive has been playing (µs). `cycleFraction` is the position within the current waveform cycle (0 to 1). `envelope` ramps from 0 to 1 over the duration so the effect fades in. `value01` is the final amplitude; adding 127 and scaling by 128 maps [0, 1] to [127, 255].

### Registering the callback

Register the callback with a name. The name is used when creating instances of the primitive. The return value of registerCustom is an integer that corresponds to the custom primitive ID. It can be used in channels.add to allow that primitive.

```cpp
int SAWTOOTH = primitives.registerCustom("SAWTOOTH", sawtoothCallback);
channels.add(1, {YOUR_OUTPUT_PIN}, {"main"}, {SAWTOOTH});
```

### Optional: constrain duration

You can set min/max duration and parameter ranges for the primitive so users cannot specify unsafe values:

```cpp
auto prim = primitives.getPrimitive("SAWTOOTH");
if (prim) {
    prim->setDefMinMaxDuration(1000.0f, 1.0f, 10000.0f);               // default, min, max in ms
    prim->setDefMinMaxParameter("intensity", 0.8f, 0.2f, 1.0f);        // default, min, max
    prim->setDefMinMaxParameter("frequency", 120.0f, 60.0f, 240.0f);   // default, min, max
}
```

### Creating and playing instances

To play the primitive, you need to create an instance with a duration and any custom parameters. Use `primitives.getPrimitive("name", duration, {{"param", value}, ...})`. A macro makes this easier:

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

vh.play(SAWTOOTH(500, 0.8f, 120.0f));
```

`getPrimitive("SAWTOOTH", 500, ...)` creates a SAWTOOTH effect with 500 ms duration and the given parameters. Those parameters are available in the callback via `getParameter("frequency", ...)` and `getParameter("intensity", ...)`.

---

## 9.4 Complete Custom Primitive Example

```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);
}
```

See [CODE_EXAMPLES.md § Custom Primitives](../CODE_EXAMPLES.md) for more examples, including queuing and frequency sweeps.