将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace XP.Hardware.PLC.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 十六进制数据格式化工具 | Hex data formatter utility
|
||||
/// 将 byte[] 格式化为标准十六进制查看器格式
|
||||
/// </summary>
|
||||
public static class HexFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 byte[] 格式化为十六进制查看器文本 | Format byte[] as hex viewer text
|
||||
/// 每行 16 字节,包含偏移量、十六进制值和 ASCII 表示
|
||||
/// </summary>
|
||||
/// <param name="data">字节数据 | Byte data</param>
|
||||
/// <param name="startOffset">起始偏移量 | Start offset</param>
|
||||
/// <returns>格式化后的字符串 | Formatted string</returns>
|
||||
public static string Format(byte[] data, int startOffset = 0)
|
||||
{
|
||||
if (data == null || data.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
int totalBytes = data.Length;
|
||||
|
||||
for (int i = 0; i < totalBytes; i += 16)
|
||||
{
|
||||
// 偏移量(8位十六进制)| Offset (8-digit hex)
|
||||
sb.Append($"{(startOffset + i):X8} ");
|
||||
|
||||
// 前 8 字节十六进制 | First 8 bytes hex
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
if (i + j < totalBytes)
|
||||
sb.Append($"{data[i + j]:X2} ");
|
||||
else
|
||||
sb.Append(" ");
|
||||
}
|
||||
|
||||
sb.Append(' ');
|
||||
|
||||
// 后 8 字节十六进制 | Last 8 bytes hex
|
||||
for (int j = 8; j < 16; j++)
|
||||
{
|
||||
if (i + j < totalBytes)
|
||||
sb.Append($"{data[i + j]:X2} ");
|
||||
else
|
||||
sb.Append(" ");
|
||||
}
|
||||
|
||||
sb.Append(' ');
|
||||
|
||||
// ASCII 表示 | ASCII representation
|
||||
for (int j = 0; j < 16; j++)
|
||||
{
|
||||
if (i + j < totalBytes)
|
||||
{
|
||||
byte b = data[i + j];
|
||||
sb.Append(b >= 0x20 && b <= 0x7E ? (char)b : '.');
|
||||
}
|
||||
}
|
||||
|
||||
if (i + 16 < totalBytes)
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace XP.Hardware.PLC.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC 数据块解析器 | PLC data block parser
|
||||
/// 将批量读取的 byte[] 按偏移量解析为各种数据类型,避免多次单点读取的通讯开销
|
||||
/// S7 PLC 使用大端字节序(Big-Endian)
|
||||
/// </summary>
|
||||
public class PlcDataBlock
|
||||
{
|
||||
private readonly byte[] _data;
|
||||
private readonly int _baseAddress;
|
||||
|
||||
/// <summary>
|
||||
/// 原始字节数据 | Raw byte data
|
||||
/// </summary>
|
||||
public byte[] RawData => _data;
|
||||
|
||||
/// <summary>
|
||||
/// 数据块起始地址 | Data block base address
|
||||
/// </summary>
|
||||
public int BaseAddress => _baseAddress;
|
||||
|
||||
/// <summary>
|
||||
/// 数据长度(字节数)| Data length in bytes
|
||||
/// </summary>
|
||||
public int Length => _data?.Length ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="data">批量读取的字节数据 | Batch read byte data</param>
|
||||
/// <param name="baseAddress">数据块起始地址 | Data block base address</param>
|
||||
public PlcDataBlock(byte[] data, int baseAddress = 0)
|
||||
{
|
||||
_data = data ?? throw new ArgumentNullException(nameof(data));
|
||||
_baseAddress = baseAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取相对于 baseAddress 的实际偏移量 | Get actual offset relative to baseAddress
|
||||
/// </summary>
|
||||
/// <param name="absoluteAddress">绝对地址 | Absolute address</param>
|
||||
/// <returns>数组内偏移量 | Array offset</returns>
|
||||
private int ToOffset(int absoluteAddress)
|
||||
{
|
||||
int offset = absoluteAddress - _baseAddress;
|
||||
if (offset < 0 || offset >= _data.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(absoluteAddress),
|
||||
$"地址 {absoluteAddress} 超出数据范围 [{_baseAddress}, {_baseAddress + _data.Length - 1}] | " +
|
||||
$"Address {absoluteAddress} out of range [{_baseAddress}, {_baseAddress + _data.Length - 1}]");
|
||||
return offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查偏移量和长度是否在范围内 | Check if offset and length are within range
|
||||
/// </summary>
|
||||
private void CheckRange(int offset, int requiredBytes)
|
||||
{
|
||||
if (offset + requiredBytes > _data.Length)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
$"地址 {_baseAddress + offset} 需要 {requiredBytes} 字节,但剩余仅 {_data.Length - offset} 字节 | " +
|
||||
$"Address {_baseAddress + offset} requires {requiredBytes} bytes, but only {_data.Length - offset} remaining");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 Bool 值(按位寻址)| Read Bool value (bit addressing)
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <param name="bitIndex">位索引 (0-7) | Bit index (0-7)</param>
|
||||
/// <returns>布尔值 | Boolean value</returns>
|
||||
public bool GetBool(int address, int bitIndex)
|
||||
{
|
||||
if (bitIndex < 0 || bitIndex > 7)
|
||||
throw new ArgumentOutOfRangeException(nameof(bitIndex),
|
||||
$"位索引必须在 0-7 之间 | Bit index must be between 0-7");
|
||||
int offset = ToOffset(address);
|
||||
return (_data[offset] & (1 << bitIndex)) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 Byte 值 | Read Byte value
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <returns>字节值 | Byte value</returns>
|
||||
public byte GetByte(int address)
|
||||
{
|
||||
int offset = ToOffset(address);
|
||||
return _data[offset];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 Int16 (Short) 值,大端字节序 | Read Int16 (Short) value, Big-Endian
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <returns>Int16 值 | Int16 value</returns>
|
||||
public short GetInt16(int address)
|
||||
{
|
||||
int offset = ToOffset(address);
|
||||
CheckRange(offset, 2);
|
||||
return (short)((_data[offset] << 8) | _data[offset + 1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 UInt16 (UShort) 值,大端字节序 | Read UInt16 (UShort) value, Big-Endian
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <returns>UInt16 值 | UInt16 value</returns>
|
||||
public ushort GetUInt16(int address)
|
||||
{
|
||||
int offset = ToOffset(address);
|
||||
CheckRange(offset, 2);
|
||||
return (ushort)((_data[offset] << 8) | _data[offset + 1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 Int32 (Int) 值,大端字节序 | Read Int32 (Int) value, Big-Endian
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <returns>Int32 值 | Int32 value</returns>
|
||||
public int GetInt32(int address)
|
||||
{
|
||||
int offset = ToOffset(address);
|
||||
CheckRange(offset, 4);
|
||||
return (_data[offset] << 24) | (_data[offset + 1] << 16) |
|
||||
(_data[offset + 2] << 8) | _data[offset + 3];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 UInt32 值,大端字节序 | Read UInt32 value, Big-Endian
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <returns>UInt32 值 | UInt32 value</returns>
|
||||
public uint GetUInt32(int address)
|
||||
{
|
||||
int offset = ToOffset(address);
|
||||
CheckRange(offset, 4);
|
||||
return (uint)((_data[offset] << 24) | (_data[offset + 1] << 16) |
|
||||
(_data[offset + 2] << 8) | _data[offset + 3]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 Float (Single) 值,大端字节序 | Read Float (Single) value, Big-Endian
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <returns>Float 值 | Float value</returns>
|
||||
public float GetFloat(int address)
|
||||
{
|
||||
int offset = ToOffset(address);
|
||||
CheckRange(offset, 4);
|
||||
// S7 大端:需要反转为小端后转换 | S7 Big-Endian: reverse to Little-Endian for conversion
|
||||
byte[] temp = new byte[4];
|
||||
temp[0] = _data[offset + 3];
|
||||
temp[1] = _data[offset + 2];
|
||||
temp[2] = _data[offset + 1];
|
||||
temp[3] = _data[offset];
|
||||
return BitConverter.ToSingle(temp, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 Double 值,大端字节序 | Read Double value, Big-Endian
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <returns>Double 值 | Double value</returns>
|
||||
public double GetDouble(int address)
|
||||
{
|
||||
int offset = ToOffset(address);
|
||||
CheckRange(offset, 8);
|
||||
// S7 大端:需要反转为小端后转换 | S7 Big-Endian: reverse to Little-Endian for conversion
|
||||
byte[] temp = new byte[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
temp[i] = _data[offset + 7 - i];
|
||||
return BitConverter.ToDouble(temp, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 ASCII 字符串 | Read ASCII string
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <param name="length">字符串长度(字节数)| String length in bytes</param>
|
||||
/// <returns>字符串值 | String value</returns>
|
||||
public string GetString(int address, int length)
|
||||
{
|
||||
if (length <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(length),
|
||||
$"字符串长度必须大于 0 | String length must be greater than 0");
|
||||
int offset = ToOffset(address);
|
||||
CheckRange(offset, length);
|
||||
// 去除尾部空字符 | Trim trailing null characters
|
||||
return Encoding.ASCII.GetString(_data, offset, length).TrimEnd('\0');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定范围的原始字节 | Read raw bytes from specified range
|
||||
/// </summary>
|
||||
/// <param name="address">绝对字节地址 | Absolute byte address</param>
|
||||
/// <param name="length">字节数 | Number of bytes</param>
|
||||
/// <returns>字节数组 | Byte array</returns>
|
||||
public byte[] GetBytes(int address, int length)
|
||||
{
|
||||
int offset = ToOffset(address);
|
||||
CheckRange(offset, length);
|
||||
byte[] result = new byte[length];
|
||||
Array.Copy(_data, offset, result, 0, length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using HslCommunication;
|
||||
using XP.Hardware.Plc.Exceptions;
|
||||
|
||||
namespace XP.Hardware.PLC.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC 辅助类,提供数据类型转换和地址解析辅助方法 | PLC helper class, provides data type conversion and address parsing helper methods
|
||||
/// </summary>
|
||||
public static class PlcHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 检查操作结果,抛出统一异常 | Check operation result and throw unified exception
|
||||
/// </summary>
|
||||
/// <param name="result">操作结果 | Operation result</param>
|
||||
public static void CheckSuccess(OperateResult result)
|
||||
{
|
||||
if (!result.IsSuccess)
|
||||
throw new PlcException($"PLC操作失败: {result.Message} (Code: {result.ErrorCode})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using XP.Hardware.PLC.Abstractions;
|
||||
using XP.Hardware.PLC.Models;
|
||||
|
||||
namespace XP.Hardware.PLC.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// XML 信号地址定义解析/序列化器 | XML signal address definition parser/serializer
|
||||
/// 负责 PlcAddrDfn.xml 的读写操作 | Responsible for reading/writing PlcAddrDfn.xml
|
||||
/// </summary>
|
||||
public class XmlSignalParser : IXmlSignalParser
|
||||
{
|
||||
/// <summary>
|
||||
/// 从文件加载信号分组列表 | Load signal group list from file
|
||||
/// </summary>
|
||||
/// <param name="filePath">XML 文件路径 | XML file path</param>
|
||||
/// <returns>信号分组列表 | Signal group list</returns>
|
||||
/// <exception cref="FileNotFoundException">文件不存在 | File not found</exception>
|
||||
/// <exception cref="XmlException">XML 格式错误或 Group 缺少必要属性 | XML format error or Group missing required attributes</exception>
|
||||
public List<SignalGroup> LoadFromFile(string filePath)
|
||||
{
|
||||
// 文件不存在时抛出异常 | Throw exception if file does not exist
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"XML 配置文件不存在 | XML config file not found: {filePath}", filePath);
|
||||
}
|
||||
|
||||
XDocument doc;
|
||||
try
|
||||
{
|
||||
doc = XDocument.Load(filePath, LoadOptions.SetLineInfo);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
throw new XmlException($"XML 格式错误 | XML format error: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
var groups = new List<SignalGroup>();
|
||||
|
||||
// 遍历 <Config> 下所有 <Group> 子节点 | Iterate all <Group> child nodes under <Config>
|
||||
var groupElements = doc.Root?.Elements("Group") ?? Enumerable.Empty<XElement>();
|
||||
foreach (var groupElement in groupElements)
|
||||
{
|
||||
// 验证 Group 必须包含 ID 属性 | Validate Group must have ID attribute
|
||||
var idAttr = groupElement.Attribute("ID");
|
||||
if (idAttr == null)
|
||||
{
|
||||
var lineInfo = (IXmlLineInfo)groupElement;
|
||||
throw new XmlException(
|
||||
$"Group 节点缺少 ID 属性 | Group node missing ID attribute. " +
|
||||
$"位置 | Position: Line {lineInfo.LineNumber}");
|
||||
}
|
||||
|
||||
// 验证 Group 必须包含 DBNumber 属性 | Validate Group must have DBNumber attribute
|
||||
var dbNumberAttr = groupElement.Attribute("DBNumber");
|
||||
if (dbNumberAttr == null)
|
||||
{
|
||||
var lineInfo = (IXmlLineInfo)groupElement;
|
||||
throw new XmlException(
|
||||
$"Group 节点缺少 DBNumber 属性 | Group node missing DBNumber attribute. " +
|
||||
$"Group ID: {idAttr.Value}, " +
|
||||
$"位置 | Position: Line {lineInfo.LineNumber}");
|
||||
}
|
||||
|
||||
// 解析 DBNumber 为整数 | Parse DBNumber as integer
|
||||
if (!int.TryParse(dbNumberAttr.Value, out int dbNumber))
|
||||
{
|
||||
var lineInfo = (IXmlLineInfo)groupElement;
|
||||
throw new XmlException(
|
||||
$"Group 节点 DBNumber 属性值无效(非整数)| Group node DBNumber attribute value is invalid (not an integer). " +
|
||||
$"Group ID: {idAttr.Value}, DBNumber: {dbNumberAttr.Value}, " +
|
||||
$"位置 | Position: Line {lineInfo.LineNumber}");
|
||||
}
|
||||
|
||||
string groupId = idAttr.Value;
|
||||
|
||||
// 解析该 Group 下所有 Signal 子节点 | Parse all Signal child nodes under this Group
|
||||
var signals = new System.Collections.ObjectModel.ObservableCollection<SignalEntry>();
|
||||
foreach (var signalElement in groupElement.Elements("Signal"))
|
||||
{
|
||||
var signal = new SignalEntry
|
||||
{
|
||||
// 使用 Name 属性作为信号名称 | Use Name attribute as signal name
|
||||
Name = signalElement.Attribute("Name")?.Value ?? string.Empty,
|
||||
Type = signalElement.Attribute("Type")?.Value ?? string.Empty,
|
||||
StartAddr = int.TryParse(signalElement.Attribute("StartAddr")?.Value, out var addr) ? addr : 0,
|
||||
IndexOrLength = (signalElement.Attribute("IndexOrLength")?.Value ?? string.Empty).Trim(),
|
||||
Remark = signalElement.Attribute("Remark")?.Value ?? string.Empty,
|
||||
// 将 Group 的 ID 和 DBNumber 赋值到 SignalEntry | Assign Group's ID and DBNumber to SignalEntry
|
||||
GroupId = groupId,
|
||||
DBNumber = dbNumber
|
||||
};
|
||||
signals.Add(signal);
|
||||
}
|
||||
|
||||
groups.Add(new SignalGroup
|
||||
{
|
||||
GroupId = groupId,
|
||||
DBNumber = dbNumber,
|
||||
Signals = signals
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 SignalGroup 结构保存信号配置到文件 | Save signal configuration to file by SignalGroup structure
|
||||
/// </summary>
|
||||
/// <param name="filePath">XML 文件路径 | XML file path</param>
|
||||
/// <param name="groups">信号分组列表 | Signal group list</param>
|
||||
public void SaveToFile(string filePath, List<SignalGroup> groups)
|
||||
{
|
||||
var doc = new XDocument(
|
||||
new XDeclaration("1.0", "utf-8", "yes"),
|
||||
new XElement("Config",
|
||||
groups.Select(group => new XElement("Group",
|
||||
new XAttribute("ID", group.GroupId ?? string.Empty),
|
||||
new XAttribute("DBNumber", group.DBNumber),
|
||||
group.Signals.Select(signal => new XElement("Signal",
|
||||
new XAttribute("Name", signal.Name ?? string.Empty),
|
||||
new XAttribute("Type", signal.Type ?? string.Empty),
|
||||
new XAttribute("StartAddr", signal.StartAddr.ToString()),
|
||||
new XAttribute("IndexOrLength", signal.IndexOrLength ?? string.Empty),
|
||||
new XAttribute("Remark", signal.Remark ?? string.Empty)
|
||||
))
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
doc.Save(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user