Implementing the Camera Interface

This guide provides a comprehensive overview of implementing the camera interface in INDI drivers. It covers the basic structure of a camera driver, how to implement the required methods, and how to handle device-specific functionality.

Introduction to the Camera Interface

The camera interface in INDI is designed for astronomical cameras and other imaging devices. It provides a standardized way for clients to control imaging devices, including setting exposure parameters, downloading images, and controlling various camera features.

The camera interface is implemented by inheriting from the INDI::CCD base class (the class name remains CCD for historical reasons, but it supports all types of cameras including CMOS sensors). This class provides a set of standard properties and methods for controlling imaging devices. By implementing this interface, your driver can be used with any INDI client that supports camera devices.

Prerequisites

Before implementing the camera interface, you should have:

  • Basic knowledge of C++ programming
  • Understanding of the INDI protocol and architecture
  • Familiarity with the device’s communication protocol
  • Development environment set up (compiler, build tools, etc.)
  • INDI library installed

Camera Interface Structure

The camera interface consists of several key components:

  • Base Class: INDI::CCD is the base class for all camera drivers.
  • Standard Properties: A set of standard properties for controlling camera devices.
  • Virtual Methods: A set of virtual methods that must be implemented by the driver.
  • Helper Methods: A set of helper methods for common camera operations.

Base Class

The INDI::CCD base class inherits from INDI::DefaultDevice and provides additional functionality specific to camera devices. It defines standard properties for exposure control, frame settings, temperature control, and more.

Standard Properties

The camera interface defines several standard properties:

  • CCD_EXPOSURE: Controls the exposure duration.
  • CCD_ABORT_EXPOSURE: Aborts the current exposure.
  • CCD_FRAME: Controls the frame settings (binning, region of interest).
  • CCD_TEMPERATURE: Controls and monitors the camera temperature.
  • CCD_COOLER: Controls the camera cooler.
  • CCD_COOLER_POWER: Monitors the camera cooler power.
  • CCD_GAIN: Controls the camera gain.
  • CCD_OFFSET: Controls the camera offset.
  • CCD_FRAME_TYPE: Controls the frame type (light, dark, bias, flat).
  • CCD_FRAME_RESET: Resets the frame to full size.
  • CCD_INFO: Provides information about the camera (pixel size, bit depth, etc.).
  • CCD_COMPRESSION: Controls image compression.
  • CCD_RAPID_GUIDE: Controls rapid guiding.
  • CCD_FAST_EXPOSURE: Controls fast exposure mode.

Virtual Methods

The camera interface defines several virtual methods that must be implemented by the driver:

  • StartExposure: Starts an exposure.
  • AbortExposure: Aborts the current exposure.
  • UpdateCCDFrame: Updates the camera frame settings.
  • UpdateCCDBin: Updates the camera binning settings.
  • UpdateCCDFrameType: Updates the camera frame type.
  • UpdateCCDCompression: Updates the camera compression settings.
  • UpdateRapidGuide: Updates the rapid guide settings.
  • UpdateCCDTemperature: Updates the camera temperature.

Helper Methods

The camera interface provides several helper methods for common camera operations:

  • SetCCDParams: Sets the camera parameters (resolution, pixel size, bit depth, etc.).
  • SetGuiderParams: Sets the guider parameters (resolution, pixel size, bit depth, etc.).
  • SetCCDCapability: Sets the camera capabilities (can bin, can subframe, has cooler, etc.).
  • SetCCDTemperature: Sets the camera temperature.
  • SetCCDTemp: Sets the camera temperature (alias for SetCCDTemperature).
  • SetGuiderExposure: Sets the guider exposure.
  • ExposureComplete: Signals that an exposure is complete.
  • GuideComplete: Signals that a guide exposure is complete.
  • GrabImage: Grabs an image from the camera.
  • GrabImageHelper: Helper function for GrabImage.

Implementing a Basic Camera Driver

Let’s create a simple INDI driver for a hypothetical camera called “MyCamera”. This camera has a simple USB interface and supports basic commands for setting exposure, binning, and temperature.

Step 1: Create the Header File

Create a file named mycameradriver.h with the following content:

#pragma once

#include <indiccd.h>

class MyCameraDriver : public INDI::CCD
{
public:
    MyCameraDriver();
    virtual ~MyCameraDriver() = default;

    // DefaultDevice overrides
    virtual const char *getDefaultName() override;
    virtual bool initProperties() override;
    virtual bool updateProperties() override;
    virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override;
    virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override;

    // Camera specific overrides
    virtual bool StartExposure(float duration) override;
    virtual bool AbortExposure() override;
    virtual bool UpdateCCDFrame(int x, int y, int w, int h) override;
    virtual bool UpdateCCDBin(int binx, int biny) override;
    virtual bool UpdateCCDFrameType(CCDChip::CCD_FRAME type) override;
    virtual bool UpdateCCDCompression(int compression) override;
    virtual bool UpdateRapidGuide(const char *name, bool enabled) override;
    virtual bool UpdateCCDTemperature() override;

protected:
    // Connection overrides
    virtual bool Connect() override;
    virtual bool Disconnect() override;

    // Helpers
    void setupParams();
    void grabImage();
    static void timerCallback(void *userpointer);

private:
    // Device handle
    int handle = -1;

    // Exposure
    double ExposureRequest = 0;
    struct timeval ExpStart;
    bool InExposure = false;
    int timerID = -1;

    // Temperature
    double TemperatureRequest = 20.0;
    bool TemperatureUpdateRunning = false;
};

Step 2: Create the Implementation File

Create a file named mycameradriver.cpp with the following content:

#include "mycameradriver.h"

#include <memory>
#include <string.h>
#include <time.h>
#include <math.h>
#include <unistd.h>
#include <sys/time.h>

// We declare an auto pointer to MyCameraDriver
static std::unique_ptr<MyCameraDriver> mycamera(new MyCameraDriver());

MyCameraDriver::MyCameraDriver()
{
    // Set the driver version
    setVersion(1, 0);

    // Set the camera capabilities
    SetCCDCapability(CCD_CAN_BIN | CCD_CAN_SUBFRAME | CCD_HAS_COOLER | CCD_HAS_SHUTTER);
}

const char *MyCameraDriver::getDefaultName()
{
    return "My Camera";
}

bool MyCameraDriver::initProperties()
{
    // Initialize the parent's properties
    INDI::CCD::initProperties();

    // Add debug, simulation, and configuration controls
    addAuxControls();

    return true;
}

bool MyCameraDriver::updateProperties()
{
    // Call the parent's updateProperties
    INDI::CCD::updateProperties();

    if (isConnected())
    {
        // Define properties when connected
        // These are already defined by the parent class
    }
    else
    {
        // Delete properties when disconnected
        // These are already deleted by the parent class
    }

    return true;
}

bool MyCameraDriver::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
{
    // Check if the message is for this device
    if (!strcmp(dev, getDeviceName()))
    {
        // Handle custom number properties here
    }

    // If the message is not for this device or property, call the parent's ISNewNumber
    return INDI::CCD::ISNewNumber(dev, name, values, names, n);
}

bool MyCameraDriver::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
{
    // Check if the message is for this device
    if (!strcmp(dev, getDeviceName()))
    {
        // Handle custom switch properties here
    }

    // If the message is not for this device or property, call the parent's ISNewSwitch
    return INDI::CCD::ISNewSwitch(dev, name, states, names, n);
}

bool MyCameraDriver::Connect()
{
    // Call the parent's Connect method
    bool result = INDI::CCD::Connect();

    if (result)
    {
        // Open the device
        handle = open("/dev/mycamera", O_RDWR);
        if (handle < 0)
        {
            LOG_ERROR("Failed to open device");
            return false;
        }

        // Set up the camera parameters
        setupParams();

        LOG_INFO("Device connected successfully");
    }

    return result;
}

