JoelBender / bacpypes

BACpypes provides a BACnet application layer and network layer written in Python for daemons, scripting, and graphical interfaces.
MIT License
302 stars 129 forks source link

raise RuntimeError("use IOCB for confirmed requests") RuntimeError: use IOCB for confirmed requests #425

Closed bbartling closed 3 years ago

bbartling commented 3 years ago

Hi Joel,

I am updating some code that works on fine bacpypes version 0.16.7

But when I run the code on bacpypes version 0.18.3 I run into some issues:

ERROR:__main__:an error has occurred: use IOCB for confirmed requests
Traceback (most recent call last):
  File "C:\Users\bbartling\OneDrive - Slipstream\Desktop\scripts\grab_config.py", line 420, in <module>
    main()
  File "C:\Users\bbartling\OneDrive - Slipstream\Desktop\scripts\grab_config.py", line 360, in main
    device_name = read_prop(this_application, target_address, "device", device_id, "objectName")
  File "C:\Users\bbartling\OneDrive - Slipstream\Desktop\scripts\grab_config.py", line 106, in read_prop
    result = app.make_request(request)
  File "C:\Users\bbartling\OneDrive - Slipstream\Desktop\scripts\grab_config.py", line 78, in make_request
    self.request(request)
  File "C:\Python39\lib\site-packages\bacpypes\app.py", line 482, in request
    raise RuntimeError("use IOCB for confirmed requests")
RuntimeError: use IOCB for confirmed requests

This maybe asking a lot but any chance I could get a high level on what needs to be changed? The traceback probably spells out everything, but I am still trying soak it all in.

In the make_request method, this is throwing the error: self.request(request)

Snip it of the code below, any tips greatly appreciated.

@bacpypes_debugging
class SynchronousApplication(BIPSimpleApplication):
    def __init__(self, *args):
        SynchronousApplication._debug("__init__ %r", args)
        BIPSimpleApplication.__init__(self, *args)
        self.expect_confirmation = True
        self.expected_device_id = ""
        self.apdu = None

    def confirmation(self, apdu):
        self.apdu = apdu
        stop()

    def indication(self, apdu):
        # We only care about indications if we sent out a who is request.
        if not isinstance(self._request, WhoIsRequest):
            _log.debug("Ignoring indication as we don't have an outstanding WhoIs")
            return

        # We only care about IAmRequest
        if not isinstance(apdu, IAmRequest):
            _log.debug("Ignoring indication as apdu is not IAm")
            return

        # Ignore IAmRequests that don't have the device id we care about.
        if self.expected_device_id is not None:            
            device_type, device_instance = apdu.iAmDeviceIdentifier

            if device_type != 'device':
                raise DecodingError("invalid object type")

            if device_instance != self.expected_device_id:
                _log.debug("Ignoring IAm. Expected ID: {} Received: {}".format(
                    self.expected_device_id, device_instance))
                return

        self.apdu = apdu
        stop()

    def make_request(self, request, expected_device_id=None):
        self.expected_device_id = expected_device_id
        self._request = request

        self.request(request)
        run()
        return self.apdu
JoelBender commented 3 years ago

Well, that was fast :-).

Zedstron commented 1 year ago

@bbartling How did you managed to solve this issue?

bbartling commented 1 year ago

to be honest I cannot remember : )

The best advice I could probably give on the matter is Plopping the code into chat GPT:

The error message you've received is coming from the BACpypes library in your Python script grab_config.py. This error occurs because the library expects a IOCB (Input/Output Control Block) for confirmed requests.

In your make_request method, you are directly calling self.request(request). However, BACpypes expects this to be wrapped with IOCB for confirmed requests.

You can modify your make_request function to use an IOCB as follows:

from bacpypes.app import IOCB

def make_request(self, request, expected_device_id=None):
    self.expected_device_id = expected_device_id
    self._request = request

    iocb = IOCB(request)  # Create an IOCB for the request
    self.request_io(iocb)  # Send the request using request_io instead of request

    iocb.wait()  # Wait for the response

    if iocb.ioError:  # If there was an error, handle it
        print(f"Error occurred: {iocb.ioError}")
    else:
        self.apdu = iocb.ioResponse  # If not, proceed as usual

    return self.apdu

In this modification, I've used the IOCB class from bacpypes.app to create an IOCB for the request. This IOCB is then passed to the request_io method, which can handle IOCBs. The wait method of the IOCB is used to pause execution until a response is received. If there's an error, it's printed out. If not, the response is stored in self.apdu as usual.

This change should resolve the error message you're seeing. However, you'll need to handle the IOCB appropriately in your other functions, especially those which call make_request.

Zedstron commented 1 year ago

Thanks @bbartling clouds are clear now, You saved much of my time.

JoelBender commented 1 year ago

That's an excellent answer, but there are dragons. The iocb.wait() will block the thread so it has to be called from a different thread than the one calling core.run() which is usually the main thread, so if there is data that is shared between threads it needs to be appropriately fenced in (resource locks or semaphores). You can also use the callback API, but the callback will execute in the main thread context, not necessarily the one that created the IOCB.

I usually do not use threads because most of my applications are headless (client and server daemons).

The advantage of BACpypes3 using asyncio is that "await" is very similar to iocb.wait() without blocking the thread and it is vastly simpler to maintain state, e.g., being able to say value = await app.read_property(...) and do something with the decoded result.