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 "Simple CCD" driver. Once received, we try to set the temperature to -20 C. In order to establish a client, we need to subclass INDI::BaseClient
.
To manipulate the selected device, we must first access it using the watchDevice
method as the device can appear dynamically. The properties of the device are also dynamic, that is, they can come and go. To obtain access to selected properties, we use the watchProperty
method, which informs us about availability and passes an object on which we can perform the operation of reading and writing the value.
The source for this tutorial can be found here.
Building
Let's follow the example code:
tutorial_client.h
class MyClient : public INDI::BaseClient
{
public:
MyClient();
~MyClient() = default;
public:
void setTemperature(double value);
void takeExposure(double seconds);
protected:
void newMessage(INDI::BaseDevice *dp, int messageID) override;
private:
INDI::BaseDevice mSimpleCCD;
};
Above, we declare our class with the methods of interest to us. By inheriting from the INDI::BaseClient
class, we get a whole set of functionalities that will allow us to communicate with the INDI server and to manipulate drivers. Additionally, the overwritten method newMessage
will allow us to read messages from the server.
tutorial_client.cpp
int main(int, char *[])
{
MyClient myClient;
myClient.setServer("localhost", 7624);
myClient.connectServer();
myClient.setBLOBMode(B_ALSO, "Simple CCD", nullptr);
myClient.enableDirectBlobAccess("Simple CCD", nullptr);
std::cout << "Press Enter key to terminate the client.\n";
std::cin.ignore();
}
In main()
, we initiate the client object as myClient, 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.
The whole process of detecting the device and properties we are interested in is entered in the constructor of our class, so that after connecting to the server, the INDI::BaseClient
class can read the received information and inform us.
MyClient::MyClient()
{
// wait for the availability of the "Simple CCD" device
watchDevice("Simple CCD", [](INDI::BaseDevice device)
{
// wait for the availability of the "CONNECTION" property
device.watchProperty("CONNECTION", [](INDI::Property)
{
/* ... */
});
// wait for the availability of the "CCD_TEMPERATURE" property
device.watchProperty("CCD_TEMPERATURE", [](INDI::PropertyNumber)
{
/* ... */
});
// wait for the availability of the "CCD1" property
device.watchProperty("CCD1", [](INDI::PropertyBlob)
{
/* ... */
});
});
}
The names of the observed properties result from the structure of the given driver. In our case, for the Simple CCD
driver, we are interested in:
CONNECTION
- property of typeINDI::PropertySwitch
, however, if we are not interested in the property, but in its appearance, we can omit the variable name in the argument as its type, using the genericINDI::Property
CCD_TEMPERATURE
- property of typeINDI::PropertyNumber
, to which we can enter the set temperature of the camera and read the current oneCCD1
- property of typeINDI::PropertyBlob
, which stores the captured photo from the camera
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.
To detect the change of a given property, we use the onUpdate
method, which calls the function we specified when the property is updated:
device.watchProperty("CCD_TEMPERATURE", [](INDI::PropertyNumber property)
{
// call lambda function if property changed
property.onUpdate([]()
{
// property changed!
});
});
Now let's add an implementation that provides us with:
- after the appearance of the
CONNECT
property, connect to the device - after the appearance of the property
CCD_TEMPERATURE
, set the temperature to -20 C - when changing the property
CCD_TEMPERATURE
, display the current temperature, and when it reaches -20 C, take a picture - if there is a new value for the property
CCD1
, save the picture to disk
MyClient::MyClient()
{
// wait for the availability of the device
watchDevice("Simple CCD", [this](INDI::BaseDevice device)
{
mSimpleCCD = device; // save device
// wait for the availability of the "CONNECTION" property
device.watchProperty("CONNECTION", [this](INDI::Property)
{
IDLog("Connecting to INDI Driver...\n");
connectDevice("Simple CCD");
});
// wait for the availability of the "CCD_TEMPERATURE" property
device.watchProperty("CCD_TEMPERATURE", [this](INDI::PropertyNumber property)
{
if (mSimpleCCD.isConnected())
{
IDLog("CCD is connected.\n");
setTemperature(-20);
}
// call lambda function if property changed
property.onUpdate([property, this]()
{
IDLog("Receving new CCD Temperature: %g C\n", property[0].getValue());
if (property[0].getValue() == -20)
{
IDLog("CCD temperature reached desired value!\n");
takeExposure(1);
}
});
});
// wait for the availability of the "CCD1" property
device.watchProperty("CCD1", [](INDI::PropertyBlob property)
{
// call lambda function if property changed
property.onUpdate([property]()
{
// Save FITS file to disk
std::ofstream myfile;
myfile.open("ccd_simulator.fits", std::ios::out | std::ios::binary);
myfile.write(static_cast<char *>(property[0].getBlob()), property[0].getBlobLen());
myfile.close();
IDLog("Received image, saved as ccd_simulator.fits\n");
});
});
});
}
We have already implemented device detection, reading properties and information about its change. Now let's move on to setting property values.
void MyClient::setTemperature(double value)
{
INDI::PropertyNumber ccdTemperature = mSimpleCCD.getProperty("CCD_TEMPERATURE");
if (!ccdTemperature.isValid())
{
IDLog("Error: unable to find Simple CCD, CCD_TEMPERATURE property...\n");
return;
}
IDLog("Setting temperature to %g C.\n", value);
ccdTemperature[0].setValue(value);
sendNewProperty(ccdTemperature);
}
Here we use the mSimpleCCD
variable that we got while watching the device to look for the property CCD_TEMPERATURE
in order to change it. The INDI::BaseDevice
and INDI::PropertyXXX
classes are constructed in such a way that in the absence of a given property, the isValid
method will return false. Then we set a new value and send it to the driver.
Finally, we should be expecting the driver to comply and update the CCD_TEMPERATURE
property.
New and Updated properties
Client can receive notifications for any newly defined property or an updated value to an existing property. Once a device is first defined newDevice, it is not possible to know which device subtypes are supported by the driver up until DRIVER_INFO is first defined. The DRIVER_INFO.DRIVER_INTERFACE text property is a number that defines the interface(s) supported by the driver. This is an ORed list of interface (e.g. TELESCOPE_INTERFACE | CAMERA_INTERFACE | FOCUSER_INTERFACE)..etc. |
void MyClient::newProperty(INDI::Property property)
{
auto baseDevice = property.getBaseDevice();
auto deviceName = baseDevice.getDeviceName();
// With DRIVER_INFO defined, we have access to getDriverInterface()
if (property.isNameMatch("DRIVER_INFO"))
{
auto interface = baseDevice.getDriverInterface();
if (interface & BaseDevice::TELESCOPE_INTERFACE)
{
IDLog("Found telescope: %s\n", deviceName);
}
else if (interface & BaseDevice::CCD_INTERFACE)
{
IDLog("Found camera: %s\n", deviceName);
setBLOBMode(B_ALSO, deviceName, nullptr);
enableDirectBlobAccess(deviceName, nullptr);
}
}
}
In any property is updated from the server, it can be inspected in updateProperty
void MyClient::updateProperty(INDI::Property property)
{
if (property.isNameMatch("CCD_TEMPERATURE"))
{
// Need to use .getNumber to convert the generic property to a NUMBER property.
// Then we access its first number elemenet and retrieve the value.
auto value = property.getNumber()->at(0).getValue();
LOGF_INFO("Temperature is %.2f C", value);
}
}
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 Simple CCD which is tutorial_three:
indiserver ./tutorial_three
On the second console, run tutorial_client:
./tutorial_client
If everything works fine, you should be getting the following output:
Press Enter key to terminate the client.
Connecting to INDI Driver...
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!
Taking a 1 second exposure.
Received image, saved as ccd_simulator.fits
That's it! It's that easy to write a client!
Receving BLOBs: By default, INDI server does not send BLOBs to client unless the client explicitly sets BLOB handling mode by making a call to setBLOBMode()
function. You can limit BLOB mode to a specific device and/or property.