API tutorial: #2 Time Point Streaming example code

How Time Point Streaming works

Time Point Streaming is our high frequency emitter mode. Control Point position and intensity values stream to the array at up to 40000 times a second.

In Amplitude Modulation, the application pushes updates by calling the update function.

The Ultrahaptics::TimePointStreaming::Emitter uses a callback mechanism:

  • Your application registers a callback function.
  • At regular intervals the Ultrahaptics SDK driver requests input by calling this registered function.

The result is a continuous stream of haptic output to the device.

How it works

It is up to you to write the callback implementation and define the Control Point behaviour. For this reason, Time Point Streaming is more complex than Amplitude Modulation. But it is also much more powerful and flexible. You have direct control of each Control Point’s waveform. This means you must apply modulation. Either modulate its intensity – amplitude modulation (small ‘a’, small ‘m’) – or position: Spatiotemporal Modulation.

Time Point Streaming walkthrough

  1. Include the UltrahapticsTimePointStreaming.hpp header file,
  2. Create a Time Point Streaming emitter Ultrahaptics::TimePointStreaming::Emitter,
  3. Set the maximum control point count using setMaximumControlPointCount. This is the number of control points your application can create,
  4. Write your emission callback function,
  5. Register the callback function with the emitter using setEmissionCallback,
  6. Start the emitter,
  7. The callback runs approximately every 1 millisecond,
  8. The callback provides an OutputInterval parameter. This represents a time window and a vector of time points. Iterate through the time points to set each Control Point’s position and intensity. The callback also provides an anonymous pointer to access any persistent context data. Use getSamplingInterval to get the duration of each time point.
  9. To stop the array either
    (a) call the emitter’s stop function,
    (b) disable individual Control Points or
    (c) set Control Point intensity to 0.

Note: the term sample is often use to refer to a time point instance. The OutputInterval is dependent on the array’s sample rate, number of control points and USB latency. This may vary between calls and your waveform can span many output intervals.

Time Point Streaming Example: TimePointStreaming_Circle.cpp

Let’s look at how the Time Point Streaming callback mechanism updates control points by using an example. Here, we will move a single control point, with constant intensity, around a circle 20 cm above the array. You can find the TimePointStreaming_Circle.cpp file in the Ultrahaptics SDK’s Examples folder.

TimePointStreaming_Circle.cpp uses Spatiotemporal Modulation to create sensation. See TimePointStreaming_Focus.cpp for an amplitude modulation example.

Add the Time Point Streaming API by including the UltrahapticsTimePointStreaming.hpp header:

#include <cmath>
#include <iostream>
#include <chrono>
#include <thread>
#include <string>

#include "UltrahapticsTimePointStreaming.hpp"

 

Define a structure to hold our context data. Here, we store the Control Point’s coordinates and intensity values between each callback. This struct also has a function to calculate the next Control Point position for a given radius and a circular frequency.

#ifndef M_PI
#define M_PI 3.14159265358979323
#endif

using Seconds = std::chrono::duration;<float>;

static auto start_time = std::chrono::steady_clock::now();

// Structure for passing information on the type of point to create
struct Circle
{
    // The position of the control point
    Ultrahaptics::Vector3 position;

    // The intensity of the control point
    float intensity;

    // The radius of the circle
    float radius;

    // The frequency at which the control point goes around the circle
    float frequency;


    const Ultrahaptics::Vector3 evaluateAt(Seconds t){
        // Calculate the x and y positions of the circle and set the height
        position.x = std::cos(2 * M_PI * frequency * t.count()) * radius;
        position.y = std::sin(2 * M_PI * frequency * t.count()) * radius;
	return position; 
    }
};

 

Here we define our callback function. This must match the format of TimePointEmissionCallback
as defined in the API. In our implementation we dereference our Circle structure and iterate through each time point, using its time stamp to calculate the new position.

Each time point (sample) references each Control Point using persistentControlPoint. Use the index of the Control Point we wish to set:

void my_emitter_callback(const Ultrahaptics::TimePointStreaming::Emitter &timepoint_emitter,
                         Ultrahaptics::TimePointStreaming::OutputInterval &interval,
                         const Ultrahaptics::HostTimePoint &submission_deadline,
                         void *user_pointer)
{
    // Cast the user pointer to the struct that describes the control point behaviour
    Circle *circle = static_cast<Circle*>(user_pointer);

    // Loop through the samples in this interval
    for (auto& sample : interval)
    {
        const Seconds t = sample - start_time;
        const Ultrahaptics::Vector3 position = circle->evaluateAt(t);

        // Set the position and intensity of the persistent control 
        // point to that of the modulated wave at this point in time.
        sample.persistentControlPoint(0).setPosition(position);
        sample.persistentControlPoint(0).setIntensity(circle->intensity);
    }
}

 

In the main function, we define our TimePointStreaming emitter, setting the maximum number of control points. We instantiate the Circle, defining its size and intensity and then register our callback and start the emitter. The emitter only stops when the Enter key is pressed.

int main(int argc, char *argv[])
{
    // Create a time point streaming emitter
    Ultrahaptics::TimePointStreaming::Emitter emitter;

    // Set the maximum control point count
    emitter.setMaximumControlPointCount(1);

    // Create a structure containing our control point data and fill it in
    Circle circle;

    // Set control point 20cm above the centre of the array at the radius
    circle.position = Ultrahaptics::Vector3(0.0f, 0.0f, 20.0f*Ultrahaptics::Units::centimetres);
    // Set the amplitude of the modulation of the wave to one (full modulation depth)
    circle.intensity = 1.0f;
    // Set the radius of the circle that the point is traversing
    circle.radius = 2.0f * Ultrahaptics::Units::centimetres;
    // Set how many times the point traverses the circle every second
    circle.frequency = 100.0f;

    // Set the callback function to the callback written above
    emitter.setEmissionCallback(my_emitter_callback, &circle);

    // Start the array
    emitter.start();

    // Wait for enter key to be pressed.
    std::cout << "Hit ENTER to quit..." << std::endl;
    std::string line;
    std::getline(std::cin, line);

    // Stop the array
    emitter.stop();

    return 0;
}

Units in the Ultrahaptics SDK

Note that in our main function, we create an Ultrahaptics::Vector3 for the initial position of the Control Point 20 cm along the z-axis. The Ultrahaptics API uses metres in any reference to coordinate space. The API provides convenience constants under the Ultrahaptics::Units namespace to convert between metric units:

const double mm = 0.001
const double millimetres = 0.001
const double cm = 0.01
const double centimetres = 0.01
const double m = 1.
const double metres = 1.
Have more questions? Submit a request

0 Comments

Article is closed for comments.