get/set GAIN and OFFSET in camera via Python bindings

Hi all,
It’s been a while since I’ve written some Python to access INDI devices. I have a script that works great for a CCD (KAF8300). Now I have a CMOS camera (IMX571), and I’d like to programmatically set GAIN and OFFSET. But there aren’t GAIN or OFFSET properties listed in the standard properties table :-(. Instead, it looks like they are part of CCD_CONTROLS. Can someone provide some example code that would just allow getting and setting of these values?

BTW, I’m using the PyIndy.py module. If there’s something better or more recent, I’d be happy to update.

Thanks for any help,
Charles

With INDI, just remember, you have to set all values in the property for it to properly configure, so you would have to set every value in CCD_CONTROLS, even if all of the values are not changing. The number of values in the property can actually change between INDI/driver releases and will be in different orders between vendors.

I borrowed some very nice methods that Marco Gulino he developed for indi-lite-tools where if you want to change a single value, it queries all of the existing values and sets each value to the existing value while only changing the properties you want to change. They also allow you set properties by name, instead of just by index.

You can find what I have in indi-allsky here: https://github.com/aaronwmorris/indi-allsky/blob/main/indi_allsky/camera/indi.py

Here is some rough code

import PyIndi
import time
import logging


logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)


    def main(self):
        ccd_device = indiclient.getDevice("CCD NAME")

        indi_config = {
            "PROPERTIES" : {
                "CCD_CONTROLS" : {
                    "Gain" : 100,
                    "Offset" : 10
                }
            },
            "SWITCHES" : {
                "CCD_VIDEO_FORMAT" : {
                    "on"  : ["ASI_IMG_RAW16"],  # example of setting a switch
                    "off" : ["ASI_IMG_RAW8"]    # off settings are optional
                }
            }
        }

        configureDevice(ccd_device, indi_config)


    def configureDevice(self, device, indi_config):
        if isinstance(device, FakeIndiCcd):
            # ignore configuration
            return

        ### Configure Device Switches
        for k, v in indi_config.get('SWITCHES', {}).items():
            logger.info('Setting switch %s', k)
            self.set_switch(device, k, on_switches=v.get('on', []), off_switches=v.get('off', []))

        ### Configure Device Properties
        for k, v in indi_config.get('PROPERTIES', {}).items():
            logger.info('Setting property (number) %s', k)
            self.set_number(device, k, v)

        ### Configure Device Text
        for k, v in indi_config.get('TEXT', {}).items():
            logger.info('Setting property (text) %s', k)
            self.set_text(device, k, v)


    def set_number(self, device, name, values, sync=True, timeout=None):
        #logger.info('Name: %s, values: %s', name, str(values))
        c = self.get_control(device, name, 'number')

        if c.getPermission() == PyIndi.IP_RO:
            logger.error('Number control %s is read only', name)
            return c

        for control_name, index in self.__map_indexes(c, values.keys()).items():
            logger.info('Setting %s = %s', c[index].getLabel(), str(values[control_name]))
            c[index].setValue(values[control_name])

        self.sendNewNumber(c)

        if sync:
            self.__wait_for_ctl_statuses(c, timeout=timeout)

        return c


    def set_switch(self, device, name, on_switches=[], off_switches=[], sync=True, timeout=None):
        c = self.get_control(device, name, 'switch')

        if c.getPermission() == PyIndi.IP_RO:
            logger.error('Switch control %s is read only', name)
            return c

        is_exclusive = c.getRule() == PyIndi.ISR_ATMOST1 or c.getRule() == PyIndi.ISR_1OFMANY
        if is_exclusive :
            on_switches = on_switches[0:1]
            off_switches = [s.getName() for s in c if s.getName() not in on_switches]

        for index in range(0, len(c)):
            current_state = c[index].getState()
            new_state = current_state

            if c[index].getName() in on_switches:
                logger.info('Enabling %s (%s)', c[index].getLabel(), c[index].getName())
                new_state = PyIndi.ISS_ON
            elif is_exclusive or c[index].getName() in off_switches:
                new_state = PyIndi.ISS_OFF

            c[index].setState(new_state)

        self.sendNewSwitch(c)

        return c


    def set_text(self, device, name, values, sync=True, timeout=None):
        c = self.get_control(device, name, 'text')

        if c.getPermission() == PyIndi.IP_RO:
            logger.error('Text control %s is read only', name)
            return c

        for control_name, index in self.__map_indexes(c, values.keys()).items():
            logger.info('Setting %s = %s', c[index].getLabel(), str(values[control_name]))
            c[index].setText(values[control_name])

        self.sendNewText(c)

        if sync:
            self.__wait_for_ctl_statuses(c, timeout=timeout)

        return c


    def __map_indexes(self, ctl, values):
        result = {}
        for i, c in enumerate(ctl):
            #logger.info('Value name: %s', c.getName())  # useful to find value names
            if c.getName() in values:
                result[c.getName()] = i
        return result


   def __wait_for_ctl_statuses(self, ctl, statuses=[PyIndi.IPS_OK, PyIndi.IPS_IDLE], timeout=None):
        started = time.time()

        if timeout is None:
            timeout = 5

        while ctl.getState() not in statuses:
            if ctl.getState() == PyIndi.IPS_ALERT and 0.5 > time.time() - started:
                raise RuntimeError('Error while changing property {0}'.format(ctl.getName()))

            elapsed = time.time() - started

            if 0 < timeout < elapsed:
                raise Exception('Timeout error while changing property {0}'.format(ctl.getName()))

            time.sleep(0.15)