indilib / pyindi-client

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

INDI 2.0.0 - newBLOB() handler not firing #32

Closed aaronwmorris closed 9 months ago

aaronwmorris commented 1 year ago

Under indi 2.0.0 it appears the newBLOB() handler is no longer being called when a blob is returned by a camera.

Is this a bug or is there some new method for accessing the blobs returned by the camera?

knro commented 1 year ago

With INDI 2.0.0, @pawel-soja added updateProperty function now, which may be used to access an updated BLOB. This requires some changes on pyindi-client side to support?

pawel-soja commented 1 year ago

From version 2.0.0, the methods to be implemented are more generic and unified.

Source interface in C++

/** @class INDI::BaseMediator
 *  @brief Meditates event notification as generated by driver and passed to clients.
 */
class INDI::BaseMediator
{
    public:
        virtual ~BaseMediator() = default;

    public:
        /** @brief Emmited when a new device is created from INDI server.
         *  @param baseDevice BaseDevice instance.
         */
        virtual void newDevice(INDI::BaseDevice baseDevice);

        /** @brief Emmited when a device is deleted from INDI server.
         *  @param baseDevice BaseDevice instance.
         */
        virtual void removeDevice(INDI::BaseDevice baseDevice);        

    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);

    public:
        /** @brief Emmited when a new message arrives from INDI server.
         *  @param baseDevice BaseDevice instance the message is sent to.
         *  @param messageID ID of the message that can be used to retrieve the message from the device's messageQueue() function.
         */
        virtual void newMessage(INDI::BaseDevice baseDevice, int messageID);

    public:
        /** @brief Emmited when the server is connected. */
        virtual void serverConnected();

        /** @brief Emmited when the server gets disconnected.
         *  @param exit_code 0 if client was requested to disconnect from server. -1 if connection to server is terminated due to remote server disconnection.
         */
        virtual void serverDisconnected(int exit_code);

    public: // deprecated interface
#if INDI_VERSION_MAJOR < 2
        /** @brief Emmited when a new device is created from INDI server.
         *  @param dp Pointer to the base device instance
         */
        virtual void newDevice(INDI::BaseDevice *dp); // deprecated

        /** @brief Emmited when a device is deleted from INDI server.
         *  @param dp Pointer to the base device instance.
         */
        virtual void removeDevice(INDI::BaseDevice *dp); // deprecated

        /** @brief Emmited when a new property is created for an INDI driver.
         *  @param property Pointer to the Property Container
         */
        virtual void newProperty(INDI::Property *property); // deprecated

        /** @brief Emmited when a property is deleted for an INDI driver.
         *  @param property Pointer to the Property Container to remove.
         */
        virtual void removeProperty(INDI::Property *property); // deprecated

        /** @brief Emmited when a new switch value arrives from INDI server.
         *  @param svp Pointer to a switch vector property.
         */
        virtual void newSwitch(ISwitchVectorProperty *svp); // deprecated

        /** @brief Emmited when a new number value arrives from INDI server.
         *  @param nvp Pointer to a number vector property.
         */
        virtual void newNumber(INumberVectorProperty *nvp); // deprecated

        /** @brief Emmited when a new text value arrives from INDI server.
         *  @param tvp Pointer to a text vector property.
         */
        virtual void newText(ITextVectorProperty *tvp); // deprecated

        /** @brief Emmited when a new light value arrives from INDI server.
         *  @param lvp Pointer to a light vector property.
         */
        virtual void newLight(ILightVectorProperty *lvp); // deprecated

        /** @brief Emmited when a new property value arrives from INDI server.
         *  @param bp Pointer to filled and process BLOB.
         */
        virtual void newBLOB(IBLOB *bp); // deprecated

        /** @brief Emmited when a new message arrives from INDI server.
         *  @param dp pointer to the INDI device the message is sent to.
         *  @param messageID ID of the message that can be used to retrieve the message from the device's messageQueue() function.
         */
        virtual void newMessage(INDI::BaseDevice *dp, int messageID); // deprecated
#endif
};

Python

On the python side, it is not required to implement all functions. Please use as needed.


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

    # Emmited when a new device is created from INDI server.
    def newDevice(self, dev):
        pass

    # Emmited when a device is deleted from INDI server.
    def removeDevice(self, dev):
        pass

    # Emmited when a new property is created for an INDI driver.
    def newProperty(self, prop):
        pass

    # Emmited when a new property value arrives from INDI server.
    def updateProperty(self, prop):
        pass

    # Emmited when a property is deleted for an INDI driver.
    def removeProperty(self, prop):
        pass

    # Emmited when a new message arrives from INDI server.
    def newMessage(self, device, messageId):
        pass

    # Emmited when the server is connected.
    def serverConnected(self):
        pass

    # Emmited when the server gets disconnected.
    def serverDisconnected(self, exitCode):
        pass

