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

Feature Request: RPC Method Calls #190

Open
chrisbeardy opened this issue Feb 1, 2021 · 9 comments
Open

Feature Request: RPC Method Calls #190

chrisbeardy opened this issue Feb 1, 2021 · 9 comments

Comments

@chrisbeardy
Copy link
Collaborator

Hello, does anyone know if using the C DLL it is possible to do RPC method call of Methods from PLC FB's, like you can in the .NETimplementation?

I have had a look around the C files and can't see anything obvious, I'm not sure if it is possible to get the symbol of the method and just do a read/write to the corect memory areas.

Could be worth looking into if it hasn't already.

@stlehmann
Copy link
Owner

@chrisbeardy sounds like a nice-to-have feature. Maybe you can contact @pbruenn from Beckhoff directly on this matter or open an issue on https://github.com/Beckhoff/ADS.

@chrisbeardy
Copy link
Collaborator Author

Link to issue in Beckhoff repo: Beckhoff/ADS#131

This looks promising.

@chrisbeardy
Copy link
Collaborator Author

chrisbeardy commented Feb 18, 2021

I suggest to keep it similar with the .NET implementation, they have this signiture:

// Call a Method that has the following signature (within MAIN Program)
/*  {attribute 'TcRpcEnable'}
METHOD PUBLIC M_Add : INT
VAR_INPUT
    i1 : INT := 0;
    i2 : INT := 0;
END_VAR 
*/

short result = (short)main.InvokeRpcMethod("M_Add", new object[] {(short) 3, (short) 4});});

we could create a method in the Connection class:

def invoke_rpc_method(method_path: str, method_name: str parameters: Optional[List[Any]] = None) -> Any:
    """Invokes an RPC method from the PLC, PLC methods must have the {attribute 'TcRpcEnable'}.

    Args:
          method_path: full variable path to method, e.g MAIN.fbTest
          method_name: method name, e.g. M_Add
          parameters: list of the methods paramers, None, if method has no parameters

    Returns:
        value: return value of the method
    """
    variable = f"{method_path}#{method_name}"
    # need to do some stuff to figure out size of params
    # do readwrite etc
    # pass back

Some thoughts:

  • method_path could be passed in as one param
    • MAIN.fbTest#M_Add, or
    • MAIN.fbTest.M_Add, then do a split by . to reconstruct the above
    • accept both of the above as inputs to the method and split only if "#" is not in method_path:

I think i just prefer telling the use they have to use MAIN.fbTest#M_Add or using the option of accepting both, because the option with the # means it can be passed into get handle and release handle directly using the same signiture and the user can then optionally acquire the handle themselves to speed up the code (as only 1 ads call).

method_name = method_path .split(".")[-1]
method_path = method_path .replace(f".{method_name}", f"#{method_name}")
  • the parameters for the method

    • does this have to be passed in like structure def for write_structure_by_name?
    • I don't see an easy way of being able to determine the size, or get the size by querying the PLC here?
  • we may only be able to support simple return types of the methods, i.e. not complex structs

@stlehmann
Copy link
Owner

This looks promising.

This looks very promising indeed.

I think i just prefer telling the use they have to use MAIN.fbTest#M_Add or using the option of accepting both, because the option with the # means it can be passed into get handle and release handle directly using the same signiture and the user can then optionally acquire the handle themselves to speed up the code (as only 1 ads call).

I prefer passing method_path as one parameter for the reasons you already named. If the #-way is the official way to go we should stick to it and not support the "."-way.

does this have to be passed in like structure def for write_structure_by_name?

For simple data types we could map a list and/or dict to a ctypes structure. The question is whether it's possible to read parameter types from the target or not. If not the use would always need to provide a structure definition. Otherwise he could omit the parameter definition for simple parameter types.

We may only be able to support simple return types of the methods, i.e. not complex structs

I could imagine passing a structure_def for the return type if necessary. So the method could look something like this:

def invoke_rpc_method(
    method_path: str,
    param_vals: Optional[Union[List[Any], Dict[str, Any]]] = None,
    param_def: Optional[StructureDef] = None,
    return_type: Optional[Union[int, StructureDef]] = None) -> Any:

