Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add python asyncio wrapper around as_ methods #164

Open
swamper123 opened this issue Jun 15, 2020 · 14 comments
Open

Add python asyncio wrapper around as_ methods #164

swamper123 opened this issue Jun 15, 2020 · 14 comments
Labels
Milestone

Comments

@swamper123
Copy link
Contributor

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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. :^)

@gijzelaerr
Copy link
Owner

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

@spreeker
Copy link
Collaborator

spreeker commented Jun 17, 2020 via email

@swamper123
Copy link
Contributor Author

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
Copy link
Contributor Author

I am a bit confused about what is behind functions like:

result = (self.library.Cli_AsDBRead(self.pointer, db_number, start,

Which async philosophy are they using? And what I want more to know, what is behind this self.library thing...

@gijzelaerr
Copy link
Owner

the philosophy is described in the snap7 documentation.

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

@swamper123
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

swamper123 commented Jul 1, 2020

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
Copy link
Contributor Author

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.

@gijzelaerr
Copy link
Owner

at what point shall we close this issue?

@swamper123
Copy link
Contributor Author

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.

@gijzelaerr gijzelaerr modified the milestones: 1.0, 1.1 Jan 7, 2021
@gijzelaerr gijzelaerr changed the title Async functions receive wrong values Add python asyncio wrapper around as_ methods Jan 7, 2021
@gijzelaerr gijzelaerr modified the milestones: 1.1, 2.1 Apr 22, 2021
@gijzelaerr gijzelaerr modified the milestones: 2.1, 2.0 Jul 7, 2021
@gijzelaerr gijzelaerr added idea and removed bug labels Nov 8, 2022
@gijzelaerr
Copy link
Owner

I've been looking into this a bit, and to avoid the need for polling one should use the blocking (non async) methods available in the snap7 API and call them in a thread pool using the async scheduler. This would look something like this

https://github.com/gijzelaerr/python-snap7/pull/525/files#diff-259c7137e7983b4162acb1ac7efe896b730917a1b561a77a3ecec0c78578552eR1

@gijzelaerr gijzelaerr modified the milestones: 2.0, 2.1 Jul 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants