python-snap7 icon indicating copy to clipboard operation
python-snap7 copied to clipboard

Add python asyncio wrapper around as_ methods

Open swamper123 opened this issue 4 years ago • 13 comments

I got my station (S1200) last week and had some time to deal with some functions. The sync functions work like expected, but the async functions receive garbage (mostly zeros).

While debugging I received sometimes the right values, so it seems to be an awaiting problem. I tried to just async these functions, but it seems to not change anything on it's behaviour.

I will have a deeper look under the hood of the plain snap7 or try some workarounds, so that the wrapper may deal with it asyncly.

swamper123 avatar Jun 15 '20 06:06 swamper123

Okay, it seems that we (python-snap7) are not responsible for that.

If I understood the snap7 Code right the order is like this:

  • create Request
  • create Thread which starts the request
  • wait for time x (?not sure? there seems to be Methods waitingFor and wait_forever)
  • return anything or random(?not sure? otherwise we wouldn't receive an answer)

It looks like it is concurrent, but in another style as I thought. =/

From the Python side, I could imagine to turn these functions still into async and await the result of what ever happens inside there(because we are waiting and we want to be informed if something received). In such a scenario, we would maybe faster/more flexible.

If I got any response in the snap7 sourceforege forum, I will let you all know.

swamper123 avatar Jun 15 '20 14:06 swamper123

Could somebody may check async requests with other stations (I just have the 1200 one)? Maybe it is caused by the reduced support for this hardware. Even better would be trying to change an as request into an asyncio function. Something like this:

   async def as_db_read(self, db_number, start, size):
        """
        This is the asynchronous counterpart of Cli_DBRead.
        :returns: user buffer.
        """
        logger.debug("db_read, db_number:%s, start:%s, size:%s" %
                     (db_number, start, size))

        type_ = snap7.snap7types.wordlen_to_ctypes[snap7.snap7types.S7WLByte]
        data = (type_ * size)()
        result = await (self.library.Cli_AsDBRead(self.pointer, db_number, start,
                                            size,  byref(data)))
        check_error(result, context="client")
        return bytearray(data)

This would may help circleing the root of this evil. :^)

swamper123 avatar Jun 17 '20 09:06 swamper123

I actually don't own a PLC, so I can't help you, unfortunately.

gijzelaerr avatar Jun 17 '20 10:06 gijzelaerr

I don't think adding 'async' to snap7 library calls is useful.

A PLC is very simple does only one command each loop. And is many times slower than a PC./ Python.

A PLC can only do one command at the moment. So if you do async you need to queue up all the commands to the PLC. But then..

If you create a separate small non async service that talks to your PLC receiving commands via a redis / db / message query / zero.mq. and publishes the the response. Using one while loop.

Then Your main application / business logic send commands to your queue of choice and subscribe to the PLC responses. You can do all the async you want. And have multiple instances running or have something separate for command logging / alarming etc etc.

If you have a lot of logic and a lot of plcs this is way more understandable IMHO.

Cheer,

Stephan.

On Wed, Jun 17, 2020, 12:18 Gijs Molenaar [email protected] wrote:

I actually don't own a PLC, so I can't help you, unfortunately.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/gijzelaerr/python-snap7/issues/164#issuecomment-645287092, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAOSMORKTKEQIGUTRYVX3DRXCJ67ANCNFSM4N53PVBQ .

spreeker avatar Jun 17 '20 11:06 spreeker

Ahoi @spreeker ,

I agree that you are right with the sync snap7 methods. You want to wait until an answer receives.

My idea was to add async to the async snap7 methods like as_db_read. While the async request is pending, it would be good for the Python program to do something else, until an answer is received, because the PLC .

This would be the philosophy of idle wait like mentioned in the snap7 docs

Also because asyncio is a build in library, it would be nice to use, to reduce other extern libraries to implement this use (like twisted, tornado etc.).

swamper123 avatar Jun 17 '20 11:06 swamper123

I am a bit confused about what is behind functions like: https://github.com/gijzelaerr/python-snap7/blob/f82c051fc020ed3951e43e62120c4023096cf3d8/snap7/client.py#L514 Which async philosophy are they using? And what I want more to know, what is behind this self.library thing...

swamper123 avatar Jun 17 '20 11:06 swamper123

