stlehmann / pyads

Python wrapper for TwinCAT ADS
MIT License
252 stars 93 forks source link

Sending write command without variable value (length 0) #335

Open CagtayFabry opened 1 year ago

CagtayFabry commented 1 year ago

I am trying to send an AdsSyncWriteReqEx command without any data using pyads

The equivalent C API command I am trying to send is using a nullptr as data (with length 0)

AdsSyncWriteReqEx(nPort, &mAddr, 0xF088, 0x00000000 | (3 & 0x000000FF), 0, nullptr);

and is taken from the TwinCAT 3 | Corrected timestamps - ADS consumer example.

I initially tried plc.write() which didn't work and also tried using the ADS bindings in pyads more directly after opening the ADS connection with plc.open()

from pyads.pyads_ex import adsSyncWriteReqEx

adsSyncWriteReqEx(plc._port, plc._adr, 0xF088, 0x00000000 | (3 & 0x000000FF), None, None)

but I run into the following error which I think indicates that None is not a valid replacement for nullptr.

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [5], line 2
      1 from pyads.pyads_ex import adsSyncWriteReqEx
----> 2 adsSyncWriteReqEx(plc._port, plc._adr, 0xF088, 0x00000000 | (3 & 0x000000FF), None, None)

File pyads/pyads_ex.py:495, in adsSyncWriteReqEx(port, address, index_group, index_offset, value, plc_data_type)
    493     data = value
    494 else:
--> 495     data = plc_data_type(value)
    497 data_pointer = ctypes.pointer(data)
    498 data_length = ctypes.sizeof(data)

TypeError: 'NoneType' object is not callable

Is there any way I could send this command using pyads? How to work with nullptr in python? I am thankful for any suggestions

chrisbeardy commented 1 year ago

I am sorry that noone replied to you on this. I think this is because noone knows the solution. Sorry! This is a pretty advanced case though and not something I have come across people wanting to do. As this is more of a question that a bug, could you please kindly close this issue to help us manage the issue tracker.

CagtayFabry commented 1 year ago

Thanks for looking into it @chrisbeardy

I agree that this is more of a feature request than a bug report (and a pretty advanced one probably) If you don't mind I would like to keep this open, maybe someone scrolls past here at some point who knows how to implement this.

As for the feature itself, I think it would be a great addition if pyads would be able to handle receive the corrected timestamp information. 🚀

chrisbeardy commented 1 year ago

Looking into this in more detail, it looks like None should be used as a null pointer in python when using ctypes.

https://docs.python.org/3/library/ctypes.html#calling-functions

The issue here i think is the pyads function adsSyncWriteReqEx does not account for this being passed in and is throwing the error before it even calls the C DLL.

It actually calls the C DLL here

    error_code = sync_write_request(
        port,
        ams_address_pointer,
        index_group_c,
        index_offset_c,
        data_length,
        data_pointer,
    )

You could experiment with editing the pyads code to account for the None type being passed in.

e.g.

        elif type(value) is plc_data_type:
            data = value
        elif plc_data_type is None:
            data = None
        else:
            data = plc_data_type(value)

This may not work due to the lines:

        data_pointer = ctypes.pointer(data)
        data_length = ctypes.sizeof(data)

You could also try calling the C DLL manually yourself by accessing the "private" variable in the library, or just adding it yourself by cribbing the following:

if platform_is_windows():  # pragma: no cover, skip Windows test
    dlldir_handle = None
    if sys.version_info >= (3, 8) and "TWINCAT3DIR" in os.environ:
        # Starting with version 3.8, CPython does not consider the PATH environment
        # variable any more when resolving DLL paths. The following works with the default
        # installation of the Beckhoff TwinCAT ADS DLL.
        dll_path = os.environ["TWINCAT3DIR"] + "\\..\\AdsApi\\TcAdsDll"
        if platform.architecture()[0] == "64bit":
            dll_path += "\\x64"
        dlldir_handle = os.add_dll_directory(dll_path)
    try:
        _adsDLL = ctypes.WinDLL("TcAdsDll.dll")  # type: ignore
    finally:
        if dlldir_handle:
            # Do not clobber the load path for other modules
            dlldir_handle.close()
    NOTEFUNC = ctypes.WINFUNCTYPE(  # type: ignore
        ctypes.c_void_p,
        ctypes.POINTER(SAmsAddr),
        ctypes.POINTER(SAdsNotificationHeader),
        ctypes.c_ulong,
    )

    sync_write_request = pyads._adsDLL.AdsSyncWriteReqEx  # something like that, can't remember exact path, you'll be able to fudge the path

    ams_address_pointer = ctypes.pointer(address.amsAddrStruct())
    index_group_c = ctypes.c_ulong(index_group)
    index_offset_c = ctypes.c_ulong(index_offset)

     error_code = sync_write_request(
         plc._port,
        ctypes.pointer(plc._adr.amsAddrStruct())
        ctypes.c_ulong(0xF088),
        ctypes.c_ulong(0x00000000 | (3 & 0x000000FF)),
        None,
        None,
)
CagtayFabry commented 1 year ago

This is some great insight, thank you! I will try to get it to work.

I agree, pyads has all the tools in place to make this work, might just need a few tweaks.

chrisbeardy commented 1 year ago

Let us know how you get on and if you can make pyads work, feel free to make a PR