diff --git a/NewLife.IoT/DataHelper.cs b/NewLife.IoT/DataHelper.cs index c865148..b3ecff0 100644 --- a/NewLife.IoT/DataHelper.cs +++ b/NewLife.IoT/DataHelper.cs @@ -22,7 +22,7 @@ public static class DataHelper using var span = DefaultTracer.Instance?.NewSpan(nameof(EncodeByThingModel), $"name={point.Name} data={data} type={type.Name} rawType={point.Type}"); // 找到物属性定义 - var pt = spec?.ExtendedProperties?.FirstOrDefault(e => e.Id.EqualIgnoreCase(point.Name)); + var pt = point.Name.IsNullOrEmpty() ? null : spec?.ExtendedProperties?.FirstOrDefault(e => e.Id.EqualIgnoreCase(point.Name)); if (pt != null) { // 反向操作常量因子和缩放因子 @@ -80,7 +80,7 @@ public static class DataHelper try { // 找到物属性定义 - var pt = spec?.ExtendedProperties?.FirstOrDefault(e => e.Id.EqualIgnoreCase(point.Name)); + var pt = point.Name.IsNullOrEmpty() ? null : spec?.ExtendedProperties?.FirstOrDefault(e => e.Id.EqualIgnoreCase(point.Name)); if (pt != null) { if (type == typeof(Boolean)) return data[0]; diff --git a/NewLife.IoT/Drivers/AsyncDriverBase.cs b/NewLife.IoT/Drivers/AsyncDriverBase.cs new file mode 100644 index 0000000..9a34075 --- /dev/null +++ b/NewLife.IoT/Drivers/AsyncDriverBase.cs @@ -0,0 +1,153 @@ +using NewLife.IoT.ThingModels; +#if !NET40 +using TaskEx = System.Threading.Tasks.Task; +#endif + +namespace NewLife.IoT.Drivers; + +/// 异步协议驱动基类。抽象各种硬件设备的数据采集及远程控制 +/// 节点类型,可使用默认Node +/// +public class AsyncDriverBase : AsyncDriverBase + where TNode : INode, new() + where TParameter : IDriverParameter, new() +{ + #region 元数据 + /// 创建驱动参数对象,分析参数配置或创建默认参数 + /// + protected override IDriverParameter OnCreateParameter() => new TParameter(); + #endregion + + #region 核心方法 + /// + /// 打开设备驱动,传入参数。一个物理设备可能有多个逻辑设备共用,需要以节点来区分 + /// + /// 逻辑设备 + /// 参数。不同驱动的参数设置相差较大,对象字典具有较好灵活性,其对应IDriverParameter + /// 节点对象,可存储站号等信息,仅驱动自己识别 + public override INode Open(IDevice device, IDriverParameter? parameter) + { + var node = new TNode + { + Driver = this, + Device = device, + Parameter = parameter, + }; + + return node; + } + #endregion +} + +/// 异步协议驱动基类。抽象各种硬件设备的数据采集及远程控制 +/// +/// 在Modbus协议上,一个通信链路(串口/ModbusTcp地址)即是IDriver,可能有多个物理设备共用,各自表示为INode。 +/// 即使是一个物理设备,也可能因为管理需要而划分为多个逻辑设备,例如变配电网关等Modbus汇集网关。 +/// +/// 架构设计需要,本类继承自DriverBase,将来可能移除该继承关系。 +/// +public abstract class AsyncDriverBase : DriverBase, IAsyncDriver +{ + #region 核心方法 + /// + /// 打开设备驱动,传入参数。一个物理设备可能有多个逻辑设备共用,需要以节点来区分 + /// + /// 逻辑设备 + /// 参数。不同驱动的参数设置相差较大,对象字典具有较好灵活性,其对应IDriverParameter + /// 节点对象,可存储站号等信息,仅驱动自己识别 + public virtual Task OpenAsync(IDevice device, IDriverParameter? parameter) + { + var node = new Node + { + Driver = this, + Device = device, + }; + + return TaskEx.FromResult(node as INode); + } + + /// + /// 关闭设备节点。多节点共用通信链路时,需等最后一个节点关闭才能断开 + /// + /// +#if NET40 || NET45 + public virtual Task CloseAsync(INode node) => TaskEx.FromResult(0); +#else + public virtual Task CloseAsync(INode node) => TaskEx.CompletedTask; +#endif + + /// 读取数据 + /// + /// 驱动实现数据采集的核心方法,各驱动全力以赴实现好该接口。 + /// 其中点位表名称和地址,仅该驱动能够识别。类型和长度等信息,则由物联网平台统一规范。 + /// + /// 节点对象,可存储站号等信息,仅驱动自己识别 + /// 点位集合 + /// + public virtual Task> ReadAsync(INode node, IPoint[] points) => throw new NotImplementedException(); + + /// 写入数据 + /// + /// 驱动实现远程控制的核心方法,各驱动全力以赴实现好该接口。 + /// 其中点位表名称和地址,仅该驱动能够识别。类型和长度等信息,则由物联网平台统一规范。 + /// + /// 节点对象,可存储站号等信息,仅驱动自己识别 + /// 点位 + /// 数值 + public virtual Task WriteAsync(INode node, IPoint point, Object? value) => throw new NotImplementedException(); + + /// 控制设备,特殊功能使用 + /// + /// 除了点位读写之外的其它控制功能。 + /// 其中点位表名称和地址,仅该驱动能够识别。类型和长度等信息,则由物联网平台统一规范。 + /// + /// 节点对象,可存储站号等信息,仅驱动自己识别 + /// 参数 + public virtual Task ControlAsync(INode node, IDictionary parameters) => throw new NotImplementedException(); + #endregion + + #region 覆盖同步接口 + /// + /// 打开设备驱动,传入参数。一个物理设备可能有多个逻辑设备共用,需要以节点来区分 + /// + /// 逻辑设备 + /// 参数。不同驱动的参数设置相差较大,对象字典具有较好灵活性,其对应IDriverParameter + /// 节点对象,可存储站号等信息,仅驱动自己识别 + public override INode Open(IDevice device, IDriverParameter? parameter) => OpenAsync(device, parameter).ConfigureAwait(false).GetAwaiter().GetResult(); + + /// + /// 关闭设备节点。多节点共用通信链路时,需等最后一个节点关闭才能断开 + /// + /// + public override void Close(INode node) => CloseAsync(node).ConfigureAwait(false).GetAwaiter().GetResult(); + + /// 读取数据 + /// + /// 驱动实现数据采集的核心方法,各驱动全力以赴实现好该接口。 + /// 其中点位表名称和地址,仅该驱动能够识别。类型和长度等信息,则由物联网平台统一规范。 + /// + /// 节点对象,可存储站号等信息,仅驱动自己识别 + /// 点位集合 + /// + public override IDictionary Read(INode node, IPoint[] points) => ReadAsync(node, points).ConfigureAwait(false).GetAwaiter().GetResult(); + + /// 写入数据 + /// + /// 驱动实现远程控制的核心方法,各驱动全力以赴实现好该接口。 + /// 其中点位表名称和地址,仅该驱动能够识别。类型和长度等信息,则由物联网平台统一规范。 + /// + /// 节点对象,可存储站号等信息,仅驱动自己识别 + /// 点位 + /// 数值 + public override Object? Write(INode node, IPoint point, Object? value) => WriteAsync(node, point, value).ConfigureAwait(false).GetAwaiter().GetResult(); + + /// 控制设备,特殊功能使用 + /// + /// 除了点位读写之外的其它控制功能。 + /// 其中点位表名称和地址,仅该驱动能够识别。类型和长度等信息,则由物联网平台统一规范。 + /// + /// 节点对象,可存储站号等信息,仅驱动自己识别 + /// 参数 + public override Object? Control(INode node, IDictionary parameters) => ControlAsync(node, parameters).ConfigureAwait(false).GetAwaiter().GetResult(); + #endregion +} \ No newline at end of file diff --git a/NewLife.IoT/Drivers/IAsyncDriver.cs b/NewLife.IoT/Drivers/IAsyncDriver.cs new file mode 100644 index 0000000..fcacbd1 --- /dev/null +++ b/NewLife.IoT/Drivers/IAsyncDriver.cs @@ -0,0 +1,81 @@ +using NewLife.IoT.ThingModels; +using NewLife.IoT.ThingSpecification; + +namespace NewLife.IoT.Drivers; + +/// 协议驱动异步接口。抽象各种硬件设备的数据采集及远程控制 +/// +/// 在Modbus协议上,一个通信链路(串口/ModbusTcp地址)即是IDriver,可能有多个物理设备共用,各自表示为INode。 +/// 即使是一个物理设备,也可能因为管理需要而划分为多个逻辑设备,例如变配电网关等Modbus汇集网关。 +/// +/// 除了具体设备实例化驱动对象,在物联网平台扫描驱动时,也有可能实例化驱动对象,以获取默认参数与产品物模型。 +/// +public interface IAsyncDriver +{ + #region 元数据 + /// 创建驱动参数对象,分析参数配置或创建默认参数 + /// + /// 可序列化成Xml/Json作为该协议的参数模板。由于Xml需要良好的注释特性,优先使用。 + /// 获取后,按新版本覆盖旧版本。 + /// + /// Xml/Json参数配置,为空时仅创建默认参数 + /// + IDriverParameter? CreateParameter(String? parameter = null); + + /// 获取产品物模型 + /// + /// 如果设备有固定点位属性、服务和事件,则直接返回,否则返回空。 + /// 物联网平台有两种情况调用该接口: + /// 1,打开设备后。常见于OPC/BACnet等,此时可获取特定设备场景的物模型。 + /// 2,扫描设备时。此时未连接任何设备,只能返回该类设备的通用物模型,常用于具体硬件产品,例如各种传感器。 + /// 获取后,按新版本覆盖旧版本。 + /// + /// + ThingSpec? GetSpecification(); + #endregion + + #region 核心方法 + /// + /// 打开设备驱动,传入参数。一个物理设备可能有多个逻辑设备共用,需要以节点来区分 + /// + /// 逻辑设备 + /// 参数。不同驱动的参数设置相差较大,对象字典具有较好灵活性,其对应IDriverParameter + /// 节点对象,可存储站号等信息,仅驱动自己识别 + Task OpenAsync(IDevice device, IDriverParameter? parameter); + + /// + /// 关闭设备节点。多节点共用通信链路时,需等最后一个节点关闭才能断开 + /// + /// + Task CloseAsync(INode node); + + /// 读取数据 + /// + /// 驱动实现数据采集的核心方法,各驱动全力以赴实现好该接口。 + /// 其中点位表名称和地址,仅该驱动能够识别。类型和长度等信息,则由物联网平台统一规范。 + /// + /// 节点对象,可存储站号等信息,仅驱动自己识别 + /// 点位集合 + /// + Task> ReadAsync(INode node, IPoint[] points); + + /// 写入数据 + /// + /// 驱动实现远程控制的核心方法,各驱动全力以赴实现好该接口。 + /// 其中点位表名称和地址,仅该驱动能够识别。类型和长度等信息,则由物联网平台统一规范。 + /// + /// 节点对象,可存储站号等信息,仅驱动自己识别 + /// 点位 + /// 数值 + Task WriteAsync(INode node, IPoint point, Object? value); + + /// 控制设备,特殊功能使用 + /// + /// 除了点位读写之外的其它控制功能。 + /// 其中点位表名称和地址,仅该驱动能够识别。类型和长度等信息,则由物联网平台统一规范。 + /// + /// 节点对象,可存储站号等信息,仅驱动自己识别 + /// 参数 + Task ControlAsync(INode node, IDictionary parameters); + #endregion +} \ No newline at end of file diff --git a/NewLife.IoT/Drivers/IDriver.cs b/NewLife.IoT/Drivers/IDriver.cs index d0c5a0a..dd24eec 100644 --- a/NewLife.IoT/Drivers/IDriver.cs +++ b/NewLife.IoT/Drivers/IDriver.cs @@ -91,10 +91,9 @@ public static class DriverExtensions /// 逻辑设备 /// 参数对象 /// - public static INode Open(this IDriver driver, IDevice device, IDictionary parameters) + public static INode Open(this IDriver driver, IDevice device, IDictionary parameters) { - var type = driver.CreateParameter()?.GetType(); - if (type == null) throw new InvalidOperationException(); + var type = (driver.CreateParameter()?.GetType()) ?? throw new InvalidOperationException(); var ps = JsonHelper.Default.Convert(parameters, type) as IDriverParameter; var node = driver.Open(device, ps); diff --git a/NewLife.IoT/NewLife.IoT.csproj b/NewLife.IoT/NewLife.IoT.csproj index 93a1cac..785d6e7 100644 --- a/NewLife.IoT/NewLife.IoT.csproj +++ b/NewLife.IoT/NewLife.IoT.csproj @@ -5,7 +5,7 @@ IoT standard library, which defines various communication protocol standards and specifications in the Internet of Things field, without specific implementation. Used for IoT platform construction and unifying various hardware driver protocols. IoT标准库,定义物联网领域的各种通信协议标准规范,不含具体实现。用于IoT平台建设,以及统一各种硬件驱动协议 新生命开发团队 ©2002-2024 新生命开发团队 - 2.0 + 2.1 $([System.DateTime]::Now.ToString(`yyyy.MMdd`)) $(VersionPrefix).$(VersionSuffix) $(Version) @@ -28,7 +28,7 @@ https://github.com/NewLifeX/NewLife.IoT git 物联网;IoT;边缘计算;Edge;新生命团队;NewLife;$(AssemblyName) - 增加控制器层,新增输入输出口、串口、Modbus接口和板卡接口等接口统一定义,约定各种板卡及工业计算机所具备的基础功能 + 新增异步驱动接口IAsyncDriver MIT true true diff --git a/NewLife.IoT/ThingModels/IPoint.cs b/NewLife.IoT/ThingModels/IPoint.cs index 672719a..94a8f06 100644 --- a/NewLife.IoT/ThingModels/IPoint.cs +++ b/NewLife.IoT/ThingModels/IPoint.cs @@ -67,7 +67,7 @@ public static Object Convert(this IPoint point, Byte[] data) return type.GetTypeCode() switch { TypeCode.Boolean => BitConverter.GetBytes((Boolean)(val ?? false)), - TypeCode.Byte => new[] { (Byte)(val ?? 0) }, + TypeCode.Byte => [(Byte)(val ?? 0)], TypeCode.Char => BitConverter.GetBytes((Char)(val ?? 0)), TypeCode.Double => BitConverter.GetBytes((Double)(val ?? 0)), TypeCode.Int16 => BitConverter.GetBytes((Int16)(val ?? 0)), @@ -93,7 +93,7 @@ public static Object Convert(this IPoint point, Byte[] data) if (type == null) { // 找到物属性定义 - var pi = spec?.Properties?.FirstOrDefault(e => e.Id.EqualIgnoreCase(point.Name)); + var pi = point.Name.IsNullOrEmpty() ? null : spec?.Properties?.FirstOrDefault(e => e.Id.EqualIgnoreCase(point.Name)); type = TypeHelper.GetNetType(pi?.DataType?.Type); } if (type == null) return null; @@ -103,17 +103,17 @@ public static Object Convert(this IPoint point, Byte[] data) case TypeCode.Boolean: case TypeCode.Byte: case TypeCode.SByte: - return data.ToBoolean() ? new[] { (UInt16)0x01 } : new[] { (UInt16)0x00 }; + return data.ToBoolean() ? [(UInt16)0x01] : [(UInt16)0x00]; case TypeCode.Int16: case TypeCode.UInt16: case TypeCode.Int32: case TypeCode.UInt32: - return data.ToInt() > 0 ? new[] { (UInt16)0x01 } : new[] { (UInt16)0x00 }; + return data.ToInt() > 0 ? [(UInt16)0x01] : [(UInt16)0x00]; case TypeCode.Int64: case TypeCode.UInt64: - return data.ToLong() > 0 ? new[] { (UInt16)0x01 } : new[] { (UInt16)0x00 }; + return data.ToLong() > 0 ? [(UInt16)0x01] : [(UInt16)0x00]; default: - return data.ToBoolean() ? new[] { (UInt16)0x01 } : new[] { (UInt16)0x00 }; + return data.ToBoolean() ? [(UInt16)0x01] : [(UInt16)0x00]; } } @@ -129,7 +129,7 @@ public static Object Convert(this IPoint point, Byte[] data) if (type == null) { // 找到物属性定义 - var pi = spec?.Properties?.FirstOrDefault(e => e.Id.EqualIgnoreCase(point.Name)); + var pi = point.Name.IsNullOrEmpty() ? null : spec?.Properties?.FirstOrDefault(e => e.Id.EqualIgnoreCase(point.Name)); type = TypeHelper.GetNetType(pi?.DataType?.Type); } if (type == null) return null; @@ -139,41 +139,41 @@ public static Object Convert(this IPoint point, Byte[] data) case TypeCode.Boolean: case TypeCode.Byte: case TypeCode.SByte: - return data.ToBoolean() ? new[] { (UInt16)0x01 } : new[] { (UInt16)0x00 }; + return data.ToBoolean() ? [(UInt16)0x01] : [(UInt16)0x00]; case TypeCode.Int16: case TypeCode.UInt16: - return new[] { (UInt16)data.ToInt() }; + return [(UInt16)data.ToInt()]; case TypeCode.Int32: case TypeCode.UInt32: { var n = data.ToInt(); - return new[] { (UInt16)(n >> 16), (UInt16)(n & 0xFFFF) }; + return [(UInt16)(n >> 16), (UInt16)(n & 0xFFFF)]; } case TypeCode.Int64: case TypeCode.UInt64: { var n = data.ToLong(); - return new[] { (UInt16)(n >> 48), (UInt16)(n >> 32), (UInt16)(n >> 16), (UInt16)(n & 0xFFFF) }; + return [(UInt16)(n >> 48), (UInt16)(n >> 32), (UInt16)(n >> 16), (UInt16)(n & 0xFFFF)]; } case TypeCode.Single: { var d = (Single)data.ToDouble(); //var n = BitConverter.SingleToInt32Bits(d); var n = (UInt32)d; - return new[] { (UInt16)(n >> 16), (UInt16)(n & 0xFFFF) }; + return [(UInt16)(n >> 16), (UInt16)(n & 0xFFFF)]; } case TypeCode.Double: { var d = (Double)data.ToDouble(); //var n = BitConverter.DoubleToInt64Bits(d); var n = (UInt64)d; - return new[] { (UInt16)(n >> 48), (UInt16)(n >> 32), (UInt16)(n >> 16), (UInt16)(n & 0xFFFF) }; + return [(UInt16)(n >> 48), (UInt16)(n >> 32), (UInt16)(n >> 16), (UInt16)(n & 0xFFFF)]; } case TypeCode.Decimal: { var d = data.ToDecimal(); var n = (UInt64)d; - return new[] { (UInt16)(n >> 48), (UInt16)(n >> 32), (UInt16)(n >> 16), (UInt16)(n & 0xFFFF) }; + return [(UInt16)(n >> 48), (UInt16)(n >> 32), (UInt16)(n >> 16), (UInt16)(n & 0xFFFF)]; } //case TypeCode.String: // break; diff --git a/NewLife.IoT/ThingSpecification/PropertyExtend.cs b/NewLife.IoT/ThingSpecification/PropertyExtend.cs index 046594f..d064195 100644 --- a/NewLife.IoT/ThingSpecification/PropertyExtend.cs +++ b/NewLife.IoT/ThingSpecification/PropertyExtend.cs @@ -46,13 +46,13 @@ public class PropertyExtend : IDictionarySource #region 方法 /// 转字典。根据不同类型,提供不一样的序列化能力 /// - public IDictionary ToDictionary() + public IDictionary ToDictionary() { var dic = CollectionHelper.ToDictionary(this); //return dic.Where(e => e.Value != null).ToDictionary(e => e.Key, e => e.Value); - var rs = new Dictionary(); + var rs = new Dictionary(); foreach (var item in dic) { if (item.Value == null) continue; diff --git a/NewLife.IoT/ThingSpecification/TypeSpec.cs b/NewLife.IoT/ThingSpecification/TypeSpec.cs index 37b18e4..c3556e3 100644 --- a/NewLife.IoT/ThingSpecification/TypeSpec.cs +++ b/NewLife.IoT/ThingSpecification/TypeSpec.cs @@ -19,13 +19,13 @@ public class TypeSpec : IDictionarySource /// 转字典。根据不同类型,提供不一样的序列化能力 /// - public IDictionary ToDictionary() + public IDictionary ToDictionary() { - if (Type.IsNullOrEmpty()) return new Dictionary(); + if (Type.IsNullOrEmpty()) return new Dictionary(); var ds = Specs?.GetDictionary(Type); - var dic = new Dictionary + var dic = new Dictionary { { nameof(Type), Type } };