Cepton
Cepton

Cepton SDK Guide

SDK

The following is an overview of the functionality provided by the Cepton SDK. We will start at a high-level overview of the architecture, then dive deeper into some of the specific functions provided.

At a glance

The SDK is based around the ideas of parsers and callbacks.

  • Parsers are registered to interpret the incoming data, for example STDV, INFZ, or custom data types.
  • Callbacks are registered to be invoked when certain types of data are received. For example, we might have one callback that is triggered when new point data comes in, and another callback that is triggered when info data is received.

Data Types

Let's start by taking a closer look at the different data being output from the sensors and SDK.

1. CeptonPoint

struct CeptonPoint {
  int16_t x;
  uint16_t y;
  int16_t z;
  uint8_t reflectivity;
  uint8_t relative_timestamp;
  uint8_t channel_id;
  uint8_t flags;
};

Fields description:

  • x: int16_t: the x position component, that is, the horizontal axes. Negative x are to the left, positive x are to the right.

  • y: uint16_t: the y position component, that is, the depth axis. Negative y points out of the screen, positive y points into the screen.

  • z: int16_t: the z position component, that is, the vertical axis. Negative z points down, positive z points up.

  • reflectivity: uint8_t: the reflectivity. For values 0-127, this is measured linearly, i.e. point.reflectivity == 20 is 20%, but beyond that the correlation scales exponentially, up to point.reflectivity == 255 is 5000%. If your processor has a floating point unit, the simplest thing is to copy this lookup table over, and use the point.reflectivity value as the index. The values in this lookup table range from 0 to 50. 50 = reflectance of 5000%.

  • channel_id: uint8_t: identify the lazer that fired this point

  • flags: uint8_t: The bits can be masked with the following enum to get each flag. Some of these flags may be sensor dependent.

// Reflectivity look up table
static const float reflectivity_LUT[256] = {
    0.000f,  0.010f,  0.020f,  0.030f,  0.040f,  0.050f,  0.060f,  0.070f,
    0.080f,  0.090f,  0.100f,  0.110f,  0.120f,  0.130f,  0.140f,  0.150f,
    0.160f,  0.170f,  0.180f,  0.190f,  0.200f,  0.210f,  0.220f,  0.230f,
    0.240f,  0.250f,  0.260f,  0.270f,  0.280f,  0.290f,  0.300f,  0.310f,
    0.320f,  0.330f,  0.340f,  0.350f,  0.360f,  0.370f,  0.380f,  0.390f,
    0.400f,  0.410f,  0.420f,  0.430f,  0.440f,  0.450f,  0.460f,  0.470f,
    0.480f,  0.490f,  0.500f,  0.510f,  0.520f,  0.530f,  0.540f,  0.550f,
    0.560f,  0.570f,  0.580f,  0.590f,  0.600f,  0.610f,  0.620f,  0.630f,
    0.640f,  0.650f,  0.660f,  0.670f,  0.680f,  0.690f,  0.700f,  0.710f,
    0.720f,  0.730f,  0.740f,  0.750f,  0.760f,  0.770f,  0.780f,  0.790f,
    0.800f,  0.810f,  0.820f,  0.830f,  0.840f,  0.850f,  0.860f,  0.870f,
    0.880f,  0.890f,  0.900f,  0.910f,  0.920f,  0.930f,  0.940f,  0.950f,
    0.960f,  0.970f,  0.980f,  0.990f,  1.000f,  1.010f,  1.020f,  1.030f,
    1.040f,  1.050f,  1.060f,  1.070f,  1.080f,  1.090f,  1.100f,  1.110f,
    1.120f,  1.130f,  1.140f,  1.150f,  1.160f,  1.170f,  1.180f,  1.190f,
    1.200f,  1.210f,  1.220f,  1.230f,  1.240f,  1.250f,  1.260f,  1.270f,
    1.307f,  1.345f,  1.384f,  1.424f,  1.466f,  1.509f,  1.553f,  1.598f,
    1.644f,  1.692f,  1.741f,  1.792f,  1.844f,  1.898f,  1.953f,  2.010f,
    2.069f,  2.129f,  2.191f,  2.254f,  2.320f,  2.388f,  2.457f,  2.529f,
    2.602f,  2.678f,  2.756f,  2.836f,  2.919f,  3.004f,  3.091f,  3.181f,
    3.274f,  3.369f,  3.467f,  3.568f,  3.672f,  3.779f,  3.889f,  4.002f,
    4.119f,  4.239f,  4.362f,  4.489f,  4.620f,  4.754f,  4.892f,  5.035f,
    5.181f,  5.332f,  5.488f,  5.647f,  5.812f,  5.981f,  6.155f,  6.334f,
    6.519f,  6.708f,  6.904f,  7.105f,  7.311f,  7.524f,  7.743f,  7.969f,
    8.201f,  8.439f,  8.685f,  8.938f,  9.198f,  9.466f,  9.741f,  10.025f,
    10.317f, 10.617f, 10.926f, 11.244f, 11.572f, 11.909f, 12.255f, 12.612f,
    12.979f, 13.357f, 13.746f, 14.146f, 14.558f, 14.982f, 15.418f, 15.866f,
    16.328f, 16.804f, 17.293f, 17.796f, 18.314f, 18.848f, 19.396f, 19.961f,
    20.542f, 21.140f, 21.755f, 22.389f, 23.040f, 23.711f, 24.401f, 25.112f,
    25.843f, 26.595f, 27.369f, 28.166f, 28.986f, 29.830f, 30.698f, 31.592f,
    32.511f, 33.458f, 34.432f, 35.434f, 36.466f, 37.527f, 38.620f, 39.744f,
    40.901f, 42.092f, 43.317f, 44.578f, 45.876f, 47.211f, 48.586f, 50.000f,
};

