indilib / pyindi-client

GNU General Public License v3.0
21 stars 8 forks source link

Segfault with INDI 1.9.9 #26

Closed aaronwmorris closed 1 year ago

aaronwmorris commented 2 years ago

Some change in the indi core library is causing a segfault or other vague errors with pyindi-client (HEAD). I believe whatever is causing the segfault was added after Nov 6th.

pyindi-client is completely non-functional with indi 1.9.9

knro commented 2 years ago

@pawel-soja Can you please check?

@aaronwmorris Can you share how we can reproduce this issue exactly step by step?

pawel-soja commented 2 years ago

@aaronwmorris do you have any logs? Have you recompiled all dependent libraries (e.g. indi-3rdparty)? Please provide additional information.

abecadel commented 2 years ago

I cannot see indi beeing tagged at 1.9.9 yet 1.9.8 seems to work (with the simple test we've got atm) https://github.com/indilib/pyindi-client/actions/runs/3238312112/jobs/5306396117 building & testing against the current master atm https://github.com/indilib/pyindi-client/actions/runs/3544738039

@aaronwmorris what exactly caused the segfault, please add a test for it or at least an instrution on how to reproduce it

BTW: it might be a good idea to create more tests to cover more area!

aaronwmorris commented 2 years ago

The indi Ubuntu PPA repo has already released packages with the 1.9.9 release. I have also tested by compiling from source.

I finally had some time tonight to write some basic code and there seems to be a problem with setting properties. None of the current script examples that set properties in the repo are working.

If you already have pyindi-client installed, you will have to run the following to ensure you get the latest version from git.

pip3 install --no-binary :all: --upgrade 'git+https://github.com/indilib/pyindi-client.git@ffd939b#egg=pyindi-client'

Attached is an example script that just sets some simple properties with the CCD Simulator. In my indi-allsky project, I get segfaults or recursion errors, but the code for setting properties is more complex than my example below.

Anytime a property is changed, I receive an exception like the following.

2022-11-25 21:38:51,893 [INFO] MainProcess __init__() #36: INDI version: 1.9.9
2022-11-25 21:38:51,893 [INFO] MainProcess <module>() #81: Connecting to indiserver
INDI::BaseClient::connectServer: creating new connection...
2022-11-25 21:38:51,895 [INFO] MainProcess <module>() #94: Connecting to CCD Simulator
2022-11-25 21:38:51,896 [INFO] MainProcess newDevice() #40: new device CCD Simulator
2022-11-25 21:38:51,896 [INFO] MainProcess newDevice() #40: new device CCD Simulator
2022-11-25 21:38:51,896 [INFO] MainProcess newDevice() #40: new device Telescope Simulator
2022-11-25 21:38:51,897 [INFO] MainProcess newDevice() #40: new device Telescope Simulator
2022-11-25 21:38:52,397 [INFO] MainProcess <module>() #100: Get CONNECTION control
Traceback (most recent call last):
  File "/home/aaron/git/indi-allsky/./testing/indi_basic.py", line 107, in <module>
    indiclient.sendNewSwitch(connection)
  File "/home/aaron/git/indi-allsky/virtualenv/indi-allsky/lib/python3.9/site-packages/PyIndi.py", line 912, in sendNewSwitch
    return _PyIndi.BaseClient_sendNewSwitch(self, pp)
TypeError: in method 'BaseClient_sendNewSwitch', argument 2 of type 'INDI::PropertyView< ISwitch > *'

Test script:

#!/usr/bin/env python3

import PyIndi
import time
import sys
import logging

CCD = "CCD Simulator"

INDI_SERVER = "localhost"
INDI_PORT = 7624

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

LOG_FORMATTER_STREAM = logging.Formatter('%(asctime)s [%(levelname)s] %(processName)s %(funcName)s() #%(lineno)d: %(message)s')
LOG_HANDLER_STREAM = logging.StreamHandler()
LOG_HANDLER_STREAM.setFormatter(LOG_FORMATTER_STREAM)
logger.addHandler(LOG_HANDLER_STREAM)

class IndiClient(PyIndi.BaseClient):
    def __init__(self):
        super(IndiClient, self).__init__()

        pyindi_version = '.'.join((
            str(getattr(PyIndi, 'INDI_VERSION_MAJOR', -1)),
            str(getattr(PyIndi, 'INDI_VERSION_MINOR', -1)),
            str(getattr(PyIndi, 'INDI_VERSION_RELEASE', -1)),
        ))

        logger.info("INDI version: %s", pyindi_version)

    def newDevice(self, d):
        logger.info("new device %s", d.getDeviceName())

    def removeDevice(self, d):
        logger.info("removed device %s", d.getDeviceName())

    def newProperty(self, p):
        pass

    def removeProperty(self, p):
        pass

    def newBLOB(self, bp):
        logger.info("new BLOB %s", bp.name)

    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):
        pass

    def serverDisconnected(self, code):
        pass

