Creating Custom INDI Drivers
This guide provides a comprehensive overview of creating custom INDI drivers for devices that aren’t already supported by the INDI library. It covers the basic structure of an INDI driver, how to implement the required methods, and how to handle device-specific functionality.
Introduction to INDI Drivers
INDI (Instrument-Neutral Distributed Interface) drivers are software components that communicate with hardware devices and expose their functionality through INDI properties. They act as a bridge between the INDI server and the hardware, translating INDI protocol messages into device-specific commands and vice versa.
Creating a custom INDI driver involves:
- Understanding the device’s communication protocol
- Implementing the INDI driver interface
- Defining properties to represent the device’s functionality
- Handling property updates and device commands
- Building and testing the driver
Prerequisites
Before creating a custom INDI driver, 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
Driver Structure
An INDI driver consists of several key components:
- Header File: Contains the class declaration, including member variables and method declarations.
- Implementation File: Contains the implementation of the methods declared in the header file.
- Main File: Contains the
main()
function that creates an instance of the driver and runs it. - CMakeLists.txt: Contains the build configuration for the driver.
- XML File: Contains the driver metadata for the INDI server.
Base Classes
INDI provides several base classes that you can inherit from to create your driver:
- INDI::DefaultDevice: The base class for all INDI devices. Use this if your device doesn’t fit into any of the specialized categories.
- INDI::CCD: For CCD cameras and other imaging devices.
- INDI::Telescope: For telescope mounts and other pointing devices.
- INDI::Focuser: For focusers and other focusing devices.
- INDI::FilterWheel: For filter wheels and other filter selection devices.
- INDI::Dome: For domes and other enclosure devices.
- INDI::Weather: For weather stations and other environmental monitoring devices.
- INDI::GPS: For GPS and other location devices.
- INDI::Rotator: For rotators and other rotation devices.
- INDI::Detector: For detectors and other sensing devices.
- INDI::AUX: For auxiliary interfaces and other miscellaneous devices.
Choose the base class that best matches your device’s functionality. If your device doesn’t fit into any of these categories, use INDI::DefaultDevice
.
Creating a Basic Driver
Let’s create a simple INDI driver for a hypothetical device called “MyDevice”. This device has a simple serial interface and supports basic commands for turning on/off and setting a value.
Step 1: Create the Header File
Create a file named mydevicedriver.h
with the following content:
#pragma once
#include <defaultdevice.h>
class MyDeviceDriver : public INDI::DefaultDevice
{
public:
MyDeviceDriver();
virtual ~MyDeviceDriver() = 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;
virtual void TimerHit() override;
// Connection overrides
virtual bool Connect() override;
virtual bool Disconnect() override;
private:
// Helper methods
bool sendCommand(const char *cmd, char *res = nullptr, int reslen = 0);
bool readResponse(char *res, int reslen);
// Properties
INDI::PropertySwitch PowerSP {2};
// Use built-in INDI_ENABLED and INDI_DISABLED for toggle switches
INDI::PropertyNumber ValueNP {1};
// No need for enum when property size is 1
// Connection
int PortFD = -1;
};
Step 2: Create the Implementation File
Create a file named mydevicedriver.cpp
with the following content:
#include "mydevicedriver.h"
#include <memory>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <connectionplugins/connectionserial.h>
// We declare an auto pointer to MyDeviceDriver
static std::unique_ptr<MyDeviceDriver> mydevice(new MyDeviceDriver());
MyDeviceDriver::MyDeviceDriver()
{
// Set the driver version
setVersion(1, 0);
// Set up the connection
setConnectionMode(CONNECTION_SERIAL);
serialConnection = new Connection::Serial(this);
serialConnection->registerHandshake([&]() { return Handshake(); });
serialConnection->setDefaultBaudRate(Connection::Serial::B_9600);
serialConnection->setDefaultPort("/dev/ttyUSB0");
registerConnection(serialConnection);
}
const char *MyDeviceDriver::getDefaultName()
{
return "My Device";
}
bool MyDeviceDriver::initProperties()
{
// Initialize the parent's properties
INDI::DefaultDevice::initProperties();
// Initialize power switch
PowerSP[INDI_ENABLED].fill("POWER_ON", "On", ISS_OFF);
PowerSP[INDI_DISABLED].fill("POWER_OFF", "Off", ISS_ON);
PowerSP.fill(getDeviceName(), "POWER", "Power", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
// Initialize value number
ValueNP[0].fill("VALUE", "Value", "%6.2f", 0, 100, 1, 50);
ValueNP.fill(getDeviceName(), "DEVICE_VALUE", "Value", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
// Add debug, simulation, and configuration controls
addAuxControls();
return true;
}
bool MyDeviceDriver::updateProperties()
{
// Call the parent's updateProperties
INDI::DefaultDevice::updateProperties();
if (isConnected())
{
// Define properties when connected
defineProperty(PowerSP);
defineProperty(ValueNP);
// Start the timer
SetTimer(POLLMS);
}
else
{
// Delete properties when disconnected
deleteProperty(PowerSP);
deleteProperty(ValueNP);
}
return true;
}
bool MyDeviceDriver::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()))
{
// Check if the message is for the value property
if (!strcmp(name, ValueNP.name))
{
// Update the property values
ValueNP.update(values, names, n);
// Send the new value to the device
char cmd[32];
snprintf(cmd, sizeof(cmd), "SET_VALUE %.2f\r\n", ValueNP[0].getValue());
if (!sendCommand(cmd))
{
LOG_ERROR("Failed to set value");
ValueNP.setState(IPS_ALERT);
ValueNP.apply();
return false;
}
// Value set successfully
LOG_INFO("Value set successfully");
ValueNP.setState(IPS_OK);
ValueNP.apply();
return true;
}
}
// If the message is not for this device or property, call the parent's ISNewNumber
return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n);
}
bool MyDeviceDriver::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()))
{
// Check if the message is for the power property
if (!strcmp(name, PowerSP.name))
{
// Update the property values
PowerSP.update(states, names, n);
// Send the power command to the device
const char *cmd = (PowerSP[INDI_ENABLED].getState() == ISS_ON) ? "POWER_ON\r\n" : "POWER_OFF\r\n";
if (!sendCommand(cmd))
{
LOG_ERROR("Failed to set power state");
PowerSP.setState(IPS_ALERT);
PowerSP.apply();
return false;
}
// Power state set successfully
LOG_INFO("Power state set successfully");
PowerSP.setState(IPS_OK);
PowerSP.apply();
return true;
}
}
// If the message is not for this device or property, call the parent's ISNewSwitch
return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);
}
void MyDeviceDriver::TimerHit()
{
// Check if the device is connected
if (!isConnected())
return;
// Get the current value from the device
char res[32];
if (sendCommand("GET_VALUE\r\n", res, sizeof(res)))
{
// Parse the response
float value;
if (sscanf(res, "VALUE=%f", &value) == 1)
{
// Update the value property
ValueNP[0].setValue(value);
ValueNP.setState(IPS_OK);
ValueNP.apply();
}
}
// Get the current power state from the device
if (sendCommand("GET_POWER\r\n", res, sizeof(res)))
{
// Parse the response
if (strstr(res, "POWER=ON"))
{
// Update the power property
PowerSP[INDI_ENABLED].setState(ISS_ON);
PowerSP[INDI_DISABLED].setState(ISS_OFF);
PowerSP.setState(IPS_OK);
PowerSP.apply();
}
else if (strstr(res, "POWER=OFF"))
{
// Update the power property
PowerSP[INDI_ENABLED].setState(ISS_OFF);
PowerSP[INDI_DISABLED].setState(ISS_ON);
PowerSP.setState(IPS_OK);
PowerSP.apply();
}
}
// Set the timer for the next update
SetTimer(POLLMS);
}
bool MyDeviceDriver::Connect()
{
// Call the parent's Connect method
bool result = INDI::DefaultDevice::Connect();
if (result)
{
// Get the file descriptor for the serial port
PortFD = serialConnection->getPortFD();
// Send a test command to verify the connection
if (!sendCommand("PING\r\n"))
{
LOG_ERROR("Failed to communicate with the device");
return false;
}
LOG_INFO("Device connected successfully");
}
return result;
}
bool MyDeviceDriver::Disconnect()
{
// Close the serial port
if (PortFD > 0)
{
close(PortFD);
PortFD = -1;
}
// Call the parent's Disconnect method
return INDI::DefaultDevice::Disconnect();
}
bool MyDeviceDriver::sendCommand(const char *cmd, char *res, int reslen)
{
// Check if the port is open
if (PortFD < 0)
{
LOG_ERROR("Serial port not open");
return false;
}
// Write the command
int nbytes_written = write(PortFD, cmd, strlen(cmd));
if (nbytes_written < 0)
{
LOGF_ERROR("Error writing to device: %s", strerror(errno));
return false;
}
// If no response is expected, return success
if (res == nullptr || reslen <= 0)
return true;
// Read the response
if (!readResponse(res, reslen))
{
LOG_ERROR("Error reading response from device");
return false;
}
return true;
}
bool MyDeviceDriver::readResponse(char *res, int reslen)
{
// Check if the port is open
if (PortFD < 0)
{
LOG_ERROR("Serial port not open");
return false;
}
// Read the response
int nbytes_read = read(PortFD, res, reslen - 1);
if (nbytes_read < 0)
{
LOGF_ERROR("Error reading from device: %s", strerror(errno));
return false;
}
// Null-terminate the response
res[nbytes_read] = '\0';
return true;
}
Step 3: Create the Main File
Create a file named main.cpp
with the following content:
#include "mydevicedriver.h"
int main(int argc, char *argv[])
{
// Create and initialize the driver
std::unique_ptr<MyDeviceDriver> mydevice(new MyDeviceDriver());
// Set the driver version
mydevice->setVersion(1, 0);
// Start the driver
mydevice->ISGetProperties(nullptr);
// Run the driver
return mydevice->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-mydevice CXX C)
include(GNUInstallDirs)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake_modules/")
find_package(INDI REQUIRED)
find_package(Nova REQUIRED)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${INDI_INCLUDE_DIR})
include_directories(${NOVA_INCLUDE_DIR})
include(CMakeCommon)
add_executable(indi_mydevice mydevicedriver.cpp main.cpp)
target_link_libraries(indi_mydevice ${INDI_LIBRARIES} ${NOVA_LIBRARIES})
install(TARGETS indi_mydevice RUNTIME DESTINATION bin)
Step 5: Create the XML File
Create a file named indi_mydevice.xml
with the following content:
<?xml version="1.0" encoding="UTF-8"?>
<driversList>
<devGroup group="Auxiliary">
<device label="My Device" manufacturer="INDI">
<driver name="My Device">indi_mydevice</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_mydevice
Then, connect to the INDI server using an INDI client, such as the INDI Control Panel:
indi_control_panel
Advanced Topics
Device Interfaces
INDI provides several device interfaces that you can implement to expose specific functionality:
- CCD Interface: For CCD cameras and other imaging devices.
- Telescope Interface: For telescope mounts and other pointing devices.
- Focuser Interface: For focusers and other focusing devices.
- Filter Wheel Interface: For filter wheels and other filter selection devices.
- Dome Interface: For domes and other enclosure devices.
- Weather Interface: For weather stations and other environmental monitoring devices.
- GPS Interface: For GPS and other location devices.
- Rotator Interface: For rotators and other rotation devices.
- Detector Interface: For detectors and other sensing devices.
- AUX Interface: For auxiliary interfaces and other miscellaneous devices.
- Output Interface: For devices that provide output control functionality.
- Guider Interface: For autoguider devices.
- Dustcap Interface: For dust cap devices.
- Lightbox Interface: For light box devices.
- Correlator Interface: For correlator devices.
- Spectrograph Interface: For spectrograph devices.
To implement a device interface, inherit from the corresponding base class and implement the required methods.
Connection Plugins
INDI provides several connection plugins that you can use to connect to devices:
- Serial Connection: For devices that connect via serial ports.
- TCP Connection: For devices that connect via TCP/IP.
To use a connection plugin, create an instance of the plugin and register it with the driver:
// Set up the serial connection
setConnectionMode(CONNECTION_SERIAL);
serialConnection = new Connection::Serial(this);
serialConnection->registerHandshake([&]() { return Handshake(); });
serialConnection->setDefaultBaudRate(Connection::Serial::B_9600);
serialConnection->setDefaultPort("/dev/ttyUSB0");
registerConnection(serialConnection);
Property Types
INDI supports several property types:
- Number: Represents a numeric value.
- Text: Represents a text value.
- Switch: Represents a boolean value or a set of mutually exclusive options.
- Light: Represents a status indicator.
- BLOB: Represents binary data.
Each property type has its own set of functions for creating, updating, and deleting properties.
Simulation Mode
INDI drivers should support simulation mode, which allows users to test the driver without connecting to the actual hardware. To implement simulation mode, check the isSimulation()
flag and provide simulated responses:
bool MyDeviceDriver::sendCommand(const char *cmd, char *res, int reslen)
{
// Check if we're in simulation mode
if (isSimulation())
{
// Simulate a response
if (res && reslen > 0)
{
if (!strcmp(cmd, "GET_VALUE\r\n"))
snprintf(res, reslen, "VALUE=%.2f\r\n", ValueNP[0].getValue());
else if (!strcmp(cmd, "GET_POWER\r\n"))
snprintf(res, reslen, "POWER=%s\r\n", (PowerSP[INDI_ENABLED].getState() == ISS_ON) ? "ON" : "OFF");
else if (!strcmp(cmd, "PING\r\n"))
snprintf(res, reslen, "PONG\r\n");
else
snprintf(res, reslen, "ERROR\r\n");
}
return true;
}
// Real command code
// ...
}
Debug Mode
INDI drivers should support debug mode, which provides additional logging information for troubleshooting. To implement debug mode, use the DEBUGF
and DEBUG
macros:
bool MyDeviceDriver::sendCommand(const char *cmd, char *res, int reslen)
{
// Log the command in debug mode
DEBUGF(INDI::Logger::DBG_DEBUG, "Sending command: %s", cmd);
// Send the command
// ...
// Log the response in debug mode
if (res && reslen > 0)
DEBUGF(INDI::Logger::DBG_DEBUG, "Received response: %s", res);
return true;
}
Configuration
INDI drivers should support saving and loading configuration. To implement configuration support, override the saveConfigItems
method:
bool MyDeviceDriver::saveConfigItems(FILE *fp)
{
// Call the parent's saveConfigItems
INDI::DefaultDevice::saveConfigItems(fp);
// Save custom configuration items
ValueNP.save(fp);
PowerSP.save(fp);
return true;
}
Error Handling
INDI drivers should handle errors gracefully and provide informative error messages. Use the LOG_ERROR
and LOGF_ERROR
macros to log errors:
bool MyDeviceDriver::sendCommand(const char *cmd, char *res, int reslen)
{
// Check if the port is open
if (PortFD < 0)
{
LOG_ERROR("Serial port not open");
return false;
}
// Write the command
int nbytes_written = write(PortFD, cmd, strlen(cmd));
if (nbytes_written < 0)
{
LOGF_ERROR("Error writing to device: %s", strerror(errno));
return false;
}
// ...
}
Best Practices
When creating custom INDI drivers, 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
Creating a custom INDI driver involves implementing the INDI driver interface, defining properties to represent the device’s functionality, and handling property updates and device commands. By following the steps and best practices outlined in this guide, you can create robust and feature-rich INDI drivers for your devices.
For more information, refer to the INDI Library Documentation and the INDI Driver Development Guide.