Client Development Tutorial

In this short tutorial, our client (tutorial_client) needs connect to an INDI server which may be running several drivers. However, we only need to receive information regarding the "CCD Simulator" driver. Once received, we try to set the temperature to -20 C. In order to establish a client, we need to subclass INDI::BaseClient and extend it as necessary.

Client Tutorial Header

class MyClient : public INDI::BaseClient
{
 public:
    MyClient();
    ~MyClient();
    void setTemperature();
protected:
    virtual void newDevice(INDI::BaseDevice *dp);
    virtual void newProperty(INDI::Property *property);
    virtual void removeProperty(INDI::Property *property) {}
    virtual void newBLOB(IBLOB *bp) {}
    virtual void newSwitch(ISwitchVectorProperty *svp) {}
    virtual void newNumber(INumberVectorProperty *nvp);
    virtual void newMessage(INDI::BaseDevice *dp, int messageID);
    virtual void newText(ITextVectorProperty *tvp) {}
    virtual void newLight(ILightVectorProperty *lvp) {}
    virtual void serverConnected() {}
    virtual void serverDisconnected(int exit_code) {}
private:
   INDI::BaseDevice * ccd_simulator;
};

Above we declared all the functions from INDI::BaseMediator. For this tutorial, we will only implement a few of those for the purpose of the tutorial.

Client Tutorial Source

#define MYCCD "Simple CCD"
/* Our client auto pointer */
auto_ptr<MyClient> camera_client(0);
int main(int argc, char *argv[])
{
    if (camera_client.get() == 0)
        camera_client.reset(new MyClient());
  camera_client->setServer("localhost", 7624);
  camera_client->watchDevice(MYCCD);
  camera_client->connectServer();
  cout << "Press any key to terminate the client.\n";
  string term;
  cin >> term;
}

In main(), we initiate the client object camera_client, then we set the server parameters. Note that by default, INDI::BaseClient will connect to an INDI server running on localhost and on port 7624. Next, we ask the client to only watch for a specific device. In case an INDI server is running multiple drivers, we are only interested in one or more devices. If watchDevice() is not set, INDI::BaseClient will fetch all devices from the INDI servers and build virtual device for each.

Finally, we connect to the server. Note that we're not checking for any error messages here to make it simple.

What happens next is as following:

  1. INDI::BaseClient will attempt to connect to an INDI server.
  2. If successful, it issues a getProperties command to INDI server with the device CCD Simulator.
  3. Upon reception of the first device property from CCD Simulator, it issues newDevice and newProperty notifications.
void MyClient::newDevice(INDI::BaseDevice *dp)
{
    if (!strcmp(dp->getDeviceName(), MYCCD))
        IDLog("Receiving %s Device...\n", dp->getDeviceName());
    ccd_simulator = dp;
}

Here we insure that the device received is indeed our simulator. Then we ask the client to gives us a pointer to the driver's instance for future use in the class.

An important note to consider is that there is no way a client can tell whether or not we have received all the properties of a particular driver. This is because of the very nature of INDI protocol where devices are discovered via introspection. Therefore, the client may either choose to wait for a period of time until it begins processing the driver, or watch for a particular property of interest. Since we are planning to change the CCD temperature, we are interested in INDI Standard Property CCD_TEMPERATURE which we will watch for in newProperty()

void MyClient::newProperty(INDI::Property *property)
{
    if (!strcmp(property->getDeviceName(), MYCCD) && !strcmp(property->getName(), "CONNECTION"))
    {
        connectDevice(MYCCD);
        return;
    }
    if (!strcmp(property->getDeviceName(), MYCCD) && !strcmp(property->getName(), "CCD_TEMPERATURE"))
    {
        if (ccd_simulator->isConnected())
        {
            IDLog("CCD is connected. Setting temperature to -20 C.\n");
            setTemperature();
        }
        return;
    }
}

Above we insure we are receiving the correct property from the correct device, then we issue our first command to the CCD driver: Connect. If setDriverConnection is passed false instead, the client will issue a Disconnect command. Once the device is connected, it will define other properties, and we are interested in CCD_TEMPERATURE property. Once this property is defined, we ensure that the device is still connected, and then issue setTemperature() command.

void MyClient::setTemperature()
{
   INumberVectorProperty *ccd_temperature = NULL;
   ccd_temperature = ccd_simulator->getNumber("CCD_TEMPERATURE");
   if (ccd_temperature == NULL)
   {
       IDLog("Error: unable to find CCD Simulator CCD_TEMPERATURE property...\n");
       return;
   }
   ccd_temperature->np[0].value = -20;
   sendNewNumber(ccd_temperature);
}

Here we get a pointer to the CCD_TEMPERATURE numeric property. If found (which we should since we just received it in newProperty), we set it to -20, and then call sendNewNumber to send the new value to the driver.