# connect to the server
indiclient = IndiClient()
indiclient.setServer(INDI_SERVER, INDI_PORT)

logger.info("Connecting to indiserver")
if not (indiclient.connectServer()):
    logger.error(
        "No indiserver running on %s:%d",
        indiclient.getHost(),
        indiclient.getPort()
    )
    sys.exit(1)

### Connect the CCD
device_ccd = None
while not device_ccd:
    logger.info("Connecting to %s", CCD)
    device_ccd = indiclient.getDevice(CCD)
    time.sleep(0.5)

connection = None
while not connection:
    logger.info("Get CONNECTION control")
    connection = device_ccd.getSwitch("CONNECTION")
    time.sleep(0.5)

if not device_ccd.isConnected():
    connection[0].setState(PyIndi.ISS_ON)   # CONNECT
    connection[1].setState(PyIndi.ISS_OFF)  # DISCONNECT
    indiclient.sendNewSwitch(connection)

while not device_ccd.isConnected():
    logger.warning('Waiting on ccd connection')
    time.sleep(0.5)

logger.info("ccd connected")

### Number control test
equatorial_pe = None
while not equatorial_pe:
    logger.info("Get EQUITORIAL_PE control (number)")
    equatorial_pe = device_ccd.getNumber("EQUATORIAL_PE")
    time.sleep(0.5)

logger.info("Set EQUATORIAL_PE to M13")

equatorial_pe[0].value = 16.7175  # RA_PE
equatorial_pe[1].value = 36.4233  # DEC_PE
indiclient.sendNewNumber(equatorial_pe)

### Text control test
ccd_directory_location = None
while not ccd_directory_location:
    logger.info("Get CCD_DIRECTORY_LOCATION control (text)")
    ccd_directory_location = device_ccd.getText("CCD_DIRECTORY_LOCATION")
    time.sleep(0.5)

logger.info("Set CCD_DIRECTORY_LOCATION to /tmp")

ccd_directory_location[0].text = "/tmp"  # LOCATION
indiclient.sendNewText(ccd_directory_location)

### Switch control test
simulate_bayer = None
while not simulate_bayer:
    logger.info("Get SIMULATE_BAYER control (switch)")
    simulate_bayer = device_ccd.getSwitch("SIMULATE_BAYER")
    time.sleep(0.5)

logger.info("Set SIMULATE_BAYER to Enable")

simulate_bayer[0].setState(PyIndi.ISS_ON)   # INDI_ENABLED
simulate_bayer[1].setState(PyIndi.ISS_OFF)  # INDI_DISABLED
indiclient.sendNewSwitch(simulate_bayer)
pawel-soja commented 2 years ago

Fantastic! Thank you very much for the example. I'll take care of this problem.

pawel-soja commented 2 years ago

The problem is with the implicit conversion between types, which is natural and default in C++.

I will add additional constructors to INDI Core to help with the conversion. In addition, a few things will need to be removed from the indiclientpython.i file.

@knro It will be easy for me to make changes after approval: https://github.com/indilib/indi/pull/1767

pawel-soja commented 2 years ago

Attached is an example script that just sets some simple properties with the CCD Simulator. In my indi-allsky project, I get segfaults or recursion errors, but the code for setting properties is more complex than my example below.

I didn't catch the segfaults.

I explain where the identified errors came from:

RecursionError: maximum recursion depth exceeded

Inheriting from BaseClient which inherits BaseMediator, virtual methods had to be implemented.

    def newDevice(self, d):
        pass

    def removeDevice(self, d):
        pass

    def newProperty(self, p):
        pass

    def removeProperty(self, p):
        pass

    def newBLOB(self, bp):
        pass

    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):
        pass

    def serverDisconnected(self, code):
        pass

The problem started when a new virtual method was added in version 1.9.9:

        /** @brief Emmited when a new property value arrives from INDI server.
         *  @param property Property container.
         */
        virtual void updateProperty(INDI::Property property);

