diff --git a/.Zeugwerk/config.json b/.Zeugwerk/config.json
index cbaa726..bbc581d 100644
--- a/.Zeugwerk/config.json
+++ b/.Zeugwerk/config.json
@@ -6,7 +6,7 @@
"name": "TcHaxx.Snappy",
"plcs": [
{
- "version": "0.1.0.0",
+ "version": "0.2.0.0",
"name": "snappy",
"type": "Library",
"packages": [
diff --git a/.editorconfig b/.editorconfig
index 9630ed9..e7a491e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -197,4 +197,5 @@ csharp_style_prefer_extended_property_pattern = true:suggestion
# SonarAnalyzer
dotnet_diagnostic.S1134.severity = suggestion
-dotnet_diagnostic.S1135.severity = suggestion
\ No newline at end of file
+dotnet_diagnostic.S1135.severity = suggestion
+dotnet_diagnostic.S6602.severity = suggestion
\ No newline at end of file
diff --git a/README.md b/README.md
index e126e39..720c067 100644
--- a/README.md
+++ b/README.md
@@ -119,15 +119,15 @@ Activate Configuration | Execute | `cmd /c start TcHaxx.Snappy.CLI verify -d \"%
## Source control: Received and Verified files
When dealing with source control, consider the following guidelines for handling **Received** and **Verified** files:
-1. **Exclusion**:
+1. **Exclude files**:
- Exclude all files with the pattern `*.received.*` from source control.
- To achieve this, add the following line to your `.gitignore` file:
```
*.received.*
```
-2. **Commitment**:
- - On the other hand, **commit** all files with the pattern `*.verified.*` to source control.
+2. **Commit files**:
+ - **Commit** all files with the pattern `*.verified.*` to source control.
> See [Verify/README](https://github.com/VerifyTests/Verify?tab=readme-ov-file#source-control-received-and-verified-files)
@@ -169,4 +169,5 @@ Option | Required | Default | Description
* [TcUnit](https://github.com/tcunit/TcUnit) - A unit testing framework for Beckhoff's TwinCAT 3
* [CommandLineParser](https://github.com/commandlineparser/commandline) - A command line parsing library for .NET applications.
* [Verify](https://github.com/VerifyTests/Verify) - A library used for snapshot testing.
-* [Serilog](https://github.com/serilog/serilog) - A logging library for .NET applications.
\ No newline at end of file
+* [Serilog](https://github.com/serilog/serilog) - A logging library for .NET applications.
+* [TF6000_ADS_DOTNET_V5_Samples](https://github.com/Beckhoff/TF6000_ADS_DOTNET_V5_Samples) - Sample code for the Version 6.X series of the TwinCAT ADS .NET Packages
\ No newline at end of file
diff --git a/src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj b/src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj
index c233345..d942cb3 100644
--- a/src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj
+++ b/src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj
@@ -10,7 +10,7 @@
A Snapshot Testing framework for TwinCAT 3
Copyright (c) 2024 densogiaichned
TwinCAT Snapshot Testing framework
- 0.1.0
+ 0.2.0
$(Version)
$(Version)
True
diff --git a/src/TcHaxx.Snappy.Common/RPC/IRpcMethodDescriptor.cs b/src/TcHaxx.Snappy.Common/RPC/IRpcMethodDescriptor.cs
index 53d9825..e323b11 100644
--- a/src/TcHaxx.Snappy.Common/RPC/IRpcMethodDescriptor.cs
+++ b/src/TcHaxx.Snappy.Common/RPC/IRpcMethodDescriptor.cs
@@ -1,6 +1,4 @@
-using TcHaxx.Snappy.Common.Verify;
-
-namespace TcHaxx.Snappy.Common.RPC;
+namespace TcHaxx.Snappy.Common.RPC;
///
/// Describes an descriptor for RPC methods.
@@ -14,8 +12,8 @@ public interface IRpcMethodDescriptor
public IEnumerable GetRpcMethodDescription();
///
- /// Registers a implementation with .
+ /// Registers a implementation with .
///
- ///
- public void Register(IVerifyMethod rpcVerifyMethod);
+ ///
+ public void Register(IRpcMethodMarker rpcMethod);
}
diff --git a/src/TcHaxx.Snappy.Common/RPC/IRpcMethodMarker.cs b/src/TcHaxx.Snappy.Common/RPC/IRpcMethodMarker.cs
new file mode 100644
index 0000000..48a411e
--- /dev/null
+++ b/src/TcHaxx.Snappy.Common/RPC/IRpcMethodMarker.cs
@@ -0,0 +1,8 @@
+namespace TcHaxx.Snappy.Common.RPC;
+
+///
+/// Empty inteface to mark RPC methods.
+///
+public interface IRpcMethodMarker
+{
+}
diff --git a/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescription.cs b/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescription.cs
index 5d714dc..7bf0e38 100644
--- a/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescription.cs
+++ b/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescription.cs
@@ -1,6 +1,8 @@
using System.Reflection;
-using TcHaxx.Snappy.Common.Verify;
namespace TcHaxx.Snappy.Common.RPC;
-public record RpcMethodDescription(MethodInfo Method, IEnumerable Parameters, ParameterInfo ReturnValue, IVerifyMethod RpcInvocableMethod);
+public record RpcMethodDescription(MethodInfo Method, IEnumerable Parameters, ParameterInfo ReturnValue, IRpcMethodMarker RpcInvocableMethod, string? Alias)
+{
+ public string InstanceName => RpcInvocableMethod.GetType().FullName!;
+}
diff --git a/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs b/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs
index b736257..2cf887f 100644
--- a/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs
+++ b/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs
@@ -1,10 +1,12 @@
-using TcHaxx.Snappy.Common.Verify;
+using System.Data;
+using System.Reflection;
+using TcHaxx.Snappy.Common.RPC.Attributes;
namespace TcHaxx.Snappy.Common.RPC;
public class RpcMethodDescriptor : IRpcMethodDescriptor
{
- private readonly Queue _verifyMethods = new();
+ private readonly Queue _verifyMethods = new();
public RpcMethodDescriptor()
{
@@ -18,20 +20,57 @@ public IEnumerable GetRpcMethodDescription()
}
}
- public void Register(IVerifyMethod rpcVerifyMethod)
+ public void Register(IRpcMethodMarker rpcMethod)
{
- _verifyMethods.Enqueue(rpcVerifyMethod);
+ _verifyMethods.Enqueue(rpcMethod);
}
-
- private RpcMethodDescription Transform(IVerifyMethod rpcVerifyMethod)
+ private static RpcMethodDescription Transform(IRpcMethodMarker rpcMethod)
{
- var method = rpcVerifyMethod.GetType().GetMethod(nameof(IVerifyMethod.Verify)) ??
- throw new RpcMethodTransformException($"Method \"{nameof(IVerifyMethod.Verify)}\" not found.");
+ var method = GetMethodInfo(rpcMethod);
+
var parameters = method.GetParameters() ??
- throw new RpcMethodTransformException($"Expected method \"{nameof(IVerifyMethod.Verify)}\" to have parameters");
+ throw new RpcMethodTransformException($"Expected method \"{method.Name}\" to have parameters");
var retVal = method.ReturnParameter;
- return new RpcMethodDescription(method, parameters, retVal, rpcVerifyMethod);
+ var alias = GetAliasAttriubte(rpcMethod);
+ return new RpcMethodDescription(method, parameters, retVal, rpcMethod, alias);
+ }
+
+ private static MethodInfo GetMethodInfo(IRpcMethodMarker rpcMethod)
+ {
+ var typeName = rpcMethod.GetType().Name;
+
+ var methodInfos = GetMethodInfos(rpcMethod);
+
+ if (methodInfos is null || methodInfos.Length == 0)
+ {
+ throw new RpcMethodTransformException($"No RPC method found in type \"{typeName}\".");
+ }
+
+ if (methodInfos.Length > 1)
+ {
+ throw new RpcMethodTransformException($"Only one RPC method supported per type ({typeName}).");
+ }
+
+ return methodInfos[0];
+ }
+
+ private static string? GetAliasAttriubte(IRpcMethodMarker rpcMethod)
+ {
+ var aliasAttribute = rpcMethod.GetType()
+ .GetMethods()
+ .Select(x => x.GetCustomAttribute())
+ .FirstOrDefault(x => x is not null);
+ return aliasAttribute?.AliasName;
+ }
+
+ private static MethodInfo[]? GetMethodInfos(IRpcMethodMarker rpcMethod)
+ {
+ var interfaces = rpcMethod.GetType().GetInterfaces();
+ var rpcInterfaces = interfaces
+ .FirstOrDefault(i => typeof(IRpcMethodMarker).IsAssignableFrom(i) && i.GetInterfaces().Length == 1 && i.GetInterfaces().Contains(typeof(IRpcMethodMarker)));
+
+ return rpcInterfaces?.GetMethods();
}
}
diff --git a/src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs b/src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs
index 8d4f20b..e558489 100644
--- a/src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs
+++ b/src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs
@@ -1,8 +1,9 @@
-using TcHaxx.Snappy.Common.RPC.Attributes;
+using TcHaxx.Snappy.Common.RPC;
+using TcHaxx.Snappy.Common.RPC.Attributes;
namespace TcHaxx.Snappy.Common.Verify;
-public interface IVerifyMethod
+public interface IVerifyMethod : IRpcMethodMarker
{
public VerificationResult Verify(
[String(Constants.DEFAULT_TEST_NAMES_LENGTH)] string testSuiteName,
diff --git a/src/TcHaxx.Snappy.TcADS/SymbolicServer.cs b/src/TcHaxx.Snappy.TcADS/SymbolicServer.cs
index a9885e9..070968f 100644
--- a/src/TcHaxx.Snappy.TcADS/SymbolicServer.cs
+++ b/src/TcHaxx.Snappy.TcADS/SymbolicServer.cs
@@ -60,6 +60,36 @@ protected override void OnConnected()
}
protected override AdsErrorCode OnRpcInvoke(IInterfaceInstance structInstance, IRpcMethod method, object[] values, out object? returnValue)
+ {
+ returnValue = null;
+ var retVal = OnRpcInvokeProxy(structInstance, method, values, out returnValue);
+
+ if (retVal != AdsErrorCode.NoError)
+ {
+ // Note:
+ // Due to a bug (?) in ADS.Net, all parameter values and the returnValue must not be NULL!
+ // Otherwise, it will throw an exception during marshalling, hence the client will only receive error 0x1
+ returnValue = 0;
+ PresetParameterValues(ref values);
+ }
+
+ return retVal;
+ }
+
+ private static void PresetParameterValues(ref object[] parameterValues)
+ {
+ for (var i = 0; i < parameterValues.Length; i++)
+ {
+ if (parameterValues[i] is not null)
+ {
+ continue;
+ }
+
+ parameterValues[i] = 0;
+ }
+ }
+
+ private AdsErrorCode OnRpcInvokeProxy(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue)
{
var iDataType = structInstance.DataType;
if (iDataType is null)
@@ -68,7 +98,8 @@ protected override AdsErrorCode OnRpcInvoke(IInterfaceInstance structInstance, I
_logger?.LogError("{OnRpcInvoke}: {IDataType} is null", nameof(OnRpcInvoke), nameof(IDataType));
return AdsErrorCode.DeviceInvalidContext;
}
+
_logger?.LogInformation("{OnRpcInvoke}: Invoking method {IRpcMethod} of {IDataTypeFullName}", nameof(OnRpcInvoke), method, iDataType.FullName);
- return _symbolFactory.InvokeRpcMethod(iDataType, values, out returnValue);
+ return _symbolFactory.InvokeRpcMethod(structInstance, method, parameterValues, out returnValue);
}
}
diff --git a/src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs b/src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs
index 9831210..1f8b58c 100644
--- a/src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs
+++ b/src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs
@@ -7,5 +7,5 @@ namespace TcHaxx.Snappy.TcADS.Symbols;
internal interface ISymbolFactory
{
void AddSymbols(ServerSymbolFactory? serverSymbolFactory);
- AdsErrorCode InvokeRpcMethod(IDataType mappedType, object[] values, out object? returnValue);
+ AdsErrorCode InvokeRpcMethod(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue);
}
diff --git a/src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs b/src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs
index 9ff4e09..d797748 100644
--- a/src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs
+++ b/src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs
@@ -4,7 +4,6 @@
using Microsoft.Extensions.Logging;
using TcHaxx.Snappy.Common.RPC;
using TcHaxx.Snappy.Common.RPC.Attributes;
-using TcHaxx.Snappy.Common.Verify;
using TwinCAT.Ads;
using TwinCAT.Ads.Server.TypeSystem;
using TwinCAT.Ads.TypeSystem;
@@ -17,7 +16,7 @@ internal class SymbolFactory : ISymbolFactory
private readonly IRpcMethodDescriptor _rpcMethodDescriptor;
private readonly ILogger? _logger;
- private readonly Dictionary _mappedStructTypeToRpcMethod = [];
+ private readonly Dictionary _mappedStructTypeToRpcMethod = [];
internal SymbolFactory(IRpcMethodDescriptor rpcMethodDescriptor, ILogger? logger)
{
@@ -42,47 +41,57 @@ public void AddSymbols(ServerSymbolFactory? serverSymbolFactory)
var retValKvp = GetMethodReturnValue(rpcMethodDescription.ReturnValue);
AddToServerSymbolFactory(serverSymbolFactory, [retValKvp]);
- var fullName = rpcMethodDescription.Method.ReflectedType?.FullName ?? rpcMethodDescription.Method.Name;
+ var fullName = rpcMethodDescription.InstanceName;
var dataArea = new DataArea($"DATA::{fullName}", idxGrp, idxOffset++, 0x10000);
_ = serverSymbolFactory.AddDataArea(dataArea);
- var rpc = BuildRpcMethod(rpcMethodDescription.Method, paramsKvp, retValKvp);
+ var rpc = BuildRpcMethod(rpcMethodDescription, paramsKvp, retValKvp);
var dtStructRpc = new StructType($"STRUCT::{fullName}");
_ = dtStructRpc.AddMethod(rpc);
_ = serverSymbolFactory.AddType(dtStructRpc);
_ = serverSymbolFactory.AddSymbol(fullName, dtStructRpc, dataArea);
+ _logger?.LogInformation("Adding RPC method {MappedTypeFullName}#{MethodName}", fullName, rpc.Name);
_mappedStructTypeToRpcMethod.Add(dtStructRpc, rpcMethodDescription.RpcInvocableMethod);
}
}
- public AdsErrorCode InvokeRpcMethod(IDataType mappedType, object[] values, out object? returnValue)
+ public AdsErrorCode InvokeRpcMethod(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue)
{
- returnValue = null;
- if (!_mappedStructTypeToRpcMethod.TryGetValue(mappedType, out var value))
+ returnValue = Activator.CreateInstance(Type.GetType(method.ReturnType, false) ?? typeof(int));
+ var mappedType = structInstance.DataType!;
+
+ if (!_mappedStructTypeToRpcMethod.TryGetValue(mappedType, out var rpcMethodType))
{
+ _logger?.LogError("No matching type found ({MappedTypeFullName})", mappedType.FullName);
return AdsErrorCode.DeviceServiceNotSupported;
}
- var rpcMethodToInvoke = value;
+ var rpcMethodToInvoke = rpcMethodType.GetType()
+ .GetMethods()
+ .FirstOrDefault(x => string.Equals(x.Name, method.Name, StringComparison.OrdinalIgnoreCase));
+ if (rpcMethodToInvoke is null)
+ {
+ _logger?.LogError("Method \"{MethodName}\" not found in type \"{MappedTypeFullName}\"", method.Name, mappedType.FullName);
+ return AdsErrorCode.DeviceServiceNotSupported;
+ }
- if (values.Length != rpcMethodToInvoke.GetType().GetMethod(nameof(IVerifyMethod.Verify))!.GetParameters().Length)
+ if (parameterValues.Length != rpcMethodToInvoke.GetParameters().Length)
{
+ _logger?.LogError("Different method parameter length: {ParameterValuesLength} != {RpcMethodParameterLength}", parameterValues.Length, rpcMethodToInvoke.GetParameters().Length);
return AdsErrorCode.DeviceInvalidParam;
}
- // FIXME: This is ugly...
- // Find a better solution, e.g. reflection.
- returnValue = rpcMethodToInvoke.Verify((string)values[0], (string)values[1], (string)values[2]);
+ returnValue = rpcMethodToInvoke.Invoke(rpcMethodType, parameterValues);
return AdsErrorCode.NoError;
}
- private static RpcMethod BuildRpcMethod(MethodInfo methodInfo, IEnumerable> paramsKvp, KeyValuePair retValKvp)
+ private static RpcMethod BuildRpcMethod(RpcMethodDescription rpcMethodDescription, IEnumerable> paramsKvp, KeyValuePair retValKvp)
{
- var nameOrAlias = methodInfo.GetCustomAttribute()?.AliasName ?? methodInfo.Name;
+ var nameOrAlias = rpcMethodDescription.Alias ?? rpcMethodDescription.Method.Name;
var rpc = new RpcMethod(nameOrAlias);
foreach (var (k, v) in paramsKvp)
{
diff --git a/src/TcHaxx.Snappy/snappy/Version/Global_Version.TcGVL b/src/TcHaxx.Snappy/snappy/Version/Global_Version.TcGVL
new file mode 100644
index 0000000..43e1e34
--- /dev/null
+++ b/src/TcHaxx.Snappy/snappy/Version/Global_Version.TcGVL
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/TcHaxx.Snappy/snappy/snappy.plcproj b/src/TcHaxx.Snappy/snappy/snappy.plcproj
index 89b4cd5..7bce371 100644
--- a/src/TcHaxx.Snappy/snappy/snappy.plcproj
+++ b/src/TcHaxx.Snappy/snappy/snappy.plcproj
@@ -19,7 +19,7 @@
TcHaxx
false
snappy
- 0.1.0.0
+ 0.2.0.0
{9c7e50a7-dead-beef-897b-4cdbc169222d}
@@ -142,6 +142,9 @@
Code
+
+ Code
+
@@ -156,6 +159,7 @@
+