For all properties i.e. PropertySwitch, PropertyNumber, PropertyText, PropertyLight, PropertyBlob, you can use the appropriate functions:

Example

import PyIndi
import sys
import time

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

    # Emmited when a new device is created from INDI server.
    def newDevice(self, dev):
        print(f"New device: {dev.getDeviceName()}")

    # Emmited when a new property is created for an INDI driver.
    def newProperty(self, prop):
        print(f"New property: {prop.getName()}")
        pass

    # Emmited when a new property value arrives from INDI server.
    def updateProperty(self, prop):
        print(f"Update property: {prop.getName()}")

        if prop.isNameMatch("CCD1"):
            blob = PyIndi.PropertyBlob(prop) # cast generic type to blob type
            print(f"      name: {blob.getName()}")
            print(f"     label: {blob.getLabel()}")
            print(f"    device: {blob.getDeviceName()}")

            # usually, the first item is the expected photo
            print(f"    format: {blob[0].getFormat()}")
            print(f"  blob len: {blob[0].getBlobLen()}")
            print(f"      size: {blob[0].getSize()}")

# e.g. run indiserver indi_simulator_ccd
deviceName = "CCD Simulator"

indiClient = IndiClient()
indiClient.setServer("localhost", 7624)

if not indiClient.connectServer():
    print("No indi server found")
    sys.exit(1)

indiClient.setBLOBMode(PyIndi.B_ALSO, deviceName, "")

# Connect to device
time.sleep(0.1)
indiClient.connectDevice(deviceName)

# Take exposure
time.sleep(0.1)
ccdExposure = indiClient.getDevice(deviceName).getNumber("CCD_EXPOSURE")
if ccdExposure.isValid():
    ccdExposure[0].setValue(1) # one second
    indiClient.sendNewProperty(ccdExposure)
else:
    print(f"Cannot find {deviceName} or CCD_EXPOSURE property")

input("")

Note that functions pass the generic PyIndi.Property type. From this type you can read the name, label, status, device name and similar common characteristics for all properties. If you want to read the characteristics that are unique to the type, e.g. a numerical value from PyIndi.PropertyNumber, you need to perform a cast, e.g. numberProperty = PyIndi.PropertyNumber(someGenericProperty)

Output:

INDI::BaseClient::connectServer: creating new connection...
New device: CCD Simulator
New property: CONNECTION
New property: DRIVER_INFO
New property: POLLING_PERIOD
New property: DEBUG
New property: CONFIG_PROCESS
New property: DEBUG_LEVEL
New property: LOGGING_LEVEL
...
New property: FILTER_SLOT
New property: FILTER_NAME
Update property: CCD_EXPOSURE
Update property: CCD_EXPOSURE
Update property: CCD_EXPOSURE
Update property: CCD1
      name: CCD1
     label: Image Data
    device: CCD Simulator
    format: .fits
  blob len: 2626560
      size: 2626560
Update property: CCD_EXPOSURE
knro commented 1 year ago

@pawel-soja Maybe good idea to add above to some 2.0.0 migration guide and make it available in the repo top level with a note in README? What do you think?

pawel-soja commented 1 year ago

Sorry, but I have to quote. Funny situation :smile:

"The library already has changes made to compile. Will you take care of other clients so that after January 31st the whole world will still compile? :)"

Sure I can write a guide and refresh the readme. For the guide, I would need to carefully analyze how client applications are currently written, convert them into examples, describe exactly what and how.

But unfortunately I'm very busy at the moment and can't promise anything :sob:

knro commented 1 year ago

I don't think we need to go into too much detail, just what you wrote above is now enough. I can even add it myself if you don't have time. The only affected client I know about now is Stellarium, and I just forked it so I can perform the fixes myself and send them a PR.

aaronwmorris commented 1 year ago

I found that this seems to work as a simple method to maintain compatibility between indi 1.x.x and 2.x.x

    # Emmited when a new property value arrives from INDI server.
    def updateProperty(self, prop):
        self.logger.info("Update property: %s", prop.getName())

        if prop.isNameMatch("CCD1"):
            blob = PyIndi.PropertyBlob(prop) # cast generic type to blob type

            # usually, the first item is the expected photo
            self.newBLOB(blob[0])

    def newBLOB(self, bp):
        self.logger.info("new BLOB %s", bp.name)
