| | |
| | | /// <summary> |
| | | /// 读取字节数据 |
| | | /// </summary> |
| | | /// <param name="address">地址(如 "M100", "DB1.DBD0", "I0.0", "T1")</param> |
| | | /// <param name="length">长度(必须大于0)</param> |
| | | /// <returns>字节数组</returns> |
| | | /// <exception cref="ArgumentException">地址为空或空白,或长度为0</exception> |
| | | /// <exception cref="ObjectDisposedException">对象已释放</exception> |
| | | public byte[] ReadBytes(string address, ushort length) |
| | | { |
| | | if (_disposed) |
| | | throw new ObjectDisposedException(nameof(MemoryStore)); |
| | | |
| | | if (string.IsNullOrEmpty(address)) |
| | | throw new ArgumentException("地址不能为空", nameof(address)); |
| | | if (string.IsNullOrWhiteSpace(address)) |
| | | throw new ArgumentException("地址不能为空或空白", nameof(address)); |
| | | |
| | | if (length == 0) |
| | | throw new ArgumentException("长度必须大于0", nameof(length)); |
| | | |
| | | var (regionType, offset, dbNumber, bitOffset) = ParseAddress(address); |
| | | |
| | |
| | | |
| | | /// <summary> |
| | | /// 读取指定类型数据 |
| | | /// 支持的类型:bool, short, int, ushort, float |
| | | /// 注意:string类型不受支持,请使用WriteBytes/ReadBytes或通过DBRegion直接访问 |
| | | /// </summary> |
| | | /// <typeparam name="T">值类型(bool, short, int, ushort, float)</typeparam> |
| | | /// <param name="address">地址</param> |
| | | /// <returns>数据值</returns> |
| | | /// <exception cref="ArgumentException">地址为空或空白,或类型不支持</exception> |
| | | /// <exception cref="ObjectDisposedException">对象已释放</exception> |
| | | public T Read<T>(string address) where T : struct |
| | | { |
| | | if (_disposed) |
| | | throw new ObjectDisposedException(nameof(MemoryStore)); |
| | | |
| | | if (string.IsNullOrEmpty(address)) |
| | | throw new ArgumentException("地址不能为空", nameof(address)); |
| | | if (string.IsNullOrWhiteSpace(address)) |
| | | throw new ArgumentException("地址不能为空或空白", nameof(address)); |
| | | |
| | | var (regionType, offset, dbNumber, bitOffset) = ParseAddress(address); |
| | | |
| | |
| | | /// <summary> |
| | | /// 写入字节数据 |
| | | /// </summary> |
| | | /// <param name="address">地址</param> |
| | | /// <param name="data">数据</param> |
| | | /// <exception cref="ArgumentException">地址为空或空白</exception> |
| | | /// <exception cref="ArgumentNullException">数据为null</exception> |
| | | /// <exception cref="ObjectDisposedException">对象已释放</exception> |
| | | public void WriteBytes(string address, byte[] data) |
| | | { |
| | | if (_disposed) |
| | | throw new ObjectDisposedException(nameof(MemoryStore)); |
| | | |
| | | if (string.IsNullOrEmpty(address)) |
| | | throw new ArgumentException("地址不能为空", nameof(address)); |
| | | if (string.IsNullOrWhiteSpace(address)) |
| | | throw new ArgumentException("地址不能为空或空白", nameof(address)); |
| | | |
| | | if (data == null) |
| | | throw new ArgumentNullException(nameof(data)); |
| | |
| | | |
| | | /// <summary> |
| | | /// 写入指定类型数据 |
| | | /// 支持的类型:bool, short, int, ushort, float |
| | | /// 注意:string类型不受支持,请使用WriteBytes/ReadBytes或通过DBRegion直接访问 |
| | | /// </summary> |
| | | /// <typeparam name="T">值类型(bool, short, int, ushort, float)</typeparam> |
| | | /// <param name="address">地址</param> |
| | | /// <param name="value">数据值</param> |
| | | /// <exception cref="ArgumentException">地址为空或空白,或类型不支持</exception> |
| | | /// <exception cref="ObjectDisposedException">对象已释放</exception> |
| | | public void Write<T>(string address, T value) where T : struct |
| | | { |
| | | if (_disposed) |
| | | throw new ObjectDisposedException(nameof(MemoryStore)); |
| | | |
| | | if (string.IsNullOrEmpty(address)) |
| | | throw new ArgumentException("地址不能为空", nameof(address)); |
| | | if (string.IsNullOrWhiteSpace(address)) |
| | | throw new ArgumentException("地址不能为空或空白", nameof(address)); |
| | | |
| | | var (regionType, offset, dbNumber, bitOffset) = ParseAddress(address); |
| | | |
| | |
| | | |
| | | /// <summary> |
| | | /// 获取内存区域 |
| | | /// 警告:返回的区域是可变的直接引用,调用者负责生命周期协调。 |
| | | /// 此方法主要用于高级场景和内部使用。一般情况下应使用Read/Write方法。 |
| | | /// </summary> |
| | | /// <param name="regionType">区域类型(M/DB/I/Q/T/C)</param> |
| | | /// <returns>内存区域接口</returns> |
| | | /// <exception cref="ArgumentException">区域类型不支持</exception> |
| | | /// <exception cref="ObjectDisposedException">对象已释放</exception> |
| | | public IMemoryRegion GetRegion(string regionType) |
| | | { |
| | | if (_disposed) |
| | |
| | | /// <summary> |
| | | /// 清空所有内存 |
| | | /// </summary> |
| | | /// <exception cref="ObjectDisposedException">对象已释放</exception> |
| | | public void Clear() |
| | | { |
| | | if (_disposed) |
| | |
| | | |
| | | /// <summary> |
| | | /// 导出内存数据 |
| | | /// 警告:这是一个昂贵的操作,会分配大量内存并复制所有区域数据。 |
| | | /// 建议仅在需要持久化或调试时使用。 |
| | | /// </summary> |
| | | /// <returns>区域类型 -> 字节数组的字典</returns> |
| | | /// <exception cref="ObjectDisposedException">对象已释放</exception> |
| | | public Dictionary<string, byte[]> Export() |
| | | { |
| | | if (_disposed) |
| | |
| | | |
| | | /// <summary> |
| | | /// 导入内存数据 |
| | | /// 会验证数据长度是否匹配区域大小,不匹配时会抛出异常。 |
| | | /// DB区支持自动创建块(如果写入时块不存在)。 |
| | | /// </summary> |
| | | /// <param name="data">区域类型 -> 字节数组的字典</param> |
| | | /// <exception cref="ArgumentNullException">数据为null</exception> |
| | | /// <exception cref="ArgumentException">数据长度不匹配</exception> |
| | | /// <exception cref="ObjectDisposedException">对象已释放</exception> |
| | | public void Import(Dictionary<string, byte[]> data) |
| | | { |
| | | if (_disposed) |
| | |
| | | throw new ArgumentNullException(nameof(data)); |
| | | |
| | | if (data.ContainsKey("M")) |
| | | { |
| | | if (data["M"].Length != _config.MRegionSize) |
| | | throw new ArgumentException($"M区数据长度不匹配:期望{_config.MRegionSize},实际{data["M"].Length}"); |
| | | _mRegion.Write(0, data["M"]); |
| | | } |
| | | |
| | | if (data.ContainsKey("DB")) |
| | | { |
| | | ImportDBRegion(data["DB"]); |
| | | } |
| | | |
| | | if (data.ContainsKey("I")) |
| | | { |
| | | if (data["I"].Length != _config.IRegionSize) |
| | | throw new ArgumentException($"I区数据长度不匹配:期望{_config.IRegionSize},实际{data["I"].Length}"); |
| | | _iRegion.Write(0, data["I"]); |
| | | } |
| | | |
| | | if (data.ContainsKey("Q")) |
| | | { |
| | | if (data["Q"].Length != _config.QRegionSize) |
| | | throw new ArgumentException($"Q区数据长度不匹配:期望{_config.QRegionSize},实际{data["Q"].Length}"); |
| | | _qRegion.Write(0, data["Q"]); |
| | | } |
| | | |
| | | if (data.ContainsKey("T")) |
| | | { |
| | | var expectedTSize = _config.TRegionCount * 2; // 每个定时器2字节 |
| | | if (data["T"].Length != expectedTSize) |
| | | throw new ArgumentException($"T区数据长度不匹配:期望{expectedTSize},实际{data["T"].Length}"); |
| | | _tRegion.Write(0, data["T"]); |
| | | } |
| | | |
| | | if (data.ContainsKey("C")) |
| | | { |
| | | var expectedCSize = _config.CRegionCount * 2; // 每个计数器2字节 |
| | | if (data["C"].Length != expectedCSize) |
| | | throw new ArgumentException($"C区数据长度不匹配:期望{expectedCSize},实际{data["C"].Length}"); |
| | | _cRegion.Write(0, data["C"]); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 解析S7地址格式 |
| | | /// 支持格式: M100, DB1.DBD0, I0.0, Q0.0, T1, C1 |
| | | /// </summary> |
| | | /// <param name="address">地址字符串</param> |
| | | /// <returns>解析结果:区域类型、偏移量、DB编号(可选)、位偏移(可选)</returns> |
| | | /// <exception cref="FormatException">地址格式无效时抛出,包含详细的上下文信息</exception> |
| | | private (string regionType, ushort offset, ushort? dbNumber, byte? bitOffset) ParseAddress(string address) |
| | | { |
| | | address = address.Trim().ToUpper(); |
| | | |
| | | // DB块地址: DB1.DBD0, DB1.DBW10, DB1.DBB20 |
| | | if (address.StartsWith("DB")) |
| | | try |
| | | { |
| | | var parts = address.Split(new[] { '.' }, 2); |
| | | var dbPart = parts[0]; // DB1 |
| | | var offsetPart = parts.Length > 1 ? parts[1] : null; // DBD0, DBW10, etc. |
| | | |
| | | var dbNumber = ushort.Parse(dbPart.Substring(2)); |
| | | ushort offset = 0; |
| | | |
| | | if (offsetPart != null) |
| | | // DB块地址: DB1.DBD0, DB1.DBW10, DB1.DBB20 |
| | | if (address.StartsWith("DB")) |
| | | { |
| | | // 解析偏移类型: DBD(4字节), DBW(2字节), DBB(1字节) |
| | | if (offsetPart.StartsWith("DBD")) |
| | | offset = ushort.Parse(offsetPart.Substring(3)); |
| | | else if (offsetPart.StartsWith("DBW")) |
| | | offset = ushort.Parse(offsetPart.Substring(3)); |
| | | else if (offsetPart.StartsWith("DBB")) |
| | | offset = ushort.Parse(offsetPart.Substring(3)); |
| | | else |
| | | offset = ushort.Parse(offsetPart); |
| | | var parts = address.Split(new[] { '.' }, 2); |
| | | var dbPart = parts[0]; // DB1 |
| | | var offsetPart = parts.Length > 1 ? parts[1] : null; // DBD0, DBW10, etc. |
| | | |
| | | var dbNumber = ushort.Parse(dbPart.Substring(2)); |
| | | ushort offset = 0; |
| | | |
| | | if (offsetPart != null) |
| | | { |
| | | // 解析偏移类型: DBD(4字节), DBW(2字节), DBB(1字节) |
| | | if (offsetPart.StartsWith("DBD")) |
| | | offset = ushort.Parse(offsetPart.Substring(3)); |
| | | else if (offsetPart.StartsWith("DBW")) |
| | | offset = ushort.Parse(offsetPart.Substring(3)); |
| | | else if (offsetPart.StartsWith("DBB")) |
| | | offset = ushort.Parse(offsetPart.Substring(3)); |
| | | else |
| | | offset = ushort.Parse(offsetPart); |
| | | } |
| | | |
| | | return ("DB", offset, dbNumber, null); |
| | | } |
| | | |
| | | return ("DB", offset, dbNumber, null); |
| | | } |
| | | // 带位地址: I0.0, Q0.0, M0.0 |
| | | if (address.Contains(".")) |
| | | { |
| | | var parts = address.Split('.'); |
| | | var regionType = new string(parts[0].TakeWhile(char.IsLetter).ToArray()); |
| | | var byteOffset = ushort.Parse(new string(parts[0].SkipWhile(char.IsLetter).ToArray())); |
| | | var bitOffset = byte.Parse(parts[1]); |
| | | |
| | | // 带位地址: I0.0, Q0.0, M0.0 |
| | | if (address.Contains(".")) |
| | | return (regionType, byteOffset, null, (byte?)bitOffset); |
| | | } |
| | | |
| | | // 定时器/计数器: T1, C1 |
| | | if (address.StartsWith("T") || address.StartsWith("C")) |
| | | { |
| | | var regionType = address[0].ToString(); |
| | | var number = ushort.Parse(address.Substring(1)); |
| | | return (regionType, number, null, null); |
| | | } |
| | | |
| | | // 普通地址: M100, I100, Q100 |
| | | var region = new string(address.TakeWhile(char.IsLetter).ToArray()); |
| | | var addr = ushort.Parse(new string(address.SkipWhile(char.IsLetter).ToArray())); |
| | | return (region, addr, null, null); |
| | | } |
| | | catch (Exception ex) when (ex is FormatException || ex is OverflowException) |
| | | { |
| | | var parts = address.Split('.'); |
| | | var regionType = new string(parts[0].TakeWhile(char.IsLetter).ToArray()); |
| | | var byteOffset = ushort.Parse(new string(parts[0].SkipWhile(char.IsLetter).ToArray())); |
| | | var bitOffset = byte.Parse(parts[1]); |
| | | |
| | | return (regionType, byteOffset, null, (byte?)bitOffset); |
| | | throw new FormatException($"地址格式无效: '{address}'。{ex.Message}", ex); |
| | | } |
| | | |
| | | // 定时器/计数器: T1, C1 |
| | | if (address.StartsWith("T") || address.StartsWith("C")) |
| | | { |
| | | var regionType = address[0].ToString(); |
| | | var number = ushort.Parse(address.Substring(1)); |
| | | return (regionType, number, null, null); |
| | | } |
| | | |
| | | // 普通地址: M100, I100, Q100 |
| | | var region = new string(address.TakeWhile(char.IsLetter).ToArray()); |
| | | var addr = ushort.Parse(new string(address.SkipWhile(char.IsLetter).ToArray())); |
| | | return (region, addr, null, null); |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | var data = _mRegion.Read(offset, (ushort)size); |
| | | |
| | | if (typeof(T) == typeof(bool)) |
| | | return default; // M区不支持bool读取,应该用Mx.x格式 |
| | | throw new NotSupportedException("M区不支持bool读取,应该使用Mx.x格式(位地址)"); |
| | | |
| | | return ConvertFromBytes<T>(data); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 读取DB区数据 |
| | | /// 注意:string类型不受此泛型方法支持(受where T : struct约束) |
| | | /// 请使用WriteBytes/ReadBytes或通过DBRegion直接访问字符串 |
| | | /// </summary> |
| | | private T ReadDBRegion<T>(ushort dbNumber, ushort offset) where T : struct |
| | | { |
| | |
| | | "Int16" => (T)(object)_dbRegion.ReadInt(dbNumber, offset), |
| | | "Int32" => (T)(object)_dbRegion.ReadDInt(dbNumber, offset), |
| | | "Single" => (T)(object)_dbRegion.ReadReal(dbNumber, offset), |
| | | "String" => (T)(object)_dbRegion.ReadString(dbNumber, offset, 254), |
| | | // String类型在泛型约束下不可达,但保留注释说明 |
| | | // "String" => (T)(object)_dbRegion.ReadString(dbNumber, offset, 254), |
| | | _ => ConvertFromBytes<T>(_dbRegion.Read(dbNumber, offset, (ushort)System.Runtime.InteropServices.Marshal.SizeOf<T>())) |
| | | }; |
| | | } |
| | |
| | | |
| | | /// <summary> |
| | | /// 写入DB区数据 |
| | | /// 注意:string类型不受此泛型方法支持(受where T : struct约束) |
| | | /// 请使用WriteBytes/ReadBytes或通过DBRegion直接访问字符串 |
| | | /// </summary> |
| | | private void WriteDBRegion<T>(ushort dbNumber, ushort offset, T value) where T : struct |
| | | { |
| | |
| | | case "Single": |
| | | _dbRegion.WriteReal(dbNumber, offset, (float)(object)value); |
| | | break; |
| | | case "String": |
| | | _dbRegion.WriteString(dbNumber, offset, (string)(object)value, 254); |
| | | break; |
| | | // String类型在泛型约束下不可达,但保留注释说明 |
| | | // case "String": |
| | | // _dbRegion.WriteString(dbNumber, offset, (string)(object)value, 254); |
| | | // break; |
| | | default: |
| | | var data = ConvertToBytes(value); |
| | | _dbRegion.Write(dbNumber, offset, data); |
| | |
| | | |
| | | /// <summary> |
| | | /// 从字节数组转换为值类型 |
| | | /// 使用大端字节序(Big-Endian),与S7 PLC规范一致 |
| | | /// </summary> |
| | | /// <exception cref="NotSupportedException">类型不支持时抛出</exception> |
| | | private T ConvertFromBytes<T>(byte[] data) where T : struct |
| | | { |
| | | if (typeof(T) == typeof(short)) |
| | |
| | | if (typeof(T) == typeof(ushort)) |
| | | return (T)(object)((ushort)((data[0] << 8) | data[1])); |
| | | |
| | | if (typeof(T) == typeof(float)) |
| | | return (T)(object)BitConverter.ToSingle(data, 0); |
| | | if (typeof(T) == typeof(uint)) |
| | | return (T)(object)((uint)((data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3])); |
| | | |
| | | return default; |
| | | if (typeof(T) == typeof(float)) |
| | | { |
| | | // BitConverter默认使用系统字节序,需要转换为大端 |
| | | if (BitConverter.IsLittleEndian) |
| | | Array.Reverse(data); |
| | | return (T)(object)BitConverter.ToSingle(data, 0); |
| | | } |
| | | |
| | | if (typeof(T) == typeof(bool)) |
| | | return (T)(object)(data[0] != 0); |
| | | |
| | | if (typeof(T) == typeof(byte)) |
| | | return (T)(object)data[0]; |
| | | |
| | | throw new NotSupportedException($"不支持的类型: {typeof(T).Name}。支持的类型:bool, byte, short, int, ushort, uint, float"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 将值类型转换为字节数组 |
| | | /// 使用大端字节序(Big-Endian),与S7 PLC规范一致 |
| | | /// </summary> |
| | | /// <exception cref="NotSupportedException">类型不支持时抛出</exception> |
| | | private byte[] ConvertToBytes<T>(T value) where T : struct |
| | | { |
| | | if (typeof(T) == typeof(short)) |
| | |
| | | if (typeof(T) == typeof(ushort)) |
| | | return new[] { (byte)((ushort)(object)value >> 8), (byte)((ushort)(object)value & 0xFF) }; |
| | | |
| | | if (typeof(T) == typeof(uint)) |
| | | return new[] { |
| | | (byte)((uint)(object)value >> 24), |
| | | (byte)(((uint)(object)value >> 16) & 0xFF), |
| | | (byte)(((uint)(object)value >> 8) & 0xFF), |
| | | (byte)((uint)(object)value & 0xFF) |
| | | }; |
| | | |
| | | if (typeof(T) == typeof(float)) |
| | | return BitConverter.GetBytes((float)(object)value); |
| | | { |
| | | var bytes = BitConverter.GetBytes((float)(object)value); |
| | | // BitConverter默认使用系统字节序,需要转换为大端 |
| | | if (BitConverter.IsLittleEndian) |
| | | Array.Reverse(bytes); |
| | | return bytes; |
| | | } |
| | | |
| | | if (typeof(T) == typeof(bool)) |
| | | return new[] { (byte)((bool)(object)value ? 1 : 0) }; |
| | | |
| | | throw new NotSupportedException($"不支持的类型: {typeof(T).Name}"); |
| | | if (typeof(T) == typeof(byte)) |
| | | return new[] { (byte)(object)value }; |
| | | |
| | | throw new NotSupportedException($"不支持的类型: {typeof(T).Name}。支持的类型:bool, byte, short, int, ushort, uint, float"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 导出DB区数据 |
| | | /// 导出所有DB块的连续数据 |
| | | /// 注意:DB区支持自动创建块,导出时只包含已创建的块 |
| | | /// </summary> |
| | | private byte[] ExportDBRegion() |
| | | { |
| | | // 简化版本:导出所有DB块的连续数据 |
| | | // 实际实现可能需要更复杂的序列化格式 |
| | | var result = new List<byte>(); |
| | | for (ushort i = 1; i <= _config.DBBlockCount; i++) |
| | | { |
| | | var blockData = _dbRegion.Read(i, 0, (ushort)_config.DBBlockSize); |
| | | result.AddRange(blockData); |
| | | try |
| | | { |
| | | var blockData = _dbRegion.Read(i, 0, (ushort)_config.DBBlockSize); |
| | | result.AddRange(blockData); |
| | | } |
| | | catch (ArgumentException) |
| | | { |
| | | // DB块不存在,跳过 |
| | | // 填充0以保持连续性 |
| | | result.AddRange(new byte[_config.DBBlockSize]); |
| | | } |
| | | } |
| | | return result.ToArray(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 导入DB区数据 |
| | | /// 按顺序导入所有DB块 |
| | | /// 注意:DB区支持自动创建块(如果写入时块不存在) |
| | | /// </summary> |
| | | private void ImportDBRegion(byte[] data) |
| | | { |
| | | // 简化版本:按顺序导入所有DB块 |
| | | int offset = 0; |
| | | for (ushort i = 1; i <= _config.DBBlockCount && offset < data.Length; i++) |
| | | { |