Skip to content

Commit 1d50af2

Browse files
committed
Development
- Added invokeRpcMethod() + tests - Added ADS_RCP_METHOD_FLAGS - Updated ADS_RCP_METHOD_PARAM_FLAGS - Added parsing of RPC method and RPC method parameter attributes (see #127)
1 parent da76384 commit 1d50af2

File tree

5 files changed

+355
-34
lines changed

5 files changed

+355
-34
lines changed

src/ads-client.ts

Lines changed: 253 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2329,7 +2329,8 @@ export class Client extends EventEmitter {
23292329
const method = {} as AdsRpcMethodEntry;
23302330

23312331
//0..3 method entry length
2332-
//let len = data.readUInt32LE(pos)
2332+
let entryLength = data.readUInt32LE(pos)
2333+
let endPos = pos + entryLength;
23332334
pos += 4;
23342335

23352336
//4..7 Version
@@ -2363,7 +2364,7 @@ export class Client extends EventEmitter {
23632364

23642365
//44..47 Flags
23652366
method.flags = data.readUInt32LE(pos);
2366-
method.flagsStr = ADS.ADS_DATA_TYPE_FLAGS.toStringArray(method.flags);
2367+
method.flagsStr = ADS.ADS_RCP_METHOD_FLAGS.toStringArray(method.flags);
23672368
pos += 4;
23682369

23692370
//48..49 Name length
@@ -2421,7 +2422,7 @@ export class Client extends EventEmitter {
24212422

24222423
//16..19 Flags
24232424
param.flags = data.readUInt16LE(pos);
2424-
param.flagsStr = ADS.RCP_METHOD_PARAM_FLAGS.toStringArray(param.flags);
2425+
param.flagsStr = ADS.ADS_RCP_METHOD_PARAM_FLAGS.toStringArray(param.flags);
24252426
pos += 4;
24262427

24272428
//20..23 Reserved
@@ -2460,6 +2461,37 @@ export class Client extends EventEmitter {
24602461
param.comment = ADS.decodePlcStringBuffer(data.subarray(pos, pos + commentLength + 1));
24612462
pos += commentLength + 1;
24622463

2464+
//Attributes
2465+
param.attributes = [];
2466+
2467+
if ((param.flags & ADS.ADS_RCP_METHOD_PARAM_FLAGS.Attributes) === ADS.ADS_RCP_METHOD_PARAM_FLAGS.Attributes) {
2468+
const attributeCount = data.readUInt16LE(pos);
2469+
pos += 2;
2470+
2471+
//Attributes
2472+
for (let i = 0; i < attributeCount; i++) {
2473+
const attr = {} as AdsAttributeEntry;
2474+
2475+
//Name length
2476+
const nameLength = data.readUInt8(pos);
2477+
pos += 1;
2478+
2479+
//Value length
2480+
const valueLength = data.readUInt8(pos);
2481+
pos += 1;
2482+
2483+
//Name
2484+
attr.name = ADS.decodePlcStringBuffer(data.subarray(pos, pos + nameLength + 1));
2485+
pos += nameLength + 1;
2486+
2487+
//Value
2488+
attr.value = ADS.decodePlcStringBuffer(data.subarray(pos, pos + valueLength + 1));
2489+
pos += valueLength + 1;
2490+
2491+
method.attributes.push(attr);
2492+
}
2493+
}
2494+
24632495
if (pos - beginPosition > entryLength) {
24642496
//There is some additional data left
24652497
param.reserved2 = data.subarray(pos);
@@ -2468,6 +2500,39 @@ export class Client extends EventEmitter {
24682500
method.parameters.push(param);
24692501
}
24702502

2503+
//Attributes
2504+
method.attributes = [];
2505+
2506+
if ((method.flags & ADS.ADS_RCP_METHOD_FLAGS.Attributes) === ADS.ADS_RCP_METHOD_FLAGS.Attributes) {
2507+
const attributeCount = data.readUInt16LE(pos);
2508+
pos += 2;
2509+
2510+
//Attributes
2511+
for (let i = 0; i < attributeCount; i++) {
2512+
const attr = {} as AdsAttributeEntry;
2513+
2514+
//Name length
2515+
const nameLength = data.readUInt8(pos);
2516+
pos += 1;
2517+
2518+
//Value length
2519+
const valueLength = data.readUInt8(pos);
2520+
pos += 1;
2521+
2522+
//Name
2523+
attr.name = ADS.decodePlcStringBuffer(data.subarray(pos, pos + nameLength + 1));
2524+
pos += nameLength + 1;
2525+
2526+
//Value
2527+
attr.value = ADS.decodePlcStringBuffer(data.subarray(pos, pos + valueLength + 1));
2528+
pos += valueLength + 1;
2529+
2530+
method.attributes.push(attr);
2531+
}
2532+
}
2533+
2534+
//Using endPos to fix issues if new data is added by Beckhoff
2535+
pos = endPos;
24712536
dataType.rpcMethods.push(method)
24722537
}
24732538
}
@@ -2876,7 +2941,7 @@ export class Client extends EventEmitter {
28762941
rawValue = Buffer.alloc(dataType.size);
28772942

28782943
for (const subItem of dataType.subItems) {
2879-
//Does that javascript object contain this field/subItem?
2944+
//Does value object contain this field/subItem?
28802945
let key: undefined | string = undefined;
28812946

28822947
//First, try the easy way (5-20x times faster)
@@ -4256,7 +4321,7 @@ export class Client extends EventEmitter {
42564321
try {
42574322
handle = await this.createVariableHandle(path);
42584323
await this.writeRawByHandle(handle, value);
4259-
4324+
42604325
} finally {
42614326
if (handle) {
42624327
await this.deleteVariableHandle(handle);
@@ -4820,7 +4885,7 @@ export class Client extends EventEmitter {
48204885
*
48214886
* @param symbol Symbol information (acquired using `getSymbolInfo()`)
48224887
* @param value Data to write
4823-
* @param targetOpts Optional target settings that override values in `settings` (NOTE: If used, no caching is available -> worse performance)
4888+
* @param targetOpts Optional target settings that override values in `settings`
48244889
*/
48254890
public async writeRawBySymbol(symbol: AdsSymbolInfo, value: Buffer, targetOpts: Partial<AmsAddress> = {}): Promise<void> {
48264891
if (!this.connection.connected) {
@@ -4839,4 +4904,186 @@ export class Client extends EventEmitter {
48394904
throw new ClientError(`writeRawBySymbol(): Writing raw data by symbol ${symbol.name} failed`, err);
48404905
}
48414906
}
4907+
4908+
/**
4909+
* Invokes/calls a function block RPC method from PLC
4910+
*
4911+
* Returns method return value and/or outputs (if any)
4912+
*
4913+
* For RPC support, the method needs to have `{attribute 'TcRpcEnable'}` attribute above the `METHOD` definition
4914+
*
4915+
* @param path Full function block instance path in the PLC (such as `GVL_Test.ExampleBlock`)
4916+
* @param method Function block method name to call
4917+
* @param parameters Function block method parameters (inputs) (if any)
4918+
* @param targetOpts Optional target settings that override values in `settings`
4919+
*
4920+
* @template T Typescript data type of the method return value, for example `invokeRpcMethod<number>(...)` or `invokeRpcMethod<ST_TypedStruct>(...)`
4921+
* @template U Typescript data type of the method outputs object, for example `invokeRpcMethod<number, ST_TypedStruct>(...)`
4922+
*/
4923+
public async invokeRpcMethod<T = any, U = Record<string, any>>(path: string, method: string, parameters: Record<string, any> = {}, targetOpts: Partial<AmsAddress> = {}): Promise<RpcMethodCallResult<T, U>> {
4924+
if (!this.connection.connected) {
4925+
throw new ClientError(`invokeRpcMethod(): Client is not connected. Use connect() to connect to the target first.`);
4926+
}
4927+
4928+
const debugPath = `${path}.${method}()`;
4929+
this.debug(`invokeRpcMethod(): Calling RPC method ${debugPath}`);
4930+
4931+
try {
4932+
//Getting the symbol information
4933+
let symbolInfo: AdsSymbolInfo;
4934+
try {
4935+
this.debugD(`invokeRpcMethod(): Getting symbol information for ${path}`);
4936+
symbolInfo = await this.getSymbolInfo(path, targetOpts);
4937+
4938+
} catch (err) {
4939+
this.debug(`invokeRpcMethod(): Getting symbol information for ${path} failed: %o`, err);
4940+
throw new ClientError(`invokeRpcMethod(): Getting symbol information for ${path} failed`, err);
4941+
}
4942+
4943+
//Getting the symbol data type
4944+
let dataType: AdsDataType;
4945+
try {
4946+
this.debugD(`invokeRpcMethod(): Getting data type for ${path}`);
4947+
dataType = await this.buildDataType(symbolInfo.type, targetOpts);
4948+
4949+
} catch (err) {
4950+
this.debug(`invokeRpcMethod(): Getting symbol information for ${path} failed: %o`, err);
4951+
throw new ClientError(`invokeRpcMethod(): Getting symbol information for ${path} failed`, err);
4952+
}
4953+
4954+
4955+
//Finding the RPC method
4956+
const rpcMethod = dataType.rpcMethods.find(m => m.name.toLowerCase() === method.toLowerCase());
4957+
4958+
if (!rpcMethod) {
4959+
throw new ClientError(`invokeRpcMethod(): Method ${method} was not found from symbol ${path}. Available RPC methods are: ${dataType.rpcMethods.map(m => m.name).join(', ')}`);
4960+
}
4961+
4962+
//Method inputs and outputs
4963+
const inputs = rpcMethod.parameters.filter(p => p.flags === ADS.ADS_RCP_METHOD_PARAM_FLAGS.In);
4964+
const outputs = rpcMethod.parameters.filter(p => p.flags === ADS.ADS_RCP_METHOD_PARAM_FLAGS.Out);
4965+
4966+
//Creating data buffer for inputs
4967+
let inputsData = Buffer.alloc(0);
4968+
4969+
for (const input of inputs) {
4970+
//Does parameter object contain this field/subItem?
4971+
let key: undefined | string = undefined;
4972+
4973+
//First, try the easy way (5-20x times faster)
4974+
if (parameters[input.name] !== undefined) {
4975+
key = input.name;
4976+
4977+
} else {
4978+
//Not found, try case-insensitive way (this is slower -> only doing if needed)
4979+
try {
4980+
key = Object.keys(parameters).find(objKey => objKey.toLowerCase() === input.name.toLowerCase());
4981+
4982+
} catch (err) {
4983+
//value is null/ not an object/something else went wrong
4984+
key = undefined;
4985+
}
4986+
}
4987+
4988+
if (key === undefined) {
4989+
throw new ClientError(`invokeRpcMethod(): Missing RPC method input parameter "${input.name}"`);
4990+
}
4991+
4992+
//Creating raw data from object
4993+
//NOTE: Using internal convertObjectToBuffer() instead of convertToRaw() to achieve better error message with missing properties
4994+
const type = await this.buildDataType(input.type, targetOpts);
4995+
let res = await this.convertObjectToBuffer(parameters[key], type);
4996+
4997+
if (res.missingProperty) {
4998+
throw new ClientError(`invokeRpcMethod(): RPC method parameter "${input.name}" is missing at least key/value "${res.missingProperty}"`);
4999+
}
5000+
5001+
inputsData = Buffer.concat([inputsData, res.rawValue]);
5002+
}
5003+
5004+
//Creating a handle to the RPC method
5005+
let handle: VariableHandle;
5006+
try {
5007+
this.debugD(`invokeRpcMethod(): Creating a variable handle for ${debugPath}`);
5008+
//NOTE: #
5009+
handle = await this.createVariableHandle(`${path}#${method}`, targetOpts);
5010+
5011+
} catch (err) {
5012+
this.debug(`invokeRpcMethod(): Creating a variable handle for ${debugPath} failed: %o`, err);
5013+
throw new ClientError(`invokeRpcMethod(): Creating a variable handle for ${debugPath} failed`, err);
5014+
}
5015+
5016+
//Allocating bytes for request
5017+
const data = Buffer.alloc(16 + inputsData.byteLength);
5018+
let pos = 0;
5019+
5020+
//0..3 IndexGroup
5021+
data.writeUInt32LE(ADS.ADS_RESERVED_INDEX_GROUPS.SymbolValueByHandle, pos);
5022+
pos += 4;
5023+
5024+
//4..7 IndexOffset
5025+
data.writeUInt32LE(handle.handle, pos);
5026+
pos += 4;
5027+
5028+
//8..11 Read data length
5029+
data.writeUInt32LE(rpcMethod.returnTypeSize + outputs.reduce((total, output) => total + output.size, 0), pos);
5030+
pos += 4;
5031+
5032+
//12..15 Write data length
5033+
data.writeUInt32LE(inputsData.byteLength, pos);
5034+
pos += 4;
5035+
5036+
//16..n Data
5037+
inputsData.copy(data, pos);
5038+
5039+
try {
5040+
const res = await this.sendAdsCommand<AdsReadWriteResponse>({
5041+
adsCommand: ADS.ADS_COMMAND.ReadWrite,
5042+
targetAmsNetId: targetOpts.amsNetId,
5043+
targetAdsPort: targetOpts.adsPort,
5044+
payload: data
5045+
});
5046+
5047+
this.debug(`invokeRpcMethod(): Calling RPC method ${debugPath} done!`);
5048+
5049+
let pos = 0;
5050+
const result: RpcMethodCallResult<T, U> = {
5051+
returnValue: undefined as T,
5052+
outputs: {} as U
5053+
};
5054+
5055+
//Method return value
5056+
if (rpcMethod.returnTypeSize > 0) {
5057+
result.returnValue = await this.convertFromRaw<T>(res.ads.payload.subarray(pos, pos + rpcMethod.returnTypeSize), rpcMethod.retunDataType);
5058+
pos += rpcMethod.returnTypeSize;
5059+
}
5060+
5061+
//Outputs (VAR_OUTPUT)
5062+
for (const output of outputs) {
5063+
(result.outputs as Record<string, any>)[output.name] = await this.convertFromRaw(res.ads.payload.subarray(pos, pos + output.size), output.type);
5064+
pos += output.size;
5065+
}
5066+
5067+
return result;
5068+
5069+
} catch (err) {
5070+
this.debug(`invokeRpcMethod(): Calling RPC method ${debugPath} failed: %o`, err);
5071+
throw new ClientError(`invokeRpcMethod(): Calling RPC method ${debugPath} failed`, err);
5072+
5073+
} finally {
5074+
//Delete handle in all cases
5075+
if (handle) {
5076+
try {
5077+
await this.deleteVariableHandle(handle);
5078+
} catch (err) {
5079+
this.debug(`invokeRpcMethod(): Deleting variable handle to RPC method failed: %o`, err);
5080+
}
5081+
}
5082+
}
5083+
5084+
} catch (err) {
5085+
this.debug(`invokeRpcMethod(): Calling RPC method ${debugPath} failed: %o`, err);
5086+
throw new ClientError(`invokeRpcMethod(): Calling RPC method ${debugPath} failed`, err);
5087+
}
5088+
}
48425089
}

0 commit comments

Comments
 (0)