INDI drivers consist of the low-level code that communicates with the device, and of the INDI API code that enables the driver to serve any INDI compatible client.

Details of the data acquisition and control methods are unique to each device, and therefore it is up to the developer to implement these functions. In this chapter, you will learn how to abstract the complicated functions of your device into a set of well-defined, simple, and coherent collection of elementary properties in the INDI architecture; namely: switches, numbers, texts, lights, and blobs.

All examples discussed hereafter are available in the INDI official distribution. Refer to README in the INDI release from sourceforge on how to build and run the example tutorials.

1. General

The basic steps required to build an INDI driver are generally the following:

  1. Define a list of properties that describe your device operation.
  2. Initialize properties initial state.
  3. When a client connects, send a list of existing properties.
  4. Implement functions that will carry out the operations offered by the properties.
  5. Report device status to client if desired.
  6. Clean up the driver if the client disconnects.

In addition to the steps outlined above, developers are strongly encouraged to follow common INDI design philosophies. The design philosophies stem from experiences gained by the INDI developer community over the years:

  • Drivers should be minimal: Your driver should support the most basic functions only. Generally speaking, there should be a 1-to-1 correspondence between the driver's properties and the device's functions. For example, if you're writing a driver for a CCD chip, don't code complicated routines to compute WCS. These functions should be handled either at the client side or via an intermediate driver layer. Minimal also means that the driver should try to avoid any unnecessary dependence on external libraries when possible. The INDI server and most drivers only require the standard headers and a compiler, we prefer to keep it this way.

  • Drivers should adhere to INDI standard properties: INDI standard properties were created to establish interoperability among drivers and clients alike. If you have a function that is similar to an existing standard property, try to adjust your function to use it. For example, both CCDs and Webcams employ the CCD_EXPOSURE property to perform an exposure. This happens despite the fact that in most webcams, you cannot control the exposure duration. Nevertheless, the video driver simply considers the exposure time to be zero and takes an exposure. Therefore, the client only needs to be aware of one property instead of two.

  • Drivers should be secure: The driver has a complete authority over the device, and it should not trust clients blindly. While some clients perform border-checks for numerical values, drivers should assume that no check took place and hence, all checks must be performed in the driver itself. Moreover, work with memory allocation, reallocation, and freeing carefully as buffer overflows pose a common security threat.

  • Drivers should support multiple simultaneous clients: INDI server takes care of most of the processing and logic related to multiple clients, but you should design your drivers to be scalable. Consider all the functions your driver provide, and whether they can be affected in any way when more clients request these functions.

  • Drivers should be safe to operate. Likewise, conflicting commands should be handled properly at the driver level. Consider all possible scenarios and make sure that actions are subjected to interlocking checks before the action is performed. The user should be able to halt operations once they started, and drivers must prepare for such sudden interruptions if necessary. For example, if your telescope tube can hit the tripod base at certain altitudes, then all necessary checks must be performed to make sure no such incident takes place.

    Always prepare for the worst case scenario when developing drivers. If your system is critical, develop stringent interlocking checks, redundancy, fail-over and notification mechanisms. If something can go wrong, it will go wrong.

  • Driver should be flexible: Be flexible in your design while remembering that drivers need to be minimal. Try to group common properties together. Arrange common properties in logical groups. A plethora of individual properties that otherwise could have been grouped together will only confuse the user and make the operation of your device tougher.

2.Properties

INDI properties can be either hard-coded in the header file of the driver, or loaded dynamically as a skeleton file. The following Telescope Simulator uses hard coded INDI properties while some drivers prefer dynamically built properties. If a skeleton file is used, the driver needs to initialize the properties in the initProperties function. The skeleton file should be installed to INDI_DATA_DIR/indi_mydriver_sk.xml where INDI_DATA_DIR is the prefix as defined by libindi installation. To enable proper loading of properties from skeleton files, perform the following steps:

Add to config.h.cmake:

/* Define INDI Data Dir */
#cmakedefine INDI_DATA_DIR "@INDI_DATA_DIR@"

In mydriver.cpp:

#include "config.h"
 
bool MyDriver::initProperties()
{
      char skelPath[MAX_PATH_LENGTH];
      const char *skelFileName = "indi_mydriver_sk.xml";
      snprintf(skelPath, MAX_PATH_LENGTH, "%s/%s", INDI_DATA_DIR, skelFileName);
      struct stat st;
 
      char *skel = getenv("INDISKEL");
      if (skel) 
          buildSkeleton(skel);
      else if (stat(skelPath,&st) == 0) 
          buildSkeleton(skelPath);
      else 
          IDLog("No skeleton file was specified. Set environment variable INDISKEL to the skeleton path and try again.\n"); 
 
      return true;
}

