DLAPI Quickstart Guide

Thank you for choosing Diffraction Limited for your imaging needs! The following guide is to help you develop applications for controlling your Aluma brand cameras. We hope you find it straightforward and informative.

Download the installers for the DLAPI SDK here:

As of this moment the SDK consists of: the API library binaries, header files, and this documentation.

Asynchronous Operation

DLAPI works on an asynchronous query and response messaging protocol that is exposed to the user via a the dl::IPromise interface class. Any API endpoint which communicates directly will return a pointer to an dl::IPromise instance which can be used to monitor the status of that message. These are divided into four types of interactions:

dl::IPromise objects report a status that notify the user when the operation they've called has completed, or thrown an error. It also stores human-readable error messages, retrieved with dl::IPromise::getLastError(), for easy troubleshooting at the call-site.

As dl::IPromise objects are created by DLAPI, their memory is owned and managed by DLAPI; It is therefore the responsibility of the user to handle dl::IPromise objects, and release them when they're done with them. This can be done by calling dl::IPromise::release() when you are done monitoring the status of a given interaction (this does not require the interaction be complete).

The example below demonstrates the workflow required to instantiate, monitor, and release a promise, as well as how to obtain any error messages from the promise (if it didn't complete successfully).

Device Interfaces

All basic components of a camera have been divided into class interfaces, such as:

  • ICamera which controls camera-level settings, status information, and provides accessors to all connected peripherals.
  • ISensor which controls imaging, and basic sensor-level settings.
  • ITEC which controls and reports the status of the thermo-electric cooler of the camera (if available).
  • IAO which controls and reports the status of any adaptive optics units connected to the camera.
  • IFW which controls and reports the status of any filter wheel units connected to the camera.

Data Structures

A number of convenience structures have been created to minimize the number of calls required for obtaining basic operating parameters for cameras, sensors, and other peripherals, as well as some convenience structures for various actions (like starting an exposure). You should familiarize yourself in particular with the dl::IPromise and IImage interfaces, as well as the dl::TSubframe and dl::TExposureOptions structs.

DLAPI has a number of convenience typedefs defined for interface classes, such as dl::IImagePtr or dl::ICameraPtr for readability.

First Steps

DLAPI is accessed entirely from a single header file: dlapi.h, and all its functionality is contained within the dl namespace. Once you've added it to your project's include path, you can access your Aluma brand cameras via the dl::IGateway interface. dl::IGateway objects are instantiated and managed completely within DLAPI via the dl::getGateway() and dl::deleteGateway() functions.

The internal implementation of dl::IGateway is a singleton, and thus you can make multiple calls to dl::getGateway() safely, so long as they are accompanied by their partnered dl::deleteGateway() call—but you should get in the habit of using dependency injection, and only calling the get/delete methods once.

Once you've got an dl::IGateway instance, you can query for USB or Network cameras, and control them normally. The sample Program below searches for a connected USB camera, and obtains the camera's serial string if found. Remember that convenience typedefs exist for all pointer-to-interface classes (e.g. dl::IGateway has an accompanying dl::IGatewayPtr).

{c++}
#include <dlapi.h>
#include <iostream>
int main()
{
dl::IGatewayPtr pGateway = dl::getGateway();
// Query USB Cameras is a blocking call
pGateway->queryUsbCameras();
// Only proceed if a camera was found
if (pGateway->getUsbCameraCount() <= 0)
return 1;
// Obtain the first USB camera found
dl::ICameraPtr pCamera = pGateway->getUsbCamera(0);
// Initialize the camera (required to enable I/O)
pCamera->initialize();
// Create a buffer for the camera's serial string &amp; retrieve it.
char buf[512] = {0};
size_t lng = 512;
pCamera->getSerial(&(buf[0]), lng);
std::cout << std::string(&(buf[0]), lng) << std::endl;
dl::deleteGateway(pGateway);
return 0;
}

Querying Camera Status

The next step is to query a parameter from the camera, which requires some knowledge about how DLAPI handles Input/Output (I/O). Because the DLAPI internals use asynchronous I/O to communicate with the camera, a promise-based approach to camera control (via the dl::IPromise interface) has been implemented—as such, accessing camera parameters requires a little bit of overhead. Any camera or peripheral endpoint which returns a pointer to an dl::IPromise object executes asynchronously on a separate thread. You can use the dl::IPromise returned object to monitor the status of the command either synchronously (blocking), or asynchronously (non-blocking).

Take, for example, the example above: if instead of retrieving the camera's serial number, we wanted to retrieve the camera status, and wait until the call returned. You would need to do a few things:

{c++}
// First, we need to retrieve a camera object from an IGateway, and pass it into our function below.
dl::ICamera::Status getCameraStatus(dl::ICameraPtr pCamera)
{
try
{
// First, we query the camera for its status
if (!pCamera) throw std::logic_error("No camera selected")
dl::IPromisePtr pPromise = pCamera->queryStatus();
// Next, we wait for the promise to complete (either successfully, or in error)
dl::IPromise::Status result = pPromise->wait();
if (result != dl::IPromise::Complete)
{
// If the promise did not complete successfully, extract any error messages and abort
char buf[512] = {0};
size_t lng = 512; // this variable will be resized to the length of the error message
pPromise->getLastError(&(buf[0]), lng);
pPromise->release(); // Always clean up.
throw std::logic_error(std::string(&(buf[0]), lng));
}
pPromise->release(); // Always clean up.
// Finally, retrieve the newly buffered camera status
return pCamera->getStatus();
}
catch (std::exception &ex)
{
throw std::logic_error(std::string("Cannot query the camera status: ") + ex.what() );
}
}

It is strongly recommended that you write some custom function to handle promises for you, monitoring their status and releasing them when they're complete. Such a function might look something like this:

{c++}
using namespace dl;
void handlePromise(IPromisePtr pPromise)
{
IPromise::Status result = pPromise->wait();
if (result != IPromise::Complete)
{
char buf[512] = {0};
size_t lng = 512; // this variable will be resized to the length of the error message
pPromise->getLastError(&(buf[0]), lng);
pPromise->release();
throw std::logic_error(std::string(&(buf[0]), lng));
}
pPromise->release();
}

Controlling Your Camera's Peripherals

Many of the settings you'll need to access during the normal operation of your camera are accessible via one of the camera's peripheral interfaces. Controlling cooling uses the dl::ITEC interface, whereas controlling the imaging sensor is done via an dl::ISensor interface. These are accessible via the dl::ICamera::getTEC() and dl::ICamera::getSensor() commands respectively.

Let's attempt to change the fan speed for the main sensor's fan. This can be achieved by updating the value of the dl::ISensor::Settings::FanSpeed setting:

{c++}
using namespace dl;
void setFanSpeed(ICameraPtr pCamera, int fanSpeed)
{
// First, get the main imaging sensor (always index-zero).
try
{
ISensorPtr pSensor = pCamera->getSensor(0);
if (!pSensor) return; // If no main sensor is present, try calling ICamera::initialize() first.
// Now that we have access to the sensor, let's change the fan speed to the requested value:
IPromise pPromise = pSensor->setSetting(ISensor::FanSpeed, fanSpeed);
handlePromise(pPromise);
}
catch (std::exception &ex)
{
std::ostringstream oss;
oss << "Failed to set fan speed to: " << fanSpeed << std::endl << ex.what();
// logMessage(oss.str());
}
}

Your First Image

Acquiring your first image follows much of the same setup as before, and requires only a little extra overhead to get the job done. First, we need to know the dimensions of the sensor we're imaging with:

{c++}
// Using the handlePromise() function from above
using namespace dl;
ISensor::Info getSensorInfo(ISensorPtr pSensor)
{
ISensor::Info info;
try
{
if (!pSensor) throw std::logic_error("No sensor present");
handlePromise(pSensor->queryInfo());
info = pSensor->getInfo();
}
catch(std::exception &ex)
{
throw std::logic_error(std::string("Failed to retrieve Sensor information: ") + ex.what());
}
return info;
}

The dl::ISensor::Info structure contains the dl::ISensor::Info::pixelsX and dl::ISensor::Info::pixelsY parameters, which represent the full height and width of the sensor in pixels. Next, we need to construct a subframe for the image. In this case, let's take a full-frame image, binned 1x1.

{c++}
using namespace dl;
void setSubframe(ISensorPtr pSensor, const ISensor::Info &info)
{
try
{
TSubframe subf;
subf.top = 0;
subf.left = 0;
subf.width = info.pixelsX;
subf.height = info.pixelsY;
subf.binX = 1;
subf.binY = 1;
if (!pSensor) throw std::logic_error("No sensor present");
handlePromise(pSensor->setSubframe(subf));
}
catch (std::exception &ex)
{
throw std::logic_error(std::string("Failed to set subframe: ") + ex.what());
}
}

With the subframe armed, we can start the exposure. For this example, let's set up a 1 second light frame exposure.

{c++}
using namespace dl;
void startExposure(ISensorPtr pSensor)
{
try
{
if (!pSensor) throw std::logic_error("No sensor present");
TExposureOptions options;
options.duration = 1.0f;
options.binX = 1;
options.binY = 1;
options.readoutMode = 0; // See: ISensor::getReadoutModes()
options.isLightFrame = true;
options.useRBIPreflash = false;
options.useExtTrigger = false;
handlePromise(pSensor->StartExposure(options));
}
catch (std::exception &ex)
{
throw std::logic_error(std::string("Failed to start exposure: ") + ex.what());
}
}

Now it's our responsibility to wait until the camera's sensor reports ready for download. We can do that using the getCameraStatus() function we wrote above, and a bit of business logic:

{c++}
using namespace dl;
bool isImageReadyForDownload(ICameraPtr pCamera, ISensorPtr pSensor)
{
ICamera::Status cameraStatus = getCameraStatus(pCamera);
unsigned int sensorId = pSensor->getSensorId();
ISensor::Status sensorStatus = ( sensorId == 0 ) ? cameraStatus.mainSensorState : cameraStatus.extSensorState;
return sensorStatus == ISensor::ReadyToDownload;
}

Now we can start the download proper using a call to dl::ISensor::startDownload(), but because this can take some time (several seconds depending on the sensor and its digitization rate), it's wise to write a promise handling function that is non-blocking, so let's do that here:

{c++}
using namespace dl;
bool isXferComplete(IPromise * pPromise)
{
// Get the status of the promise
auto status = pPromise->getStatus();
if (status == IPromise::Complete)
{
// The image is ready for retrieval.
pPromise->release();
return true;
}
else if (status == IPromise::Error)
{
// If an error occurred, report it to the user.
char buf[512] = {0};
size_t blng = 512;
pPromise->getLastError(&(buf[0]), blng);
pPromise->release();
throw std::logic_error(&(buf[0]));
}
// Otherwise, we wait.
return false;
}

Once that function returns true, we can retrieve the image buffer from the camera. It's good practice to make a local copy of the buffer as soon as you retrieve it as follows:

{c++}
#include <algorithm>
using namespace dl;
void copyImageBuffer(ISensorPtr pSensor, unsigned short * pBuffer, size_t bufferLength)
{
IImagePtr pImage = pSensor->getImage();
auto pImgBuf = pImage->getBufferData();
auto bufLng = pImage->getBufferLength();
// Take the minimum of the image's size and the pre-allocated pBuffer size, and multiply it by the size of its elements.
auto cpyLng = sizeof(unsigned short) * std::min<size_t>(bufLng, bufferLength);
memmove(pBuffer, pImgBuf, cpyLng);
}

Now let's tie this all together from start to finish:

{c++}
using namespace dl;
void takeImage(ICameraPtr pCamera, unsigned int sensorId, unsigned short *& pBuffer, size_t &bufferLength)
{
// Do some book-keeping to keep things clean in the event of an error;
pBuffer = nullptr;
bufferLength = 0;
if (!pCamera) return;
ISensorPtr pSensor = pCamera->getSensor(sensorId);
if (!pSensor) return;
try
{
// Perform pre-exposure setup
ISensor::Info info = getSensorInfo(pSensor);
setSubframe(pSensor, info);
// Start the exposure & wait for ISensor::ReadyToDownload
startExposure(pSensor);
while (!isImageReadyForDownload(pCamera, pSensor))
{
// Do other stuff: pump messages, process things, sleep, etc...
}
// Start the download & wait for transfer complete
IPromisePtr pImgPromise = pSensor->startDownload();
while (!isXferComplete(pImgPromise))
{
// Do other stuff: pump messages, process things, sleep, etc...
}
// Finally, copy the image to the local buffer & return. In this instance, the calling function takes ownership of pBuffer. Don't forget to clean up!
bufferLength = info.pixelsX * info.pixelsY;
pBuffer = new unsigned short[bufferLength];
copyImageBuffer(pSensor, pBuffer, bufferLength);
}
catch (std::exception &ex)
{
// If an error occurs, it's good practice to abort any lingering exposures
if (pSensor) pSensor->abortExposure();
// Log any error messages via your favorite error reporting mechanism
logMessage(std::string("Failed to take image: ") + ex.what());
}
}

And that's the basics of camera control. See the ICamera, ISensor, ITEC, IFW, and IAO classes for more details on how to control your camera and its peripherals.

Cooler Control Best Practices

Cameras with coolers can be controlled via the ITEC interface, and there are some considerations we advise application developers in following when using the ITEC interface.

Initialization

After retrieving a pointer to an ICamera object from the Gateway, you need to check whether the camera supports the use of a cooler. This is achieved via the ICamera::Capability API.

{c++}
bool supportsCooler(ICameraPtr pCamera)
{
if (!pCamera) return false;
auto pPromise = pCamera->queryCapability(ICamera::eSupportsCooler);
auto supportsCooler = false;
try
{
handlePromise(pPromise);
supportsCooler = pCamera->getCapability(ICamera::eSupportsCooler);
}
catch (std::exception & ex)
{
logMessage("Failed to query camera capabilities: " + ex.what());
}
return supportsCooler;
}

Assuming the camera you've connected to supports a cooler, we advise retrieving the min/max cooler setpoints from the sensor info:

{c++}
void getCoolerMinMax(ISensorPtr pSensor, int & min, int & max)
{
min = INT_MIN;
max = INT_MAX;
auto pPromise = pSensor->queryInfo();
try
{
handlePromise(pPromise);
auto info = pSensor->getInfo();
min = minCoolerSetpoint;
max = maxCoolerSetpoint;
}
catch (std::exception &ex)
{
logMessage("Failed to query sensor info: " + ex.what());
}
}

Once you know the minimum and maximum cooler setpoints, and whether the camera supports the use of a cooler, you can control the cooler via the ITEC interface after you've called ICamera::initialize().

Some special considerations are necessary: The majority of sensors recommend not exceeding temperature changes greater than 10 degrees Celcius per minute (especially for extended periods of time). This is especially true for cameras with high cooling power (i.e. greater than 50 C changes). We advise slowly ramping up/down the temperature in 5 degree increments every 30 seconds until you reach the desired setpoint. Cameras with high cooling power perform this function automatically, but it never hurts to be cautious.

{c++}
void setCoolerSetpoint(ICameraPtr pCamera, ISensorPtr pSensor, bool enable, int setpoint)
{
// Always nullguard
if (!pCamera) throw std::invalid_argument("Camera pointer was null");
if (!pSensor) throw std::invalid_argument("Sensor pointer was null");
// Check to see if the camera supports the use of a cooler
if (!supportsCooler(pCamera)) throw std::runtime_error("Camera does not support the use of a cooler");
// Retrieve the cooler interface
auto pTEC = pCamera->getTEC();
if (!pTEC) throw std::runtime_error("TEC was not found in Camera interface");
// Get the limits of the cooler setpoint
int minSetpoint, maxSetpoint;
getCoolerMinMax(pSensor, minSetpoint, maxSetpoint);
// Check your inputs (though the Camera's firmware checks this as well)
if (setpoint < minSetpoint) throw std::invalid_argument("Provided setpoint is below minimum cooler capability");
if (setpoint > maxSetpoint) throw std::invalid_argument("Provided setpoint is above maximum cooler capability");
try
{
// Activate the cooler, and set your desired setpoint
auto pPromise = pTEC->setState(enable, setpoint);
handlePromise(pPromise);
}
catch (std::exception &ex)
{
logMessage("Failed to set the cooler's setpoint: " + ex.what());
}
}

During normal operation, the cooler status is reported as part of the ICamera::Status object. You can retrieve that value using a normal camera status query. Values of note include: ICamera::Status::coolerPower which measures the duty cycle of the cooler's power consumption in percent, ICamera::Status::sensorTemperature which measures the temperature of the sensor in degrees Celcius (if ICamera::Capability::eSupportsCoolerTemp is true), and ICamera::Status::heatSinkTemperature which measures the camera's heatsink temperature in degrees Celcius (if ICamera::Capability::eSupportsHeatSinkTemp is true).

When shutting down the cooler, you make the call to ITEC::setState() with the first parameter set to false. You can set the cooler setpoint to whatever temperature you like (ambient, 0, etc) as it is ignored by the camera. Remember to ramp up the temperature to prevent sensor damage.