pawel-soja commented 1 year ago

@aaronwmorris Thanks, this is also the solution I proposed for PHD2. https://github.com/indilib/indi/issues/1833

And this is a good solution for python. Here it's so much easier that such a solution is acceptable in the long run, because there are no types here and raw indicators disappear.

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

    # Call functions in old style 
    def updateProperty(self, prop):
        if prop.getType() == PyIndi.INDI_NUMBER:
            self.newNumber(PyIndi.PropertyNumber(prop))
        elif prop.getType() == PyIndi.INDI_SWITCH:
            self.newSwitch(PyIndi.PropertySwitch(prop))
        elif prop.getType() == PyIndi.INDI_TEXT:
            self.newText(PyIndi.PropertyText(prop))
        elif prop.getType() == PyIndi.INDI_LIGHT:
            self.newLight(PyIndi.PropertyLight(prop))
        elif prop.getType() == PyIndi.INDI_BLOB:
            self.newBLOB(PyIndi.PropertyBlob(prop)[0])

    # The methods work again with INDI Core 2.0.0!
    def newBLOB(self, prop):
        print(f"new BLOB {prop.getName()}")
    def newSwitch(self, prop):
        print(f"new Switch {prop.getName()} for device {prop.getDeviceName()}")
    def newNumber(self, prop):
        print(f"new Number {prop.getName()} for device {prop.getDeviceName()}")
    def newText(self, prop):
        print(f"new Text {prop.getName()} for device {prop.getDeviceName()}")
    def newLight(self, prop):
        print(f"new Light {prop.getName()} for device {prop.getDeviceName()}")
pawel-soja commented 1 year ago

Pull request https://github.com/indilib/pyindi-client/pull/33

Guys, can you take care of the next steps related to the pull request? I didn't change the version, etc. I see that something else is hanging in pull requests and I'm not sure how you want to arrange it. Improved examples, tests and readme. If there's anything else I can add or clarify, please let me know.

aaronwmorris commented 1 year ago

I have discovered that if you are running 1.9.9 both the updateProperty() and newBLOB() methods will be called. If both are implemented, blobs will be processed twice if they both use the same code path.

There is nothing that can really be done about this from the indi side since the code is already in the wild, so this condition would have to be managed in the client.

pawel-soja commented 1 year ago

Right, if we want to include the previous version, some kind of test would be useful. Maybe something like this?

    # Emmited when a new property value arrives from INDI server.
    def updateProperty(self, prop):
        if hasattr(PyIndi.BaseMediator, 'newNumber'):
            pass
        elif prop.getType() == PyIndi.INDI_NUMBER:
            self.newNumber(PyIndi.PropertyNumber(prop))
        elif prop.getType() == PyIndi.INDI_SWITCH:
            self.newSwitch(PyIndi.PropertySwitch(prop))
        elif prop.getType() == PyIndi.INDI_TEXT:
            self.newText(PyIndi.PropertyText(prop))
        elif prop.getType() == PyIndi.INDI_LIGHT:
            self.newLight(PyIndi.PropertyLight(prop))
        elif prop.getType() == PyIndi.INDI_BLOB:
            self.newBLOB(PyIndi.PropertyBlob(prop)[0])

@aaronwmorris Can you check is it ok? Have any other idea?

aaronwmorris commented 1 year ago

That would work, although I might go with something a little different style-wise. Either way, it works.

    # Emmited when a new property value arrives from INDI server.
    def updateProperty(self, prop):
        if hasattr(PyIndi.BaseMediator, 'newNumber'):
            # indi 1.x bypass
            return

        if prop.getType() == PyIndi.INDI_NUMBER:
            self.newNumber(PyIndi.PropertyNumber(prop))
        elif prop.getType() == PyIndi.INDI_SWITCH:
            self.newSwitch(PyIndi.PropertySwitch(prop))
        elif prop.getType() == PyIndi.INDI_TEXT:
            self.newText(PyIndi.PropertyText(prop))
        elif prop.getType() == PyIndi.INDI_LIGHT:
            self.newLight(PyIndi.PropertyLight(prop))
        elif prop.getType() == PyIndi.INDI_BLOB:
            self.newBLOB(PyIndi.PropertyBlob(prop)[0])