The method was not implemented on the Python side and Swig generated a function that calls itself. However, the new library does not require virtual functions to be implemented as it implements empty functions by default.

To solve the problem, and not have to implement all functions if not needed, you should also mark the BaseMediator as (similar to BaseClient):

%feature("director") BaseMediator;

TypeError: in method 'BaseClient_sendNewNumber', argument 2 of type

I thought there was a problem with assigning a PropertyXXX value type to a Property type, which shouldn't be because PropertyXXX inherits from Property.

The culprit of this problem is the extra lines: https://github.com/indilib/pyindi-client/blob/ffd939bebabdb5423cd1666f84288eafcb0996ff/indiclientpython.i#L222-L231

After removing them, the problem disappears.

I guess where they came from, in version 1.9.8 (basedevice.h):

        /** @return Return vector number property given its name */
        INDI::PropertyView<INumber> *getNumber(const char *name) const;
        /** @return Return vector text property given its name */
        INDI::PropertyView<IText>   *getText(const char *name) const;
        /** @return Return vector switch property given its name */
        INDI::PropertyView<ISwitch> *getSwitch(const char *name) const;
        /** @return Return vector light property given its name */
        INDI::PropertyView<ILight>  *getLight(const char *name) const;
        /** @return Return vector BLOB property given its name */
        INDI::PropertyView<IBLOB>   *getBLOB(const char *name) const;

For version 1.9.9 low-level magic types are slowly disappearing (basedevice.h):

        /** @return Return vector number property given its name */
        INDI::PropertyNumber getNumber(const char *name) const;
        /** @return Return vector text property given its name */
        INDI::PropertyText getText(const char *name) const;
        /** @return Return vector switch property given its name */
        INDI::PropertySwitch getSwitch(const char *name) const;
        /** @return Return vector light property given its name */
        INDI::PropertyLight getLight(const char *name) const;
        /** @return Return vector BLOB property given its name */
        INDI::PropertyBlob getBLOB(const char *name) const;

There are still places where the previous types are behind, but I will try to use the new ones as soon as possible and add the appropriate constructors to the 1.9.9 version.

Summary

Soon I will upload appropriate pull requests to both repositories, I am currently testing if everything is ok.

aaronwmorris commented 2 years ago

I have tested the script above with the changes you implemented and the [simple] script works now.

However, I am still receiving a segfault with my more complex method of setting properties. I will upload a new example to demonstrate the segfault.

aaronwmorris commented 2 years ago

This is a more complex method of setting properties that allows setting property values by names instead of just manually setting the indexes. I left the unneeded methods in for testing with older versions of indi.

This is the segfault I receive:

$ ./indi_complex.py 
2022-11-30 21:48:11,660 [INFO] MainProcess __init__() #75: creating an instance of IndiClient
2022-11-30 21:48:11,661 [INFO] MainProcess <module>() #288: Connecting to indiserver
INDI::BaseClient::connectServer: creating new connection...
2022-11-30 21:48:11,661 [INFO] MainProcess serverConnected() #121: Server connected (localhost:7624)
2022-11-30 21:48:11,662 [INFO] MainProcess newDevice() #79: new device CCD Simulator
2022-11-30 21:48:11,663 [INFO] MainProcess newDevice() #79: new device CCD Simulator
2022-11-30 21:48:11,664 [INFO] MainProcess newDevice() #79: new device Telescope Simulator
2022-11-30 21:48:11,665 [INFO] MainProcess newDevice() #79: new device Telescope Simulator
2022-11-30 21:48:19,670 [INFO] MainProcess findCcds() #144: Found device CCD Simulator
2022-11-30 21:48:19,671 [INFO] MainProcess findCcds() #149:  Detected ccd
2022-11-30 21:48:19,672 [INFO] MainProcess findCcds() #149:  Detected guider
2022-11-30 21:48:19,672 [INFO] MainProcess findCcds() #149:  Detected filter
2022-11-30 21:48:19,673 [INFO] MainProcess findCcds() #144: Found device Telescope Simulator
2022-11-30 21:48:19,673 [INFO] MainProcess findCcds() #149:  Detected telescope
2022-11-30 21:48:19,674 [INFO] MainProcess findCcds() #149:  Detected guider
2022-11-30 21:48:19,675 [INFO] MainProcess <module>() #306: Found 1 CCDs
2022-11-30 21:48:19,675 [WARNING] MainProcess <module>() #309: Connecting to device CCD Simulator
2022-11-30 21:48:19,675 [INFO] MainProcess configureDevice() #159: Setting switch SIMULATE_BAYER
Segmentation fault
#!/usr/bin/env python3

