3
\$\begingroup\$

I have a piece of equipment that I am interacting with. The manufacturer provided an SDK and some simple examples of how to interact with the device. However, I need to write an API for interacting with the device with proper error handling.

The device returns a 5-digit unsigned integer to report the status of the operation. The issue is that many of the status codes are reused across different operations, with descriptions. I've attempted to implement a means of throwing exceptions that are designed to provide the specific status description for each operation.

I've grouped similar operations in individual files for accessing the SDK. Here is one.

from AndorException import AndorException, open_shutter_errors, standard_errors
from pyAndorSDK2.atmcd import atmcd


class ShutterControl:
    def get_shutter_min_times(self):
        """
        Function will return the minimum opening and closing times in milliseconds for the shutter on the current camera.

        Returns:
            status (unsigned int):
                DRV_SUCCESS: 20002 # Minimum times successfully returned.
                DRV_NOT_INITIALIZED: 20075 # System not initialized.
                DRV_P1INVALID: 20066 # Parameter is NULL.
                DRV_P2INVALID: 20067 # Parameter is NULL.
            min_close_time (int): Minimum closing time in milliseconds that the shutter of the camera supports
            min_open_time (int): Minimum opening time in milliseconds that the shutter of the camera supports

        """
        status, min_close_time, min_open_time = self.sdk.GetShutterMinTimes()

        if status != atmcd.DRV_SUCCESS:
            raise AndorException(status, standard_errors[str(status)])

        return min_close_time, min_open_time

    def set_shutter(self, typ, mode, closing_time, opening_time):
        """
        Function controls the behavior of the shutter.

        Args:
            typ (int): Allows the user to control the TTL signal output to an external shutter.
                Valid values:
                    0: Output TTL low signal to open shutter
                    1: Output TTL high signal to open shutter
            mode (int): Configures whether the shutter opens & closes automatically (controlled by camera) or is permanently open or permanently closed.
                Valid values:
                    0: Fully Auto
                    1: Permanently Open
                    2: Permanently Closed
                    3: Open for FVB series
                    4: Open for any series
            closing_time (int): Specifies the time required to close the shutter.
            opening_time (int): Specifies the time required to open the shutter.

        Returns:
            status (unsigned int):
                DRV_SUCCESS: 20002 # Shutter set.
                DRV_NOT_INITIALIZED: 20075 # System not initialized.
                DRV_ACQUIRING: 20072 # Acquisition in progress.
                DRV_ERROR_ACK: 20098 # Unable to communicate with card.
                DRV_NOT_SUPPORTED: 20086 # Camera does not support shutter control.
                DRV_P1INVALID: 20066 # Invalid TTL type.
                DRV_P2INVALID: 20067 # Invalid mode.
                DRV_P3INVALID: 20068 # Invalid time to close.
                DRV_P4INVALID: 20069 # Invalid time to open
        """

        status = self.sdk.SetShutter(
            typ=typ,
            mode=mode,  # type: ignore
            closingtime=closing_time,
            openingtime=opening_time,
        )

        if status != atmcd.DRV_SUCCESS:
            raise AndorException(status, open_shutter_errors[str(status)])

As you can see DRV_P1INVALID and P2INVALID both are shared status results of both operations, however have different descriptions.

I ended up creating a new exception class, very simple. And a series of dictionaries that can be used for providing specific dictionaries for the different status codes. I feel like this can be handled differently, perhaps better.

There is a primary dictionary, standard_errors that contains most if not all the Status codes. This dictionary is used as a base for other dictionaries that require field descriptions to be overridden. This is a small example there are a few dozen.

 class AndorException(Exception):
    """
        Exception raised for errors in the Andor SDK.

    Args:
        Error Code - See Error Code Definitions for details ErrorCodeDefinitions.py
        Error Message - Explanation of the error
    """

    def __init__(self, error_code, error_message):
        self.error_code = error_code
        self.error_message = error_message


standard_errors = {
    20002: "Successful function call.",
    20003: "VxD not loaded.",
    20010: "Unable to allocate memory.",
    20013: "Unable to communicate with card.",
    20014: "Overflow of the spool buffer",
    20015: "Computer unable to read data fast enough to stop camera memory going full.",
    20016: "Computer unable to read the data via the ISA slot at the required rate.",
    20017: "Acquisition settings invalid.",
    20022: "Unable to meet kinetic cycle time.",
    20023: "Unable to meet Accumulate Cycle time.",
    20024: "There is no new data yet.",
    20027: "Error with spool settings.",
    20034: "Temperature is OFF.",
    20035: "Temperature reached but not stabilized.",
    20036: "Temperature has stabilized at set point.",
    20037: "Temperature has not reached set point.",
    20040: "Temperature has stabilized but has since drifted.",
    20049: "The series is out of range.",
    20066: "Invalid external output index.",
    20067: "Array size is incorrect.",
    20068: "Invalid pointer (i.e NULL).",
    20069: "Array size is incorrect",
    20070: "Error reading 'DETECTOR.INI'.",
    20072: "Acquisition in progress.",
    20073: "IDLE waiting on instructions",
    20074: "Executing temperature cycle.",
    20075: "System not initialized.",
    20076: "Invalid pointer (i.e NULL).",
    20077: "Invalid pointer (i.e NULL).",
    20079: "Filter not available for current acquisition.",
    20081: "I2C device not present.",
    20082: "I2C command timed out.",
    20086: "Your system does not support this feature.",
    20090: "Integrate On Chip setup error.",
    20099: "Range not multiple of horizontal binning.",
    20991: "Camera does not support switching cooler OFF.",
}

open_shutter_errors = {
    **standard_errors,
    20086: "Camera does not support shutter control.",
    20066: "Invalid TTL type.",
    20067: "Invalid mode.",
    20068: "Invalid time to close.",
    20069: "Invalid time to open.",
}
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

The device returns a 5-digit unsigned integer

Unless it's ctypes, there's no such thing as an unsigned integer in Python. But of course this is splitting hairs, and you just mean a positive integer.

I've grouped similar operations in individual files

This is good; keep doing it.

In this block of code:

        """
        Returns:
            status (unsigned int):
                DRV_SUCCESS: 20002 # Minimum times successfully returned.
                DRV_NOT_INITIALIZED: 20075 # System not initialized.
                DRV_P1INVALID: 20066 # Parameter is NULL.
                DRV_P2INVALID: 20067 # Parameter is NULL.
            min_close_time (int): Minimum closing time in milliseconds that the shutter of the camera supports
            min_open_time (int): Minimum opening time in milliseconds that the shutter of the camera supports
        """
        # ...
        return min_close_time, min_open_time

the first return description is a lie, since you don't return the status; you convert it into an exception. What you're doing is (mostly) good, but what you're documenting isn't; remove the status from the return descriptor. If you want, document that the function may raise an AndorException and leave it at that.

I ended up creating a new exception class, very simple.

This is good; keep doing it.

Use plain-old classes and subclasses. Don't metaclass or decorate; that's extreme overkill for this application. Add a convenience method to check a status and conditionally throw:

DRV_SUCCESS = 20002


class AndorError(Exception):
    __slots__ = (
        'status',
        'name',
    )

    CODES = {
        20002: "Successful function call.",
        20003: "VxD not loaded.",
        20010: "Unable to allocate memory.",
        20013: "Unable to communicate with card.",
        20014: "Overflow of the spool buffer",
        20015: "Computer unable to read data fast enough to stop camera memory going full.",
        20016: "Computer unable to read the data via the ISA slot at the required rate.",
        20017: "Acquisition settings invalid.",
        20022: "Unable to meet kinetic cycle time.",
        20023: "Unable to meet Accumulate Cycle time.",
        20024: "There is no new data yet.",
        20027: "Error with spool settings.",
        20034: "Temperature is OFF.",
        20035: "Temperature reached but not stabilized.",
        20036: "Temperature has stabilized at set point.",
        20037: "Temperature has not reached set point.",
        20040: "Temperature has stabilized but has since drifted.",
        20049: "The series is out of range.",
        20066: "Invalid external output index.",
        20067: "Array size is incorrect.",
        20068: "Invalid pointer (i.e NULL).",
        20069: "Array size is incorrect",
        20070: "Error reading 'DETECTOR.INI'.",
        20072: "Acquisition in progress.",
        20073: "IDLE waiting on instructions",
        20074: "Executing temperature cycle.",
        20075: "System not initialized.",
        20076: "Invalid pointer (i.e NULL).",
        20077: "Invalid pointer (i.e NULL).",
        20079: "Filter not available for current acquisition.",
        20081: "I2C device not present.",
        20082: "I2C command timed out.",
        20086: "Your system does not support this feature.",
        20090: "Integrate On Chip setup error.",
        20099: "Range not multiple of horizontal binning.",
        20991: "Camera does not support switching cooler OFF.",
    }
    
    def __init__(self, status: int) -> None:
        self.status = status
        self.name = self.CODES.get(status, 'Unknown error')
        super().__init__(str(self))

    @classmethod
    def check(cls, status: int) -> None:
        if status != DRV_SUCCESS:
            raise cls(status)

    def __str__(self) -> str:
        return f'{self.name} ({self.status})'