bool MyCameraDriver::Disconnect()
{
    // Close the device
    if (handle >= 0)
    {
        close(handle);
        handle = -1;
    }

    // Call the parent's Disconnect method
    return INDI::CCD::Disconnect();
}

void MyCameraDriver::setupParams()
{
    // Set the camera parameters
    // These are the parameters of the camera sensor
    SetCCDParams(1280, 1024, 16, 5.2, 5.2);

    // Set the minimum exposure time
    PrimaryCCD.setMinMaxStep("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", 0.001, 3600, 0.001);

    // Set the image size
    uint32_t nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8;
    PrimaryCCD.setFrameBufferSize(nbuf);
}

bool MyCameraDriver::StartExposure(float duration)
{
    // Check if we're already in an exposure
    if (InExposure)
    {
        LOG_ERROR("Camera is already exposing");
        return false;
    }

    // Check if the duration is valid
    if (duration < PrimaryCCD.getMinX())
    {
        LOGF_ERROR("Exposure duration %f is less than minimum %f", duration, PrimaryCCD.getMinX());
        return false;
    }

    // Set the exposure duration
    ExposureRequest = duration;

    // Start the exposure
    gettimeofday(&ExpStart, nullptr);
    InExposure = true;

    // Set the timer for the exposure
    timerID = SetTimer(duration * 1000);

    // Update the exposure status
    PrimaryCCD.setExposureDuration(duration);
    PrimaryCCD.setExposureLeft(duration);

    // Log the exposure start
    LOGF_INFO("Starting exposure of %f seconds", duration);

    return true;
}

bool MyCameraDriver::AbortExposure()
{
    // Check if we're in an exposure
    if (!InExposure)
    {
        LOG_WARNING("No exposure in progress");
        return true;
    }

    // Abort the exposure
    InExposure = false;

    // Remove the timer
    if (timerID > 0)
    {
        RemoveTimer(timerID);
        timerID = -1;
    }

    // Log the exposure abort
    LOG_INFO("Exposure aborted");

    return true;
}

bool MyCameraDriver::UpdateCCDFrame(int x, int y, int w, int h)
{
    // Check if the frame is valid
    if (x < 0 || y < 0 || w <= 0 || h <= 0 || x + w > PrimaryCCD.getXRes() || y + h > PrimaryCCD.getYRes())
    {
        LOGF_ERROR("Invalid frame: x=%d, y=%d, w=%d, h=%d", x, y, w, h);
        return false;
    }

    // Set the frame
    PrimaryCCD.setFrame(x, y, w, h);

    // Calculate the new frame buffer size
    uint32_t nbuf = w * h * PrimaryCCD.getBPP() / 8;
    PrimaryCCD.setFrameBufferSize(nbuf);

    // Log the frame update
    LOGF_INFO("Frame updated: x=%d, y=%d, w=%d, h=%d", x, y, w, h);

    return true;
}

bool MyCameraDriver::UpdateCCDBin(int binx, int biny)
{
    // Check if the binning is valid
    if (binx < 1 || biny < 1 || binx > 4 || biny > 4)
    {
        LOGF_ERROR("Invalid binning: %dx%d", binx, biny);
        return false;
    }

    // Set the binning
    PrimaryCCD.setBin(binx, biny);

    // Calculate the new frame buffer size
    uint32_t nbuf = PrimaryCCD.getSubW() * PrimaryCCD.getSubH() * PrimaryCCD.getBPP() / 8;
    PrimaryCCD.setFrameBufferSize(nbuf);

    // Log the binning update
    LOGF_INFO("Binning updated: %dx%d", binx, biny);

    return true;
}

bool MyCameraDriver::UpdateCCDFrameType(CCDChip::CCD_FRAME type)
{
    // Set the frame type
    PrimaryCCD.setFrameType(type);

    // Log the frame type update
    LOGF_INFO("Frame type updated: %d", type);

    return true;
}