Where MAX_PATH_LENGTH can be set to any length (e.g. 512).

3. Your first driver: Simple Device

We demonstrate the most basic Device driver in the INDI framework. It only consists of the standard CONNECTION and DRIVER_INFO properties. To build this device, we subclass INDI::DefaultDevice, and implement the INDI standard ISxxx() function calls. In simpledevice.h, we have:

#include "indibase/defaultdevice.h"
class SimpleDevice : public INDI::DefaultDevice
{
public:
    SimpleDevice();
protected:
    bool Connect();
    bool Disconnect();
    const char *getDefaultName();
};

Since this is a general device, we subclass INDI::DefaultDevice, and we need to implement three critical functions:

  • Connect: This is called by the client when the user wants to establish connection to the device. In this function, we typically establish the the connection to our physical device and if successful, we return true, otherwise, we return false. We also inform the client about the status of the connection by using the IDMessage() function.

  • Disconnect: This is called by the client when the user wants to disconnect from the device.

  • getDefaultName: Return the default name of the device that gets passed to the client

In simpledevice.cpp, we declate a smart pointer to hold our simpleDevice object:

std::auto_ptr simpleDevice(0);

Next, we need to implement the INDI Library ISxxx functions that get called by the INDI framework. Those must be implemented in all drivers. Here, we simply forward the ISxxx functions to the simpleDevice object, which are then internally handled by the parent class INDI::DefaultDevice.

/**************************************************************************************
** Initilize SimpleDevice object
***************************************************************************************/
void ISInit()
{
 static int isInit=0;
 if (isInit)
  return;
 if (simpleDevice.get() == 0)
 {
     isInit = 1;
     simpleDevice.reset(new SimpleDevice());
 }
}
/**************************************************************************************
** Return properties of device.
***************************************************************************************/
void ISGetProperties (const char *dev)
{
 ISInit();
 simpleDevice->ISGetProperties(dev);
}
/**************************************************************************************
** Process new switch from client
***************************************************************************************/
void ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n)
{
 ISInit();
 simpleDevice->ISNewSwitch(dev, name, states, names, n);
}
/**************************************************************************************
** Process new text from client
***************************************************************************************/
void ISNewText (const char *dev, const char *name, char *texts[], char *names[], int n)
{
 ISInit();
 simpleDevice->ISNewText(dev, name, texts, names, n);
}
/**************************************************************************************
** Process new number from client
***************************************************************************************/
void ISNewNumber (const char *dev, const char *name, double values[], char *names[], int n)
{
 ISInit();
 simpleDevice->ISNewNumber(dev, name, values, names, n);
}
/**************************************************************************************
** Process new blob from client
***************************************************************************************/
void ISNewBLOB (const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n)
{
    ISInit();
    simpleDevice->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n);
}
/**************************************************************************************
** Process snooped property from another driver
***************************************************************************************/
void ISSnoopDevice (XMLEle *root)
{
  INDI_UNUSED(root);
}

Next, we implement the three functions we defined in the header file

SimpleDevice::SimpleDevice()
{
}
/**************************************************************************************
** Client is asking us to establish connection to the device
***************************************************************************************/
bool SimpleDevice::Connect()
{
    IDMessage(getDeviceName(), "Simple device connected successfully!");
    return true;
}
/**************************************************************************************
** Client is asking us to terminate connection to the device
***************************************************************************************/
bool SimpleDevice::Disconnect()
{
    IDMessage(getDeviceName(), "Simple device disconnected successfully!");
    return true;
}
/**************************************************************************************
** INDI is asking us for our default device name
***************************************************************************************/
const char * SimpleDevice::getDefaultName()
{
    return "Simple Device";
}

So that's it! SimpleDevice is available in the INDI Library distribution (tutorial_one). You can test it by running indiserver:

indiserver -v ./tutorial_one

This establishes INDI server on the localhost at port 7624 by default. You can then connect to the INDI server using your favorite client. The following screenshot is of the Simple Device running in KStars

Simple Device under KStarsSimple Device under KStars

More in depth tutorials for different classes of devices are available in the Tutorials section.

Login

3rd Party

Choose from the numerous 3rd party INDI drivers to suit your needs!

Got Problem?

Check out the FAQ, the forum, and the bug tracking system to resolve any issues you might have!
You can also subscribe to INDI newsletter and development mailing lists to get the latest updates on INDI!