class AndorShutterError(AndorError):
    CODES = AndorError.CODES | {
        20086: "Camera does not support shutter control.",
        20066: "Invalid TTL type.",
        20067: "Invalid mode.",
        20068: "Invalid time to close.",
        20069: "Invalid time to open.",
    }
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Most of the documentation was taken directly from the SDK documentation, I didn't even think about the fact Python doesn't have unsigned int. Great call out thank you. I was attempting to document the SDK call, but you are 100% right. I'll update the documentation to reflect the actual function, not the sdk call. \$\endgroup\$
    – Michael
    Dec 8, 2023 at 12:36
  • \$\begingroup\$ You didn't mention how you would use AndorError. looking at it and playing around with it, it seems I can use it to either raise an exception explicitly by 'raise AndorError(20066)' or I can call the check and allow the class to raise it for me. Is there any reason why I should use one or the other? \$\endgroup\$
    – Michael
    Dec 8, 2023 at 12:56
  • 1
    \$\begingroup\$ Use check when that logic is what you want. raise when you need to do something more complicated, like filtering out other non-fatal return codes. \$\endgroup\$
    – Reinderien
    Dec 8, 2023 at 13:18
3
\$\begingroup\$

Reinderien has a much better solution : https://codereview.stackexchange.com/a/288304/271531

Absolutely loved Error Code 20002. It is a prime example of task failed successfully.

  1. Your problem is kind of similar to Posix errno. Most Posix errnos have distinct values but EWOULDBLOCK and EAGAIN have the same value. OSExcept in python has to deal with it. They do this be subclassing the OSExcept exception. I would suggest you do the same. Currently, if someone calls set_shutter() and does not catch the error and it propogates, there is no good information except for the string as to what happened. Subclassing it will fix that.
  2. If you are already passing status there is no point asking for an error string. The exception class must figure that out.
  3. The error dictonary must be a part of the class.
  4. As per PEP-8, Exception in python must end in Error not Exception.
  5. Override the str dunder method so printing is easier.
class AndorError(Exception):
    """
        Exception raised for errors in the Andor SDK.

    Args:
        Error Code - See Error Code Definitions for details ErrorCodeDefinitions.py
    """

    standard_errors = {
        20002: "Successful function call.",
        20003: "VxD not loaded.",
        20010: "Unable to allocate memory.",
        20013: "Unable to communicate with card.",
        20014: "Overflow of the spool buffer",
        ...
        20991: "Camera does not support switching cooler OFF.",
    }

    def __init__(self, error_code):
        self.code = error_code

    def __str__(self):
        return AndorError.standard_errors[self.error_code]


class AndorShutterError(AndorError):

    """
        Exception raised for shutter errors in the Andor SDK.

    Args:
        Error Code - See Error Code Definitions for details ErrorCodeDefinitions.py
    """

    
    open_shutter_errors = {
        20086: "Camera does not support shutter control.",
        20066: "Invalid TTL type.",
        20067: "Invalid mode.",
        20068: "Invalid time to close.",
        20069: "Invalid time to open.",
    }

    def __init__(self, error_code):
        super().__init__(error_code)

    def __str__(self):
        if self.code in AndorShutterError().open_shutter_errors:
            return self.open_shutter_errors[self.code]
        else:
            return super().__str__()

Edit

def create_andor_error_class(cls):
    class Error(AndorError):

        __doc__ = cls.__doc__
        
        specific_errors = cls.specific_errors

        def __init__(self, error_code):
            super().__init__(error_code)
            self.code = error_code

        def __str__(self):
            if self.code in self.specific_errors:
                return self.specific_errors[self.code]
            else:
                return super().__str__()
            
    return Error

@create_andor_error_class
class AndorShutterError:

    """
        Exception raised for shutter errors in the Andor SDK.

    Args:
        Error Code - See Error Code Definitions for details ErrorCodeDefinitions.py
    """

    specific_errors = {
        20086: "Camera does not support shutter control.",
        20066: "Invalid TTL type.",
        20067: "Invalid mode.",
        20068: "Invalid time to close.",
        20069: "Invalid time to open.",
    }

Here we create a decorator called andor_error. When we use it on a class with a dictonary named specific_error. It adds all the other details. With this adding new error classes is as easy as your dictonary approach

\$\endgroup\$
8
  • \$\begingroup\$ This makes a lot of sense to me. However, my hesitation is that I would need to create a minimum of 21 new classes. None of which can be reused, for instance, status code 20066 has 19 different responses. I don't see anything anything wrong with this, but II have the same hesitation as what brought me to ask in the first place. Is this still the direction you would suggest? \$\endgroup\$
    – Michael
    Dec 7, 2023 at 19:42
  • \$\begingroup\$ You are reusing the AndorError class in this solution. You have to create 21 different dictonaries any ways. But class creation can probably be easier by using MetaClasses. Havent found any use for it before let me confirm and edit the answer \$\endgroup\$ Dec 7, 2023 at 20:07
  • 1
    \$\begingroup\$ Turns out I was wrong. All you need is a decorator for the classes. \$\endgroup\$ Dec 7, 2023 at 20:26
  • 1
    \$\begingroup\$ Ah thats a typo fixed it \$\endgroup\$ Dec 7, 2023 at 21:05
  • 1
    \$\begingroup\$ @Reinderien has a much better solution \$\endgroup\$ Dec 8, 2023 at 11:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.