import sys
import logging
import time
from collections import OrderedDict
import ctypes
import PyIndi

INDI_SERVER = 'localhost'
INDI_PORT = 7624

INDI_CONFIG = OrderedDict({
    "SWITCHES" : {
        "SIMULATE_BAYER" : {
            "on"  : ["INDI_ENABLED"],
            "off" : ["INDI_DISABLED"],
        },
    },
    "PROPERTIES" : {
        "EQUATORIAL_PE" : {
            "RA_PE"  : 16.7175,
            "DEC_PE" : 36.4233
        },
    },
    "TEXT" : {
        "CCD_DIRECTORY_LOCATION" : {
            "LOCATION" : "/tmp",
        },
    },
})

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

LOG_FORMATTER_STREAM = logging.Formatter('%(asctime)s [%(levelname)s] %(processName)s %(funcName)s() #%(lineno)d: %(message)s')
LOG_HANDLER_STREAM = logging.StreamHandler()
LOG_HANDLER_STREAM.setFormatter(LOG_FORMATTER_STREAM)
logger.addHandler(LOG_HANDLER_STREAM)

class IndiClient(PyIndi.BaseClient):

    __state_to_str = {
        PyIndi.IPS_IDLE  : 'IDLE',
        PyIndi.IPS_OK    : 'OK',
        PyIndi.IPS_BUSY  : 'BUSY',
        PyIndi.IPS_ALERT : 'ALERT',
    }

    __indi_interfaces = {
        PyIndi.BaseDevice.GENERAL_INTERFACE   : 'general',
        PyIndi.BaseDevice.TELESCOPE_INTERFACE : 'telescope',
        PyIndi.BaseDevice.CCD_INTERFACE       : 'ccd',
        PyIndi.BaseDevice.GUIDER_INTERFACE    : 'guider',
        PyIndi.BaseDevice.FOCUSER_INTERFACE   : 'focuser',
        PyIndi.BaseDevice.FILTER_INTERFACE    : 'filter',
        PyIndi.BaseDevice.DOME_INTERFACE      : 'dome',
        PyIndi.BaseDevice.GPS_INTERFACE       : 'gps',
        PyIndi.BaseDevice.WEATHER_INTERFACE   : 'weather',
        PyIndi.BaseDevice.AO_INTERFACE        : 'ao',
        PyIndi.BaseDevice.DUSTCAP_INTERFACE   : 'dustcap',
        PyIndi.BaseDevice.LIGHTBOX_INTERFACE  : 'lightbox',
        PyIndi.BaseDevice.DETECTOR_INTERFACE  : 'detector',
        PyIndi.BaseDevice.ROTATOR_INTERFACE   : 'rotator',
        PyIndi.BaseDevice.AUX_INTERFACE       : 'aux',
    }

    def __init__(self):
        super(IndiClient, self).__init__()
        self._timeout = 60.0
        logger.info('creating an instance of IndiClient')

    def newDevice(self, d):
        logger.info("new device %s", d.getDeviceName())

    def removeDevice(self, d):
        logger.info("remove device %s", d.getDeviceName())

    def newProperty(self, p):
        #logger.info("new property %s for device %s", p.getName(), p.getDeviceName())
        pass

    def removeProperty(self, p):
        logger.info("remove property %s for device %s", p.getName(), p.getDeviceName())

    def newBLOB(self, bp):
        logger.info("new BLOB %s", bp.name)

        #start = time.time()

        ### get image data
        bp.getblobdata()

        #elapsed_s = time.time() - start
        #logger.info('Blob downloaded in %0.4f s', elapsed_s)

    def newSwitch(self, svp):
        logger.info("new Switch %s for device %s", svp.name, svp.device)

    def newNumber(self, nvp):
        #logger.info("new Number %s for device %s", nvp.name, nvp.device)
        pass

    def newText(self, tvp):
        logger.info("new Text %s for device %s", tvp.name, tvp.device)

    def newLight(self, lvp):
        logger.info("new Light %s for device %s", lvp.name, lvp.device)

    def newMessage(self, d, m):
        logger.info("new Message %s", d.messageQueue(m))

    def serverConnected(self):
        logger.info("Server connected (%s:%d)", self.getHost(), self.getPort())

    def serverDisconnected(self, code):
        logger.info("Server disconnected (exit code = %d, %s, %d", code, str(self.getHost()), self.getPort())

    def findDeviceInterfaces(self, device):
        interface = device.getDriverInterface()
        if type(interface) is int:
            device_interfaces = interface
        else:
            interface.acquire()
            device_interfaces = int(ctypes.cast(interface.__int__(), ctypes.POINTER(ctypes.c_uint16)).contents.value)
            interface.disown()

        return device_interfaces

    def findCcds(self):
        ccd_list = list()

        for device in self.getDevices():
            logger.info('Found device %s', device.getDeviceName())
            device_interfaces = self.findDeviceInterfaces(device)

            for k, v in self.__indi_interfaces.items():
                if device_interfaces & k:
                    logger.info(' Detected %s', v)
                    if k == PyIndi.BaseDevice.CCD_INTERFACE:
                        ccd_list.append(device)

        return ccd_list

    def configureDevice(self, device, indi_config):
        ### 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)

        # Sleep after configuration
        time.sleep(1.0)

    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')
        for control_name, index in self.__map_indexes(c, values.keys()).items():
            c[index].value = 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')

        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.name for s in c if s.name not in on_switches]

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

            if c[index].name in on_switches:
                new_state = PyIndi.ISS_ON
            elif is_exclusive or c[index].name in off_switches:
                new_state = PyIndi.ISS_OFF

            c[index].setState(new_state)

        self.sendNewSwitch(c)

        return c

    def set_text(self, device, control_name, values, sync=True, timeout=None):
        c = self.get_control(device, control_name, 'text')
        for control_name, index in self.__map_indexes(c, values.keys()).items():
            c[index].text = values[control_name]
        self.sendNewText(c)

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

        return c

    def get_control(self, device, name, ctl_type, timeout=None):
        if timeout is None:
            timeout = self._timeout

        ctl = None
        attr = {
            'number'  : 'getNumber',
            'switch'  : 'getSwitch',
            'text'    : 'getText',
            'light'   : 'getLight',
            'blob'    : 'getBLOB'
        }[ctl_type]

        started = time.time()
        while not ctl:
            ctl = getattr(device, attr)(name)

            if not ctl and 0 < timeout < time.time() - started:
                raise TimeOutException('Timeout finding control {0}'.format(name))

            time.sleep(0.1)

        return ctl

    def __map_indexes(self, ctl, values):
        result = {}
        for i, c in enumerate(ctl):
            #logger.info('Value name: %s', c.name)  # useful to find value names
            if c.name in values:
                result[c.name] = 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 = self._timeout

        while ctl.getState() not in statuses:
            #logger.info('%s/%s/%s: %s', ctl.getDeviceName(), ctl.getGroupName(), ctl.getName(), self.__state_to_str[ctl.getState()])
            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 TimeOutException('Timeout error while changing property {0}: elapsed={1}, timeout={2}, status={3}'.format(ctl.getName(), elapsed, timeout, self.__state_to_str[ctl.getState()] ))

            time.sleep(0.05)

