mirror of
https://gitee.com/langsisi_admin/serein-flow
synced 2026-03-02 15:50:47 +08:00
通过Emit优化Script脚本的解释执行;出于后期更新的方向,暂时隐藏表达式节点、条件表达式节点、全局数据节点;流程图转c#代码新增对于Script脚本的支持,Script脚本现在可以原生导出为C#代码。
This commit is contained in:
@@ -342,7 +342,7 @@ namespace Serein.Library
|
||||
args[i] = mainArgTasks[i].Result;
|
||||
}
|
||||
|
||||
// 并发处理 params 参数
|
||||
// 并发处理 params 类型的入参参数
|
||||
if (paramsArgs != null)
|
||||
{
|
||||
int paramsLength = paramsArgs.Length;
|
||||
|
||||
@@ -11,41 +11,81 @@ using static Serein.Library.Utils.EmitHelper;
|
||||
namespace Serein.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Emit创建的委托描述,用于WebApi、WebSocket、NodeFlow动态调用方法的场景。
|
||||
/// 通过 Emit 创建委托,代替反射调用方法,实现高性能的动态调用。
|
||||
/// 一般情况下你无须内部细节,只需要调用 Invoke() 方法即可。
|
||||
/// </summary>
|
||||
public class DelegateDetails
|
||||
{
|
||||
private readonly EmitType emitType = EmitType.None;
|
||||
|
||||
/// <summary>
|
||||
/// 创建的委托类型
|
||||
/// </summary>
|
||||
public enum EmitType
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认
|
||||
/// </summary>
|
||||
None,
|
||||
/// <summary>
|
||||
/// 方法调用
|
||||
/// </summary>
|
||||
MethodInvoke,
|
||||
/// <summary>
|
||||
/// 字段赋值
|
||||
/// </summary>
|
||||
FieldSetter,
|
||||
/// <summary>
|
||||
/// 字段取值
|
||||
/// </summary>
|
||||
FieldGetter,
|
||||
/// <summary>
|
||||
/// 属性赋值
|
||||
/// </summary>
|
||||
PropertySetter,
|
||||
/// <summary>
|
||||
/// 属性取值
|
||||
/// </summary>
|
||||
PropertyGetter,
|
||||
/// <summary>
|
||||
/// 集合取值
|
||||
/// </summary>
|
||||
CollectionGetter,
|
||||
/// <summary>
|
||||
/// 集合赋值
|
||||
/// </summary>
|
||||
CollectionSetter
|
||||
}
|
||||
|
||||
public enum GSType
|
||||
{
|
||||
Get,
|
||||
Set,
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 根据方法信息构建Emit委托
|
||||
/// </summary>
|
||||
/// <param name="methodInfo"></param>
|
||||
public DelegateDetails(MethodInfo methodInfo)
|
||||
{
|
||||
emitType = EmitType.MethodInvoke;
|
||||
var emitMethodType = EmitHelper.CreateDynamicMethod(methodInfo, out var emitDelegate);
|
||||
_emitMethodInfo = emitMethodType;
|
||||
_emitDelegate = emitDelegate;
|
||||
|
||||
SetFunc();
|
||||
}
|
||||
|
||||
|
||||
private void SetFunc()
|
||||
{
|
||||
methodType = _emitMethodInfo.EmitMethodType;
|
||||
if (_emitDelegate is Func<object, object[], Task<object>> hasResultTask)
|
||||
{
|
||||
this.hasResultTask = hasResultTask;
|
||||
funcType = 2;
|
||||
this.methodHasResultTask = hasResultTask;
|
||||
}
|
||||
else if (_emitDelegate is Func<object, object[], Task> task)
|
||||
{
|
||||
this.task = task;
|
||||
funcType = 1;
|
||||
this.methodTask = task;
|
||||
}
|
||||
else if (_emitDelegate is Func<object, object[], object> func)
|
||||
{
|
||||
this.func = func;
|
||||
funcType = 0;
|
||||
this.methodInvoke = func;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -54,9 +94,97 @@ namespace Serein.Library
|
||||
}
|
||||
|
||||
|
||||
public Func<object, object[], Task<object>> hasResultTask;
|
||||
public Func<object, object[], Task> task;
|
||||
public Func<object, object[], object> func;
|
||||
|
||||
/// <summary>
|
||||
/// 根据字段信息构建Emit取/赋值委托
|
||||
/// </summary>
|
||||
/// <param name="fieldInfo">字段信息</param>
|
||||
/// <param name="gsType">是否为 get,如果不是,则为 set</param>
|
||||
public DelegateDetails(FieldInfo fieldInfo, GSType gsType)
|
||||
{
|
||||
if (gsType == GSType.Get)
|
||||
{
|
||||
emitType = EmitType.FieldGetter;
|
||||
getter = EmitHelper.CreateFieldGetter(fieldInfo);
|
||||
|
||||
}
|
||||
else if (gsType == GSType.Set)
|
||||
{
|
||||
emitType = EmitType.FieldSetter;
|
||||
setter = EmitHelper.CreateFieldSetter(fieldInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("错误的构建类型");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据字段信息构建Emit取/赋值委托
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">字段信息</param>
|
||||
/// <param name="gsType">是否为 get,如果不是,则为 set</param>
|
||||
public DelegateDetails(PropertyInfo propertyInfo, GSType gsType)
|
||||
{
|
||||
if (gsType == GSType.Get)
|
||||
{
|
||||
emitType = EmitType.PropertyGetter;
|
||||
getter = EmitHelper.CreatePropertyGetter(propertyInfo);
|
||||
|
||||
}
|
||||
else if (gsType == GSType.Set)
|
||||
{
|
||||
emitType = EmitType.PropertySetter;
|
||||
setter = EmitHelper.CreatePropertySetter(propertyInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("错误的构建类型");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目前提供了创建集合取值/赋值委托
|
||||
/// </summary>
|
||||
/// <param name="type">类型信息</param>
|
||||
/// <param name="gsType">操作类型</param>
|
||||
public DelegateDetails(Type type, EmitType emitType)
|
||||
{
|
||||
if (emitType == EmitType.CollectionSetter)
|
||||
{
|
||||
emitType = EmitType.CollectionSetter;
|
||||
collectionSetter = EmitHelper.CreateCollectionSetter(type);
|
||||
|
||||
}
|
||||
else if (emitType == EmitType.CollectionGetter)
|
||||
{
|
||||
emitType = EmitType.CollectionGetter;
|
||||
collectionGetter = EmitHelper.CreateCollectionGetter(type);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("错误的构建类型");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private Func<object,object, object> collectionGetter= null;
|
||||
private Action<object,object, object> collectionSetter = null;
|
||||
|
||||
|
||||
private Func<object, object> getter = null;
|
||||
private Action<object, object> setter = null;
|
||||
|
||||
|
||||
private Func<object, object[], Task<object>> methodHasResultTask = null;
|
||||
private Func<object, object[], Task> methodTask = null;
|
||||
private Func<object, object[], object> methodInvoke = null;
|
||||
|
||||
|
||||
/*/// <summary>
|
||||
@@ -72,12 +200,9 @@ namespace Serein.Library
|
||||
|
||||
private Delegate _emitDelegate;
|
||||
private EmitMethodInfo _emitMethodInfo;
|
||||
/// <summary>
|
||||
/// 0是普通
|
||||
/// 1是异步无返回值
|
||||
/// 2是异步有返回值
|
||||
/// </summary>
|
||||
private int funcType;
|
||||
|
||||
|
||||
private EmitMethodType methodType;
|
||||
|
||||
/// <summary>
|
||||
/// 该Emit委托的相应信息
|
||||
@@ -96,50 +221,65 @@ namespace Serein.Library
|
||||
//public EmitMethodType EmitMethodType { get => _emitMethodType; }
|
||||
|
||||
|
||||
|
||||
public async Task<object> AutoInvokeAsync(object[] args)
|
||||
{
|
||||
if (_emitMethodInfo.IsStatic)
|
||||
{
|
||||
return await InvokeAsync(null, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
var obj = Activator.CreateInstance(_emitMethodInfo.DeclaringType);
|
||||
return await InvokeAsync(obj, args);
|
||||
}
|
||||
throw new Exception("Not static method");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>使用的实例必须能够正确调用该委托,传入的参数也必须符合方法入参信息。</para>
|
||||
/// </summary>
|
||||
/// <param name="instance">拥有符合委托签名的方法信息的实例</param>
|
||||
/// <param name="args">如果方法没有入参,也需要传入一个空数组</param>
|
||||
/// <returns>void方法自动返回null</returns>
|
||||
/// <param name="instance">拥有符合委托签名的实例</param>
|
||||
/// <param name="args">如果不需要入参,也需要传入一个空数组,而不能为 null</param>
|
||||
/// <returns>void方法、setter自动返回null</returns>
|
||||
public async Task<object> InvokeAsync(object instance, object[] args)
|
||||
{
|
||||
if (emitType == EmitType.MethodInvoke)
|
||||
{
|
||||
return await MethodInvoke(instance, args);
|
||||
}
|
||||
else if (emitType == EmitType.PropertyGetter || emitType == EmitType.FieldGetter)
|
||||
{
|
||||
return getter(instance);
|
||||
}
|
||||
else if (emitType == EmitType.PropertySetter || emitType == EmitType.FieldSetter)
|
||||
{
|
||||
setter(instance, args[0]);
|
||||
return null;
|
||||
}
|
||||
else if (emitType == EmitType.CollectionGetter)
|
||||
{
|
||||
return collectionGetter(instance, args[0]);
|
||||
}
|
||||
else if (emitType == EmitType.CollectionSetter)
|
||||
{
|
||||
collectionSetter(instance, args[0], args[1]);
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("当前委托类型不支持 InvokeAsync 方法。请使用其他方法调用。");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<object> MethodInvoke(object instance, object[] args)
|
||||
{
|
||||
if (args is null)
|
||||
{
|
||||
args = Array.Empty<object>();
|
||||
}
|
||||
if(_emitMethodInfo.IsStatic)
|
||||
if (_emitMethodInfo.IsStatic)
|
||||
{
|
||||
instance = null;
|
||||
}
|
||||
object result = null;
|
||||
if(funcType == 0)
|
||||
if (methodType == EmitMethodType.Func)
|
||||
{
|
||||
result = func.Invoke(instance, args);
|
||||
result = methodInvoke.Invoke(instance, args);
|
||||
|
||||
}
|
||||
else if (funcType == 2)
|
||||
else if (methodType == EmitMethodType.TaskHasResult)
|
||||
{
|
||||
result = await hasResultTask(instance, args);
|
||||
}
|
||||
else if (funcType == 1)
|
||||
result = await methodHasResultTask(instance, args);
|
||||
}
|
||||
else if (methodType == EmitMethodType.Task)
|
||||
{
|
||||
await task(instance, args);
|
||||
await methodTask(instance, args);
|
||||
result = null;
|
||||
}
|
||||
else
|
||||
@@ -148,5 +288,6 @@ namespace Serein.Library
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Serein.Library
|
||||
|
||||
/// <summary>
|
||||
/// <para>是否为显式参数(固定值/表达式)</para>
|
||||
/// <para>如果为 true ,则使用UI输入的文本值作为入参数据。</para>
|
||||
/// <para>如果为 true ,则使用输入的文本值作为入参数据。</para>
|
||||
/// <para>如果为 false ,则在当前流程上下文中,根据 ArgDataSourceNodeGuid 查找到对应节点,并根据 ArgDataSourceNodeGuid 判断如何获取其返回的数据,以此作为入参数据。</para>
|
||||
/// </summary>
|
||||
[PropertyInfo(IsNotification = true)]
|
||||
@@ -224,23 +224,14 @@ namespace Serein.Library
|
||||
if (IsExplicitData && !DataValue.StartsWith("@", StringComparison.OrdinalIgnoreCase))
|
||||
return DataValue.ToConvert(DataType);
|
||||
|
||||
/* // 4. 枚举绑定类型
|
||||
if (ExplicitType is not null && ExplicitType.IsEnum && DataType != ExplicitType)
|
||||
{
|
||||
var resultEnum = Enum.Parse(ExplicitType, DataValue);
|
||||
var boundType = EnumHelper.GetBoundValue(ExplicitType, resultEnum, attr => attr.Value) as Type;
|
||||
if (boundType != null)
|
||||
return NodeModel.Env.IOC.CreateObject(boundType);
|
||||
}*/
|
||||
|
||||
// 5. 来自其他节点
|
||||
// 4. 来自其他节点
|
||||
object inputParameter = null;
|
||||
var env = NodeModel.Env;
|
||||
|
||||
if (ArgDataSourceType == ConnectionArgSourceType.GetPreviousNodeData)
|
||||
{
|
||||
var prevNode = context.GetPreviousNode(NodeModel.Guid);
|
||||
inputParameter = prevNode != null ? context.GetFlowData(prevNode)?.Value : null;
|
||||
var prevNodeGuid = context.GetPreviousNode(NodeModel.Guid);
|
||||
inputParameter = prevNodeGuid != null ? context.GetFlowData(prevNodeGuid)?.Value : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -250,7 +241,7 @@ namespace Serein.Library
|
||||
if (!env.TryGetNodeModel(ArgDataSourceNodeGuid, out var sourceNode))
|
||||
throw new Exception($"[arg{Index}] 节点[{ArgDataSourceNodeGuid}]不存在");
|
||||
|
||||
if (sourceNode.IsPublic
|
||||
if (sourceNode.IsPublic // 如果运行上一节点是[FlowCall]节点(则从该节点获取入参)
|
||||
&& env.TryGetNodeModel(prevNodeGuid, out var prevNode)
|
||||
&& prevNode.ControlType == NodeControlType.FlowCall
|
||||
&& env.TryGetNodeModel(context.GetPreviousNode(NodeModel.Guid), out var sourceNodeTemp)
|
||||
@@ -259,13 +250,13 @@ namespace Serein.Library
|
||||
|
||||
if (ArgDataSourceType == ConnectionArgSourceType.GetOtherNodeData)
|
||||
inputParameter = context.GetFlowData(sourceNode.Guid)?.Value;
|
||||
else if (ArgDataSourceType == ConnectionArgSourceType.GetOtherNodeDataOfInvoke)
|
||||
else if (ArgDataSourceType == ConnectionArgSourceType.GetOtherNodeDataOfInvoke) // 立刻执行目标节点获取参数
|
||||
inputParameter = (await sourceNode.ExecutingAsync(context, CancellationToken.None)).Value;
|
||||
else
|
||||
throw new Exception("无效的 ArgDataSourceType");
|
||||
}
|
||||
|
||||
// 6. 表达式处理
|
||||
// 5. 表达式处理
|
||||
if (IsExplicitData && DataValue.StartsWith("@", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var lower = DataValue.ToLowerInvariant();
|
||||
@@ -275,7 +266,7 @@ namespace Serein.Library
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 类型转换
|
||||
// 6. 类型转换
|
||||
if (!DataType.IsValueType && inputParameter is null)
|
||||
throw new Exception($"[arg{Index}] 参数不能为null");
|
||||
|
||||
|
||||
17
Library/Network/Modbus/HexExtensions.cs
Normal file
17
Library/Network/Modbus/HexExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public static class HexExtensions
|
||||
{
|
||||
public static string ToHexString(this byte[] data, string separator = " ")
|
||||
{
|
||||
if (data == null) return string.Empty;
|
||||
return BitConverter.ToString(data).Replace("-", separator);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Library/Network/Modbus/IModbusClient.cs
Normal file
61
Library/Network/Modbus/IModbusClient.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus 客户端通用接口 (TCP/RTU 通用)
|
||||
/// </summary>
|
||||
public interface IModbusClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 报文发送时
|
||||
/// </summary>
|
||||
Action<byte[]> OnTx { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收到报文时
|
||||
/// </summary>
|
||||
Action<byte[]> OnRx { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 读取线圈状态 (0x01)
|
||||
/// </summary>
|
||||
Task<bool[]> ReadCoils(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// 读取离散输入状态 (0x02)
|
||||
/// </summary>
|
||||
Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// 读取保持寄存器 (0x03)
|
||||
/// </summary>
|
||||
Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// 读取输入寄存器 (0x04)
|
||||
/// </summary>
|
||||
Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// 写单个线圈 (0x05)
|
||||
/// </summary>
|
||||
Task WriteSingleCoil(ushort address, bool value);
|
||||
|
||||
/// <summary>
|
||||
/// 写单个寄存器 (0x06)
|
||||
/// </summary>
|
||||
Task WriteSingleRegister(ushort address, ushort value);
|
||||
|
||||
/// <summary>
|
||||
/// 写多个线圈 (0x0F)
|
||||
/// </summary>
|
||||
Task WriteMultipleCoils(ushort startAddress, bool[] values);
|
||||
|
||||
/// <summary>
|
||||
/// 写多个寄存器 (0x10)
|
||||
/// </summary>
|
||||
Task WriteMultipleRegisters(ushort startAddress, ushort[] values);
|
||||
}
|
||||
}
|
||||
119
Library/Network/Modbus/ModbusClientFactory.cs
Normal file
119
Library/Network/Modbus/ModbusClientFactory.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public static class ModbusClientFactory
|
||||
{
|
||||
private static readonly char[] separator = new[] { ':' };
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Modbus 客户端实例
|
||||
/// </summary>
|
||||
/// <param name="connectionString">
|
||||
/// 连接字符串格式:
|
||||
/// TCP示例:"tcp:192.168.1.100:502"
|
||||
/// UCP示例:"ucp:192.168.1.100:502"
|
||||
/// RTU示例:"rtu:COM3:9600:1" (格式:rtu:串口名:波特率:从站地址)
|
||||
/// </param>
|
||||
public static IModbusClient Create(string connectionString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
throw new ArgumentException("connectionString 不能为空");
|
||||
var parts = connectionString.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
||||
//var parts = connectionString.Split(':',options: StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 2)
|
||||
throw new ArgumentException("connectionString 格式错误");
|
||||
|
||||
var protocol = parts[0].ToLower();
|
||||
|
||||
if (protocol == "tcp")
|
||||
{
|
||||
// tcp:host:port
|
||||
if (parts.Length < 3)
|
||||
throw new ArgumentException("TCP格式应为 tcp:host:port");
|
||||
|
||||
string host = parts[1];
|
||||
if (!int.TryParse(parts[2], out int port))
|
||||
port = 502; // 默认端口
|
||||
|
||||
return new ModbusTcpClient(host, port);
|
||||
}
|
||||
else if (protocol == "ucp")
|
||||
{
|
||||
// ucp:host:port
|
||||
if (parts.Length < 3)
|
||||
throw new ArgumentException("TCP格式应为 tcp:host:port");
|
||||
|
||||
string host = parts[1];
|
||||
if (!int.TryParse(parts[2], out int port))
|
||||
port = 502; // 默认端口
|
||||
|
||||
return new ModbusUdpClient(host, port);
|
||||
}
|
||||
else if (protocol == "rtu")
|
||||
{
|
||||
// rtu:portName:baudRate:slaveId
|
||||
if (parts.Length < 4)
|
||||
throw new ArgumentException("RTU格式应为 rtu:portName:baudRate:slaveId");
|
||||
|
||||
string portName = parts[1];
|
||||
if (!int.TryParse(parts[2], out int baudRate))
|
||||
baudRate = 9600;
|
||||
|
||||
if (!byte.TryParse(parts[3], out byte slaveId))
|
||||
slaveId = 1;
|
||||
|
||||
return new ModbusRtuClient(portName, baudRate, slaveId: slaveId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"不支持的协议类型: {protocol}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Modbus TCP 客户端
|
||||
/// </summary>
|
||||
/// <param name="host">服务器地址</param>
|
||||
/// <param name="port">端口,默认502</param>
|
||||
public static ModbusTcpClient CreateTcpClient(string host, int port = 502)
|
||||
{
|
||||
return new ModbusTcpClient(host, port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Modbus TCP 客户端
|
||||
/// </summary>
|
||||
/// <param name="host">服务器地址</param>
|
||||
/// <param name="port">端口,默认502</param>
|
||||
public static ModbusUdpClient CreateUdpClient(string host, int port = 502)
|
||||
{
|
||||
return new ModbusUdpClient(host, port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Modbus RTU 客户端
|
||||
/// </summary>
|
||||
/// <param name="portName">串口名,比如 "COM3"</param>
|
||||
/// <param name="baudRate">波特率,默认9600</param>
|
||||
/// <param name="parity">校验,默认None</param>
|
||||
/// <param name="dataBits">数据位,默认8</param>
|
||||
/// <param name="stopBits">停止位,默认1</param>
|
||||
/// <param name="slaveId">从站地址,默认1</param>
|
||||
public static ModbusRtuClient CreateRtuClient(string portName,
|
||||
int baudRate = 9600,
|
||||
Parity parity = Parity.None,
|
||||
int dataBits = 8,
|
||||
StopBits stopBits = StopBits.One,
|
||||
byte slaveId = 1)
|
||||
{
|
||||
return new ModbusRtuClient(portName, baudRate, parity, dataBits, stopBits, slaveId);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Library/Network/Modbus/ModbusException.cs
Normal file
35
Library/Network/Modbus/ModbusException.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public class ModbusException : Exception
|
||||
{
|
||||
public byte FunctionCode { get; }
|
||||
public byte ExceptionCode { get; }
|
||||
|
||||
public ModbusException(byte functionCode, byte exceptionCode)
|
||||
: base($"Modbus异常码=0x{functionCode:X2},0x{exceptionCode:X2}({GetExceptionMessage(exceptionCode)})")
|
||||
{
|
||||
FunctionCode = functionCode;
|
||||
ExceptionCode = exceptionCode;
|
||||
}
|
||||
|
||||
private static string GetExceptionMessage(byte code) => code switch
|
||||
{
|
||||
0x01 => "非法功能。确认功能码是否被目标设备支持;检查设备固件版本是否过低;修改主站请求为设备支持的功能码", // 功能码错误
|
||||
0x02 => "非法数据地址。检查主站请求的寄存器地址和长度是否越界;确保设备配置的寄存器数量正确", // 数据地址错误
|
||||
0x03 => "非法数据值。检查写入的数值是否在设备支持的范围内;核对协议文档中对应寄存器的取值要求", // 数据值错误
|
||||
0x04 => "从站设备故障。检查设备运行状态和日志;尝试重启设备;排查硬件或内部程序错误", // 从设备故障
|
||||
0x05 => "确认。主站需通过轮询或延时机制等待处理完成,再次查询结果", // 确认
|
||||
0x06 => "从站设备忙。增加请求重试延时;避免高频率发送编程指令", // 从设备忙
|
||||
0x08 => "存储奇偶性差错。尝试重新发送请求;如错误持续出现,检查存储器硬件或文件一致性", // 内存奇偶校验错误
|
||||
0x0A => "不可用网关路径。检查网关配置和负载;确认目标设备的网络连接可用性", // 网关路径不可用
|
||||
0x0B => "网关目标设备响应失败。检查目标设备是否在线;检查网关的路由配置与网络连接", // 网关目标设备未响应
|
||||
_ => $"未知错误" // 未知错误
|
||||
};
|
||||
}
|
||||
}
|
||||
25
Library/Network/Modbus/ModbusRequest.cs
Normal file
25
Library/Network/Modbus/ModbusRequest.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
using Serein.Library.Network.Modbus;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public class ModbusRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 功能码
|
||||
/// </summary>
|
||||
public ModbusFunctionCode FunctionCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// PDU (Protocol Data Unit) 数据,不包括从站地址和CRC
|
||||
/// </summary>
|
||||
public byte[] PDU { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 异步任务完成源,用于等待响应
|
||||
/// </summary>
|
||||
public TaskCompletionSource<byte[]> Completion { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
323
Library/Network/Modbus/ModbusRtuClient.cs
Normal file
323
Library/Network/Modbus/ModbusRtuClient.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using Serein.Library.Network.Modbus;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
|
||||
|
||||
public class ModbusRtuClient : IModbusClient
|
||||
{
|
||||
public Action<byte[]> OnTx { get; set; }
|
||||
public Action<byte[]> OnRx { get; set; }
|
||||
|
||||
|
||||
private readonly SerialPort _serialPort;
|
||||
private readonly SemaphoreSlim _requestLock = new SemaphoreSlim(1, 1);
|
||||
private readonly byte _slaveId;
|
||||
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public ModbusRtuClient(string portName, int baudRate = 9600, Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One, byte slaveId = 1)
|
||||
{
|
||||
_slaveId = slaveId;
|
||||
_serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits)
|
||||
{
|
||||
ReadTimeout = 1000,
|
||||
WriteTimeout = 1000
|
||||
};
|
||||
_serialPort.Open();
|
||||
|
||||
}
|
||||
|
||||
#region 功能码封装
|
||||
|
||||
public async Task<bool[]> ReadCoils(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var response = await SendAsync(ModbusFunctionCode.ReadCoils, pdu);
|
||||
return ParseDiscreteBits(response, quantity);
|
||||
}
|
||||
|
||||
public async Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var response = await SendAsync(ModbusFunctionCode.ReadDiscreteInputs, pdu);
|
||||
return ParseDiscreteBits(response, quantity);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var response = await SendAsync(ModbusFunctionCode.ReadHoldingRegisters, pdu);
|
||||
return ParseRegisters(response, quantity);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var response = await SendAsync(ModbusFunctionCode.ReadInputRegisters, pdu);
|
||||
return ParseRegisters(response, quantity);
|
||||
}
|
||||
|
||||
public async Task WriteSingleCoil(ushort address, bool value)
|
||||
{
|
||||
var pdu = new byte[]
|
||||
{
|
||||
(byte)(address >> 8),
|
||||
(byte)(address & 0xFF),
|
||||
value ? (byte)0xFF : (byte)0x00,
|
||||
0x00
|
||||
};
|
||||
await SendAsync(ModbusFunctionCode.WriteSingleCoil, pdu);
|
||||
}
|
||||
|
||||
public async Task WriteSingleRegister(ushort address, ushort value)
|
||||
{
|
||||
var pdu = new byte[]
|
||||
{
|
||||
(byte)(address >> 8),
|
||||
(byte)(address & 0xFF),
|
||||
(byte)(value >> 8),
|
||||
(byte)(value & 0xFF)
|
||||
};
|
||||
await SendAsync(ModbusFunctionCode.WriteSingleRegister, pdu);
|
||||
}
|
||||
|
||||
|
||||
public async Task WriteMultipleCoils(ushort startAddress, bool[] values)
|
||||
{
|
||||
if (values == null || values.Length == 0)
|
||||
throw new ArgumentException("values 不能为空");
|
||||
|
||||
int byteCount = (values.Length + 7) / 8; // 需要多少字节
|
||||
byte[] coilData = new byte[byteCount];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i])
|
||||
coilData[i / 8] |= (byte)(1 << (i % 8)); // 设置对应位
|
||||
}
|
||||
|
||||
var pdu = new List<byte>
|
||||
{
|
||||
(byte)(startAddress >> 8), // 起始地址高字节
|
||||
(byte)(startAddress & 0xFF), // 起始地址低字节
|
||||
(byte)(values.Length >> 8), // 数量高字节
|
||||
(byte)(values.Length & 0xFF), // 数量低字节
|
||||
(byte)coilData.Length // 数据字节数
|
||||
};
|
||||
pdu.AddRange(coilData);
|
||||
|
||||
await SendAsync(ModbusFunctionCode.WriteMultipleCoils, pdu.ToArray());
|
||||
}
|
||||
|
||||
public async Task WriteMultipleRegisters(ushort startAddress, ushort[] values)
|
||||
{
|
||||
if (values == null || values.Length == 0)
|
||||
throw new ArgumentException("values 不能为空");
|
||||
|
||||
var arrlen = 5 + values.Length * 2;
|
||||
var pdu = new byte[arrlen];
|
||||
|
||||
pdu[0] = (byte)(startAddress >> 8); // 起始地址高字节
|
||||
pdu[1] = (byte)(startAddress & 0xFF); // 起始地址低字节
|
||||
pdu[2] = (byte)(values.Length >> 8); // 寄存器数量高字节
|
||||
pdu[3] = (byte)(values.Length & 0xFF); // 寄存器数量低字节
|
||||
pdu[4] = (byte)(values.Length * 2); // 数据字节数
|
||||
|
||||
// 添加寄存器数据(每个寄存器 2 字节:高字节在前)
|
||||
var index = 5;
|
||||
foreach(var val in values)
|
||||
{
|
||||
pdu[index++] = (byte)(val >> 8);
|
||||
pdu[index++] = (byte)(val & 0xFF);
|
||||
|
||||
}
|
||||
|
||||
/* var pdu = new List<byte>
|
||||
{
|
||||
(byte)(startAddress >> 8), // 起始地址高字节
|
||||
(byte)(startAddress & 0xFF), // 起始地址低字节
|
||||
(byte)(values.Length >> 8), // 寄存器数量高字节
|
||||
(byte)(values.Length & 0xFF), // 寄存器数量低字节
|
||||
(byte)(values.Length * 2) // 数据字节数
|
||||
};
|
||||
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
pdu.Add((byte)(val >> 8));
|
||||
pdu.Add((byte)(val & 0xFF));
|
||||
}*/
|
||||
|
||||
await SendAsync(ModbusFunctionCode.WriteMultipleRegister, pdu);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 核心通信
|
||||
|
||||
public async Task<byte[]> SendAsync(ModbusFunctionCode functionCode, byte[] pdu)
|
||||
{
|
||||
await _requestLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// 构造 RTU 帧
|
||||
byte[] frame = BuildFrame(_slaveId, (byte)functionCode, pdu);
|
||||
OnTx?.Invoke(frame); // 触发发送日志
|
||||
await _serialPort.BaseStream.WriteAsync(frame, 0, frame.Length, _cts.Token);
|
||||
await _serialPort.BaseStream.FlushAsync(_cts.Token);
|
||||
|
||||
// 接收响应
|
||||
var response = await ReceiveResponseAsync();
|
||||
OnRx?.Invoke(response); // 触发接收日志
|
||||
// 检查功能码是否异常响应
|
||||
if ((response[1] & 0x80) != 0)
|
||||
{
|
||||
byte exceptionCode = response[2];
|
||||
throw new ModbusException(response[1], exceptionCode);
|
||||
}
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_requestLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 接收响应
|
||||
/// </summary>
|
||||
private async Task<byte[]> ReceiveResponseAsync()
|
||||
{
|
||||
var buffer = new byte[256];
|
||||
int offset = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int read = await _serialPort.BaseStream.ReadAsync(buffer, offset, buffer.Length - offset, _cts.Token);
|
||||
offset += read;
|
||||
|
||||
// 最小RTU帧:地址(1) + 功能码(1) + 数据(N) + CRC(2)
|
||||
if (offset >= 5)
|
||||
{
|
||||
int frameLength = offset;
|
||||
if (!ValidateCrc(buffer, 0, frameLength))
|
||||
throw new IOException("CRC 校验失败");
|
||||
|
||||
byte[] response = new byte[frameLength - 2];
|
||||
Array.Copy(buffer, 0, response, 0, frameLength - 2);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] BuildFrame(byte slaveAddr, byte functionCode, byte[] pdu)
|
||||
{
|
||||
var frame = new byte[2 + pdu.Length + 2]; // 地址 + 功能码 + PDU + CRC
|
||||
frame[0] = slaveAddr;
|
||||
frame[1] = functionCode;
|
||||
Array.Copy(pdu, 0, frame, 2, pdu.Length);
|
||||
ushort crc = Crc16(frame, 0, frame.Length - 2);
|
||||
frame[frame.Length - 2] = (byte)(crc & 0xFF);
|
||||
frame[frame.Length - 1] = (byte)(crc >> 8);
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static bool ValidateCrc(byte[] buffer, int offset, int length)
|
||||
{
|
||||
ushort crcCalc = Crc16(buffer, offset, length - 2);
|
||||
ushort crcRecv = (ushort)(buffer[length - 2] | (buffer[length - 1] << 8));
|
||||
return crcCalc == crcRecv;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PDU与解析
|
||||
|
||||
private byte[] BuildReadPdu(ushort startAddress, ushort quantity)
|
||||
{
|
||||
|
||||
byte[] buffer = new byte[4];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), startAddress); // 起始地址高低字节
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2, 2), quantity); // 读取数量高低字节
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private bool[] ParseDiscreteBits(byte[] pdu, ushort count)
|
||||
{
|
||||
int byteCount = pdu[2]; // 第2字节是后续的字节数量
|
||||
int dataIndex = 3; // 数据从第3字节开始(0-based)
|
||||
|
||||
var result = new bool[count];
|
||||
|
||||
for (int i = 0, bytePos = 0, bitPos = 0; i < count; i++, bitPos++)
|
||||
{
|
||||
if (bitPos == 8)
|
||||
{
|
||||
bitPos = 0;
|
||||
bytePos++;
|
||||
}
|
||||
result[i] = ((pdu[dataIndex + bytePos] >> bitPos) & 0x01) != 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ushort[] ParseRegisters(byte[] pdu, ushort count)
|
||||
{
|
||||
var result = new ushort[count];
|
||||
int dataStart = 3; // 数据从第3字节开始
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int offset = dataStart + i * 2;
|
||||
result[i] = (ushort)((pdu[offset] << 8) | pdu[offset + 1]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CRC16
|
||||
private static ushort Crc16(byte[] data, int offset, int length)
|
||||
{
|
||||
const ushort polynomial = 0xA001;
|
||||
ushort crc = 0xFFFF;
|
||||
|
||||
for (int i = offset; i < offset + length; i++)
|
||||
{
|
||||
crc ^= data[i];
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
if ((crc & 0x0001) != 0)
|
||||
crc = (ushort)((crc >> 1) ^ polynomial);
|
||||
else
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_serialPort?.Close();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
19
Library/Network/Modbus/ModbusRtuRequest.cs
Normal file
19
Library/Network/Modbus/ModbusRtuRequest.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
using Serein.Library.Network.Modbus;
|
||||
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus RTU 请求实体(串口模式下无效)
|
||||
/// </summary>
|
||||
public sealed class ModbusRtuRequest : ModbusRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 从站地址(1~247)
|
||||
/// </summary>
|
||||
public byte SlaveAddress { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,19 +4,21 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus TCP 客户端
|
||||
/// </summary>
|
||||
public class ModbusTcpClient : IDisposable
|
||||
public class ModbusTcpClient : IModbusClient
|
||||
{
|
||||
public Action<byte[]> OnTx { get; set; }
|
||||
public Action<byte[]> OnRx { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息通道
|
||||
/// </summary>
|
||||
@@ -264,10 +266,15 @@ namespace Serein.Library.Network.Modbus
|
||||
/// <returns></returns>
|
||||
private async Task ProcessQueueAsync()
|
||||
{
|
||||
var request = await _channel.Reader.ReadAsync();
|
||||
byte[] packet = BuildPacket(request.TransactionId, 0x01, (byte)request.FunctionCode, request.PDU);
|
||||
await _stream.WriteAsync(packet, 0, packet.Length);
|
||||
await _stream.FlushAsync();
|
||||
while (_tcpClient.Connected)
|
||||
{
|
||||
var request = await _channel.Reader.ReadAsync();
|
||||
byte[] packet = BuildPacket(request.TransactionId, 0x01, (byte)request.FunctionCode, request.PDU);
|
||||
OnTx?.Invoke(packet); // 触发发送日志
|
||||
await _stream.WriteAsync(packet, 0, packet.Length);
|
||||
await _stream.FlushAsync();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -275,6 +282,8 @@ namespace Serein.Library.Network.Modbus
|
||||
/// </summary>
|
||||
/// <param name="functionCode">功能码</param>
|
||||
/// <param name="pdu">内容</param>
|
||||
/// <param name="timeout">超时时间</param>
|
||||
/// <param name="maxRetries">最大重发次数</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="TimeoutException"></exception>
|
||||
public Task<byte[]> SendAsync(ModbusFunctionCode functionCode, byte[] pdu)
|
||||
@@ -305,12 +314,8 @@ namespace Serein.Library.Network.Modbus
|
||||
var buffer = new byte[1024];
|
||||
while (true)
|
||||
{
|
||||
#if NET462
|
||||
int len = await _stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
#else
|
||||
int len = await _stream.ReadAsync(buffer.AsMemory(0, buffer.Length)).ConfigureAwait(false);
|
||||
#endif
|
||||
|
||||
//int len = await _stream.ReadAsync(buffer.AsMemory(0, buffer.Length)).ConfigureAwait(false);
|
||||
if (len == 0) return; // 连接关闭
|
||||
|
||||
if (len < 6)
|
||||
@@ -320,6 +325,7 @@ namespace Serein.Library.Network.Modbus
|
||||
}
|
||||
|
||||
|
||||
|
||||
ushort protocolId = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(2, 2));
|
||||
if (protocolId != 0x0000)
|
||||
{
|
||||
@@ -339,7 +345,22 @@ namespace Serein.Library.Network.Modbus
|
||||
if (_pendingRequests.TryRemove(transactionId, out var tcs))
|
||||
{
|
||||
var responsePdu = new ReadOnlySpan<byte>(buffer, 6, dataLength).ToArray();
|
||||
tcs.SetResult(responsePdu); // 如需 byte[] 则 ToArray
|
||||
if (OnRx is not null)
|
||||
{
|
||||
var packet = new ReadOnlySpan<byte>(buffer, 0, 6 + dataLength).ToArray();
|
||||
OnRx?.Invoke(packet); // 触发接收日志
|
||||
}
|
||||
|
||||
// 检查是否异常响应
|
||||
if ((responsePdu[1] & 0x80) != 0)
|
||||
{
|
||||
byte exceptionCode = responsePdu[2];
|
||||
tcs.SetException(new ModbusException(responsePdu[1], exceptionCode));
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.SetResult(responsePdu);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -400,4 +421,6 @@ namespace Serein.Library.Network.Modbus
|
||||
_tcpClient?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus TCP 请求实体
|
||||
/// </summary>
|
||||
public class ModbusTcpRequest
|
||||
public class ModbusTcpRequest : ModbusRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 事务ID
|
||||
/// </summary>
|
||||
public ushort TransactionId { get; set; }
|
||||
/// <summary>
|
||||
/// 功能码
|
||||
/// </summary>
|
||||
public ModbusFunctionCode FunctionCode { get; set; }
|
||||
/// <summary>
|
||||
/// PDU 数据
|
||||
/// </summary>
|
||||
public byte[] PDU { get; set; }
|
||||
/// <summary>
|
||||
/// 请求的完成源,用于异步等待响应
|
||||
/// </summary>
|
||||
public TaskCompletionSource<byte[]> Completion { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
260
Library/Network/Modbus/ModbusUdpClient.cs
Normal file
260
Library/Network/Modbus/ModbusUdpClient.cs
Normal file
@@ -0,0 +1,260 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public class ModbusUdpClient : IModbusClient
|
||||
{
|
||||
public Action<byte[]> OnTx { get; set; }
|
||||
public Action<byte[]> OnRx { get; set; }
|
||||
|
||||
private readonly Channel<ModbusTcpRequest> _channel = Channel.CreateUnbounded<ModbusTcpRequest>();
|
||||
private readonly UdpClient _udpClient;
|
||||
private readonly IPEndPoint _remoteEndPoint;
|
||||
private readonly ConcurrentDictionary<ushort, TaskCompletionSource<byte[]>> _pendingRequests = new();
|
||||
private int _transactionId = 0;
|
||||
|
||||
public ModbusUdpClient(string host, int port = 502)
|
||||
{
|
||||
_remoteEndPoint = new IPEndPoint(IPAddress.Parse(host), port);
|
||||
_udpClient = new UdpClient();
|
||||
_udpClient.Connect(_remoteEndPoint);
|
||||
|
||||
_ = ProcessQueueAsync();
|
||||
_ = ReceiveLoopAsync();
|
||||
}
|
||||
|
||||
#region 功能码封装
|
||||
public async Task<bool[]> ReadCoils(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var responsePdu = await SendAsync(ModbusFunctionCode.ReadCoils, pdu);
|
||||
return ParseDiscreteBits(responsePdu, quantity);
|
||||
}
|
||||
|
||||
public async Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var responsePdu = await SendAsync(ModbusFunctionCode.ReadDiscreteInputs, pdu);
|
||||
return ParseDiscreteBits(responsePdu, quantity);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var responsePdu = await SendAsync(ModbusFunctionCode.ReadHoldingRegisters, pdu);
|
||||
return ParseRegisters(responsePdu, quantity);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var responsePdu = await SendAsync(ModbusFunctionCode.ReadInputRegisters, pdu);
|
||||
return ParseRegisters(responsePdu, quantity);
|
||||
}
|
||||
|
||||
public async Task WriteSingleCoil(ushort address, bool value)
|
||||
{
|
||||
var pdu = new byte[]
|
||||
{
|
||||
(byte)(address >> 8),
|
||||
(byte)(address & 0xFF),
|
||||
value ? (byte)0xFF : (byte)0x00,
|
||||
0x00
|
||||
};
|
||||
await SendAsync(ModbusFunctionCode.WriteSingleCoil, pdu);
|
||||
}
|
||||
|
||||
public async Task WriteSingleRegister(ushort address, ushort value)
|
||||
{
|
||||
var pdu = new byte[]
|
||||
{
|
||||
(byte)(address >> 8),
|
||||
(byte)(address & 0xFF),
|
||||
(byte)(value >> 8),
|
||||
(byte)(value & 0xFF)
|
||||
};
|
||||
await SendAsync(ModbusFunctionCode.WriteSingleRegister, pdu);
|
||||
}
|
||||
|
||||
public async Task WriteMultipleCoils(ushort startAddress, bool[] values)
|
||||
{
|
||||
int byteCount = (values.Length + 7) / 8;
|
||||
byte[] data = new byte[byteCount];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i])
|
||||
data[i / 8] |= (byte)(1 << (i % 8));
|
||||
}
|
||||
|
||||
var pdu = new List<byte>
|
||||
{
|
||||
(byte)(startAddress >> 8),
|
||||
(byte)(startAddress & 0xFF),
|
||||
(byte)(values.Length >> 8),
|
||||
(byte)(values.Length & 0xFF),
|
||||
(byte)data.Length
|
||||
};
|
||||
pdu.AddRange(data);
|
||||
|
||||
await SendAsync(ModbusFunctionCode.WriteMultipleCoils, pdu.ToArray());
|
||||
}
|
||||
|
||||
public async Task WriteMultipleRegisters(ushort startAddress, ushort[] values)
|
||||
{
|
||||
var pdu = new List<byte>
|
||||
{
|
||||
(byte)(startAddress >> 8),
|
||||
(byte)(startAddress & 0xFF),
|
||||
(byte)(values.Length >> 8),
|
||||
(byte)(values.Length & 0xFF),
|
||||
(byte)(values.Length * 2)
|
||||
};
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
pdu.Add((byte)(val >> 8));
|
||||
pdu.Add((byte)(val & 0xFF));
|
||||
}
|
||||
|
||||
await SendAsync(ModbusFunctionCode.WriteMultipleRegister, pdu.ToArray());
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 核心通信
|
||||
|
||||
public Task<byte[]> SendAsync(ModbusFunctionCode functionCode, byte[] pdu)
|
||||
{
|
||||
int id = Interlocked.Increment(ref _transactionId);
|
||||
var transactionId = (ushort)(id % ushort.MaxValue);
|
||||
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var request = new ModbusTcpRequest
|
||||
{
|
||||
TransactionId = transactionId,
|
||||
FunctionCode = functionCode,
|
||||
PDU = pdu,
|
||||
Completion = tcs
|
||||
};
|
||||
|
||||
_pendingRequests[transactionId] = tcs;
|
||||
_channel.Writer.TryWrite(request);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private async Task ProcessQueueAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var request = await _channel.Reader.ReadAsync();
|
||||
byte[] packet = BuildPacket(request.TransactionId, 0x01, (byte)request.FunctionCode, request.PDU);
|
||||
OnTx?.Invoke(packet);
|
||||
await _udpClient.SendAsync(packet, packet.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
UdpReceiveResult result = await _udpClient.ReceiveAsync();
|
||||
var buffer = result.Buffer;
|
||||
|
||||
if (buffer.Length < 6) continue;
|
||||
|
||||
ushort transactionId = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(0, 2));
|
||||
if (_pendingRequests.TryRemove(transactionId, out var tcs))
|
||||
{
|
||||
OnRx?.Invoke(buffer);
|
||||
var responsePdu = new ReadOnlySpan<byte>(buffer, 6, buffer.Length - 6).ToArray();
|
||||
|
||||
if ((responsePdu[1] & 0x80) != 0)
|
||||
{
|
||||
byte exceptionCode = responsePdu[2];
|
||||
tcs.SetException(new ModbusException(responsePdu[1], exceptionCode));
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.SetResult(responsePdu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] BuildPacket(ushort transactionId, byte unitId, byte functionCode, byte[] pduData)
|
||||
{
|
||||
int pduLength = 1 + pduData.Length;
|
||||
int totalLength = 7 + pduLength;
|
||||
|
||||
Span<byte> packet = totalLength <= 256 ? stackalloc byte[totalLength] : new byte[totalLength];
|
||||
packet[0] = (byte)(transactionId >> 8);
|
||||
packet[1] = (byte)(transactionId);
|
||||
packet[2] = 0; packet[3] = 0;
|
||||
ushort length = (ushort)(pduLength + 1);
|
||||
packet[4] = (byte)(length >> 8);
|
||||
packet[5] = (byte)(length);
|
||||
packet[6] = unitId;
|
||||
packet[7] = functionCode;
|
||||
pduData.AsSpan().CopyTo(packet.Slice(8));
|
||||
return packet.ToArray();
|
||||
}
|
||||
#endregion
|
||||
|
||||
private byte[] BuildReadPdu(ushort startAddress, ushort quantity)
|
||||
{
|
||||
byte[] buffer = new byte[4];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), startAddress);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2, 2), quantity);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private bool[] ParseDiscreteBits(byte[] pdu, ushort count)
|
||||
{
|
||||
var result = new bool[count];
|
||||
int byteCount = pdu[2];
|
||||
int dataIndex = 3;
|
||||
|
||||
for (int i = 0, bytePos = 0, bitPos = 0; i < count; i++, bitPos++)
|
||||
{
|
||||
if (bitPos == 8)
|
||||
{
|
||||
bitPos = 0;
|
||||
bytePos++;
|
||||
}
|
||||
result[i] = ((pdu[dataIndex + bytePos] >> bitPos) & 0x01) != 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ushort[] ParseRegisters(byte[] pdu, ushort count)
|
||||
{
|
||||
var result = new ushort[count];
|
||||
int dataStart = 3;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int offset = dataStart + i * 2;
|
||||
result[i] = (ushort)((pdu[offset] << 8) | pdu[offset + 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var tcs in _pendingRequests.Values)
|
||||
tcs.TrySetCanceled();
|
||||
|
||||
_udpClient?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,9 +45,8 @@
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" />
|
||||
|
||||
<PackageReference Include="System.IO.Ports" Version="9.0.7" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
||||
|
||||
@@ -210,7 +210,310 @@ namespace Serein.Library.Utils
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 创建字段 Getter 委托:Func<object, object>
|
||||
/// </summary>
|
||||
public static Func<object, object> CreateFieldGetter(FieldInfo fieldInfo)
|
||||
{
|
||||
if (fieldInfo == null)
|
||||
throw new ArgumentNullException(nameof(fieldInfo));
|
||||
|
||||
var method = new DynamicMethod(
|
||||
fieldInfo.Name + "_Get",
|
||||
typeof(object),
|
||||
new[] { typeof(object) },
|
||||
fieldInfo.DeclaringType,
|
||||
true);
|
||||
|
||||
ILGenerator il = method.GetILGenerator();
|
||||
|
||||
if (!fieldInfo.IsStatic)
|
||||
{
|
||||
// 加载实例
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Castclass, fieldInfo.DeclaringType);
|
||||
il.Emit(OpCodes.Ldfld, fieldInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
il.Emit(OpCodes.Ldsfld, fieldInfo);
|
||||
}
|
||||
|
||||
// 如果是值类型,装箱
|
||||
if (fieldInfo.FieldType.IsValueType)
|
||||
il.Emit(OpCodes.Box, fieldInfo.FieldType);
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
|
||||
return (Func<object, object>)method.CreateDelegate(typeof(Func<object, object>));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建字段 Setter 委托:Action<object, object>
|
||||
/// </summary>
|
||||
public static Action<object, object> CreateFieldSetter(FieldInfo fieldInfo)
|
||||
{
|
||||
if (fieldInfo == null)
|
||||
throw new ArgumentNullException(nameof(fieldInfo));
|
||||
if (fieldInfo.IsInitOnly)
|
||||
throw new InvalidOperationException($"字段 {fieldInfo.Name} 是只读字段,无法设置值。");
|
||||
|
||||
var method = new DynamicMethod(
|
||||
fieldInfo.Name + "_Set",
|
||||
null,
|
||||
new[] { typeof(object), typeof(object) },
|
||||
fieldInfo.DeclaringType,
|
||||
true);
|
||||
|
||||
ILGenerator il = method.GetILGenerator();
|
||||
|
||||
if (!fieldInfo.IsStatic)
|
||||
{
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Castclass, fieldInfo.DeclaringType);
|
||||
}
|
||||
|
||||
// 加载值
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
if (fieldInfo.FieldType.IsValueType)
|
||||
il.Emit(OpCodes.Unbox_Any, fieldInfo.FieldType);
|
||||
else
|
||||
il.Emit(OpCodes.Castclass, fieldInfo.FieldType);
|
||||
|
||||
if (fieldInfo.IsStatic)
|
||||
il.Emit(OpCodes.Stsfld, fieldInfo);
|
||||
else
|
||||
il.Emit(OpCodes.Stfld, fieldInfo);
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
|
||||
return (Action<object, object>)method.CreateDelegate(typeof(Action<object, object>));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建属性 Getter 委托:Func<object, object>
|
||||
/// </summary>
|
||||
public static Func<object, object> CreatePropertyGetter(PropertyInfo propertyInfo)
|
||||
{
|
||||
if (propertyInfo == null)
|
||||
throw new ArgumentNullException(nameof(propertyInfo));
|
||||
var getMethod = propertyInfo.GetGetMethod(true);
|
||||
if (getMethod == null)
|
||||
throw new InvalidOperationException($"属性 {propertyInfo.Name} 没有可用的 Getter。");
|
||||
|
||||
var method = new DynamicMethod(
|
||||
propertyInfo.Name + "_Get",
|
||||
typeof(object),
|
||||
new[] { typeof(object) },
|
||||
propertyInfo.DeclaringType,
|
||||
true);
|
||||
|
||||
ILGenerator il = method.GetILGenerator();
|
||||
|
||||
if (!getMethod.IsStatic)
|
||||
{
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
|
||||
il.EmitCall(OpCodes.Callvirt, getMethod, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
il.EmitCall(OpCodes.Call, getMethod, null);
|
||||
}
|
||||
|
||||
// 装箱
|
||||
if (propertyInfo.PropertyType.IsValueType)
|
||||
il.Emit(OpCodes.Box, propertyInfo.PropertyType);
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
|
||||
return (Func<object, object>)method.CreateDelegate(typeof(Func<object, object>));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建属性 Setter 委托:Action<object, object>
|
||||
/// </summary>
|
||||
public static Action<object, object> CreatePropertySetter(PropertyInfo propertyInfo)
|
||||
{
|
||||
if (propertyInfo == null)
|
||||
throw new ArgumentNullException(nameof(propertyInfo));
|
||||
var setMethod = propertyInfo.GetSetMethod(true);
|
||||
if (setMethod == null)
|
||||
throw new InvalidOperationException($"属性 {propertyInfo.Name} 没有可用的 Setter。");
|
||||
|
||||
var method = new DynamicMethod(
|
||||
propertyInfo.Name + "_Set",
|
||||
null,
|
||||
new[] { typeof(object), typeof(object) },
|
||||
propertyInfo.DeclaringType,
|
||||
true);
|
||||
|
||||
ILGenerator il = method.GetILGenerator();
|
||||
|
||||
if (!setMethod.IsStatic)
|
||||
{
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
|
||||
}
|
||||
|
||||
// 加载值
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
if (propertyInfo.PropertyType.IsValueType)
|
||||
il.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType);
|
||||
else
|
||||
il.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
|
||||
|
||||
if (setMethod.IsStatic)
|
||||
il.EmitCall(OpCodes.Call, setMethod, null);
|
||||
else
|
||||
il.EmitCall(OpCodes.Callvirt, setMethod, null);
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
|
||||
return (Action<object, object>)method.CreateDelegate(typeof(Action<object, object>));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 创建集合赋值委托:Action<object, object, object>
|
||||
/// </summary>
|
||||
/// <param name="collectionType"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
public static Action<object, object, object> CreateCollectionSetter(Type collectionType)
|
||||
{
|
||||
DynamicMethod dm = new DynamicMethod(
|
||||
"SetCollectionValue",
|
||||
null,
|
||||
new[] { typeof(object), typeof(object), typeof(object) },
|
||||
typeof(EmitHelper).Module,
|
||||
true);
|
||||
|
||||
ILGenerator il = dm.GetILGenerator();
|
||||
|
||||
if (collectionType.IsArray)
|
||||
{
|
||||
// (object array, object index, object value) => ((T[])array)[(int)index] = (T)value;
|
||||
var elementType = collectionType.GetElementType()!;
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Castclass, collectionType); // 转为真实数组类型
|
||||
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
il.Emit(OpCodes.Unbox_Any, typeof(int)); // index
|
||||
|
||||
il.Emit(OpCodes.Ldarg_2);
|
||||
if (elementType.IsValueType)
|
||||
il.Emit(OpCodes.Unbox_Any, elementType);
|
||||
else
|
||||
il.Emit(OpCodes.Castclass, elementType);
|
||||
|
||||
il.Emit(OpCodes.Stelem, elementType); // 设置数组元素
|
||||
il.Emit(OpCodes.Ret);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 尝试获取 set_Item 方法
|
||||
MethodInfo? setItem = collectionType.GetMethod("set_Item", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (setItem == null)
|
||||
throw new NotSupportedException($"类型 {collectionType} 不支持 set_Item。");
|
||||
|
||||
var parameters = setItem.GetParameters();
|
||||
var indexType = parameters[0].ParameterType;
|
||||
var valueType = parameters[1].ParameterType;
|
||||
|
||||
// (object collection, object index, object value) => ((CollectionType)collection)[(IndexType)index] = (ValueType)value;
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Castclass, collectionType);
|
||||
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
if (indexType.IsValueType)
|
||||
il.Emit(OpCodes.Unbox_Any, indexType);
|
||||
else
|
||||
il.Emit(OpCodes.Castclass, indexType);
|
||||
|
||||
il.Emit(OpCodes.Ldarg_2);
|
||||
if (valueType.IsValueType)
|
||||
il.Emit(OpCodes.Unbox_Any, valueType);
|
||||
else
|
||||
il.Emit(OpCodes.Castclass, valueType);
|
||||
|
||||
il.Emit(OpCodes.Callvirt, setItem);
|
||||
il.Emit(OpCodes.Ret);
|
||||
}
|
||||
|
||||
return (Action<object, object, object>)dm.CreateDelegate(typeof(Action<object, object, object>));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 创建集合获取委托:Func<object, object, object>
|
||||
/// </summary>
|
||||
/// <param name="collectionType"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
public static Func<object, object, object> CreateCollectionGetter(Type collectionType)
|
||||
{
|
||||
DynamicMethod dm = new DynamicMethod(
|
||||
"GetCollectionValue",
|
||||
typeof(object),
|
||||
new[] { typeof(object), typeof(object) },
|
||||
typeof(EmitHelper).Module,
|
||||
true);
|
||||
|
||||
ILGenerator il = dm.GetILGenerator();
|
||||
|
||||
if (collectionType.IsArray)
|
||||
{
|
||||
// (object array, object index) => ((T[])array)[(int)index]
|
||||
var elementType = collectionType.GetElementType()!;
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Castclass, collectionType); // 转为真实数组类型
|
||||
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
il.Emit(OpCodes.Unbox_Any, typeof(int)); // index
|
||||
|
||||
il.Emit(OpCodes.Ldelem, elementType); // 取值
|
||||
|
||||
if (elementType.IsValueType)
|
||||
il.Emit(OpCodes.Box, elementType); // 装箱
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 调用 get_Item 方法
|
||||
MethodInfo? getItem = collectionType.GetMethod("get_Item", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (getItem == null)
|
||||
throw new NotSupportedException($"类型 {collectionType} 不支持 get_Item。");
|
||||
|
||||
var parameters = getItem.GetParameters();
|
||||
var indexType = parameters[0].ParameterType;
|
||||
var returnType = getItem.ReturnType;
|
||||
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Castclass, collectionType);
|
||||
|
||||
il.Emit(OpCodes.Ldarg_1);
|
||||
if (indexType.IsValueType)
|
||||
il.Emit(OpCodes.Unbox_Any, indexType);
|
||||
else
|
||||
il.Emit(OpCodes.Castclass, indexType);
|
||||
|
||||
il.Emit(OpCodes.Callvirt, getItem);
|
||||
|
||||
if (returnType.IsValueType)
|
||||
il.Emit(OpCodes.Box, returnType);
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
}
|
||||
|
||||
return (Func<object, object, object>)dm.CreateDelegate(typeof(Func<object, object, object>));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,16 @@ namespace Serein.Library.Utils.SereinExpression
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接字符串数组的指定部分
|
||||
/// </summary>
|
||||
/// <param name="parts"></param>
|
||||
/// <param name="startIndex"></param>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="separator"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public static string JoinStrings(string[] parts, int startIndex, int count, char separator)
|
||||
{
|
||||
if (parts == null)
|
||||
@@ -51,14 +61,21 @@ namespace Serein.Library.Utils.SereinExpression
|
||||
// 使用 string.Join 连接
|
||||
return string.Join(separator.ToString(), subArray);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 条件解析器(生成IL进行判断)
|
||||
/// 格式: data.[propertyName] [operator] [value]
|
||||
/// </summary>
|
||||
public class SereinConditionParser
|
||||
{
|
||||
/// <summary>
|
||||
/// 条件表达式
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
public static bool To<T>(T data, string expression)
|
||||
{
|
||||
try
|
||||
@@ -67,8 +84,8 @@ namespace Serein.Library.Utils.SereinExpression
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var parse = ConditionParse(data, expression);
|
||||
var result = parse.Evaluate(data);
|
||||
var parse = ConditionParse(data, expression); // 解析条件
|
||||
var result = parse.Evaluate(data); // 执行判断
|
||||
return result;
|
||||
|
||||
}
|
||||
@@ -79,15 +96,23 @@ namespace Serein.Library.Utils.SereinExpression
|
||||
}
|
||||
}
|
||||
|
||||
public static SereinConditionResolver ConditionParse(object data, string expression)
|
||||
/// <summary>
|
||||
/// 解析条件
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
private static SereinConditionResolver ConditionParse(object data, string expression)
|
||||
{
|
||||
//ReadOnlySpan<char> expSpan = expression.AsSpan();
|
||||
|
||||
if (expression[0] == '.') // 表达式前缀属于从上一个节点数据对象获取成员值
|
||||
{
|
||||
return ParseObjectExpression(data, expression);
|
||||
return ParseObjectExpression(data, expression); // 对象表达式解析
|
||||
}
|
||||
else
|
||||
{
|
||||
return ParseSimpleExpression(data, expression);
|
||||
return ParseSimpleExpression(data, expression); // 简单表达式解析
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +140,7 @@ namespace Serein.Library.Utils.SereinExpression
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象指定名称的成员
|
||||
/// </summary>
|
||||
@@ -505,42 +531,7 @@ namespace Serein.Library.Utils.SereinExpression
|
||||
|
||||
}
|
||||
}
|
||||
//public static T ValueParse<T>(object value) where T : struct, IComparable<T>
|
||||
//{
|
||||
// return (T)ValueParse(typeof(T), value);
|
||||
//}
|
||||
|
||||
//public static object ValueParse(Type type, object value)
|
||||
//{
|
||||
|
||||
// string? valueStr = value.ToString();
|
||||
// if (string.IsNullOrEmpty(valueStr))
|
||||
// {
|
||||
// throw new ArgumentException("value is null");
|
||||
// }
|
||||
// object result = type switch
|
||||
// {
|
||||
// Type t when t.IsEnum => Enum.Parse(type, valueStr),
|
||||
// Type t when t == typeof(bool) => bool.Parse(valueStr),
|
||||
// Type t when t == typeof(float) => float.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(decimal) => decimal.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(double) => double.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(sbyte) => sbyte.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(byte) => byte.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(short) => short.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(ushort) => ushort.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(int) => int.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(uint) => uint.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(long) => long.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(ulong) => ulong.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(nint) => nint.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(nuint) => nuint.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// _ => throw new ArgumentException("非预期值类型")
|
||||
// };
|
||||
// return result;
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 数值操作类型
|
||||
@@ -648,3 +639,41 @@ namespace Serein.Library.Utils.SereinExpression
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//public static T ValueParse<T>(object value) where T : struct, IComparable<T>
|
||||
//{
|
||||
// return (T)ValueParse(typeof(T), value);
|
||||
//}
|
||||
|
||||
//public static object ValueParse(Type type, object value)
|
||||
//{
|
||||
|
||||
// string? valueStr = value.ToString();
|
||||
// if (string.IsNullOrEmpty(valueStr))
|
||||
// {
|
||||
// throw new ArgumentException("value is null");
|
||||
// }
|
||||
// object result = type switch
|
||||
// {
|
||||
// Type t when t.IsEnum => Enum.Parse(type, valueStr),
|
||||
// Type t when t == typeof(bool) => bool.Parse(valueStr),
|
||||
// Type t when t == typeof(float) => float.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(decimal) => decimal.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(double) => double.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(sbyte) => sbyte.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(byte) => byte.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(short) => short.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(ushort) => ushort.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(int) => int.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(uint) => uint.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(long) => long.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(ulong) => ulong.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(nint) => nint.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// Type t when t == typeof(nuint) => nuint.Parse(valueStr, CultureInfo.InvariantCulture),
|
||||
// _ => throw new ArgumentException("非预期值类型")
|
||||
// };
|
||||
// return result;
|
||||
//}
|
||||
|
||||
|
||||
@@ -57,51 +57,6 @@ namespace Serein.NodeFlow.Model
|
||||
return;
|
||||
}
|
||||
|
||||
/* /// <summary>
|
||||
/// 移除该节点
|
||||
/// </summary>
|
||||
public virtual void Remove()
|
||||
{
|
||||
if (this.DebugSetting.CancelInterrupt != null)
|
||||
{
|
||||
this.DebugSetting.CancelInterrupt?.Invoke();
|
||||
}
|
||||
|
||||
if (this.IsPublic)
|
||||
{
|
||||
this.CanvasDetails.PublicNodes.Remove(this);
|
||||
}
|
||||
|
||||
this.DebugSetting.NodeModel = null;
|
||||
this.DebugSetting = null;
|
||||
if(this.MethodDetails is not null)
|
||||
{
|
||||
if (this.MethodDetails.ParameterDetailss != null)
|
||||
{
|
||||
foreach (var pd in this.MethodDetails.ParameterDetailss)
|
||||
{
|
||||
pd.DataValue = null;
|
||||
pd.Items = null;
|
||||
pd.NodeModel = null;
|
||||
pd.ExplicitType = null;
|
||||
pd.DataType = null;
|
||||
pd.Name = null;
|
||||
pd.ArgDataSourceNodeGuid = null;
|
||||
pd.InputType = ParameterValueInputType.Input;
|
||||
}
|
||||
}
|
||||
this.MethodDetails.ParameterDetailss = null;
|
||||
this.MethodDetails.NodeModel = null;
|
||||
this.MethodDetails.ReturnType = null;
|
||||
this.MethodDetails.ActingInstanceType = null;
|
||||
this.MethodDetails = null;
|
||||
}
|
||||
|
||||
this.Position = null;
|
||||
this.DisplayName = null;
|
||||
|
||||
this.Env = null;
|
||||
}*/
|
||||
|
||||
/// <summary>
|
||||
/// 执行节点对应的方法
|
||||
@@ -122,16 +77,16 @@ namespace Serein.NodeFlow.Model
|
||||
if (token.IsCancellationRequested) { return null; }
|
||||
}
|
||||
|
||||
MethodDetails md = MethodDetails;
|
||||
MethodDetails? md = MethodDetails;
|
||||
if (md is null)
|
||||
{
|
||||
throw new Exception($"节点{this.Guid}不存在方法信息,请检查是否需要重写节点的ExecutingAsync");
|
||||
throw new Exception($"节点{Guid}不存在方法信息,请检查是否需要重写节点的ExecutingAsync");
|
||||
}
|
||||
if (!context.Env.TryGetDelegateDetails(md.AssemblyName, md.MethodName, out var dd)) // 流程运行到某个节点
|
||||
{
|
||||
|
||||
throw new Exception($"节点{this.Guid}不存在对应委托");
|
||||
}
|
||||
|
||||
if (md.IsStatic)
|
||||
{
|
||||
object[] args = await this.GetParametersAsync(context, token);
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace Serein.NodeFlow.Model
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出脚本代码
|
||||
/// 保存项目时保存脚本代码
|
||||
/// </summary>
|
||||
/// <param name="nodeInfo"></param>
|
||||
/// <returns></returns>
|
||||
@@ -143,6 +143,61 @@ namespace Serein.NodeFlow.Model
|
||||
{
|
||||
try
|
||||
{
|
||||
HashSet<string> varNames = new HashSet<string>();
|
||||
foreach (var pd in MethodDetails.ParameterDetailss)
|
||||
{
|
||||
if (varNames.Contains(pd.Name))
|
||||
{
|
||||
throw new Exception($"脚本节点重复的变量名称:{pd.Name} - {Guid}");
|
||||
}
|
||||
varNames.Add(pd.Name);
|
||||
}
|
||||
|
||||
var argTypes = MethodDetails.ParameterDetailss
|
||||
.Select(pd =>
|
||||
{
|
||||
if (pd.ArgDataSourceType == ConnectionArgSourceType.GetPreviousNodeData)
|
||||
{
|
||||
var Type = pd.DataType;
|
||||
return (pd.Name, Type);
|
||||
}
|
||||
if (Env.TryGetNodeModel(pd.ArgDataSourceNodeGuid, out var node) &&
|
||||
node.MethodDetails?.ReturnType is not null)
|
||||
{
|
||||
pd.DataType = node.MethodDetails.ReturnType;
|
||||
var Type = node.MethodDetails.ReturnType;
|
||||
return (pd.Name, Type);
|
||||
}
|
||||
return default;
|
||||
})
|
||||
.Where(x => x != default)
|
||||
.ToDictionary(x => x.Name, x => x.Type); // 准备预定义类型
|
||||
|
||||
|
||||
var returnType = sereinScript.ParserScript(Script, argTypes); // 开始解析获取程序主节点
|
||||
MethodDetails.ReturnType = returnType;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SereinEnv.WriteLine(InfoType.WARN, ex.Message);
|
||||
return false; // 解析失败
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为 C# 代码,并且附带方法信息
|
||||
/// </summary>
|
||||
public SereinScriptMethodInfo? ToCsharpMethodInfo(string methodName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(methodName))
|
||||
{
|
||||
var tmp = Guid.Replace("-", "");
|
||||
methodName = $"FlowMethod_{tmp}";
|
||||
}
|
||||
|
||||
HashSet<string> varNames = new HashSet<string>();
|
||||
foreach (var pd in MethodDetails.ParameterDetailss)
|
||||
{
|
||||
@@ -153,7 +208,6 @@ namespace Serein.NodeFlow.Model
|
||||
varNames.Add(pd.Name);
|
||||
}
|
||||
|
||||
|
||||
var argTypes = MethodDetails.ParameterDetailss
|
||||
.Select(pd =>
|
||||
{
|
||||
@@ -176,17 +230,14 @@ namespace Serein.NodeFlow.Model
|
||||
|
||||
|
||||
var returnType = sereinScript.ParserScript(Script, argTypes); // 开始解析获取程序主节点
|
||||
|
||||
MethodDetails.ReturnType = returnType;
|
||||
|
||||
var code = sereinScript.ConvertCSharpCode("Test", argTypes);
|
||||
Debug.WriteLine(code);
|
||||
return true;
|
||||
var scriptMethodInfo = sereinScript.ConvertCSharpCode(methodName, argTypes);
|
||||
return scriptMethodInfo;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SereinEnv.WriteLine(InfoType.WARN, ex.Message);
|
||||
return false; // 解析失败
|
||||
return null; // 解析失败
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,11 +263,10 @@ namespace Serein.NodeFlow.Model
|
||||
{
|
||||
if (token.IsCancellationRequested) return new FlowResult(this.Guid, context);
|
||||
var @params = await flowCallNode.GetParametersAsync(context, token);
|
||||
if (token.IsCancellationRequested) return new FlowResult(this.Guid, context);
|
||||
|
||||
//if (token.IsCancellationRequested) return new FlowResult(this.Guid, context);
|
||||
//context.AddOrUpdate($"{context.Guid}_{this.Guid}_Params", @params[0]); // 后面再改
|
||||
|
||||
if (IsScriptChanged)
|
||||
/* if (IsScriptChanged)
|
||||
{
|
||||
lock (@params) {
|
||||
if (IsScriptChanged)
|
||||
@@ -226,7 +276,7 @@ namespace Serein.NodeFlow.Model
|
||||
context.Env.WriteLine(InfoType.INFO, $"[{Guid}]脚本解析完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
IScriptInvokeContext scriptContext = new ScriptInvokeContext(context);
|
||||
|
||||
@@ -248,7 +298,7 @@ namespace Serein.NodeFlow.Model
|
||||
var envEvent = context.Env.Event;
|
||||
envEvent.FlowRunComplete += onFlowStop; // 防止运行后台流程
|
||||
|
||||
if (token.IsCancellationRequested) return null;
|
||||
if (token.IsCancellationRequested) return new FlowResult(this.Guid, context);
|
||||
|
||||
var result = await sereinScript.InterpreterAsync(scriptContext); // 从入口节点执行
|
||||
envEvent.FlowRunComplete -= onFlowStop;
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
<ProjectReference Include="..\Serein.Library.MyGenerator\Serein.Library.NodeGenerator.csproj" OutputItemType="Analyzer" />
|
||||
|
||||
<ProjectReference Include="..\Library\Serein.Library.csproj" />
|
||||
|
||||
<ProjectReference Include="..\Serein.Script\Serein.Script.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Serein.Library;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serein.Library;
|
||||
using Serein.Library.Api;
|
||||
using Serein.Library.Utils;
|
||||
using Serein.NodeFlow.Env;
|
||||
using Serein.NodeFlow.Model;
|
||||
using Serein.Script;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -10,6 +12,7 @@ using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Serein.NodeFlow.Services
|
||||
{
|
||||
@@ -46,8 +49,11 @@ namespace Serein.NodeFlow.Services
|
||||
}
|
||||
|
||||
}
|
||||
var flowCallNode = flowModelService.GetAllNodeModel().Where(n => n.ControlType == NodeControlType.FlowCall).Select(n => (SingleFlowCallNode)n).ToArray();
|
||||
GenerateFlowApi_InitFlowApiMethodInfos(flowCallNode);
|
||||
var scriptNodes = flowModelService.GetAllNodeModel().Where(n => n.ControlType == NodeControlType.Script).OfType<SingleScriptNode>().ToArray();
|
||||
GenerateScript_InitSereinScriptMethodInfos(scriptNodes); // 初始化脚本方法
|
||||
|
||||
var flowCallNode = flowModelService.GetAllNodeModel().Where(n => n.ControlType == NodeControlType.FlowCall).OfType<SingleFlowCallNode>().ToArray();
|
||||
GenerateFlowApi_InitFlowApiMethodInfos(flowCallNode); // 初始化流程接口信息
|
||||
GenerateFlowApi_InterfaceAndImpleClass(stringBuilder); // 生成接口类
|
||||
GenerateFlowApi_ApiParamClass(stringBuilder); // 生成接口参数类
|
||||
string flowTemplateClassName = $"FlowTemplate"; // 类名
|
||||
@@ -66,16 +72,24 @@ namespace Serein.NodeFlow.Services
|
||||
}
|
||||
|
||||
// 生成实现流程接口的实现方法
|
||||
var infos = flowApiMethodInfos.Values.ToArray();
|
||||
foreach (var info in infos)
|
||||
var flowApiInfos = flowApiMethodInfos.Values.ToArray();
|
||||
foreach (var info in flowApiInfos)
|
||||
{
|
||||
stringBuilder.AppendCode(2, info.ToObjPoolSignature());
|
||||
stringBuilder.AppendCode(2, info.ToImpleMethodSignature(FlowApiMethodInfo.ParamType.Defute));
|
||||
stringBuilder.AppendCode(2, info.ToImpleMethodSignature(FlowApiMethodInfo.ParamType.HasToken));
|
||||
stringBuilder.AppendCode(2, info.ToImpleMethodSignature(FlowApiMethodInfo.ParamType.HasContextAndToken));
|
||||
}
|
||||
|
||||
stringBuilder.AppendCode(0, $"}}");
|
||||
|
||||
// 载入脚本节点转换的C#代码
|
||||
var scriptInfos = scriptMethodInfos.Values.ToArray();
|
||||
foreach (var info in scriptInfos)
|
||||
{
|
||||
stringBuilder.AppendCode(2, info.CsharpCode);
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
@@ -132,18 +146,19 @@ namespace Serein.NodeFlow.Services
|
||||
/// <exception cref="Exception"></exception>
|
||||
private void GenerateMethod(StringBuilder sb_main, IFlowNode flowNode)
|
||||
{
|
||||
string? dynamicContextTypeName = typeof(IFlowContext).FullName;
|
||||
string? flowContextTypeName = typeof(IFlowContext).FullName;
|
||||
string? flowContext = nameof(flowContext);
|
||||
|
||||
if (flowNode.ControlType == NodeControlType.Action && flowNode is SingleActionNode actionNode)
|
||||
{
|
||||
CreateMethodCore_Action(sb_main, actionNode, dynamicContextTypeName, flowContext);
|
||||
CreateMethodCore_Action(sb_main, actionNode, flowContextTypeName, flowContext);
|
||||
}
|
||||
else if (flowNode.ControlType == NodeControlType.Flipflop)
|
||||
{
|
||||
}
|
||||
else if (flowNode.ControlType == NodeControlType.Script)
|
||||
else if (flowNode.ControlType == NodeControlType.Script && flowNode is SingleScriptNode singleScriptNode)
|
||||
{
|
||||
CreateMethodCore_Script(sb_main, singleScriptNode, flowContextTypeName, flowContext);
|
||||
}
|
||||
else if (flowNode.ControlType == NodeControlType.UI)
|
||||
{
|
||||
@@ -157,7 +172,7 @@ namespace Serein.NodeFlow.Services
|
||||
}
|
||||
else if (flowNode.ControlType == NodeControlType.FlowCall && flowNode is SingleFlowCallNode flowCallNode)
|
||||
{
|
||||
CreateMethodCore_FlowCall(sb_main, flowCallNode, dynamicContextTypeName, flowContext);
|
||||
CreateMethodCore_FlowCall(sb_main, flowCallNode, flowContextTypeName, flowContext);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -169,10 +184,10 @@ namespace Serein.NodeFlow.Services
|
||||
/// </summary>
|
||||
/// <param name="sb_main"></param>
|
||||
/// <param name="actionNode"></param>
|
||||
/// <param name="dynamicContextTypeName"></param>
|
||||
/// <param name="flowContextTypeName"></param>
|
||||
/// <param name="flowContext"></param>
|
||||
/// <exception cref="Exception"></exception>
|
||||
private void CreateMethodCore_Action(StringBuilder sb_main, SingleActionNode actionNode, string? dynamicContextTypeName, string flowContext)
|
||||
private void CreateMethodCore_Action(StringBuilder sb_main, SingleActionNode actionNode, string? flowContextTypeName, string flowContext)
|
||||
{
|
||||
if (!flowLibraryService.TryGetMethodInfo(actionNode.MethodDetails.AssemblyName,
|
||||
actionNode.MethodDetails.MethodName,
|
||||
@@ -339,7 +354,7 @@ namespace Serein.NodeFlow.Services
|
||||
var resultTypeName = actionNode.MethodDetails.IsAsync ? "async Task" : "void";
|
||||
|
||||
sb_main.AppendCode(2, $"[Description(\"{instanceTypeFullName}.{methodInfo.Name}\")]");
|
||||
sb_main.AppendCode(2, $"private {resultTypeName} {actionNode.ToNodeMethodName()}(global::{dynamicContextTypeName} {flowContext})");
|
||||
sb_main.AppendCode(2, $"private {resultTypeName} {actionNode.ToNodeMethodName()}(global::{flowContextTypeName} {flowContext})");
|
||||
sb_main.AppendCode(2, $"{{");
|
||||
sb_main.AppendCode(0, sb_invoke_login.ToString());
|
||||
sb_main.AppendCode(2, $"}}"); // 方法结束
|
||||
@@ -352,103 +367,372 @@ namespace Serein.NodeFlow.Services
|
||||
/// </summary>
|
||||
/// <param name="sb_main"></param>
|
||||
/// <param name="flowCallNode"></param>
|
||||
/// <param name="dynamicContextTypeName"></param>
|
||||
/// <param name="flowContextTypeName"></param>
|
||||
/// <param name="flowContext"></param>
|
||||
/// <exception cref="Exception"></exception>
|
||||
private void CreateMethodCore_FlowCall(StringBuilder sb_main, SingleFlowCallNode flowCallNode, string? dynamicContextTypeName, string flowContext)
|
||||
private void CreateMethodCore_FlowCall(StringBuilder sb_main, SingleFlowCallNode flowCallNode, string? flowContextTypeName, string flowContext)
|
||||
{
|
||||
if (!flowApiMethodInfos.TryGetValue(flowCallNode, out var flowApiMethodInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!flowLibraryService.TryGetMethodInfo(flowCallNode.MethodDetails.AssemblyName,
|
||||
flowCallNode.MethodDetails.MethodName,
|
||||
out var methodInfo) || methodInfo is null)
|
||||
|
||||
if(flowCallNode.TargetNode is SingleScriptNode singleScriptNode)
|
||||
{
|
||||
if (!scriptMethodInfos.TryGetValue(singleScriptNode, out var scriptMethodInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceType = flowCallNode.MethodDetails.ActingInstanceType;
|
||||
var returnType = singleScriptNode.MethodDetails.ReturnType;
|
||||
|
||||
//var instanceName = instanceType.ToCamelCase();// $"instance_{instanceType.Name}";
|
||||
|
||||
//var instanceTypeFullName = instanceType.FullName;
|
||||
var returnTypeFullName = returnType == typeof(void) ? "void" : returnType.FullName;
|
||||
|
||||
#region 方法内部逻辑
|
||||
StringBuilder sb_invoke_login = new StringBuilder();
|
||||
|
||||
if (flowCallNode.MethodDetails is null) return;
|
||||
//var param = methodInfo.GetParameters();
|
||||
var md = flowCallNode.MethodDetails;
|
||||
var pds = flowCallNode.MethodDetails.ParameterDetailss;
|
||||
//if (param is null) return;
|
||||
if (pds is null) return;
|
||||
|
||||
/* for (int index = 0; index < pds.Length; index++)
|
||||
{
|
||||
ParameterDetails? pd = pds[index];
|
||||
ParameterInfo parameterInfo = param[index];
|
||||
var paramtTypeFullName = parameterInfo.ParameterType.FullName;
|
||||
}*/
|
||||
|
||||
var flowDataName = $"flowData{flowApiMethodInfo.ApiMethodName}";
|
||||
var apiData = $"apiData{flowApiMethodInfo.ApiMethodName}";
|
||||
sb_invoke_login.AppendCode(3, $"global::{typeof(object).FullName} {flowDataName} = {flowContext}.GetFlowData(\"{flowApiMethodInfo.ApiMethodName}\").Value;");
|
||||
sb_invoke_login.AppendCode(3, $"if({flowDataName} is {flowApiMethodInfo.ParamTypeName} {apiData})");
|
||||
sb_invoke_login.AppendCode(3, $"{{");
|
||||
foreach (var info in flowApiMethodInfo.ParamInfos)
|
||||
{
|
||||
sb_invoke_login.AppendCode(4, $"global::{info.Type.FullName} {info.ParamName} = {apiData}.{info.ParamName.ToPascalCase()}; ");
|
||||
}
|
||||
var invokeParamContext = string.Join(", ", flowApiMethodInfo.ParamInfos.Select(x => x.ParamName));
|
||||
if (flowApiMethodInfo.IsVoid)
|
||||
{
|
||||
// 调用无返回值方法
|
||||
var invokeFunctionContext = $"{scriptMethodInfo.ClassName}.{scriptMethodInfo.MethodName}";
|
||||
// 如果目标方法是异步的,则自动 await 进行等待
|
||||
sb_invoke_login.AppendCode(4, $"{(md.IsAsync ? $"await" : string.Empty)} {invokeFunctionContext}({invokeParamContext});");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 调用有返回值方法
|
||||
var resultName = $"result{flowApiMethodInfo.ApiMethodName}";
|
||||
var invokeFunctionContext = $"{scriptMethodInfo.ClassName}.{scriptMethodInfo.MethodName}";
|
||||
// 如果目标方法是异步的,则自动 await 进行等待
|
||||
sb_invoke_login.AppendCode(4, $"var {resultName} = {(md.IsAsync ? $"await" : string.Empty)} {invokeFunctionContext}({invokeParamContext});");
|
||||
|
||||
sb_invoke_login.AppendCode(4, $"{flowContext}.{nameof(IFlowContext.AddOrUpdate)}(\"{flowCallNode.TargetNode.Guid}\", {resultName});"); // 更新数据
|
||||
sb_invoke_login.AppendCode(4, $"{flowApiMethodInfo.ObjPoolName}.Return({apiData});"); // 归还到对象池
|
||||
//sb_invoke_login.AppendCode(3, $"return result;", false);
|
||||
}
|
||||
sb_invoke_login.AppendCode(3, $"}}");
|
||||
sb_invoke_login.AppendCode(3, $"else");
|
||||
sb_invoke_login.AppendCode(3, $"{{");
|
||||
sb_invoke_login.AppendCode(4, $"throw new Exception(\"接口参数类型异常\");");
|
||||
sb_invoke_login.AppendCode(3, $"}}");
|
||||
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
var resultTypeName = flowCallNode.MethodDetails.IsAsync ? "async Task" : "void";
|
||||
|
||||
sb_main.AppendCode(2, $"[Description(\"脚本节点接口\")]");
|
||||
sb_main.AppendCode(2, $"private {resultTypeName} {flowCallNode.ToNodeMethodName()}(global::{flowContextTypeName} {flowContext})");
|
||||
sb_main.AppendCode(2, $"{{");
|
||||
sb_main.AppendCode(0, sb_invoke_login.ToString());
|
||||
sb_main.AppendCode(2, $"}} "); // 方法结束
|
||||
|
||||
sb_main.AppendLine(); // 方法结束
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!flowLibraryService.TryGetMethodInfo(flowCallNode.MethodDetails.AssemblyName,
|
||||
flowCallNode.MethodDetails.MethodName,
|
||||
out var methodInfo) || methodInfo is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isRootNode = flowCallNode.IsRoot();
|
||||
|
||||
var instanceType = flowCallNode.MethodDetails.ActingInstanceType;
|
||||
var returnType = methodInfo.ReturnType;
|
||||
|
||||
var instanceName = instanceType.ToCamelCase();// $"instance_{instanceType.Name}";
|
||||
|
||||
var instanceTypeFullName = instanceType.FullName;
|
||||
var returnTypeFullName = returnType == typeof(void) ? "void" : returnType.FullName;
|
||||
|
||||
#region 方法内部逻辑
|
||||
StringBuilder sb_invoke_login = new StringBuilder();
|
||||
|
||||
if (flowCallNode.MethodDetails is null) return;
|
||||
//var param = methodInfo.GetParameters();
|
||||
var md = flowCallNode.MethodDetails;
|
||||
var pds = flowCallNode.MethodDetails.ParameterDetailss;
|
||||
//if (param is null) return;
|
||||
if (pds is null) return;
|
||||
|
||||
/* for (int index = 0; index < pds.Length; index++)
|
||||
{
|
||||
ParameterDetails? pd = pds[index];
|
||||
ParameterInfo parameterInfo = param[index];
|
||||
var paramtTypeFullName = parameterInfo.ParameterType.FullName;
|
||||
}*/
|
||||
|
||||
var flowDataName = $"flowData{flowApiMethodInfo.ApiMethodName}";
|
||||
var apiData = $"apiData{flowApiMethodInfo.ApiMethodName}";
|
||||
sb_invoke_login.AppendCode(3, $"global::{typeof(object).FullName} {flowDataName} = {flowContext}.GetFlowData(\"{flowApiMethodInfo.ApiMethodName}\").Value;");
|
||||
sb_invoke_login.AppendCode(3, $"if({flowDataName} is {flowApiMethodInfo.ParamTypeName} {apiData})");
|
||||
sb_invoke_login.AppendCode(3, $"{{");
|
||||
foreach (var info in flowApiMethodInfo.ParamInfos)
|
||||
{
|
||||
sb_invoke_login.AppendCode(4, $"global::{info.Type.FullName} {info.ParamName} = {apiData}.{info.ParamName.ToPascalCase()}; ");
|
||||
}
|
||||
var invokeParamContext = string.Join(", ", flowApiMethodInfo.ParamInfos.Select(x => x.ParamName));
|
||||
if (flowApiMethodInfo.IsVoid)
|
||||
{
|
||||
// 调用无返回值方法
|
||||
// 如果目标方法是静态的,则以“命名空间.类.方法”形式调用,否则以“实例.方法”形式调用
|
||||
var invokeFunctionContext = methodInfo.IsStatic ? $"global::{instanceType}.{methodInfo.Name}" : $"{instanceName}.{methodInfo.Name}";
|
||||
// 如果目标方法是异步的,则自动 await 进行等待
|
||||
sb_invoke_login.AppendCode(4, $"{(md.IsAsync ? $"await" : string.Empty)} {invokeFunctionContext}({invokeParamContext});");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 调用有返回值方法
|
||||
var resultName = $"result{flowApiMethodInfo.ApiMethodName}";
|
||||
// 如果目标方法是静态的,则以“命名空间.类.方法”形式调用,否则以“实例.方法”形式调用
|
||||
var invokeFunctionContext = methodInfo.IsStatic ? $"global::{instanceType}.{methodInfo.Name}" : $"{instanceName}.{methodInfo.Name}";
|
||||
// 如果目标方法是异步的,则自动 await 进行等待
|
||||
sb_invoke_login.AppendCode(4, $"var {resultName} = {(md.IsAsync ? $"await" : string.Empty)} {invokeFunctionContext}({invokeParamContext});");
|
||||
|
||||
sb_invoke_login.AppendCode(4, $"{flowContext}.{nameof(IFlowContext.AddOrUpdate)}(\"{flowCallNode.TargetNode.Guid}\", {resultName});"); // 更新数据
|
||||
sb_invoke_login.AppendCode(4, $"{flowApiMethodInfo.ObjPoolName}.Return({apiData});"); // 归还到对象池
|
||||
//sb_invoke_login.AppendCode(3, $"return result;", false);
|
||||
}
|
||||
sb_invoke_login.AppendCode(3, $"}}");
|
||||
sb_invoke_login.AppendCode(3, $"else");
|
||||
sb_invoke_login.AppendCode(3, $"{{");
|
||||
sb_invoke_login.AppendCode(4, $"throw new Exception(\"接口参数类型异常\");");
|
||||
sb_invoke_login.AppendCode(3, $"}}");
|
||||
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
var resultTypeName = flowCallNode.MethodDetails.IsAsync ? "async Task" : "void";
|
||||
|
||||
sb_main.AppendCode(2, $"[Description(\"{instanceTypeFullName}.{methodInfo.Name}\")]");
|
||||
sb_main.AppendCode(2, $"private {resultTypeName} {flowCallNode.ToNodeMethodName()}(global::{flowContextTypeName} {flowContext})");
|
||||
sb_main.AppendCode(2, $"{{");
|
||||
sb_main.AppendCode(0, sb_invoke_login.ToString());
|
||||
sb_main.AppendCode(2, $"}} "); // 方法结束
|
||||
|
||||
sb_main.AppendLine(); // 方法结束
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 生成[Script]节点的方法调用
|
||||
/// </summary>
|
||||
/// <param name="sb_main"></param>
|
||||
/// <param name="singleScriptNode"></param>
|
||||
/// <param name="flowContextTypeName"></param>
|
||||
/// <param name="flowContext"></param>
|
||||
private void CreateMethodCore_Script(StringBuilder sb_main, SingleScriptNode singleScriptNode, string? flowContextTypeName, string flowContext)
|
||||
{
|
||||
|
||||
|
||||
if (!scriptMethodInfos.TryGetValue(singleScriptNode, out var scriptMethodInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isRootNode = flowCallNode.IsRoot();
|
||||
var isRootNode = singleScriptNode.IsRoot();
|
||||
|
||||
var instanceType = flowCallNode.MethodDetails.ActingInstanceType;
|
||||
var returnType = methodInfo.ReturnType;
|
||||
|
||||
var instanceName = instanceType.ToCamelCase();// $"instance_{instanceType.Name}";
|
||||
|
||||
var instanceTypeFullName = instanceType.FullName;
|
||||
var returnType = scriptMethodInfo.ReturnType;
|
||||
var returnTypeFullName = returnType == typeof(void) ? "void" : returnType.FullName;
|
||||
|
||||
#region 方法内部逻辑
|
||||
StringBuilder sb_invoke_login = new StringBuilder();
|
||||
|
||||
if (flowCallNode.MethodDetails is null) return;
|
||||
var param = methodInfo.GetParameters();
|
||||
var md = flowCallNode.MethodDetails;
|
||||
var pds = flowCallNode.MethodDetails.ParameterDetailss;
|
||||
if (singleScriptNode.MethodDetails is null) return;
|
||||
var param = scriptMethodInfo.ParamInfos;
|
||||
var md = singleScriptNode.MethodDetails;
|
||||
var pds = singleScriptNode.MethodDetails.ParameterDetailss;
|
||||
if (param is null) return;
|
||||
if (pds is null) return;
|
||||
|
||||
bool isGetPreviousNode = false;
|
||||
for (int index = 0; index < pds.Length; index++)
|
||||
{
|
||||
ParameterDetails? pd = pds[index];
|
||||
ParameterInfo parameterInfo = param[index];
|
||||
SereinScriptMethodInfo.SereinScriptParamInfo parameterInfo = param[index];
|
||||
var paramtTypeFullName = parameterInfo.ParameterType.FullName;
|
||||
|
||||
if (pd.IsExplicitData)
|
||||
{
|
||||
// 只能是 数值、 文本、枚举, 才能作为显式参数
|
||||
if (parameterInfo.ParameterType.IsValueType)
|
||||
{
|
||||
if (parameterInfo.ParameterType.IsEnum)
|
||||
{
|
||||
sb_invoke_login.AppendCode(3, $"global::{paramtTypeFullName} value{index} = global::{paramtTypeFullName}.{pd.DataValue}; // 获取当前节点的上一节点数据");
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = pd.DataValue.ToConvert(parameterInfo.ParameterType);
|
||||
sb_invoke_login.AppendCode(3, $"global::{paramtTypeFullName} value{index} = (global::{paramtTypeFullName}){value}; // 获取当前节点的上一节点数据");
|
||||
|
||||
}
|
||||
}
|
||||
else if (parameterInfo.ParameterType == typeof(string))
|
||||
{
|
||||
sb_invoke_login.AppendCode(3, $"global::{paramtTypeFullName} value{index} = \"{pd.DataValue}\"; // 获取当前节点的上一节点数据");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 处理表达式
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
#region 非显式设置的参数以正常方式获取
|
||||
if (pd.ArgDataSourceType == ConnectionArgSourceType.GetPreviousNodeData)
|
||||
{
|
||||
var previousNode = $"previousNode{index}";
|
||||
var valueType = pd.IsParams ? $"global::{pd.DataType.FullName}" : $"global::{paramtTypeFullName}";
|
||||
sb_invoke_login.AppendCode(3, $"global::System.String {previousNode} = {flowContext}.GetPreviousNode(\"{singleScriptNode.Guid}\");"); // 获取运行时上一节点Guid
|
||||
sb_invoke_login.AppendCode(3, $"{valueType} value{index} = {previousNode} == null ? default : ({valueType}){flowContext}.{nameof(IFlowContext.GetFlowData)}({previousNode}).Value; // 获取运行时上一节点的数据");
|
||||
}
|
||||
else if (pd.ArgDataSourceType == ConnectionArgSourceType.GetOtherNodeData)
|
||||
{
|
||||
if (flowModelService.TryGetNodeModel(pd.ArgDataSourceNodeGuid, out var otherNode))
|
||||
{
|
||||
var valueType = pd.IsParams ? $"global::{pd.DataType.FullName}" : $"global::{paramtTypeFullName}";
|
||||
var otherNodeReturnType = otherNode.MethodDetails.ReturnType;
|
||||
if (otherNodeReturnType == typeof(object))
|
||||
{
|
||||
sb_invoke_login.AppendCode(3, $"{valueType} value{index} = ({valueType}){flowContext}.{nameof(IFlowContext.GetFlowData)}(\"{pd.ArgDataSourceNodeGuid}\").Value; // 获取指定节点的数据");
|
||||
}
|
||||
else if (pd.DataType.IsAssignableFrom(otherNodeReturnType))
|
||||
{
|
||||
sb_invoke_login.AppendCode(3, $"{valueType} value{index} = ({valueType}){flowContext}.{nameof(IFlowContext.GetFlowData)}(\"{pd.ArgDataSourceNodeGuid}\").Value; // 获取指定节点的数据");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 获取的数据无法转换为目标方法入参类型
|
||||
throw new Exception("获取的数据无法转换为目标方法入参类型");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 指定了Guid,但项目中不存在对应的节点,需要抛出异常
|
||||
throw new Exception("指定了Guid,但项目中不存在对应的节点");
|
||||
}
|
||||
}
|
||||
else if (pd.ArgDataSourceType == ConnectionArgSourceType.GetOtherNodeDataOfInvoke)
|
||||
{
|
||||
if (flowModelService.TryGetNodeModel(pd.ArgDataSourceNodeGuid, out var otherNode)) // 获取指定节点
|
||||
{
|
||||
var otherNodeReturnType = otherNode.MethodDetails.ReturnType;
|
||||
var valueType = pd.IsParams ? $"global::{pd.DataType.FullName}" : $"global::{otherNode.MethodDetails.ReturnType.FullName}";
|
||||
if (otherNodeReturnType == typeof(object))
|
||||
{
|
||||
sb_invoke_login.AppendCode(3, $"{valueType} value{index} = ({valueType}){flowContext}.{nameof(IFlowContext.GetFlowData)}(\"{pd.ArgDataSourceNodeGuid}\").Value; // 获取指定节点的数据");
|
||||
}
|
||||
else if (pd.DataType.IsAssignableFrom(otherNodeReturnType))
|
||||
{
|
||||
sb_invoke_login.AppendCode(3, $"{valueType} value{index} = {otherNode.ToNodeMethodName()}({flowContext}); // 获取指定节点的数据");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 获取的数据无法转换为目标方法入参类型
|
||||
throw new Exception("获取的数据无法转换为目标方法入参类型");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// 指定了Guid,但项目中不存在对应的节点,需要抛出异常
|
||||
throw new Exception("指定了Guid,但项目中不存在对应的节点");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var flowDataName = $"flowData{flowApiMethodInfo.ApiMethodName}";
|
||||
var apiData = $"apiData{flowApiMethodInfo.ApiMethodName}";
|
||||
sb_invoke_login.AppendCode(3, $"global::{typeof(object).FullName} {flowDataName} = {flowContext}.GetFlowData(\"{flowApiMethodInfo.ApiMethodName}\").Value;");
|
||||
sb_invoke_login.AppendCode(3, $"if({flowDataName} is {flowApiMethodInfo.ParamTypeName} {apiData})");
|
||||
sb_invoke_login.AppendCode(3, $"{{");
|
||||
foreach (var info in flowApiMethodInfo.ParamInfos)
|
||||
{
|
||||
sb_invoke_login.AppendCode(4, $"global::{info.Type.FullName} {info.ParamName} = {apiData}.{info.ParamName.ToPascalCase()}; ");
|
||||
}
|
||||
var invokeParamContext = string.Join(", ", flowApiMethodInfo.ParamInfos.Select(x => x.ParamName));
|
||||
if (flowApiMethodInfo.IsVoid)
|
||||
if (scriptMethodInfo.ReturnType == typeof(void))
|
||||
{
|
||||
// 调用无返回值方法
|
||||
// 如果目标方法是静态的,则以“命名空间.类.方法”形式调用,否则以“实例.方法”形式调用
|
||||
var invokeFunctionContext = methodInfo.IsStatic ? $"global::{instanceType}.{methodInfo.Name}" : $"{instanceName}.{methodInfo.Name}";
|
||||
var invokeFunctionContext = $"{scriptMethodInfo.ClassName}.{scriptMethodInfo.MethodName}";
|
||||
// 如果目标方法是异步的,则自动 await 进行等待
|
||||
sb_invoke_login.AppendCode(4, $"{(md.IsAsync ? $"await" : string.Empty)} {invokeFunctionContext}({invokeParamContext});");
|
||||
invokeFunctionContext = md.IsAsync ? $"await {invokeFunctionContext}" : invokeFunctionContext;
|
||||
sb_invoke_login.AppendCode(3, $"global::{invokeFunctionContext}(", false);
|
||||
for (int index = 0; index < pds.Length; index++)
|
||||
{
|
||||
sb_invoke_login.Append($"{(index == 0 ? "" : ",")}value{index}");
|
||||
}
|
||||
sb_invoke_login.AppendCode(0, $"); // 调用生成的C#代码");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 调用有返回值方法
|
||||
var resultName = $"result{flowApiMethodInfo.ApiMethodName}";
|
||||
// 如果目标方法是静态的,则以“命名空间.类.方法”形式调用,否则以“实例.方法”形式调用
|
||||
var invokeFunctionContext = methodInfo.IsStatic ? $"global::{instanceType}.{methodInfo.Name}" : $"{instanceName}.{methodInfo.Name}";
|
||||
var invokeFunctionContext = $"{scriptMethodInfo.ClassName}.{scriptMethodInfo.MethodName}";
|
||||
// 如果目标方法是异步的,则自动 await 进行等待
|
||||
sb_invoke_login.AppendCode(4, $"var {resultName} = {(md.IsAsync ? $"await" : string.Empty)} {invokeFunctionContext}({invokeParamContext});");
|
||||
invokeFunctionContext = md.IsAsync ? $"await {invokeFunctionContext}" : invokeFunctionContext;
|
||||
sb_invoke_login.AppendCode(3, $"var result = {invokeFunctionContext}(", false);
|
||||
for (int index = 0; index < pds.Length; index++)
|
||||
{
|
||||
sb_invoke_login.Append($"{(index == 0 ? "" : ",")}value{index}");
|
||||
}
|
||||
sb_invoke_login.AppendCode(0, $"); // 调用生成的C#代码");
|
||||
|
||||
sb_invoke_login.AppendCode(4, $"{flowContext}.{nameof(IFlowContext.AddOrUpdate)}(\"{flowCallNode.TargetNode.Guid}\", {resultName});"); // 更新数据
|
||||
sb_invoke_login.AppendCode(4, $"{flowApiMethodInfo.ObjPoolName}.Return({apiData});"); // 归还到对象池
|
||||
sb_invoke_login.AppendCode(3, $"{flowContext}.{nameof(IFlowContext.AddOrUpdate)}(\"{singleScriptNode.Guid}\", result);", false); // 更新数据
|
||||
//sb_invoke_login.AppendCode(3, $"return result;", false);
|
||||
}
|
||||
sb_invoke_login.AppendCode(3, $"}}");
|
||||
sb_invoke_login.AppendCode(3, $"else");
|
||||
sb_invoke_login.AppendCode(3, $"{{");
|
||||
sb_invoke_login.AppendCode(4, $"throw new Exception(\"接口参数类型异常\");");
|
||||
sb_invoke_login.AppendCode(3, $"}}");
|
||||
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
// global::{returnTypeFullName}
|
||||
|
||||
var resultTypeName = flowCallNode.MethodDetails.IsAsync ? "async Task" : "void";
|
||||
var resultTypeName = singleScriptNode.MethodDetails.IsAsync ? "async Task" : "void";
|
||||
|
||||
sb_main.AppendCode(2, $"[Description(\"{instanceTypeFullName}.{methodInfo.Name}\")]");
|
||||
sb_main.AppendCode(2, $"private {resultTypeName} {flowCallNode.ToNodeMethodName()}(global::{dynamicContextTypeName} {flowContext})");
|
||||
sb_main.AppendCode(2, $"[Description(\"脚本节点\")]");
|
||||
sb_main.AppendCode(2, $"private {resultTypeName} {singleScriptNode.ToNodeMethodName()}(global::{flowContextTypeName} {flowContext})");
|
||||
sb_main.AppendCode(2, $"{{");
|
||||
sb_main.AppendCode(0, sb_invoke_login.ToString());
|
||||
sb_main.AppendCode(2, $"}} "); // 方法结束
|
||||
|
||||
sb_main.AppendCode(2, $"}}"); // 方法结束
|
||||
sb_main.AppendLine(); // 方法结束
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -478,7 +762,10 @@ namespace Serein.NodeFlow.Services
|
||||
foreach (var node in flowNodes)
|
||||
{
|
||||
var nodeMethod = node.ToNodeMethodName(); // 节点对应的方法名称
|
||||
if (node.ControlType == NodeControlType.Action || node.ControlType == NodeControlType.FlowCall)
|
||||
if (node.ControlType == NodeControlType.Action
|
||||
|| node.ControlType == NodeControlType.FlowCall
|
||||
|| node.ControlType == NodeControlType.Script
|
||||
)
|
||||
{
|
||||
sb.AppendCode(3, $"Get(\"{node.Guid}\").SetAction({nodeMethod});");
|
||||
}
|
||||
@@ -649,6 +936,38 @@ namespace Serein.NodeFlow.Services
|
||||
sb.AppendCode(2, $"}}");
|
||||
}
|
||||
|
||||
#region 脚本节点的代码生成
|
||||
private Dictionary<SingleScriptNode, SereinScriptMethodInfo> scriptMethodInfos = [];
|
||||
private void GenerateScript_InitSereinScriptMethodInfos(SingleScriptNode[] flowCallNodes)
|
||||
{
|
||||
bool isError = false;
|
||||
foreach(var node in flowCallNodes)
|
||||
{
|
||||
var methodName = node.ToNodeMethodName();
|
||||
var info = node.ToCsharpMethodInfo(methodName);
|
||||
if(info is null)
|
||||
{
|
||||
isError = true;
|
||||
SereinEnv.WriteLine(InfoType.WARN, $"脚本节点[{node.Guid}]无法生成代码信息");
|
||||
}
|
||||
else
|
||||
{
|
||||
scriptMethodInfos[node] = info;
|
||||
}
|
||||
|
||||
}
|
||||
if (isError)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 流程接口节点的代码生成
|
||||
|
||||
/// <summary>
|
||||
/// 流程接口节点与对应的流程方法信息
|
||||
/// </summary>
|
||||
|
||||
private Dictionary<SingleFlowCallNode, FlowApiMethodInfo> flowApiMethodInfos = [];
|
||||
/// <summary>
|
||||
@@ -667,7 +986,7 @@ namespace Serein.NodeFlow.Services
|
||||
var info = flowCallNode.ToFlowApiMethodInfo();
|
||||
if (info is not null)
|
||||
{
|
||||
flowApiMethodInfos.TryAdd(flowCallNode, info);
|
||||
flowApiMethodInfos[flowCallNode] = info;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -725,6 +1044,7 @@ namespace Serein.NodeFlow.Services
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助代码生成的方法
|
||||
|
||||
@@ -783,7 +1103,9 @@ namespace Serein.NodeFlow.Services
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 指示流程接口方法需要生成什么代码
|
||||
/// </summary>
|
||||
internal class FlowApiMethodInfo
|
||||
{
|
||||
public FlowApiMethodInfo(SingleFlowCallNode singleFlowCallNode)
|
||||
@@ -1063,6 +1385,14 @@ namespace Serein.NodeFlow.Services
|
||||
sb.AppendCode(3, $"{flowContext}.{nameof(IFlowContext.SetPreviousNode)}(\"{NodeModel.Guid}\", \"{ApiMethodName}\");");
|
||||
sb.AppendCode(3, $"global::{typeof(CallNode).FullName} node = Get(\"{NodeModel.Guid}\");");
|
||||
sb.AppendCode(3, $"global::{typeof(FlowResult).FullName} {flowResult} = await node.{nameof(CallNode.StartFlowAsync)}({flowContext}, {token}); // 调用目标方法");
|
||||
if(ReturnType == typeof(object))
|
||||
{
|
||||
sb.AppendCode(3, $"if ({flowResult}.{nameof(FlowResult.Value)} is null)");
|
||||
sb.AppendCode(3, $"{{");
|
||||
sb.AppendCode(4, $"return null;");
|
||||
sb.AppendCode(3, $"}}");
|
||||
}
|
||||
|
||||
sb.AppendCode(3, $"if ({flowResult}.{nameof(FlowResult.Value)} is global::{ReturnType.FullName} result)");
|
||||
sb.AppendCode(3, $"{{");
|
||||
sb.AppendCode(4, $"return result;");
|
||||
@@ -1127,6 +1457,8 @@ namespace Serein.NodeFlow.Services
|
||||
return flowApiMethodInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
||||
74
README.md
74
README.md
@@ -96,80 +96,6 @@ https://space.bilibili.com/33526379
|
||||
SereinEnv.GetFlowGlobalData("KeyName"); // 获取全局数据
|
||||
SereinEnv.AddOrUpdateFlowGlobalData("KeyName", obj); // 设置/更新全局数据,不建议
|
||||
~~~
|
||||
* **ExpOp- 表达式节点**
|
||||
* 入参: 自定义的表达式。
|
||||
* 取值表达式:@Get
|
||||
* 描述:有时节点返回了object,但下一个节点只需要对象中某个属性,而非整个对象。如果修改节点的定义,有可能破坏了代码的封装,为了解决这个痛点,于是增加了表达式功能。
|
||||
* 使用方法:
|
||||
1. 获取对象的属性成员:
|
||||
~~~~
|
||||
@Get .[property]/[field]
|
||||
~~~~
|
||||
2. 获取对象的数组成员中下标为22的项:
|
||||
~~~~
|
||||
@Get .array[22]
|
||||
~~~~
|
||||
3. 获取对象的字典成员中键为“33”的值:
|
||||
~~~
|
||||
@Get .dict[33]
|
||||
~~~
|
||||
4. 获取对象“ID”属性并转为int:
|
||||
~~~
|
||||
@Get .ID<int>
|
||||
~~~
|
||||
5. 获取KeyName为【 MyDevice 】全局数据:
|
||||
~~~
|
||||
@Get #MyDevice#
|
||||
~~~
|
||||
6. 从全局数据【 MyDevice 】中获取“IP”属性:
|
||||
~~~
|
||||
@Get #MyDevice#.IP
|
||||
~~~
|
||||
* 数据类型转换表达式:@Dtc
|
||||
* 描述:有时需要显式的设置节点参数值,但参数接收了其它的类型,需要经过一次转换,将显式的文本值转为入参数据类型。
|
||||
* 使用方法:
|
||||
~~~
|
||||
@Dtc <long>1233
|
||||
@Dtc <bool>True
|
||||
@Dtc <DateTime>2024-12-24 11:13:42 (注:如果右值为“now”,则自动获取当前时间)
|
||||
~~~
|
||||
* **ExpCondition - 条件表达式节点**
|
||||
* 入参: 自定义。
|
||||
* 描述:与表达式节点不同,条件表达式节点是判断条件是否成立,如果成立,返回true,否则返回false,如果表达式执行失败,而进入 error 分支。
|
||||
* 使用方式:
|
||||
* 入参说明:默认从上一节点获取,也可以显式设定值,也可以参考表达式节点,使用“@Get .[property]/[field]”的方式重新定义入参数据,用以条件表达式判断。
|
||||
* 条件表达式:默认“PASS”,代表跳过判断直接进入下一个分支。
|
||||
* 条件表达式格式“.[property]/[field]< type> [op] value”,注意,开头必须使用“.”符号,这有助于显然的表达需要从入参对象中取内部某个值。
|
||||
* 表达式符号说明:
|
||||
* [property] /[field] : 属性/字段
|
||||
* [op] : 操作符
|
||||
1. bool表达式:==
|
||||
2. 数值表达式 :==、>=、 <=、in a-b (表示判断是否在a至b的数值范围内), !in a-b(取反);
|
||||
3. 文本表达式:==/equals(等于)、!=/notequals(不等于)、c/contains(出现过)、nc/doesnotcontain(没有出现过)、sw/startswith(开头等于)、ew/endswith(结尾等于)
|
||||
* < type> :指定需要转换为某个类型,可不选。
|
||||
* [value] : 条件值
|
||||
* 使用示例:
|
||||
~~~
|
||||
场景1:上一个节点传入了该对象(伪代码):
|
||||
class Data
|
||||
{
|
||||
string Name; // 性能
|
||||
string Age; // 年龄,外部传入了文本类型
|
||||
string IdentityCardNumber; // 身份证号
|
||||
|
||||
}
|
||||
需求:需要判断年龄是否在某个区间,例如需要大于18岁,小于35岁。
|
||||
条件表达式:.Age<int> in 18-35
|
||||
|
||||
|
||||
|
||||
需求:需要判断是否是北京身份证(开头为”1100”)。
|
||||
条件表达式:.IdentityCardNumber sw 1100
|
||||
另一种方法:
|
||||
入参使用表达式:@Get .IdentityCardNumber
|
||||
条件表达式:sw 1100
|
||||
|
||||
~~~
|
||||
## 3. 从DLL生成控件的枚举值:
|
||||
* **Action - 动作**
|
||||
* 入参:自定义。如果入参类型为IFlowContext,会传入当前的上下文;如果入参类型为IFlowNode,会传入节点对应的实体Model。如果不显式指定参数来源,参数会尝试获取运行时上一节点返回值,并根据当前入参类型尝试进行类型转换。
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
using Serein.Script.Node;
|
||||
|
||||
namespace Serein.Script
|
||||
{
|
||||
public sealed class SereinSciptException : Exception
|
||||
{
|
||||
//public ASTNode Node { get; }
|
||||
public override string Message { get; }
|
||||
|
||||
public SereinSciptException(ASTNode node, string message)
|
||||
{
|
||||
//this.Node = node;
|
||||
Message = $"异常信息 : {message} ,代码在第{node.Row}行: {node.Code.Trim()}";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
32
Serein.Script/SereinSciptParserException.cs
Normal file
32
Serein.Script/SereinSciptParserException.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Serein.Script.Node;
|
||||
|
||||
namespace Serein.Script
|
||||
{
|
||||
public sealed class SereinSciptParserException : Exception
|
||||
{
|
||||
//public ASTNode Node { get; }
|
||||
public override string Message { get; }
|
||||
|
||||
public SereinSciptParserException(ASTNode node, string message)
|
||||
{
|
||||
//this.Node = node;
|
||||
Message = $"异常信息 : {message} ,代码在第{node.Row}行: {node.Code.Trim()}";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SereinSciptInterpreterExceptio : Exception
|
||||
{
|
||||
//public ASTNode Node { get; }
|
||||
public override string Message { get; }
|
||||
|
||||
public SereinSciptInterpreterExceptio(ASTNode node, string message)
|
||||
{
|
||||
//this.Node = node;
|
||||
Message = $"异常信息 : {message} ,代码在第{node.Row}行: {node.Code.Trim()}";
|
||||
}
|
||||
|
||||
|
||||
public override string StackTrace => string.Empty;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,8 +12,6 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Script
|
||||
{
|
||||
|
||||
|
||||
|
||||
public class SereinScript
|
||||
{
|
||||
@@ -83,15 +81,15 @@ namespace Serein.Script
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 转换为c#代码
|
||||
/// 转换为 C# 代码,并且附带方法信息
|
||||
/// </summary>
|
||||
/// <param name="script">脚本</param>
|
||||
/// <param name="argTypes">挂载的变量</param>
|
||||
/// <returns></returns>
|
||||
public string ConvertCSharpCode(string mehtodName, Dictionary<string, Type>? argTypes = null)
|
||||
public SereinScriptMethodInfo? ConvertCSharpCode(string mehtodName, Dictionary<string, Type>? argTypes = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mehtodName)) return string.Empty;
|
||||
if (programNode is null) return string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(mehtodName)) return null;
|
||||
if (programNode is null) return null;
|
||||
SereinScriptToCsharpScript tool = new SereinScriptToCsharpScript(TypeAnalysis);
|
||||
return tool.CompileToCSharp(mehtodName, programNode, argTypes);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ namespace Serein.Script
|
||||
/// <summary>
|
||||
/// 缓存对象方法调用节点
|
||||
/// </summary>
|
||||
private Dictionary<MemberFunctionCallNode, DelegateDetails> MethodNodeDelegateCaches { get; } = new Dictionary<MemberFunctionCallNode, DelegateDetails>();
|
||||
//private Dictionary<MemberFunctionCallNode, DelegateDetails> MethodNodeDelegateCaches { get; } = new Dictionary<MemberFunctionCallNode, DelegateDetails>();
|
||||
private static Dictionary<ASTNode, DelegateDetails> ASTDelegateDetails { get; } = new Dictionary<ASTNode, DelegateDetails>();
|
||||
|
||||
|
||||
public SereinScriptInterpreter(Dictionary<ASTNode, Type> symbolInfos)
|
||||
{
|
||||
@@ -79,8 +81,8 @@ namespace Serein.Script
|
||||
case IfNode ifNode: // if语句结构
|
||||
async Task<object?> InterpreterIfNodeAsync(IScriptInvokeContext context, IfNode ifNode)
|
||||
{
|
||||
var result = await InterpretAsync(context, ifNode.Condition) ?? throw new SereinSciptException(ifNode, $"条件语句返回了 null");
|
||||
if (result is not bool condition) throw new SereinSciptException(ifNode, "条件语句返回值不为 bool 类型");
|
||||
var result = await InterpretAsync(context, ifNode.Condition) ?? throw new SereinSciptParserException(ifNode, $"条件语句返回了 null");
|
||||
if (result is not bool condition) throw new SereinSciptParserException(ifNode, "条件语句返回值不为 bool 类型");
|
||||
var branchNodes = condition ? ifNode.TrueBranch : ifNode.FalseBranch;
|
||||
if (branchNodes.Count == 0) return default;
|
||||
object? data = default;
|
||||
@@ -103,8 +105,8 @@ namespace Serein.Script
|
||||
object? data = default;
|
||||
while (true)
|
||||
{
|
||||
var result = await InterpretAsync(context, whileNode.Condition) ?? throw new SereinSciptException(whileNode, $"循环节点条件返回了 null");
|
||||
if (result is not bool condition) throw new SereinSciptException(whileNode, "循环节点条件返回值不为 bool 类型");
|
||||
var result = await InterpretAsync(context, whileNode.Condition) ?? throw new SereinSciptParserException(whileNode, $"循环节点条件返回了 null");
|
||||
if (result is not bool condition) throw new SereinSciptParserException(whileNode, "循环节点条件返回值不为 bool 类型");
|
||||
if (!condition) break;
|
||||
if (whileNode.Body.Count == 0) break;
|
||||
foreach (var node in whileNode.Body)
|
||||
@@ -137,9 +139,9 @@ namespace Serein.Script
|
||||
{
|
||||
// 递归计算二元操作
|
||||
var left = await InterpretAsync(context, binaryOperationNode.Left);
|
||||
if (left == null ) throw new SereinSciptException(binaryOperationNode.Left, $"左值尝试使用 null");
|
||||
if (left == null ) throw new SereinSciptParserException(binaryOperationNode.Left, $"左值尝试使用 null");
|
||||
var right = await InterpretAsync(context, binaryOperationNode.Right);
|
||||
if (right == null) throw new SereinSciptException(binaryOperationNode.Right, "右值尝试使用计算 null");
|
||||
if (right == null) throw new SereinSciptParserException(binaryOperationNode.Right, "右值尝试使用计算 null");
|
||||
var op = binaryOperationNode.Operator;
|
||||
var result = BinaryOperationEvaluator.EvaluateValue(left, op, right);
|
||||
return result;
|
||||
@@ -159,7 +161,7 @@ namespace Serein.Script
|
||||
throw new ArgumentNullException($"解析{collectionAssignmentNode}节点时,索引返回空。");
|
||||
}
|
||||
var valueValue = await InterpretAsync(context, collectionAssignmentNode.Value);
|
||||
SetCollectionValue(collectionValue, indexValue, valueValue);
|
||||
await SetCollectionValueAsync(collectionAssignmentNode, collectionValue, indexValue, valueValue);
|
||||
}
|
||||
await InterpreterCollectionAssignmentNodeAsync(context, collectionAssignmentNode);
|
||||
return default;
|
||||
@@ -176,7 +178,7 @@ namespace Serein.Script
|
||||
{
|
||||
throw new ArgumentNullException($"解析{collectionIndexNode}节点时,索引返回空。");
|
||||
}
|
||||
var result = GetCollectionValue(collectionValue, indexValue);
|
||||
var result = await GetCollectionValueAsync(collectionIndexNode, collectionValue, indexValue);
|
||||
return result;
|
||||
}
|
||||
return await InterpreterCollectionIndexNodeAsync(context, collectionIndexNode);
|
||||
@@ -208,7 +210,7 @@ namespace Serein.Script
|
||||
type = symbolInfos[objectInstantiationNode.Type];
|
||||
if (type is null)
|
||||
{
|
||||
throw new SereinSciptException(objectInstantiationNode, $"使用了未定义的类型\"{objectInstantiationNode.Type.TypeName}\"");
|
||||
throw new SereinSciptParserException(objectInstantiationNode, $"使用了未定义的类型\"{objectInstantiationNode.Type.TypeName}\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +223,7 @@ namespace Serein.Script
|
||||
var obj = Activator.CreateInstance(type, args: args);// 创建对象
|
||||
if (obj is null)
|
||||
{
|
||||
throw new SereinSciptException(objectInstantiationNode, $"类型创建失败\"{objectInstantiationNode.Type.TypeName}\"");
|
||||
throw new SereinSciptParserException(objectInstantiationNode, $"类型创建失败\"{objectInstantiationNode.Type.TypeName}\"");
|
||||
}
|
||||
|
||||
for (int i = 0; i < objectInstantiationNode.CtorAssignments.Count; i++)
|
||||
@@ -229,7 +231,7 @@ namespace Serein.Script
|
||||
var ctorAssignmentNode = objectInstantiationNode.CtorAssignments[i];
|
||||
var propertyName = ctorAssignmentNode.MemberName;
|
||||
var value = await InterpretAsync(context, ctorAssignmentNode.Value);
|
||||
SetPropertyValue(obj, propertyName, value);
|
||||
await SetPropertyValueAsync(ctorAssignmentNode, obj, propertyName, value);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
@@ -243,8 +245,8 @@ namespace Serein.Script
|
||||
{
|
||||
var target = await InterpretAsync(context, memberAccessNode.Object);
|
||||
var memberName = memberAccessNode.MemberName;
|
||||
if(target is null) throw new SereinSciptException(memberAccessNode, $"无法获取成员,对象为 null \"{memberAccessNode.Object.Code}\"");
|
||||
var value = GetPropertyValue(target, memberName);
|
||||
if(target is null) throw new SereinSciptParserException(memberAccessNode, $"无法获取成员,对象为 null \"{memberAccessNode.Object.Code}\"");
|
||||
var value = await GetPropertyValueAsync(memberAccessNode, target, memberName);
|
||||
return value;
|
||||
}
|
||||
return await InterpreterMemberAccessNodeAsync(context, memberAccessNode);
|
||||
@@ -254,8 +256,8 @@ namespace Serein.Script
|
||||
var target = await InterpretAsync(context, memberAssignmentNode.Object);
|
||||
var memberName = memberAssignmentNode.MemberName;
|
||||
var value = await InterpretAsync(context, memberAssignmentNode.Value);
|
||||
if (target is null) throw new SereinSciptException(memberAssignmentNode, $"无法设置成员,对象为 null \"{memberAssignmentNode.Object.Code}\"");
|
||||
SetPropertyValue(target, memberName, value);
|
||||
if (target is null) throw new SereinSciptParserException(memberAssignmentNode, $"无法设置成员,对象为 null \"{memberAssignmentNode.Object.Code}\"");
|
||||
await SetPropertyValueAsync(memberAssignmentNode, target, memberName, value);
|
||||
return value;
|
||||
}
|
||||
return await InterpreterMemberAssignmentNodeAsync(context, memberAssignmentNode);
|
||||
@@ -263,13 +265,13 @@ namespace Serein.Script
|
||||
async Task<object?> InterpreterMemberFunctionCallNodeAsync(IScriptInvokeContext context, MemberFunctionCallNode memberFunctionCallNode)
|
||||
{
|
||||
var target = await InterpretAsync(context, memberFunctionCallNode.Object);
|
||||
if (!MethodNodeDelegateCaches.TryGetValue(memberFunctionCallNode, out DelegateDetails? delegateDetails))
|
||||
if (!ASTDelegateDetails.TryGetValue(memberFunctionCallNode, out DelegateDetails? delegateDetails))
|
||||
{
|
||||
var methodName = memberFunctionCallNode.FunctionName;
|
||||
var methodInfo = memberFunctionCallNode.Arguments.Count == 0 ? target?.GetType().GetMethod(methodName, []) : target?.GetType().GetMethod(methodName);// 获取参数列表的类型
|
||||
if (methodInfo is null) throw new SereinSciptException(memberFunctionCallNode, $"对象没有方法\"{memberFunctionCallNode.FunctionName}\"");
|
||||
if (methodInfo is null) throw new SereinSciptParserException(memberFunctionCallNode, $"对象没有方法\"{memberFunctionCallNode.FunctionName}\"");
|
||||
delegateDetails = new DelegateDetails(methodInfo);
|
||||
MethodNodeDelegateCaches[memberFunctionCallNode] = delegateDetails;
|
||||
ASTDelegateDetails[memberFunctionCallNode] = delegateDetails;
|
||||
} // 查询是否有缓存
|
||||
|
||||
|
||||
@@ -310,7 +312,7 @@ namespace Serein.Script
|
||||
|
||||
// 查找并执行对应的函数
|
||||
if (!SereinScript.FunctionDelegates.TryGetValue(funcName, out DelegateDetails? function))
|
||||
throw new SereinSciptException(functionCallNode, $"没有挂载方法\"{functionCallNode.FunctionName}\"");
|
||||
throw new SereinSciptParserException(functionCallNode, $"没有挂载方法\"{functionCallNode.FunctionName}\"");
|
||||
|
||||
/* if (!function.EmitMethodInfo.IsStatic)
|
||||
{
|
||||
@@ -336,34 +338,50 @@ namespace Serein.Script
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 设置对象成员
|
||||
/// </summary>
|
||||
/// <param name="node">节点,用于缓存委托避免重复反射</param>
|
||||
/// <param name="target">对象</param>
|
||||
/// <param name="memberName">属性名称</param>
|
||||
/// <param name="value">属性值</param>
|
||||
/// <exception cref="Exception"></exception>
|
||||
private void SetPropertyValue(object target, string memberName, object? value)
|
||||
private async Task SetPropertyValueAsync(ASTNode node, object target, string memberName, object? value)
|
||||
{
|
||||
if(ASTDelegateDetails.TryGetValue(node ,out DelegateDetails? delegateDetails))
|
||||
{
|
||||
await delegateDetails.InvokeAsync(target, [value]);
|
||||
return;
|
||||
}
|
||||
|
||||
var targetType = target?.GetType();
|
||||
if (targetType is null) return;
|
||||
var propertyInfo = targetType.GetProperty(memberName);
|
||||
if (propertyInfo is null)
|
||||
{
|
||||
var fieldInfo = target?.GetType().GetRuntimeField(memberName);
|
||||
FieldInfo? fieldInfo = target?.GetType().GetRuntimeField(memberName);
|
||||
if (fieldInfo is null)
|
||||
{
|
||||
throw new Exception($"类型 {targetType} 对象没有成员\"{memberName}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
var convertedValue = Convert.ChangeType(value, fieldInfo.FieldType);
|
||||
fieldInfo.SetValue(target, convertedValue);
|
||||
delegateDetails = new DelegateDetails(fieldInfo, DelegateDetails.GSType.Set);
|
||||
ASTDelegateDetails[node] = delegateDetails; // 缓存委托
|
||||
await delegateDetails.InvokeAsync(target, [value]);
|
||||
|
||||
//var convertedValue = Convert.ChangeType(value, fieldInfo.FieldType);
|
||||
//fieldInfo.SetValue(target, convertedValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value is null)
|
||||
delegateDetails = new DelegateDetails(propertyInfo, DelegateDetails.GSType.Set);
|
||||
ASTDelegateDetails[node] = delegateDetails; // 缓存委托
|
||||
await delegateDetails.InvokeAsync(target, [value]);
|
||||
|
||||
/*if (value is null)
|
||||
{
|
||||
propertyInfo.SetValue(target, null);
|
||||
return;
|
||||
@@ -380,19 +398,27 @@ namespace Serein.Script
|
||||
else
|
||||
{
|
||||
throw new Exception($"类型 {targetType} 对象成员\"{memberName}\" 赋值时异常");
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从对象获取值
|
||||
/// </summary>
|
||||
/// <param name="node">节点,用于缓存委托避免重复反射</param>
|
||||
/// <param name="target">对象</param>
|
||||
/// <param name="memberName">成员名称</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
private object? GetPropertyValue(object target, string memberName)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private async Task<object?> GetPropertyValueAsync(ASTNode node, object target, string memberName)
|
||||
{
|
||||
if (ASTDelegateDetails.TryGetValue(node, out DelegateDetails? delegateDetails))
|
||||
{
|
||||
var result = await delegateDetails.InvokeAsync(target, []);
|
||||
return result;
|
||||
}
|
||||
|
||||
var targetType = target?.GetType();
|
||||
if (targetType is null) return null;
|
||||
var propertyInfo = targetType.GetProperty(memberName);
|
||||
@@ -405,35 +431,63 @@ namespace Serein.Script
|
||||
}
|
||||
else
|
||||
{
|
||||
return fieldInfo.GetValue(target);
|
||||
delegateDetails = new DelegateDetails(propertyInfo, DelegateDetails.GSType.Get);
|
||||
ASTDelegateDetails[node] = delegateDetails; // 缓存委托
|
||||
var result = await delegateDetails.InvokeAsync(target, []);
|
||||
return result;
|
||||
|
||||
//return fieldInfo.GetValue(target);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return propertyInfo.GetValue(target);
|
||||
delegateDetails = new DelegateDetails(propertyInfo, DelegateDetails.GSType.Get);
|
||||
ASTDelegateDetails[node] = delegateDetails; // 缓存委托
|
||||
var result = await delegateDetails.InvokeAsync(target, []);
|
||||
return result;
|
||||
//return propertyInfo.GetValue(target);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 设置集合成员
|
||||
/// </summary>
|
||||
/// <param name="node">节点,用于缓存委托避免重复反射</param>
|
||||
/// <param name="collectionValue"></param>
|
||||
/// <param name="indexValue"></param>
|
||||
/// <param name="valueValue"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
private void SetCollectionValue(object collectionValue, object indexValue, object valueValue)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private async Task SetCollectionValueAsync(ASTNode node, object collectionValue, object indexValue, object valueValue)
|
||||
{
|
||||
if (ASTDelegateDetails.TryGetValue(node, out DelegateDetails? delegateDetails))
|
||||
{
|
||||
await delegateDetails.InvokeAsync(collectionValue, [indexValue, valueValue]);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var collectionType = collectionValue.GetType(); // 目标对象的类型
|
||||
delegateDetails = new DelegateDetails(collectionType, DelegateDetails.EmitType.CollectionSetter);
|
||||
ASTDelegateDetails[node] = delegateDetails; // 缓存委托
|
||||
await delegateDetails.InvokeAsync(collectionValue, [indexValue, valueValue]);
|
||||
return;
|
||||
}
|
||||
|
||||
#if false
|
||||
|
||||
// 解析数组/集合名与索引部分
|
||||
var targetType = collectionValue.GetType(); // 目标对象的类型
|
||||
#region 处理键值对
|
||||
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
// 目标是键值对
|
||||
var method = targetType.GetMethod("set_Item", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (method is not null)
|
||||
var methodInfo = targetType.GetMethod("set_Item", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (methodInfo is not null)
|
||||
{
|
||||
method.Invoke(collectionValue, [indexValue, valueValue]);
|
||||
|
||||
methodInfo.Invoke(collectionValue, [indexValue, valueValue]);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
@@ -465,18 +519,40 @@ namespace Serein.Script
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
throw new ArgumentException($"解析异常, {collectionValue} 并非有效集合。");
|
||||
throw new ArgumentException($"解析异常, {collectionValue} 并非有效集合。");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取集合中的成员
|
||||
/// </summary>
|
||||
/// <param name="node">节点,用于缓存委托避免重复反射</param>
|
||||
/// <param name="collectionValue"></param>
|
||||
/// <param name="indexValue"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
private object? GetCollectionValue(object collectionValue, object indexValue)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private async Task<object?> GetCollectionValueAsync(ASTNode node, object collectionValue, object indexValue)
|
||||
{
|
||||
if (ASTDelegateDetails.TryGetValue(node, out DelegateDetails? delegateDetails))
|
||||
{
|
||||
var result = await delegateDetails.InvokeAsync(collectionValue, [indexValue]);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 针对 string 特化
|
||||
if(collectionValue is string chars && indexValue is int index)
|
||||
{
|
||||
return chars[index];
|
||||
}
|
||||
var collectionType = collectionValue.GetType(); // 目标对象的类型
|
||||
delegateDetails = new DelegateDetails(collectionType, DelegateDetails.EmitType.CollectionGetter);
|
||||
ASTDelegateDetails[node] = delegateDetails; // 缓存委托
|
||||
var result = await delegateDetails.InvokeAsync(collectionValue, [indexValue]);
|
||||
return result;
|
||||
}
|
||||
|
||||
#if false
|
||||
// 解析数组/集合名与索引部分
|
||||
var targetType = collectionValue.GetType(); // 目标对象的类型
|
||||
#region 处理键值对
|
||||
@@ -504,7 +580,7 @@ namespace Serein.Script
|
||||
{
|
||||
throw new ArgumentException($"解析{collectionValue}节点时,数组下标越界。");
|
||||
}
|
||||
|
||||
|
||||
return array.GetValue(index);
|
||||
}
|
||||
else if (collectionValue is IList<object> list)
|
||||
@@ -519,7 +595,8 @@ namespace Serein.Script
|
||||
}
|
||||
#endregion
|
||||
|
||||
throw new ArgumentException($"解析{collectionValue}节点时,左值并非有效集合。");
|
||||
throw new ArgumentException($"解析{collectionValue}节点时,左值并非有效集合。");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
22
Serein.Script/SereinScriptMethodInfo.cs
Normal file
22
Serein.Script/SereinScriptMethodInfo.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Serein.Script
|
||||
{
|
||||
public class SereinScriptMethodInfo
|
||||
{
|
||||
public string ClassName { get; set; }
|
||||
public string MethodName { get; set; }
|
||||
|
||||
public Type? ReturnType { get; set; }
|
||||
|
||||
public bool IsAsync { get; set; }
|
||||
|
||||
public List<SereinScriptParamInfo> ParamInfos { get; set; }
|
||||
|
||||
public string CsharpCode { get; set; }
|
||||
|
||||
public class SereinScriptParamInfo
|
||||
{
|
||||
public string ParamName { get; set; }
|
||||
public Type ParameterType { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,12 @@ namespace Serein.Script
|
||||
/// <summary>
|
||||
/// 将 Serein 脚本转换为 C# 脚本的类
|
||||
/// </summary>
|
||||
internal class SereinScriptToCsharpScript
|
||||
public class SereinScriptToCsharpScript
|
||||
{
|
||||
public const string ClassName = nameof(SereinScriptToCsharpScript);
|
||||
|
||||
public SereinScriptMethodInfo sereinScriptMethodInfo ;
|
||||
|
||||
/// <summary>
|
||||
/// 符号表
|
||||
/// </summary>
|
||||
@@ -62,19 +66,35 @@ namespace Serein.Script
|
||||
|
||||
private List<Action<StringBuilder>> _classDefinitions = new List<Action<StringBuilder>>();
|
||||
|
||||
public string CompileToCSharp(string mehtodName, ProgramNode programNode, Dictionary<string, Type>? param)
|
||||
public SereinScriptMethodInfo CompileToCSharp(string mehtodName, ProgramNode programNode, Dictionary<string, Type>? param)
|
||||
{
|
||||
_codeBuilder.Clear();
|
||||
sereinScriptMethodInfo = new SereinScriptMethodInfo()
|
||||
{
|
||||
ClassName = ClassName,
|
||||
ParamInfos = new List<SereinScriptMethodInfo.SereinScriptParamInfo>(),
|
||||
MethodName = mehtodName,
|
||||
};
|
||||
var sb = _codeBuilder;
|
||||
var methodResultType = _symbolInfos[programNode];
|
||||
if(methodResultType == typeof(void))
|
||||
{
|
||||
methodResultType = typeof(object);
|
||||
}
|
||||
var taskFullName = typeof(Task).FullName;
|
||||
var returnContent = _isTaskMain ? $"global::{taskFullName}<global::{methodResultType.FullName}>" : $"global::{methodResultType.FullName}";
|
||||
var taskFullName = typeof(Task).FullName;
|
||||
string? returnContent;
|
||||
if (_isTaskMain)
|
||||
{
|
||||
returnContent = $"global::{taskFullName}<global::{methodResultType.FullName}>";
|
||||
sereinScriptMethodInfo.IsAsync = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
returnContent = $"global::{methodResultType.FullName}";
|
||||
sereinScriptMethodInfo.IsAsync = false;
|
||||
}
|
||||
|
||||
AppendLine("public class SereinScriptToCsharp");
|
||||
AppendLine($"public partial class {ClassName}");
|
||||
AppendLine( "{");
|
||||
Indent();
|
||||
if(param is null || param.Count == 0)
|
||||
@@ -92,10 +112,13 @@ namespace Serein.Script
|
||||
ConvertCode(stmt); // 递归遍历
|
||||
Append(";");
|
||||
}
|
||||
if(!_symbolInfos.Keys.Any(node => node is ReturnNode))
|
||||
|
||||
if (_symbolInfos[programNode] == typeof(void))
|
||||
{
|
||||
AppendLine("");
|
||||
AppendLine("return null;");
|
||||
}
|
||||
|
||||
Unindent();
|
||||
AppendLine("}");
|
||||
Unindent();
|
||||
@@ -105,16 +128,24 @@ namespace Serein.Script
|
||||
{
|
||||
cd.Invoke(sb);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
sereinScriptMethodInfo.CsharpCode = sb.ToString();
|
||||
sereinScriptMethodInfo.ReturnType = methodResultType;
|
||||
return sereinScriptMethodInfo;
|
||||
}
|
||||
|
||||
private string GetMethodParamster(Dictionary<string, Type> param)
|
||||
{
|
||||
var values = param.Select(kvp =>
|
||||
{
|
||||
_local[kvp.Key] = kvp.Value;
|
||||
return $"global::{kvp.Value.FullName} {kvp.Key}";
|
||||
var paramName = kvp.Key;
|
||||
var type = kvp.Value;
|
||||
_local[paramName] = type;
|
||||
sereinScriptMethodInfo.ParamInfos.Add(new SereinScriptMethodInfo.SereinScriptParamInfo
|
||||
{
|
||||
ParameterType = type,
|
||||
ParamName = paramName,
|
||||
});
|
||||
return $"global::{type.FullName} {paramName}";
|
||||
});
|
||||
return string.Join(',', values);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ namespace Serein.Workbench.Node.View
|
||||
{
|
||||
DataContext = viewModel;
|
||||
InitializeComponent();
|
||||
/*if(ExecuteJunctionControl.MyNode != null)
|
||||
{
|
||||
ExecuteJunctionControl.MyNode.Guid = viewModel.NodeModel.Guid;
|
||||
}*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
|
||||
<Button Content="加载" Margin="3,0,1,0" Command="{Binding CommandLoadScript}" Height="17.2"></Button>
|
||||
<Button Content="执行" Margin="3,0,1,0" Command="{Binding CommandExecuting}" Height="17.2"></Button>
|
||||
<Button Content="c#代码" Margin="3,0,1,0" Command="{Binding CommandGenerateCode}" Height="17.2"></Button>
|
||||
</StackPanel>
|
||||
<themes:MethodDetailsControl Grid.Row="1" x:Name="MethodDetailsControl" MethodDetails="{Binding NodeModel.MethodDetails}" NodeViewModel="{Binding}"/>
|
||||
<!--<TextBox Grid.Row="2" MinHeight="20" MinWidth="100" TextWrapping="Wrap" AcceptsReturn="True" IsEnabled="{Binding IsEnabledOnView}" Text="{Binding Script}"></TextBox>-->
|
||||
|
||||
@@ -3,6 +3,7 @@ using Serein.Library.Utils;
|
||||
using Serein.NodeFlow.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@@ -45,6 +46,14 @@ namespace Serein.Workbench.Node.ViewModel
|
||||
{
|
||||
NodeModel.ReloadScript(); // 工作台重新加载脚本
|
||||
});
|
||||
|
||||
CommandGenerateCode = new RelayCommand(o =>
|
||||
{
|
||||
var info = NodeModel.ToCsharpMethodInfo($"Test_{nodeModel.Guid.Replace("-","_")}"); // 工作台重新加载脚本
|
||||
if (info is null) return;
|
||||
SereinEnv.WriteLine(InfoType.INFO, $"{info.ClassName}.{info.MethodName}({string.Join(",", info.ParamInfos.Select(i => $"global::{i.ParameterType.FullName} {i.ParamName}"))})");
|
||||
SereinEnv.WriteLine(InfoType.INFO, info.CsharpCode);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +67,11 @@ namespace Serein.Workbench.Node.ViewModel
|
||||
/// </summary>
|
||||
public ICommand CommandExecuting { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 生成c#代码
|
||||
/// </summary>
|
||||
public ICommand CommandGenerateCode { get; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ namespace Serein.Workbench.ViewModels
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Debug.WriteLine(ex.Message);
|
||||
}
|
||||
flowEnvironment.StartRemoteServerAsync();
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<!--<nodeView:NetScriptNodeControl x:Name="NetScriptNodeControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>-->
|
||||
<nodeView:FlowCallNodeControl MaxWidth="110" MaxHeight="160" x:Name="FlowCallNodeControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>
|
||||
<nodeView:ScriptNodeControl MaxWidth="110" MaxHeight="160" x:Name="ScriptNodeControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>
|
||||
<nodeView:GlobalDataControl MaxWidth="110" MaxHeight="160" x:Name="GlobalDataControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>
|
||||
<nodeView:ExpOpNodeControl MaxWidth="110" MaxHeight="160" x:Name="ExpOpNodeControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>
|
||||
<nodeView:ConditionNodeControl MaxWidth="110" MaxHeight="160" x:Name="ConditionNodeControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>
|
||||
<!--<nodeView:GlobalDataControl MaxWidth="110" MaxHeight="160" x:Name="GlobalDataControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>-->
|
||||
<!--<nodeView:ExpOpNodeControl MaxWidth="110" MaxHeight="160" x:Name="ExpOpNodeControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>-->
|
||||
<!--<nodeView:ConditionNodeControl MaxWidth="110" MaxHeight="160" x:Name="ConditionNodeControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>-->
|
||||
<!--<nodeView:ConditionRegionControl x:Name="ConditionRegionControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>-->
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
Reference in New Issue
Block a user