Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Indi scanner #80

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,15 @@ virtual environment and installing ALS's dependencies into it.

- `gcc` and `python3-dev` to compile some dependencies (don't be scared)
- `python3-venv` to handle virtualenvs
- `indi` and gsc catalog `gsc` to enable indi support

.. code-block::

$ sudo apt update && sudo apt install -y gcc python3-dev python3-venv
$ sudo add-apt-repository -y ppa:mutlaqja/ppa && sudo apt update

.. code-block::

$ sudo apt install -y gcc python3-dev python3-venv indi-full gsc swig zlib1g-dev


2. **Dive into ALS folder**.
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ astropy==3.2.1
dtcwt==0.12.0
numpy==1.16.4
opencv-python==4.1.0.25
pyindi-client==0.2.2
PyQt5==5.13.0
pywi==0.3.dev12
qimage2ndarray==1.8
Expand Down
226 changes: 226 additions & 0 deletions src/als/IndiCamera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Basic stuff
import io
import json
import logging

# Numerical stuff
import numpy as np

# Indi stuff
import PyIndi

# Local stuff
from als.IndiDevice import IndiDevice
from als.IndiClient import IndiClientGlobalBlobEvent

# Imaging and Fits stuff
from astropy.io import fits

# Astropy
import astropy.units as u

_LOGGER = logging.getLogger(__name__)

class IndiCamera(IndiDevice):
""" Indi Camera """

UploadModeDict = {
'local': 'UPLOAD_LOCAL',
'client': 'UPLOAD_CLIENT',
'both': 'UPLOAD_BOTH'}
DEFAULT_EXP_TIME_SEC = 5
MAXIMUM_EXP_TIME_SEC = 3601

def __init__(self, indi_client, config=None,
connect_on_create=True):
if config is None:
config = dict(camera_name = 'CCD Simulator')

device_name = config['camera_name']
_LOGGER.debug(f"Indi camera, camera name is: {device_name}")

# device related intialization
IndiDevice.__init__(self, device_name=device_name,
indi_client=indi_client)

# Frame Blob: reference that will be used to receive binary
self.frame_blob = None

# Default exposureTime, gain
self.exp_time_sec=5
self.gain=400

if connect_on_create:
self.connect()
self.prepare_shoot()

# Finished configuring
_LOGGER.debug(f"Configured Indi Camera successfully")

@property
def dynamic(self):
return 2**self.get_dynamic()

'''
Indi CCD related stuff
'''
def prepare_shoot(self):
'''
We should inform the indi server that we want to receive the
"CCD1" blob from this device
'''
_LOGGER.debug(f"Indi client will register to server in order to "
f"receive blob CCD1 when it is ready")
self.indi_client.setBLOBMode(PyIndi.B_ALSO, self.device_name, 'CCD1')
self.frame_blob = self.get_prop(propName='CCD1', propType='blob')

def synchronize_with_image_reception(self):
try:
global IndiClientGlobalBlobEvent
_LOGGER.debug("synchronizeWithImageReception: Start waiting")
IndiClientGlobalBlobEvent.wait()
IndiClientGlobalBlobEvent.clear()
_LOGGER.debug(f"synchronizeWithImageReception: Done")
except Exception as e:
_LOGGER.error(f"Indi Camera Error in "
f"synchronizeWithImageReception: {e}")

def get_received_image(self):
try:
ret = []
_LOGGER.debug(f"getReceivedImage frame_blob: {self.frame_blob}")
for blob in self.frame_blob:
_LOGGER.debug(f"Indi camera, processing blob with name: "
f"{blob.name}, size: {blob.size}, format: "
f"{blob.format}")
# pyindi-client adds a getblobdata() method to IBLOB item
# for accessing the contents of the blob, which is a bytearray
return fits.open(io.BytesIO(blob.getblobdata()))
except Exception as e:
_LOGGER.error(f"Indi Camera Error in getReceivedImage: {e}")

def shoot_async(self):
try:
_LOGGER.info(f"Launching acquisition with {self.exp_time_sec} "
f"sec exposure time")
self.setNumber('CCD_EXPOSURE',
{'CCD_EXPOSURE_VALUE': self.sanitize_exp_time(
self.exp_time_sec)}, sync=False)
except Exception as e:
_LOGGER.error(f"Indi Camera Error in shoot: {e}")


def abort_shoot(self, sync=True):
self.setNumber('CCD_ABORT_EXPOSURE', {'ABORT': 1}, sync=sync)

def launch_streaming(self):
self.setSwitch('VIDEO_STREAM',['ON'])

def set_upload_path(self, path, prefix = 'IMAGE_XXX'):
self.setText('UPLOAD_SETTINGS', {'UPLOAD_DIR': path,\
'UPLOAD_PREFIX': prefix})

def get_binning(self):
return self.getPropertyValueVector('CCD_BINNING', 'number')

def set_binning(self, hbin, vbin = None):
if vbin == None:
vbin = hbin
self.setNumber('CCD_BINNING', {'HOR_BIN': hbin, 'VER_BIN': vbin })

def get_roi(self):
return self.getPropertyValueVector('CCD_FRAME', 'number')

def set_roi(self, roi):
""""
X: Left-most pixel position
Y: Top-most pixel position
WIDTH: Frame width in pixels
HEIGHT: Frame width in pixels
ex: cam.setRoi({'X':256, 'Y':480, 'WIDTH':512, 'HEIGHT':640})
"""
self.setNumber('CCD_FRAME', roi)

def get_dynamic(self):
return self.get_number('CCD_INFO')['CCD_BITSPERPIXEL']['value']

def get_maximum_dynamic(self):
return get_dynamic()

def get_temperature(self):
return self.get_number(
'CCD_TEMPERATURE')['CCD_TEMPERATURE_VALUE']['value']

def set_temperature(self, temperature):
""" It may take time to lower the temperature of a ccd """
if isinstance(temperature, u.Quantity):
temperature = temperature.to(u.deg_C).value
if np.isfinite(temperature):
self.setNumber('CCD_TEMPERATURE',
{ 'CCD_TEMPERATURE_VALUE' : temperature },
sync=True, timeout=1200)

def set_cooling_on(self):
self.setSwitch('CCD_COOLER',['COOLER_ON'])

def set_cooling_off(self):
self.setSwitch('CCD_COOLER',['COOLER_OFF'])

def set_gain(self, value):
pass
#TODO TN, Try to solve this
#self.setNumber('DETECTOR_GAIN', [{'Gain': value}])

def get_gain(self):
gain = self.get_number('CCD_GAIN')
print('returned Gain is {}'.format(gain))
return gain

def get_frame_type(self):
return self.get_prop('CCD_FRAME_TYPE','switch')

def set_frame_type(self, frame_type):
"""
FRAME_LIGHT Take a light frame exposure
FRAME_BIAS Take a bias frame exposure
FRAME_DARK Take a dark frame exposure
FRAME_FLAT Take a flat field frame exposure
"""
self.setSwitch('CCD_FRAME_TYPE', [frame_type])

def sanitize_exp_time(self, exp_time_sec):
if isinstance(exp_time_sec, u.Quantity):
exp_time_sec = exp_time_sec.to(u.s).value
if not isinstance(exp_time_sec, float):
try:
float_exp_time_sec = float(exp_time_sec)
except Exception as e:
float_exp_time_sec = self.DEFAULT_EXP_TIME_SEC
elif exp_time_sec < 0:
float_exp_time_sec = abs(float_exp_time_sec)
elif exp_time_sec == 0:
float_exp_time_sec = self.DEFAULT_EXP_TIME_SEC
elif exp_time_sec > self.MAXIMUM_EXP_TIME_SEC:
float_exp_time_sec = self.MAXIMUM_EXP_TIME_SEC
else:
float_exp_time_sec = exp_time_sec
# Show warning if needed
if float_exp_time_sec != exp_time_sec:
_LOGGER.warning(f"Sanitizing exposition time: cannot accept "
"{exp_time_sec}, using {float_exp_time_sec} instead")

return float_exp_time_sec

def get_exp_time_sec(self):
return self.sanitize_exp_time(self.exp_time_sec)

def set_exp_time_sec(self, exp_time_sec):
self.exp_time_sec = self.sanitize_exp_time(exp_time_sec)

def __str__(self):
return f"INDI Camera {self.name}"

def __repr__(self):
return self.__str__()


107 changes: 107 additions & 0 deletions src/als/IndiClient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Basic stuff
import json
import logging
import threading

# Indi stuff
import PyIndi

# configure global variables
global IndiClientGlobalBlobEvent
IndiClientGlobalBlobEvent = threading.Event()

_LOGGER = logging.getLogger(__name__)

class IndiClient(PyIndi.BaseClient):
'''
This Indi Client class can be used as a singleton, so that it can be used
to interact with multiple devices, instead of declaring one client per
device to manage.

Every virtual function instanciated is called asynchronously by an external
C++ thread, and not by the python main thread, so be careful.
'''

def __init__(self, config, connect_on_create=True):
# Call indi client base classe ctor
PyIndi.BaseClient.__init__(self)

if config is None:
config = dict(indi_host = "localhost",
indi_port = 7624)

self.remoteHost = config['indi_host']
self.remotePort = int(config['indi_port'])

self.setServer(self.remoteHost, self.remotePort)
_LOGGER.debug(f"Indi Client, remote host is: {self.getHost()}:"
f"{self.getPort()}")

if connect_on_create:
self.connect()

# Finished configuring
_LOGGER.debug(f"Configured Indi Client successfully")

def connect(self):
if self.isServerConnected():
_LOGGER.warning(f"Already connected to server")
else:
_LOGGER.info(f"Connecting to server at {self.getHost()}:{self.getPort()}")

if not self.connectServer():
_LOGGER.error(f"No indiserver running on {self.getHost()}:"
f"{self.getPort()} - Try to run "
f"indiserver indi_simulator_telescope indi_simulator_ccd")
else:
_LOGGER.info(f"Successfully connected to server at "
f"{self.getHost()}:{self.getPort()}")

'''
Indi related stuff (implementing BaseClient methods)
'''
def device_names(self):
return [d.getDeviceName() for d in self.getDevices()]

def newDevice(self, d):
pass

def newProperty(self, p):
pass

def removeProperty(self, p):
pass

def newBLOB(self, bp):
# this threading.Event is used for sync purpose in other part of the code
_LOGGER.debug(f"new BLOB received: {bp.name}")
global IndiClientGlobalBlobEvent
IndiClientGlobalBlobEvent.set()

def newSwitch(self, svp):
pass

def newNumber(self, nvp):
pass

def newText(self, tvp):
pass

def newLight(self, lvp):
pass

def newMessage(self, d, m):
pass

def serverConnected(self):
_LOGGER.debug(f"Server connected")

def serverDisconnected(self, code):
_LOGGER.debug(f"Server disconnected")

def __str__(self):
return f"INDI client connected to {self.remoteHost}:{self.remotePort}"

def __repr__(self):
return self.__str__()

Loading