Contents[Hide]

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. 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).

For hardcoded properties, there are defined explicitly in the header file as illustrated in Extending Simple Device section.

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 DEBUG macro from INDI Debugging and Logging API.

  • 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::unique_ptr simpleDevice(new SimpleDevice());

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.

/**************************************************************************************
** Return properties of device.
***************************************************************************************/
void ISGetProperties (const char *dev)
{
 simpleDevice->ISGetProperties(dev);
}
/**************************************************************************************
** Process new switch from client
***************************************************************************************/
void ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n)
{
 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)
{
 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)
{
 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)
{
    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()
{
    DEBUG(INDI::Logger::DBG_SESSION, "Simple device connected successfully!");
    return true;
}
/**************************************************************************************
** Client is asking us to terminate connection to the device
***************************************************************************************/
bool SimpleDevice::Disconnect()
{
    DEBUG(INDI::Logger::DBG_SESSION, "Simple device disconnected successfully!");
    return true;
}
/**************************************************************************************
** INDI is asking us for our default device name
***************************************************************************************/
const char * SimpleDevice::getDefaultName()
{
    return "Simple Device";
}

The DEBUG macro enables us to log all message to both the client and optionally a file. Above we are sending a message to the client session; other debugging categories include DBG_DEBUG for debugging purposes and DBG_ERROR for errors among others. 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 KStars

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

4. Extending Simple Device

To add more feature to Simple Device, let us add a Switch Property to open and close a door. The property has two switches: Open and Close. We need to define the switch in the header file. Furthermore, we need to override a few functions in order to define and process this new switch property:

#include "indibase/defaultdevice.h"
class SimpleDevice : public INDI::DefaultDevice
{
public:
    SimpleDevice();

    virtual bool ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n) override;

protected:
    bool Connect();
    bool Disconnect();
    const char *getDefaultName();

    virtual bool initProperties() override;
    virtual bool updateProperties() override;
    
private:
   ISwitch DoorS[2];
   ISwitchVectorProperty DoorSP;
   enum { DOOR_OPEN, DOOR_CLOSED };
};

Note we define 2 switches with the name DoorS[2]. This is the coding convention used in INDI where all property start with a Capital letter and indiviual switches end with S. The Switch vector property that contains the two switches is called DoorSP which is short for Switch Property. The enum defined above is optional, but it can help in maintaining a cleaner code by making sure you are accessing the correct indices for the vector throughout the driver code. You shall see this coding convention used throughout INDI drivers.

Now we need to initialize the properties. This is done in the initProperties() function which we override from our parent class.

bool SimpleDevice::initProperties()
{
    // We must ALWAYS init the properties of the parent class first
    DefaultDevice::initProperties();

    // Now let us initilize the switch properties. By default, the door is closed.
    IUFillSwitch(&DoorS[DOOR_OPEN], "DOOR_OPEN", "Open", ISS_OFF);
    IUFillSwitch(&DoorS[DOOR_CLOSED], "DOOR_CLOSED", "Closed", ISS_ON);
    IUFillSwitchVector(&DoorSP, DoorS, 2, getDeviceName(), "DOOR_CONTROL", "Door Control", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
}

Above we are assigning intial names, labels, and states of the switches. Then we initilize the vector property that holds the two switches, including its group, state, and permission level. Since we only want one switch active at a time, we set the switch policy to One of Many. Next we will define our switch property to the client once the device is connected. That is, once the user clicks Connect in our simple device and connection is successfully established. This functionality is performed in updateProperties function which we must also override from the parent class:

bool SimpleDevice::updateProperties()
{
  // We must ALWAYS call the parent class updateProperties() first
  DefaultDevice::updateProperties();

 // If we are connected, we define the property to the client.
 if (isConnected())
    defineSwitch(&DoorSP);
 // Otherwise, we delete the property from the client
 else
    deleteProperty(DoorSP.name);

 return true;
}

Finally, we need to process the switch once the client clicks on it and changes it state. We process switches in ISNewSwitch(..) function which must also be overrideden from the parent class.

bool SimpleDevice::ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n)
{
   // Make sure the call is for our device
   if(!strcmp(dev,getDeviceName()))
   {
      // Check if the call for our door switch
      if (!strcmp(name, DoorSP.name))
      {
          // Find out which state is requested by the client
          const char *actionName = IUFindOnSwitchName(states, names, n);
          // If door is the same state as actionName, then we do nothing. i.e. if actionName is DOOR_OPEN and our door is already open, we return
          int currentDoorIndex = IUFindOnSwitchIndex(&DoorSP);
          if (!strcmp(actionName, DoorS[currentDoorIndex].name))
          {
             DEBUGF(INDI::Logger::DBG_SESSION, "Door is already %s", DoorS[currentDoorIndex].label);
             DoorSP.s = IPS_IDLE;
             IDSetSwitch(&DoorSP, NULL);
             return true;
          }
          
          // Otherwise, let us update the switch state
          IUUpdateSwitch(&DoorSP, states, names, n);
          currentDoorIndex = IUFindOnSwitchIndex(&DoorSP);
          DEBUGF(INDI::Logger::DBG_SESSION, "Door is now %s", DoorS[currentDoorIndex].label);
          DoorSP.s = IPS_OK;
          IDSetSwitch(&DoorSP, NULL);
          return true;
      }
   }

// If we did not process the switch, let us pass it to the parent class to process it
return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);

}

Now compile Simple Device and test its functionality with any INDI compatible client. Study the other tutorials and check out existing INDI drivers for more insight on how to develop drivers for the INDI framework.