the philosophy is described in the snap7 documentation.

the library object is a ctypes wrapper around the lower level snap7 shared library.

gijzelaerr avatar Jun 17 '20 11:06 gijzelaerr

the philosophy is described in the snap7 documentation.

From the docs:

Basically there are three completion models: · Polling · Idle wait · Callback There is no better than the others, it depends on the context. Snap7 Client supports all three models, or a combination of them, if wanted.

I am still not sure what is chosen or how I can select one of the upper three possebilities for the async methods (but Polling would be senseless, because then I can make it sync anyway). There seems to be something missing or is nor clear enough.

swamper123 avatar Jun 17 '20 12:06 swamper123

After a while I am back here again. I guess that StartAsyncJob() is not working right and so return 0 returns as a faulty result (unless enough time was passed in any kind of way, like it happens in debugging mode). So after studying the library, there could be a few ways dealing with that stuff, but I would need some help integrating it, because of my lack of knowledge in ctypes.

So either after sending an async request, we could check in a loop if Cli_CheckAsCompletion() like:

async def as_db_read(self, db_number, start, size):
    [...]
    result = (self.library.Cli_AsDBRead(self.pointer, db_numver, start, size, byref(data)))
    time_start = time.process_time()
    while not self.CheckAsCompletion(): # this should check if the job is still pending
         await asyncio.sleep(0) # Do some other async stuff while waiting
         if time.process_time() - time_start >= timeout:
            raise TimeoutError    # Or similar Error
    [...]

Another way could be dealing with Snap7Jobs as asyncio Future-Objects, because they seem to be similar in some points. But it would may ends in bigger effort to deal.

This would lead to something like:

async def as_db_read(self, db_number, start, size):
    [...]
    result = (self.library.Cli_AsDBRead(self.pointer, db_numver, start, size, byref(data)))
    try:
        await result
    except TimeoutError:
        print("As-Request timed out")
        _[... do exception handling ...]_
    [...]

swamper123 avatar Jun 29 '20 11:06 swamper123

I made some lines but I'm still confused a little bit.

So I created an async "waiting_loop" until the async request is done. The thing is, that this still don't work as expected.

The Code:

    async def async_wait_loop(self, data_byteref, data):
        while self.library.Cli_CheckAsCompletion(self.pointer, data_byteref):
            await asyncio.sleep(0)


    async def as_db_read(self, db_number, start, size):
        """
        This is the asynchronous counterpart of Cli_DBRead.

        :returns: user buffer.
        """
        logger.debug("db_read, db_number:%s, start:%s, size:%s" %
                     (db_number, start, size))

        type_ = snap7.snap7types.wordlen_to_ctypes[snap7.snap7types.S7WLByte]
        data = (type_ * size)()
        res = asyncio.Future()
        result = (self.library.Cli_AsDBRead(self.pointer, db_number, start, size, byref(data)))
#        await asyncio.sleep(0.5)
        try:
            await asyncio.wait_for(self.async_wait_loop(byref(data), data), 2)
        except asyncio.TimeoutError:
            logger.warning("timeouted as request")
        check_error(result, context="client")
        return bytearray(data)

So while just adding an await asyncio.sleep(0.5) the hardcoded way (which I don't want) after the result = (self.library.Cli_AsDBRead(self.pointer, db_number, start, size, byref(data))) I receive a plausible and correct value.

But if I use the waiting_loop, where I check if the job is done, I just get zeros as result. Even if I add an async.sleep(1) (with any timedelay) when the job seems to be done, still zeros.

So if I understood this right, Cli_CheckAsCompletion is one, if the job is pending and zero if not. Can be there some interference between checking if job is done and set an result?

swamper123 avatar Jul 01 '20 10:07 swamper123

Because this is still an issue (no check if Job is pending and waiting) in Client.py it should stay open, until fixed by somebody.

swamper123 avatar Sep 02 '20 11:09 swamper123

at what point shall we close this issue?

gijzelaerr avatar Nov 02 '20 14:11 gijzelaerr

This issue should be closed, when all now existing as_methods are fixed about the return value (or shall be replaced with NotImplementedError) and the corresponding tests are implemented/fixed or set to NotImplementedError as well.

swamper123 avatar Nov 02 '20 14:11 swamper123