Finally, we should be expecting the driver to comply and update the CCD_TEMPERATURE property.

void MyClient::newNumber(INumberVectorProperty *nvp)
{
    // Let's check if we get any new values for CCD_TEMPERATURE
    if (!strcmp(nvp->name, "CCD_TEMPERATURE"))
    {
       IDLog("Receving new CCD Temperature: %g C\n", nvp->np[0].value);
       if (nvp->np[0].value == -20)
           IDLog("CCD temperature reached desired value!\n");
   }
}

Running

Open two console windows, and in each go to libindi cmake build directory (e.g. /home/jsmith/libindi/build) as these tutorials do not get installed to /usr/bin. On the first console, run indiserver with the CCD Simulator which is tutorial_three:

indiserver ./tutorial_three

On the second console, run tutorial_client:

./tutorial_client

f everything works fine, you should be getting the following output:

Press any key to terminate the client.
Receiving CCD Simulator Device...
CCD_TEMPERATURE standard property defined. Attempting connection to CCD...
CCD is connected. Setting temperature to -20 C.
Receving new CCD Temperature: 0 C
Receving new CCD Temperature: -1 C
Receving new CCD Temperature: -2 C
Receving new CCD Temperature: -3 C
Receving new CCD Temperature: -4 C
Receving new CCD Temperature: -5 C
Receving new CCD Temperature: -6 C
Receving new CCD Temperature: -7 C
Receving new CCD Temperature: -8 C
Receving new CCD Temperature: -9 C
Receving new CCD Temperature: -10 C
Receving new CCD Temperature: -11 C
Receving new CCD Temperature: -12 C
Receving new CCD Temperature: -13 C
Receving new CCD Temperature: -14 C
Receving new CCD Temperature: -15 C
Receving new CCD Temperature: -16 C
Receving new CCD Temperature: -17 C
Receving new CCD Temperature: -18 C
Receving new CCD Temperature: -19 C
Receving new CCD Temperature: -20 C
CCD temperature reached desired value!
Receving new CCD Temperature: -20 C
CCD temperature reached desired value!

That's it! tt's that easy to write a client!

Simple Device Tutorial

In this tutorial we will create the most basic INDI driver: a simple device! The simple device performs no functions, it just connects and disconnects.

All INDI devices inherit from INDI::DefaultDevice. Some specialized classes like INDI::Telescope also inherits from INDI::DefaultDevice and specific telescope drivers then inherit from INDI::Telescope.

Let's checkout the header file for Simple Device, which is included in the INDI Library package

#include "indibase/defaultdevice.h"

class SimpleDevice : public INDI::DefaultDevice
{
public:
    SimpleDevice();

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

};

Above, we declared a SimpleDevice class and inherited from INDI::DefaultDevice. We declared two functions that are called by the INDI framework when a client wants to Connect to or Disconnect from the device. The last function is called by INDI framework to find out the default name of the device, which may change at run time if necessary.

Let's look now at the implementation of the driver.

std::auto_ptr simpleDevice(0);

First, we declare an auto pointer to our class, we will use the pointer below to instantiate the object.

void ISInit()
{
 static int isInit=0;
 if (isInit)
  return;
 if (simpleDevice.get() == 0)
 {
     isInit = 1;
     simpleDevice.reset(new SimpleDevice());
 }
}

There are six mandatory INDI functions called by the INDI framework that must be declared in ALL INDI drivers:

  • ISGetProperties(): Is called when a client wants to retrieve a list of properties from the device.
  • ISNewSwitch: Is called when a client wants to set a new switch value.
  • ISNewText: Is called when a client wants to set a new text value.
  • ISNewNumber: Is called when a client wants to set a new number value.
  • ISNewBLOB: Is called when a client wants to set a new blob (Binary Large Object) value.
  • ISSnoopDevice: Is called when we receive property updates from other drivers we are monitoring for changes.

We call ISInit() from these functions to ensure proper initialization of the SimpleDevice pointer. We then simply pass the calls of the functions above to the SimpleDevice object which will handle them properly.

/**************************************************************************************
** 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 Connect() and Disconnect() functions. We simply inform the client that the device was connected/disconnected successfully and return true.

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;
}

Finally, we implement the getDefaultName() function.

/**************************************************************************************
** INDI is asking us for our default device name
***************************************************************************************/
const char * SimpleDevice::getDefaultName()
{
    return "Simple Device";
}

That's all for the most simple INDI device driver! You will also note that SimpleDevice create another tab Options, this is created by default for all devices and includes options to load/save configuration in addition to advanced options such as simulation or debugging.

Subscribe to INDI

SUBSCRIBE TO THE NEWSLETTER
NEWSLETTER
NAME
EMAIL{emailcloak=off}
 

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!