class TimeOutException(Exception):
    pass

# instantiate the client
indiclient = IndiClient()

# set indi server localhost and port
indiclient.setServer(INDI_SERVER, INDI_PORT)

logger.info("Connecting to indiserver")
if not indiclient.connectServer():
    logger.error("No indiserver running on %s:%d - Try to run", indiclient.getHost(), indiclient.getPort())
    logger.error("  indiserver indi_simulator_telescope indi_simulator_ccd")
    sys.exit(1)

# give devices a chance to register
time.sleep(8)

ccd_list = indiclient.findCcds()

if len(ccd_list) == 0:
    logger.error('No CCDs detected')
    time.sleep(1)
    sys.exit(1)

logger.info('Found %d CCDs', len(ccd_list))
ccdDevice = ccd_list[0]

logger.warning('Connecting to device %s', ccdDevice.getDeviceName())
indiclient.connectDevice(ccdDevice.getDeviceName())

indiclient.configureDevice(ccdDevice, INDI_CONFIG)
pawel-soja commented 2 years ago

Okay I have it.

            off_switches = [s.name for s in c if s.name not in on_switches]

When items with a loop after any object, only the __getitem__ method is used. The __len__ function is not performed. In order for the loop to end at the right time, the __getitem__ function must quit the exception.

I added the appropriate fixes to https://github.com/indilib/pyindi-client/pull/27