bool MyCameraDriver::UpdateCCDCompression(int compression)
{
    // Set the compression
    PrimaryCCD.setCompression(compression);

    // Log the compression update
    LOGF_INFO("Compression updated: %d", compression);

    return true;
}

bool MyCameraDriver::UpdateRapidGuide(const char *name, bool enabled)
{
    // Check if rapid guide is supported
    if (strcmp(name, "TELESCOPE_TIMED_GUIDE_WE") && strcmp(name, "TELESCOPE_TIMED_GUIDE_NS") && strcmp(name, "SEND_GUIDE_SECONDARY"))
    {
        LOG_ERROR("Rapid guide not supported");
        return false;
    }

    // Set the rapid guide
    RapidGuideEnabled = enabled;

    // Log the rapid guide update
    LOGF_INFO("Rapid guide updated: %s", enabled ? "enabled" : "disabled");

    return true;
}

bool MyCameraDriver::UpdateCCDTemperature()
{
    // Check if we're already updating the temperature
    if (TemperatureUpdateRunning)
        return true;

    // Set the temperature update flag
    TemperatureUpdateRunning = true;

    // Get the current temperature
    double temperature = 0;
    // In a real driver, you would get the temperature from the device
    // For this example, we'll simulate a temperature that approaches the requested temperature
    temperature = TemperatureNP[0].getValue() + (TemperatureRequest - TemperatureNP[0].getValue()) * 0.1;

    // Set the temperature
    TemperatureNP[0].setValue(temperature);

    // Update the temperature status
    if (fabs(temperature - TemperatureRequest) < 0.1)
    {
        TemperatureNP.setState(IPS_OK);
        TemperatureUpdateRunning = false;
    }
    else
    {
        TemperatureNP.setState(IPS_BUSY);
        // Set a timer to update the temperature again
        SetTimer(1000);
    }

    // Send the temperature update to the client
    TemperatureNP.apply();

    return true;
}

void MyCameraDriver::timerCallback(void *userpointer)
{
    // Cast the userpointer to a MyCameraDriver pointer
    MyCameraDriver *driver = static_cast<MyCameraDriver *>(userpointer);

    // Check if we're in an exposure
    if (driver->InExposure)
    {
        // Check if the exposure is complete
        struct timeval now;
        gettimeofday(&now, nullptr);
        double elapsed = now.tv_sec - driver->ExpStart.tv_sec + (now.tv_usec - driver->ExpStart.tv_usec) / 1e6;
        if (elapsed >= driver->ExposureRequest)
        {
            // Exposure is complete
            driver->InExposure = false;
            driver->timerID = -1;

            // Grab the image
            driver->grabImage();
        }
        else
        {
            // Exposure is still in progress
            driver->PrimaryCCD.setExposureLeft(driver->ExposureRequest - elapsed);
            driver->timerID = driver->SetTimer(100);
        }
    }
    else if (driver->TemperatureUpdateRunning)
    {
        // Update the temperature
        driver->UpdateCCDTemperature();
    }
}

void MyCameraDriver::grabImage()
{
    // Allocate memory for the image
    uint8_t *image = PrimaryCCD.getFrameBuffer();
    uint32_t width = PrimaryCCD.getSubW() / PrimaryCCD.getBinX();
    uint32_t height = PrimaryCCD.getSubH() / PrimaryCCD.getBinY();
    uint32_t bpp = PrimaryCCD.getBPP();
    uint32_t nbuf = width * height * bpp / 8;

    // In a real driver, you would get the image from the device
    // For this example, we'll generate a simple gradient
    for (uint32_t y = 0; y < height; y++)
    {
        for (uint32_t x = 0; x < width; x++)
        {
            uint32_t index = (y * width + x) * bpp / 8;
            uint16_t value = (x + y) % 65535;
            if (bpp == 8)
            {
                image[index] = value % 255;
            }
            else
            {
                image[index] = value & 0xFF;
                image[index + 1] = (value >> 8) & 0xFF;
            }
        }
    }

    // Set the image data
    PrimaryCCD.setFrameBuffer(image);
    PrimaryCCD.setFrameBufferSize(nbuf);
    PrimaryCCD.setResolution(width, height);
    PrimaryCCD.setNAxis(2);
    PrimaryCCD.setBPP(bpp);

    // Signal that the exposure is complete
    ExposureComplete(&PrimaryCCD);
}

Step 3: Create the Main File

Create a file named main.cpp with the following content:

#include "mycameradriver.h"

int main(int argc, char *argv[])
{
    // Create and initialize the driver
    std::unique_ptr<MyCameraDriver> mycamera(new MyCameraDriver());

    // Set the driver version
    mycamera->setVersion(1, 0);

    // Start the driver
    mycamera->ISGetProperties(nullptr);

    // Run the driver
    return mycamera->run();
}

Step 4: Create the CMakeLists.txt File

Create a file named CMakeLists.txt with the following content:

cmake_minimum_required(VERSION 3.0)
project(indi-mycamera CXX C)

include(GNUInstallDirs)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake_modules/")

find_package(INDI REQUIRED)
find_package(Nova REQUIRED)
find_package(CFITSIO REQUIRED)
find_package(ZLIB REQUIRED)

include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${INDI_INCLUDE_DIR})
include_directories(${NOVA_INCLUDE_DIR})
include_directories(${CFITSIO_INCLUDE_DIR})

include(CMakeCommon)

add_executable(indi_mycamera mycameradriver.cpp main.cpp)

target_link_libraries(indi_mycamera ${INDI_LIBRARIES} ${NOVA_LIBRARIES} ${CFITSIO_LIBRARIES} ${ZLIB_LIBRARIES})

install(TARGETS indi_mycamera RUNTIME DESTINATION bin)

Step 5: Create the XML File

Create a file named indi_mycamera.xml with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<driversList>
   <devGroup group="CCDs">
      <device label="My Camera" manufacturer="INDI">
         <driver name="My Camera">indi_mycamera</driver>
         <version>1.0</version>
      </device>
   </devGroup>
</driversList>

Step 6: Build the Driver

To build the driver, create a build directory and run CMake:

mkdir build
cd build
cmake ..
make

Step 7: Install the Driver

To install the driver, run:

sudo make install

This will install the driver executable to /usr/bin and the XML file to /usr/share/indi.

Step 8: Test the Driver

To test the driver, start the INDI server with your driver:

indiserver -v indi_mycamera

Then, connect to the INDI server using an INDI client, such as the INDI Control Panel:

indi_control_panel

Advanced Topics

FITS Header

The camera interface allows you to add custom FITS header keywords to the image. This can be useful for adding metadata about the image, such as the telescope position, filter used, or other relevant information.

To add a FITS header keyword, use the addFITSKeywords method:

void MyCameraDriver::addFITSKeywords(fitsfile *fptr, CCDChip *targetChip)
{
    // Call the parent's addFITSKeywords
    INDI::CCD::addFITSKeywords(fptr, targetChip);

    // Add custom keywords
    int status = 0;
    fits_update_key_s(fptr, TSTRING, "OBSERVER", "John Doe", "Observer name", &status);
    fits_update_key_s(fptr, TSTRING, "OBJECT", "M42", "Object name", &status);
    fits_update_key_s(fptr, TSTRING, "TELESCOP", "My Telescope", "Telescope name", &status);
}

Guider Interface

The camera interface includes support for a guider chip, which can be used for autoguiding. To implement the guider interface, you need to set up the guider parameters and implement the guider-specific methods.

To set up the guider parameters, use the SetGuiderParams method:

void MyCameraDriver::setupParams()
{
    // Set the camera parameters
    SetCCDParams(1280, 1024, 16, 5.2, 5.2);

    // Set the guider parameters
    SetGuiderParams(512, 512, 16, 5.2, 5.2);

    // ...
}

To implement the guider-specific methods, override the StartGuideExposure and AbortGuideExposure methods:

bool MyCameraDriver::StartGuideExposure(float duration)
{
    // Start the guider exposure
    // ...
    return true;
}

bool MyCameraDriver::AbortGuideExposure()
{
    // Abort the guider exposure
    // ...
    return true;
}

Streaming Interface

The camera interface includes support for streaming, which can be used for video capture or live viewing. To implement the streaming interface, you need to set the streaming capability and implement the streaming-specific methods.

To set the streaming capability, use the SetCCDCapability method:

MyCameraDriver::MyCameraDriver()
{
    // Set the driver version
    setVersion(1, 0);

    // Set the camera capabilities
    SetCCDCapability(CCD_CAN_BIN | CCD_CAN_SUBFRAME | CCD_HAS_COOLER | CCD_HAS_SHUTTER | CCD_HAS_STREAMING);
}

To implement the streaming-specific methods, override the StartStreaming and StopStreaming methods:

bool MyCameraDriver::StartStreaming()
{
    // Start the streaming
    // ...
    return true;
}

bool MyCameraDriver::StopStreaming()
{
    // Stop the streaming
    // ...
    return true;
}

Temperature Control

The camera interface includes support for temperature control, which can be used to cool the camera sensor. To implement temperature control, you need to set the temperature capability and implement the temperature-specific methods.

To set the temperature capability, use the SetCCDCapability method:

MyCameraDriver::MyCameraDriver()
{
    // Set the driver version
    setVersion(1, 0);

    // Set the camera capabilities
    SetCCDCapability(CCD_CAN_BIN | CCD_CAN_SUBFRAME | CCD_HAS_COOLER | CCD_HAS_SHUTTER);
}

To implement the temperature-specific methods, override the SetTemperature method:

bool MyCameraDriver::SetTemperature(double temperature)
{
    // Set the temperature
    TemperatureRequest = temperature;
    TemperatureUpdateRunning = true;

    // Start the temperature update
    UpdateCCDTemperature();

    return true;
}

Simulation Mode

The camera interface includes support for simulation mode, which can be used to test the driver without connecting to the actual hardware. To implement simulation mode, check the isSimulation() flag and provide simulated responses.

bool MyCameraDriver::Connect()
{
    // Call the parent's Connect method
    bool result = INDI::CCD::Connect();

    if (result)
    {
        // Check if we're in simulation mode
        if (isSimulation())
        {
            LOG_INFO("Simulation mode enabled");
            handle = 1;
        }
        else
        {
            // Open the device
            handle = open("/dev/mycamera", O_RDWR);
            if (handle < 0)
            {
                LOG_ERROR("Failed to open device");
                return false;
            }
        }

        // Set up the camera parameters
        setupParams();

        LOG_INFO("Device connected successfully");
    }

    return result;
}

Best Practices

When implementing the camera interface, follow these best practices:

  • Use the appropriate base class for your device.
  • Implement simulation mode to allow testing without hardware.
  • Provide informative error messages to help users troubleshoot issues.
  • Handle connection and disconnection gracefully to avoid resource leaks.
  • Update property states to reflect the current state of the device.
  • Use appropriate property types for different kinds of data.
  • Follow the INDI naming conventions for properties and elements.
  • Document your driver to help users understand how to use it.
  • Test your driver thoroughly with different clients and configurations.

Conclusion

Implementing the camera interface in INDI drivers involves inheriting from the INDI::CCD base class, implementing the required methods, and handling device-specific functionality. By following the steps and best practices outlined in this guide, you can create robust and feature-rich camera drivers for your devices.

For more information, refer to the INDI Library Documentation and the INDI Driver Development Guide.