CeptonPoint point = ... 
float reflectivity = reflectivity_LUT[point.reflectivity]; 
enum {
  CEPTON_POINT_SATURATED = 1 << 0,
  CEPTON_POINT_BLOOMING = 1 << 1,
  CEPTON_POINT_FRAME_PARITY = 1 << 2,
  CEPTON_POINT_FRAME_BOUNDARY = 1 << 3,
  CEPTON_POINT_SECOND_RETURN = 1 << 4,
  CEPTON_POINT_NO_RETURN = 1 << 5,
  CEPTON_POINT_NOISE = 1 << 6,
  CEPTON_POINT_BLOCKED = 1 << 7,
};

2. CeptonPointEx

struct CeptonPointEx {
  int32_t x;  // Unit is 1/65536 m or ~0.015mm
  int32_t y;
  int32_t z;
  uint16_t reflectivity;        // Unit is 1%, no lookup table
  uint16_t relative_timestamp;  // Unit is 1 us
  uint16_t flags;
  uint16_t channel_id;
};

Parsers

In general, you don't need worry about using parsers for Cepton lidars. The SDK has implemented parsers for all our data types.

APIs to be aware of:

CeptonRegisterParser
CeptonUnregisterParser

Callbacks

Callbacks are the part of the API that you will be using.

APIs to be aware of:

CeptonListenPoints()
CeptonUnlistenPoints()
CeptonListenPointsEx()
CeptonUnlistenPointsEx()
CeptonListenFrames()
CeptonUnlistenFrames()
CeptonListenFramesEx()
CeptonUnlistenFramesEx()

Though there are a lot of functions here, don't worry. They are broken into two main categories: Listen and ListenEx functions. For VistaP, VistaX90, and Nova, you can use the non-ex functions. The EX callbacks are for X120-Ultra and other newer lidars that require extended data.

Let's take a look at the simplest function, CeptonListenPoints.

int CeptonListenPoints(CeptonPointsCallback callback, void *user_data);

Points callbacks are invoked whenever a new UDP data packet comes in, which will contain ~144 points.

There are two parameters: callback and user_data. Let's first take a look at the callback, and break down each of the parameters.

typedef void (*CeptonPointsCallback)(CeptonSensorHandle handle,
                                     int64_t start_timestamp, size_t n_points,
                                     size_t stride, const uint8_t *points,
                                     void *user_data);
  • handle: CeptonSensorHandle: this is a handle that can be used to identify data coming from each sensor. In general this will be the encoding of the sensor's IP address. For example, 192.168.32.32 => 0xc0a82020 => 3232243744.
  • start_timestamp: int64: this is the timestamp of the data, measured in microseconds. If the sensor is PTP-synced, this will the PTP timestamp. If the sensor is not PTP-synced, it will be the sensor's power-up timestamp; that is, how long the sensor has been powered up.
  • n_points: size_t: the number of points in this data
  • stride: size_t: for the non-ex callbacks, all data is strided. This is for future compatibility in case the size of CeptonPoint changes, but for now this will always be 10 bytes. This is used to index into points
  • points: const uint8_t*: you can see here that this data is a pointer to some generic bytes, there is no type assigned. To get the i'th point, you can do the following:
CeptonPoint point = *(CeptonPoint const*)(points + i * stride);
  • user_data: void*: this will be the pointer that was passed in as the user_data parameter to CeptonListenPoints. Depending on your use case, this can just be passed as nullptr if you're only modifying global state. My typical use for this is to pass object pointers, if I have a callback that I want to access an instance of some object.
void callback(CeptonSensorHandle handle,
              int64_t start_timestamp, size_t n_points,
              size_t stride, const uint8_t *points,
              void *user_data);

class MyObject {
  public:
    MyObject() {}

    size_t counter = 0;

    void start() {
      ...
      CeptonListenPoints(callback, this);
      ...
    }
}

void callback(CeptonSensorHandle handle,
              int64_t start_timestamp, size_t n_points,
              size_t stride, const uint8_t *points,
              void *user_data)
{
  // Increment the object's counter
  reinterpret_cast<MyObject*>(user_data)->counter += 1;
}

int main() 
{
  // ... Initialize the SDK ...

  MyObject mobj;
  mobj.start();

  // See that the counter is being incremented
  while (true) {
    cout << mobj.counter << endl;
    this_thread::sleep_for(milliseconds(100));
  }
}

Callbacks - Frame Aggregation

APIs to be aware of:

CeptonListenFrames
CeptonUnlistenFrames
CeptonListenFramesEx
CeptonUnlistenFramesEx

Getting packets of 144 points is great, but that is no where near the size of a full frame. Most useful processing will be done on a completed data, containing tens of thousands of points. Let's look at a couple ways to get these completed frames.

1. Using natural frames

Each lidar has its own notion of a "natural frame". This is determined by the firmware and is exposed by the parity bit flag in each point.

To check the parity for a given point, you can do:

CeptonPoint point;
bool parity = (point.flag & CEPTON_POINT_FRAME_PARITY) == 0;

You don't need to worry about this because it's taken care of by the SDK frame aggregators, but the idea is that each time the parity changes, the frame is completed and published. So if we have a stream of incoming points, the logic looks like this:

0 0 0 0 0 1 <publish frame with parity 0> 1 1 1 1 0 <publish frame with parity 1>

Again, you don't need to worry about this.

The callback type is the same as the point callback we set up above. To subscribe to these natural frames, the code looks like this:

void callback(CeptonSensorHandle handle,
              int64_t start_timestamp, size_t n_points,
              size_t stride, const uint8_t *points,
              void *user_data)
{
  cout << "Got " << n_points << " points" << endl;
}

int main() {
  // ... other initialization code
  CeptonListenFrames(CEPTON_AGGREGATION_MODE_NATURAL, callback, nullptr);
}

2. Using timed frames

The second option, is to used timed frames. Generally this would not be used for modern sensors, but it is an option in case you want the SDK to take care of accumulating longer frames, rather than doing so yourself.

Rather than using CEPTON_AGGREGATION_MODE_NATURAL, we instead pass in an integer for the frame duration we want, measured in microseconds. So if we wanted to accumulate 0.2-second frames, the API call would be:

int main() {
  // .. other initialization code
  CeptonListenFrames(200000, callback, nullptr);
}

Note that the duration must be at least 1000 microseconds, anything less will return an SDK error.

A complete example with a live sensor

// basic.c
#include <stdio.h>
#include <stdlib.h>

#include "cepton_sdk2.h"

void check_api_error(int err, char const *api) {
  if (err != CEPTON_SUCCESS) {
    printf("API Error for %s: %s\n", api, CeptonGetErrorCodeName(err));
    exit(1);
  }
}

int n_frames = 0;
void frameCallback(CeptonSensorHandle handle, int64_t start_timestamp,
                   size_t n_points, size_t stride, const uint8_t *points,
                   void *user_data) {
  printf("Got %d frames: %d points\n", ++n_frames, (int)n_points);
}