@chrisbeardy
Copy link
Collaborator Author

This appears to be the c# sample that @dayaftereh was reffering too. And it seems that they kindly translated it to C for us.

It looks to me that the user in both examples needs to know the parameter types up front. Or at least to query them you need to know what the variables are called, in which case you may as well define a structure anyway.

namespace _30_ADS.NET_MethodCall
{

    public partial class Form1 : Form
    {
        TcAdsClient tcClient;
        AdsStream readStream, writeStream;
        AdsBinaryReader binReader;
        AdsBinaryWriter binWriter;
        int hMethod = 0;

        public Form1()
        {
            //Create a new instance of class TcAdsClient
            tcClient = new TcAdsClient();
            //Allocate memory [2 bytes] for read parameter (sum in this case)
            readStream = new AdsStream(sizeof(int));
            binReader = new AdsBinaryReader(readStream);
            //Allocate memory [4 bytes] for write parameter (summands A and B in this case)
            writeStream = new AdsStream(2 * sizeof(int));
            binWriter = new AdsBinaryWriter(writeStream);
            InitializeComponent();
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            try
            {
                //Connect to local PLC - Port 851
                tcClient.Connect(851);

                //Get the handle of the PLC method "Addition" of POU MAIN.fbMath
                hMethod = tcClient.CreateVariableHandle("MAIN.fbMath#Addition");
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
        private void btnMethodCall_Click(object sender, EventArgs e)
        {
            try
            {
                //Write value A to stream
                binWriter.Write(Convert.ToInt32(tbValueA.Text));
                //Set stream position for next value
                writeStream.Position = sizeof(int);
                //Write value B to stream
                binWriter.Write(Convert.ToInt32(tbValueB.Text));
                
                //Call PLC method via handle
                tcClient.ReadWrite((long)AdsReservedIndexGroups.SymbolValueByHandle, hMethod, readStream, writeStream);
                
                //Read sum
                tbSumAB.Text = Convert.ToString(binReader.ReadInt32());
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
            finally
            {
                // Reset stream position
                readStream.Position = 0;
                writeStream.Position = 0;
            }
        }
        private void Form1_Closing(object sender, FormClosingEventArgs e)
        {
            try
            {
                tcClient.DeleteVariableHandle(hMethod);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
            finally
            {
                tcClient.Dispose();
            }
        }
    }
}

@xwavex
Copy link

xwavex commented Jan 25, 2023

Hello, are there any news on this topic? :)

@chrisbeardy
Copy link
Collaborator Author

This is not currently being actively worked on. It's on my TODO list at some point. In the meantime there will be a way of calling RPC methods by calling the CreateVariableHandle and ReadWrite functions yourself. This is just what the planned changes in would do anyway. You can follow the comment chain in above if you wish to implement this is your own program. If you do find the time and do this in your own application, please feel free to implement this feature into pyads, it will be appreciated!

@chrisbeardy chrisbeardy changed the title Feature Request (if possible): RPC Method Calls Feature Request: RPC Method Calls Jan 25, 2023
@chrisbeardy
Copy link
Collaborator Author

example of current way of calling methods in #356

@MordorDev
Copy link

MordorDev commented Jul 10, 2024

i have implemented it in that way:

def call_method(self, handle: int, structureDef: pyads.StructureDef, returnValue: Optional[PLCDataType], *_, **kwargs) -> any:
        paramsDict = {}
        for name, value in kwargs.items():
            if not isinstance(value, get_args(PLCDataType)):
                raise ValueError(f'Value of parameter {name} is not a PLCDataType[{get_args(PLCDataType)}] but {type(value).__name__} {get_args(value)}')
            try:
                paramsDict[name] = value.value
            except AttributeError:
                paramsDict[name] = value
        if not paramsDict:
            return self.read_write(pyads.constants.ADSIGRP_SYM_VALBYHND, handle, returnValue, None, None)
        parameters = pyads.bytes_from_dict(paramsDict, structureDef)
        return self.read_write(pyads.constants.ADSIGRP_SYM_VALBYHND, handle, returnValue, parameters, pyads.constants.PLCTYPE_ARR_USINT(len(parameters)))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants