In this tutorial we create a simple time lapse capture script with Python for an INDI CCD camera. It works with both Python2 and Python3.
First, we need to install some needed packages for Python INDI wrapper pyindi-client.
If you run Ubuntu or Debian just install the packages with:
sudo apt-get install indi-full swig libcfitsio-dev libnova-dev
If you use a Raspberry PI, just install INDI Library for Raspberry PI instead of indi-full.
For Python2 support install Python2 headers and pip:
sudo apt-get install python-dev python-pip
For Python3 support install Python3 headers and pip3:
sudo apt-get install python3-dev python3-pip
Then we can install the INDI wrapper for Python2 with pip:
sudo pip install pyindi-client
Or for Python3 with pip3:
sudo pip3 install pyindi-client
A Python script is needed to test the wrapper. So we create a Python file client.py and add a simple IndiClient that prints new devices and properties to the terminal:
import sys, time, logging import PyIndi class IndiClient(PyIndi.BaseClient): def __init__(self): super(IndiClient, self).__init__() self.logger = logging.getLogger('PyQtIndi.IndiClient') self.logger.info('creating an instance of PyQtIndi.IndiClient') def newDevice(self, d): self.logger.info("new device " + d.getDeviceName()) def newProperty(self, p): self.logger.info("new property "+ p.getName() + " for device "+ p.getDeviceName()) def removeProperty(self, p): self.logger.info("remove property "+ p.getName() + " for device "+ p.getDeviceName()) def newBLOB(self, bp): self.logger.info("new BLOB "+ bp.name) def newSwitch(self, svp): self.logger.info ("new Switch "+ svp.name + " for device "+ svp.device) def newNumber(self, nvp): self.logger.info("new Number "+ nvp.name + " for device "+ nvp.device) def newText(self, tvp): self.logger.info("new Text "+ tvp.name + " for device "+ tvp.device) def newLight(self, lvp): self.logger.info("new Light "+ lvp.name + " for device "+ lvp.device) def newMessage(self, d, m): #self.logger.info("new Message "+ d.messageQueue(m)) pass def serverConnected(self): print("Server connected ("+self.getHost()+":"+str(self.getPort())+")") def serverDisconnected(self, code): self.logger.info("Server disconnected (exit code = "+str(code)+","+str(self.getHost())+":"+str(self.getPort())+")") logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO) # instantiate the client indiclient=IndiClient() # set indi server localhost and port 7624 indiclient.setServer("localhost",7624) # connect to indi server print("Connecting to indiserver") if (not(indiclient.connectServer())): print("No indiserver running on "+indiclient.getHost()+":"+str(indiclient.getPort())+" - Try to run") print(" indiserver indi_simulator_telescope indi_simulator_ccd") sys.exit(1) # start endless loop, client works asynchron in background while True: time.sleep(1)
Now we can test the wrappers. Open a second terminal and start an indi server with the CCD Simulator:
indiserver -v indi_simulator_ccd
Then switch to the first terminal and run the client script:
python client.py
The script will connect to your indiserver on localhost and prints connected devices and their properties. The output for CCD Simulatoir should look like this:
2017-07-11 22:08:21,984 creating an instance of PyQtIndi.IndiClient
Connecting to indiserver
Server connected (localhost:7624)
2017-07-11 22:08:21,993 new device CCD Simulator
2017-07-11 22:08:21,995 new property CONNECTION for device CCD Simulator
2017-07-11 22:08:21,998 new property DRIVER_INFO for device CCD Simulator
2017-07-11 22:08:22,001 new property DEBUG for device CCD Simulator
2017-07-11 22:08:22,002 new property CONFIG_PROCESS for device CCD Simulator
2017-07-11 22:08:22,002 new property ACTIVE_DEVICES for device CCD Simulator
2017-07-11 22:08:22,003 new property SIMULATOR_SETTINGS for device CCD Simulator
2017-07-11 22:08:22,004 new property ON_TIME_FACTOR for device CCD Simulator
But how to get images from the camera? It's easy, beacuse we alredy have a client. The client inherits from INDI BaseClient and implements the virtual functions. These functions are called when the server sends information like new numbers (newNumber), new devices (newDevice) and so on. At the moment the new values are just printed to the terminal.
The code connects to the server and then it prevents the program from terminating with an endless loop (the client runs asynchronous in the background). If the connection was successfull, you will see some new properties. Now you can connect from another client for example Ekos to your INDI server. Connect Ekos to remote (localhost, 7624) and then connect to the CCD Simulator and change some settings (set exposure, gain etc.). You will see the server answers in Ekos and in your Python client.
To start an exposure with the Python client we have to connect our CCD camera and then set the "CCD_EXPOSURE" to a value > 0.
At first we just add a member variable "device" to our client:
class IndiClient(PyIndi.BaseClient): device = None
Then we edit the newDevice() function. This function is called when a new device is detected. We just test if the device is the CCD Simulator and then we save a reference in our member variable "device":
def newDevice(self, d): self.logger.info("new device " + d.getDeviceName()) if d.getDeviceName() == "CCD Simulator": self.logger.info("Set new device CCD Simulator!") # save reference to the device in member variable self.device = d
Next step is to connect to CCD Simulator. We just wait for the CONNECTION property of CCD Simulator, connect to the device, set BLOB mode so we can get BLOBs (image data) and then we wait for the new property CCD_EXPOSURE by editing the newProperty() function:
def newProperty(self, p): self.logger.info("new property "+ p.getName() + " for device "+ p.getDeviceName()) if self.device is not None and p.getName() == "CONNECTION" and p.getDeviceName() == self.device.getDeviceName(): self.logger.info("Got property CONNECTION for CCD Simulator!") # connect to device self.connectDevice(self.device.getDeviceName()) # set BLOB mode to BLOB_ALSO self.setBLOBMode(1, self.device.getDeviceName(), None) if p.getName() == "CCD_EXPOSURE": # take first exposure self.takeExposure()
If CCD_EXPOSURE property was found, we call takeExposure(). in takeExposure() we just get the CCD_EXPOSURE property, set a new exposure time and send it back to the server/client:
def takeExposure(self): self.logger.info("<<<<<<<< Exposure >>>>>>>>>") #get current exposure time exp = self.device.getNumber("CCD_EXPOSURE") # set exposure time to 5 seconds exp[0].value = 5 # send new exposure time to server/device self.sendNewNumber(exp)
Now the CCD starts a new exposure and the newNumber() function is called every second. When the exposure finished, the newBlob() function with our image data is called. We just extract the image data from the BLOB and save it to a fits file ("frame.fit") on our harddisk. Then we start a new exposure:
def newBLOB(self, bp): self.logger.info("new BLOB "+ bp.name) # get image data img = bp.getblobdata() # write image data to BytesIO buffer import io blobfile = io.BytesIO(img) # open a file and save buffer to disk with open("frame.fit", "wb") as f: f.write(blobfile.getvalue()) # start new exposure self.takeExposure()
That's it! The whole script should now look like this:
import sys, time, logging import PyIndi class IndiClient(PyIndi.BaseClient): device = None def __init__(self): super(IndiClient, self).__init__() self.logger = logging.getLogger('PyQtIndi.IndiClient') self.logger.info('creating an instance of PyQtIndi.IndiClient') def newDevice(self, d): self.logger.info("new device " + d.getDeviceName()) if d.getDeviceName() == "CCD Simulator": self.logger.info("Set new device CCD Simulator!") # save reference to the device in member variable self.device = d def newProperty(self, p): self.logger.info("new property "+ p.getName() + " for device "+ p.getDeviceName()) if self.device is not None and p.getName() == "CONNECTION" and p.getDeviceName() == self.device.getDeviceName(): self.logger.info("Got property CONNECTION for CCD Simulator!") # connect to device self.connectDevice(self.device.getDeviceName()) # set BLOB mode to BLOB_ALSO self.setBLOBMode(1, self.device.getDeviceName(), None) if p.getName() == "CCD_EXPOSURE": # take first exposure self.takeExposure() def removeProperty(self, p): self.logger.info("remove property "+ p.getName() + " for device "+ p.getDeviceName()) def newBLOB(self, bp): self.logger.info("new BLOB "+ bp.name) # get image data img = bp.getblobdata() # write image data to BytesIO buffer import io blobfile = io.BytesIO(img) # open a file and save buffer to disk with open("frame.fit", "wb") as f: f.write(blobfile.getvalue()) # start new exposure self.takeExposure() def newSwitch(self, svp): self.logger.info ("new Switch "+ svp.name + " for device "+ svp.device) def newNumber(self, nvp): self.logger.info("new Number "+ nvp.name + " for device "+ nvp.device) def newText(self, tvp): self.logger.info("new Text "+ tvp.name + " for device "+ tvp.device) def newLight(self, lvp): self.logger.info("new Light "+ lvp.name + " for device "+ lvp.device) def newMessage(self, d, m): #self.logger.info("new Message "+ d.messageQueue(m)) pass def serverConnected(self): print("Server connected ("+self.getHost()+":"+str(self.getPort())+")") def serverDisconnected(self, code): self.logger.info("Server disconnected (exit code = "+str(code)+","+str(self.getHost())+":"+str(self.getPort())+")") def takeExposure(self): self.logger.info(">>>>>>>>") #get current exposure time exp = self.device.getNumber("CCD_EXPOSURE") # set exposure time to 5 seconds exp[0].value = 5 # send new exposure time to server/device self.sendNewNumber(exp) logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO) # instantiate the client indiclient=IndiClient() # set indi server localhost and port 7624 indiclient.setServer("localhost",7624) # connect to indi server print("Connecting to indiserver") if (not(indiclient.connectServer())): print("No indiserver running on "+indiclient.getHost()+":"+str(indiclient.getPort())+" - Try to run") print(" indiserver indi_simulator_telescope indi_simulator_ccd") sys.exit(1) # start endless loop, client works asynchron in background while True: time.sleep(1)