int main() {
  int ret;

  // Initialize
  ret = CeptonInitialize(CEPTON_API_VERSION, sensorErrorCallback);
  check_api_error(ret, "CeptonInitialize");

  ret = CeptonEnableLegacyTranslation();
  check_api_error(ret, "EnableLegacyTranslation");

  // Start networking listener thread
  ret = CeptonStartNetworking();
  check_api_error(ret, "CeptonStartNetworking");

  // Listen for frames
  ret = CeptonListenFrames(CEPTON_AGGREGATION_MODE_NATURAL, frameCallback, 0);
  check_api_error(ret, "CeptonListenFrames");

  // Sleep
  while (n_frames < 10)
    ;

  // Deinitialize
  ret = CeptonDeinitialize();
  check_api_error(ret, "CeptonDeinitialize");
  return 0;
}

A couple notes on this sample:

  • See the call to CeptonStartNetworking(). This starts an internal socket listener to receive the UDP data. If running with a live sensor, this function must be called after CeptonInitialize() in order to receive any data.
  • See the first parameter to CeptonInitialize, CEPTON_API_VERSION. This variable is defined in cepton_sdk2.h, and is used to ensure consistency between the included header file and the SDK binary. If the two do not match, an error is returned.
  • See the call to CeptonEnableLegacyTranslation(). Legacy sensors (VistaP, SoraP) use a different library which must loaded via this API. The legacy SDK binary must be located in the same directory as the SDK2 binary

EX Points

For Vista X90 and Nova sensors, CeptonPoint is the data type naturally output by the sensor. For the X120 Ultra, 16 bits is no longer enough to encapsulate the max range measurement, so we introduce CeptonPointEx, which has a couple nice additional features:

  • Reflectivity is output directly in units of 1%, no more need for a lookup table
  • Point size is fixed for these types, so stride is no longer needed.
struct CeptonPointEx {
  int32_t x;  // Unit is 1/65536 m or ~0.015mm
  int32_t y;
  int32_t z;
  uint16_t reflectivity;        // Unit is 1%, no lookup table
  uint16_t relative_timestamp;  // Unit is 1 us
  uint16_t flags;
  uint16_t channel_id;
};

In any of the above code where CeptonListenFrames, or CeptonListenPoints was used, you can equivalently use CeptonListenFramesEx and CeptonListenPointsEx. This is compatible with any sensor that outputs the CeptonPoint data above - the SDK will convert.

Let's revisit the above example, this time using EX points

A complete example using EX points

// basic.c
#include <stdio.h>
#include <stdlib.h>

#include "cepton_sdk2.h"

void check_api_error(int err, char const *api) {
  if (err != CEPTON_SUCCESS) {
    printf("API Error for %s: %s\n", api, CeptonGetErrorCodeName(err));
    exit(1);
  }
}

int n_frames = 0;
void frameCallbackEx(CeptonSensorHandle handle, int64_t start_timestamp,
                   size_t n_points, const CeptonPointEx* points,
                   void *user_data) {
  printf("Got %d frames: %d points\n", ++n_frames, (int)n_points);

  // This is how we get the position in meters
  float x_meters = points[0] / 65536.0;
  float y_meters = points[1] / 65536.0;
}

int main() {
  int ret;

  // Initialize
  ret = CeptonInitialize(CEPTON_API_VERSION, sensorErrorCallback);
  check_api_error(ret, "CeptonInitialize");

  ret = CeptonEnableLegacyTranslation();
  check_api_error(ret, "EnableLegacyTranslation");

  // Start networking listener thread
  ret = CeptonStartNetworking();
  check_api_error(ret, "CeptonStartNetworking");

  // Listen for frames
  ret = CeptonListenFramesEx(CEPTON_AGGREGATION_MODE_NATURAL, frameCallbackEx, 0);
  check_api_error(ret, "CeptonListenFrames");

  // Sleep
  while (n_frames < 10)
    ;

  // Deinitialize
  ret = CeptonDeinitialize();
  check_api_error(ret, "CeptonDeinitialize");
  return 0;
}

A few things to note:

  • You should use either CeptonListenFrames/Points, or CeptonListenFrameEx/PointsEx, but not both.