aaronwmorris commented 2 years ago

That is awesome! You are killing it. :-) I tested the changes and it appears to be working.

Final question: With these changes to pyindi-client, will it be compatible with older versions of indi or will it require 1.9.9?

pawel-soja commented 2 years ago

It should be backwards compatible, although I haven't tested all versions.

I recommend using getters and setters, additionally note that there are built-in functions that return string as state or rule.

Property Example

#!/usr/bin/env python3
import PyIndi

prop = PyIndi.PropertySwitch(2)

# setters
prop.setDeviceName("Property Device Name")
prop.setName("Property Name")
prop.setLabel("Property Label")
prop.setGroupName("Property Group Name")
prop.setPermission(PyIndi.IP_RW)
prop.setTimeout(123)
prop.setState(PyIndi.IPS_BUSY)
prop.setTimestamp("1234")
prop.setRule(PyIndi.ISR_ATMOST1)

prop[0].setName("Widget Name 1")
prop[0].setLabel("Widget Label 1")
prop[0].setState(PyIndi.ISS_OFF)

prop[1].setName("Widget Name 2")
prop[1].setLabel("Widget Label 2")
prop[1].setState(PyIndi.ISS_ON)

# getters
print('---- property ----')
print('        getDeviceName:', prop.getDeviceName())
print('              getName:', prop.getName())
print('             getLabel:', prop.getLabel())
print('         getGroupName:', prop.getGroupName())
print('        getPermission:', prop.getPermission())
print('getPermissionAsString:', prop.getPermissionAsString())
print('           getTimeout:', prop.getTimeout())
print('         getTimestamp:', prop.getTimestamp())
print('              getRule:', prop.getRule())
print('      getRuleAsString:', prop.getRuleAsString())

for widget in prop:
    print('')
    print('---- widget ----')
    print('         getName:', widget.getName())
    print('        getLabel:', widget.getLabel())
    print('        getState:', widget.getState())
    print('getStateAsString:', widget.getStateAsString())

Output:

---- property ----
        getDeviceName: Property Device Name
              getName: Property Name
             getLabel: Property Label
         getGroupName: Property Group Name
        getPermission: 2
getPermissionAsString: rw
           getTimeout: 123.0
         getTimestamp: 1234
              getRule: 1
      getRuleAsString: AtMostOne

---- widget ----
         getName: Widget Name 1
        getLabel: Widget Label 1
        getState: 0
getStateAsString: Off

---- widget ----
         getName: Widget Name 2
        getLabel: Widget Label 2
        getState: 1
getStateAsString: On

Property Cast Example

#!/usr/bin/env python3
import PyIndi

prop = PyIndi.Property(PyIndi.PropertySwitch(2))

print('Type:', prop.getTypeAsString())

print('Is light? ', PyIndi.PropertyLight(prop).isValid())
print('Is Switch?', PyIndi.PropertySwitch(prop).isValid())
print('Is Number?', PyIndi.PropertyNumber(prop).isValid())

switch = PyIndi.PropertySwitch(prop)
if switch:
    print('if switch - true')

light = PyIndi.PropertyLight(prop)
if not light:
    print('if not light - true')

Output

Type: INDI_SWITCH
Is light?  False
Is Switch? True
Is Number? False
if switch - true
if not light - true

Conversion can be useful when reading a given property through new methods in the mediator (now 3 methods are enough to handle all cases for a property). https://github.com/indilib/indi/blob/8d9ba364579804b0eebc5f3971844a34ac056d32/libs/indidevice/indibase.h#L105-L119

    public:
        /** @brief Emmited when a new property is created for an INDI driver.
         *  @param property Property container.
         */
        virtual void newProperty(INDI::Property property);

        /** @brief Emmited when a new property value arrives from INDI server.
         *  @param property Property container.
         */
        virtual void updateProperty(INDI::Property property);

        /** @brief Emmited when a property is deleted for an INDI driver.
         *  @param property Property container.
         */
        virtual void removeProperty(INDI::Property property);
aaronwmorris commented 2 years ago

Very interesting information.

I had already converted some of the code to use getters and setters since direct access to some of the object properties no longer worked. Looks like I need to finish that work.

aaronwmorris commented 1 year ago

Looks like this fixes indi 1.9.9, unfortunately it is not backwards compatible with indi 1.9.8 or 1.9.7.

I will submit a PR to increment the version number of pyindi-client.