PyIndi-Client
The PyIndi-Client
library provides Python bindings for the INDI (Instrument-Neutral Distributed Interface) protocol, enabling seamless control of astronomical devices like telescopes, CCDs, filter wheels, focusers, and more. It bridges Python applications and the INDI server using SWIG-generated interfaces.
Overview
PyIndi-Client
allows developers to:
- Connect to a running INDI server
- Communicate with various INDI-compatible devices
- Monitor and control device properties and states
- Automate observation tasks using Python scripts
It is ideal for astronomers and developers seeking to build custom observatory control software in Python.
Installation
Prerequisites
Ensure the following dependencies are installed:
Ubuntu/Debian:
sudo apt-get install python3-dev python3-setuptools libindi-dev swig libcfitsio-dev libnova-dev
Fedora:
sudo dnf install python3-devel python3-setuptools libindi-devel swig libcfitsio-devel libnova-devel
Installation Methods
From APT Repository (Ubuntu-based):
sudo add-apt-repository ppa:mutlaqja/ppa
sudo apt update
sudo apt install python3-indi-client
From PyPI:
pip install pyindi-client
From Source:
git clone https://github.com/indilib/pyindi-client.git
cd pyindi-client
python3 setup.py install
If you encounter build errors, ensure libindiclient.a
is accessible. You may need to update the libindisearchpaths
in setup.py
accordingly.
Basic Usage
In the following simple example, an INDI 2.0.0 client class is defined giving the implementation of the virtual INDI client functions. This is not mandatory. This class is instantiated once, and after defining server host and port in this object, a list of devices together with their properties is printed on the console.
# for logging
import sys
import time
import logging
# import the PyIndi module
import PyIndi
# The IndiClient class which inherits from the module PyIndi.BaseClient class
# Note that all INDI constants are accessible from the module as PyIndi.CONSTANTNAME
class IndiClient(PyIndi.BaseClient):
def __init__(self):
super(IndiClient, self).__init__()
self.logger = logging.getLogger('IndiClient')
self.logger.info('creating an instance of IndiClient')
def newDevice(self, d):
'''Emmited when a new device is created from INDI server.'''
self.logger.info(f"new device {d.getDeviceName()}")
def removeDevice(self, d):
'''Emmited when a device is deleted from INDI server.'''
self.logger.info(f"remove device {d.getDeviceName()}")
def newProperty(self, p):
'''Emmited when a new property is created for an INDI driver.'''
self.logger.info(f"new property {p.getName()} as {p.getTypeAsString()} for device {p.getDeviceName()}")
def updateProperty(self, p):
'''Emmited when a new property value arrives from INDI server.'''
self.logger.info(f"update property {p.getName()} as {p.getTypeAsString()} for device {p.getDeviceName()}")
def removeProperty(self, p):
'''Emmited when a property is deleted for an INDI driver.'''
self.logger.info(f"remove property {p.getName()} as {p.getTypeAsString()} for device {p.getDeviceName()}")
def newMessage(self, d, m):
'''Emmited when a new message arrives from INDI server.'''
self.logger.info(f"new Message {d.messageQueue(m)}")
def serverConnected(self):
'''Emmited when the server is connected.'''
self.logger.info(f"Server connected ({self.getHost()}:{self.getPort()})")
def serverDisconnected(self, code):
'''Emmited when the server gets disconnected.'''
self.logger.info(f"Server disconnected (exit code = {code},{self.getHost()}:{self.getPort()})")
logging.basicConfig(format = '%(asctime)s %(message)s', level = logging.INFO)
# Create an instance of the IndiClient class and initialize its host/port members
indiClient=IndiClient()
indiClient.setServer("localhost", 7624)
# Connect to server
print("Connecting and waiting 1 sec")
if not indiClient.connectServer():
print(f"No indiserver running on {indiClient.getHost()}:{indiClient.getPort()} - Try to run")
print(" indiserver indi_simulator_telescope indi_simulator_ccd")
sys.exit(1)
# Waiting for discover devices
time.sleep(1)
# Print list of devices. The list is obtained from the wrapper function getDevices as indiClient is an instance
# of PyIndi.BaseClient and the original C++ array is mapped to a Python List. Each device in this list is an
# instance of PyIndi.BaseDevice, so we use getDeviceName to print its actual name.
print("List of devices")
deviceList = indiClient.getDevices()
for device in deviceList:
print(f" > {device.getDeviceName()}")
# Print all properties and their associated values.
print("List of Device Properties")
for device in deviceList:
print(f"-- {device.getDeviceName()}")
genericPropertyList = device.getProperties()
for genericProperty in genericPropertyList:
print(f" > {genericProperty.getName()} {genericProperty.getTypeAsString()}")
if genericProperty.getType() == PyIndi.INDI_TEXT:
for widget in PyIndi.PropertyText(genericProperty):
print(f" {widget.getName()}({widget.getLabel()}) = {widget.getText()}")
if genericProperty.getType() == PyIndi.INDI_NUMBER:
for widget in PyIndi.PropertyNumber(genericProperty):
print(f" {widget.getName()}({widget.getLabel()}) = {widget.getValue()}")
if genericProperty.getType() == PyIndi.INDI_SWITCH:
for widget in PyIndi.PropertySwitch(genericProperty):
print(f" {widget.getName()}({widget.getLabel()}) = {widget.getStateAsString()}")
if genericProperty.getType() == PyIndi.INDI_LIGHT:
for widget in PyIndi.PropertyLight(genericProperty):
print(f" {widget.getLabel()}({widget.getLabel()}) = {widget.getStateAsString()}")
if genericProperty.getType() == PyIndi.INDI_BLOB:
for widget in PyIndi.PropertyBlob(genericProperty):
print(f" {widget.getName()}({widget.getLabel()}) = <blob {widget.getSize()} bytes>")
# Disconnect from the indiserver
print("Disconnecting")
indiClient.disconnectServer()
API Overview
BaseClient
The main interface to the INDI server.
Methods:
setServer(hostname, port)
: Set the target server address.connectServer()
: Connect to the INDI server.disconnectServer()
: Disconnect from the server.getDevices()
: Returns a list of connected INDI devices.getDevice(name)
: Fetch a device by name.
Callbacks (to override):
newProperty(property)
: Triggered when a new property is defined.removeProperty(property)
: Triggered when a property is removed.updateProperty(property)
: Triggered when a property is updated.
Property Classes
INDI::PropertyNumber
Control numerical properties such as focus position or CCD temperature.
INDI::PropertySwitch
Manage toggle and selection properties (e.g., turning tracking on/off).
INDI::PropertyText
Send or receive text-based commands or data.
INDI::PropertyBLOB
Used for transmitting image or binary data from devices.
Advanced Examples
Setting a Property
You can set the value directly if you know the element (widget) index or try to find it first, then set its value
ccd = client.getDevice("CCD Simulator")
prop = ccd.getNumber("CCD_EXPOSURE")
# Set by index
prop[0].setValue(-5)
# OR Find Widget and Set value
widget = prop.findWidgetByName("CCD_TEMPERATURE_VALUE")
widget.setValue(-5)
# After adjusting property value, send back to client
client.sendNewNumber(prop)
NewProperty and UpdateProperty
You may want to define how your client reacts when the INDI server sends notifications about new or updated properties.
import PyIndi
import logging
class MyIndiClient(PyIndi.BaseClient):
def __init__(self):
super(MyIndiClient, self).__init__()
self.logger = logging.getLogger('MyIndiClient')
def newDevice(self, d):
self.logger.info(f"New device connected: {d.getDeviceName()}")
def newProperty(self, p):
self.logger.info(f"New property '{p.getName()}' for device '{p.getDeviceName()}' of type '{p.getTypeAsString()}'")
# You might want to store this property object or process its initial state here
if p.getName() == "CCD_TEMPERATURE":
number_property = PyIndi.PropertyNumber(p)
for element in number_property:
self.logger.info(f" {element.name} ({element.label}) = {element.value}")
elif p.getName() == "CCD_EXPOSURE":
number_property = PyIndi.PropertyNumber(p)
for element in number_property:
self.logger.info(f" {element.name} ({element.label}) = {element.value}")
# You could set an initial exposure time here, for example:
# element.value = 10.0
# self.sendNewNumber(number_property)
def updateProperty(self, p):
self.logger.info(f"Property '{p.getName()}' for device '{p.getDeviceName()}' updated")
if p.getName() == "CCD_TEMPERATURE":
number_property = PyIndi.PropertyNumber(p)
for element in number_property:
self.logger.info(f" {element.name} ({element.label}) = {element.value}")
# You could react to temperature changes here
elif p.getName() == "CCD_EXPOSURE":
number_property = PyIndi.PropertyNumber(p)
for element in number_property:
self.logger.info(f" {element.name} ({element.label}) = {element.value}")
# You might check if the exposure has finished here
# ... other necessary methods like newMessage, serverConnected, serverDisconnected, etc.
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
indi_client = MyIndiClient()
indi_client.setServer("localhost", 7624) # Replace with your INDI server address and port
if not indi_client.connectServer():
print("Could not connect to INDI server!")
exit(1)
try:
while True:
pass # Keep the client running to receive events
except KeyboardInterrupt:
indi_client.disconnectServer()
Enabling BLOB Mode
client.setBLOBMode(PyIndi.B_ALSO, "CCD Simulator", None)
Switching Telescope Tracking On
telescope = client.getDevice("Telescope Simulator")
switch_vector = telescope.getSwitch("TELESCOPE_TRACK_STATE")
# By index
switch_vector[0].setState(PyIndi.ISS_ON)
switch_vector[1].setState(PyIndi.ISS_OFF)
# OR Find Widget and Set value
widget = prop.findWidgetByName("TRACK_ON")
if widget:
widget.setState(PyIndi.ISS_ON)
widget = prop.findWidgetByName("TRACK_OFF")
if widget:
widget.setState(PyIndi.ISS_OFF)
client.sendNewSwitch(switch_vector)
Slewing Telescope to a Target
telescope = client.getDevice("Telescope Simulator")
coords = telescope.getNumber("EQUATORIAL_EOD_COORD")
coords[0].setValue(5.5) # RA in hours
coords[1].setValue(-10.0) # DEC in degrees
client.sendNewNumber(coords)
Capturing and Saving CCD Image
class MyClient(PyIndi.BaseClient):
def newBLOB(self, bp):
for b in bp:
with open("image.fits", "wb") as f:
f.write(b.getblob())
print("Saved image.fits")
client = MyClient()
client.setServer("localhost", 7624)
client.connectServer()
client.setBLOBMode(PyIndi.B_ALSO, "CCD Simulator", None)
ccd = client.getDevice("CCD Simulator")
ccd_exposure = ccd.getNumber("CCD_EXPOSURE")
ccd_exposure[0].setValue(2.0)
client.sendNewNumber(ccd_exposure)
Full Automation Script Example
import time
class AutomationClient(PyIndi.BaseClient):
def newBLOB(self, bp):
for b in bp:
filename = f"capture_{int(time.time())}.fits"
with open(filename, "wb") as f:
f.write(b.getblob())
print(f"Saved {filename}")
client = AutomationClient()
client.setServer("localhost", 7624)
client.connectServer()
client.setBLOBMode(PyIndi.B_ALSO, "CCD Simulator", None)
# Slew telescope
telescope = client.getDevice("Telescope Simulator")
coords = telescope.getNumber("EQUATORIAL_EOD_COORD")
coords[0].value = 6.0
coords[1].value = -5.0
client.sendNewNumber(coords)
# Wait for settling
time.sleep(10)
# Capture image
ccd = client.getDevice("CCD Simulator")
exposure = ccd.getNumber("CCD_EXPOSURE")
exposure[0].value = 3.0
client.sendNewNumber(exposure)
Notes
See the examples for more simple demos of using pyindi-client.
See the interface file for an insight of what is wrapped and how.
For documentation on the methods of INDI Client API, refer to the INDI C++ API documentation.
INDI Version Compatibility
Versions | commit | pip |
---|---|---|
v2.0.4 - latest | HEAD | pip3 install 'git+https://github.com/indilib/pyindi-client.git' |
v2.0.0 - v2.0.3 | indilib/pyindi-client@674706f | pip3 install 'git+https://github.com/indilib/pyindi-client.git@674706f#egg=pyindi-client' |
v1.9.9 | indilib/pyindi-client@ce808b7 | pip3 install 'git+https://github.com/indilib/pyindi-client.git@ce808b7#egg=pyindi-client' |
v1.9.8 | indilib/pyindi-client@ffd939b | pip3 install 'git+https://github.com/indilib/pyindi-client.git@ffd939b#egg=pyindi-client' |
Docker Support
The repository includes a Dockerfile
for containerized development and testing.
docker build -t pyindi-client .
docker run -it pyindi-client
Contributing
We welcome contributions! Please submit issues or pull requests via the GitHub repository.
License
PyIndi-Client
is licensed under the GPLv3. See the LICENSE file for details.