在编写上位机软件时,需要经常处理命令拼接与其他设备进行通信,通常对不同的命令封装成不同的方法,扩展稍许麻烦。
本次拟以特性方式实现,以兼顾维护性与扩展性。
思想
一种命令对应一个类,其类中的各个属性对应各个命令段,通过特性的方式,实现其在这包数据命令中的位置、大端或小端及其转换为对应的目标类型;
然后通过反射对其进行拼包,从而得到一包完整数据。
场景
将一个轴移动到对应的X,Y,Z位置,为了演示,对其共用一个速度
这个移动到指定位置的命令假设按以下顺序构成(为了展示,草率的命令结构):
序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
字节 | 2 | s32 | u16 | u16 | u32 | s32 | s32 | s32 | 2 |
说明 | 包头 | 步骤号(ID) | 功能码 | 轴 | 速度 | X位置 | Y位置 | Z位置 | 包尾 |
实现
创建特性 CmdPropertyAttribute
[AttributeUsage(AttributeTargets.Property)]
internal class CmdPropertyAttribute : Attribute
{
public Type? TargetType { get; set; }
public int Number { get; set; }
public bool IsReverse { get; set; }
public CmdPropertyAttribute(int number)
{
Number = number;
}
public CmdPropertyAttribute(int number, Type targetType)
{
Number = number;
TargetType = targetType;
}
public CmdPropertyAttribute(int number, bool isReverse)
{
Number = number;
IsReverse = isReverse;
}
public CmdPropertyAttribute(int number, Type targetType, bool isReverse)
{
Number = number;
IsReverse = isReverse;
TargetType = targetType;
}
}
参数类,每一种命令对应一个参数类,它们继承于参数基类
创建参数基类 ParamBase ,每种数据都是步骤号处于第一位,特把其放入到基类中
public class ParamBase
{
[CmdProperty(0, true)]
public int StepNum { get; set; }
}
创建轴枚举 Axis
public enum Axis : ushort
{
Axis_1 = 1,
Axis_2 = 2,
}
创建功能码枚举 FunctionCode
public enum FunctionCode
{
Move = 1
}
创建移动类 MoveParam ,为了更好展示高低位转换,特对Speed属性进行反转
public class MoveParam : ParamBase
{
[CmdProperty(1, typeof(ushort))]
public FunctionCode Function { get; init; }
[CmdProperty(2, typeof(ushort))]
public Axis Axis { get; set; }
[CmdProperty(3, true)]
public uint Speed { get; set; }
[CmdProperty(4)]
public int XPoint { get; set; }
[CmdProperty(5)]
public int YPoint { get; set; }
[CmdProperty(6)]
public int ZPoint { get; set; }
public MoveParam()
{
Function = FunctionCode.Move;
}
public MoveParam(int stepNum, Axis axis, uint speed, int xPoint, int yPoint, int zPoint)
{
Function = FunctionCode.Move;
StepNum = stepNum;
Axis = axis;
Speed = speed;
XPoint = xPoint;
YPoint = yPoint;
ZPoint = zPoint;
}
}
对参数对象进行反射解析,生成对应的数据命令集合
创建扩展类 ParamBaseExtensions
public static class ParamBaseExtensions
{
public static byte[] ToCmd(this ParamBase param)
{
var properties = param.GetType().GetProperties()
.Where(x => x.IsDefined(typeof(CmdPropertyAttribute), false))
.OrderBy(x => ((CmdPropertyAttribute)x.GetCustomAttribute(typeof(CmdPropertyAttribute))).Number);
List<byte> result = new();
foreach (var item in properties)
{
var cmdAttribute = item.GetCustomAttribute(typeof(CmdPropertyAttribute)) as CmdPropertyAttribute;
var value = item.GetValue(param);
if (cmdAttribute.TargetType is not null)
{
value = Convert.ChangeType(value, cmdAttribute.TargetType);
}
var propertyBytes = value.ToBytes();
if (cmdAttribute.IsReverse)
propertyBytes = propertyBytes.Reverse().ToArray();
result.AddRange(propertyBytes);
}
return result.ToArray();
}
private static byte[] ToBytes(this object obj)
{
return obj switch
{
short s => BitConverter.GetBytes(s),
ushort s => BitConverter.GetBytes(s),
int s => BitConverter.GetBytes(s),
uint s => BitConverter.GetBytes(s),
float s => BitConverter.GetBytes(s),
double s => BitConverter.GetBytes(s),
byte s => [s],
_ => throw new NotImplementedException(),
};
}
}
将数据命令与包头,包尾拼接,从而组合成一包完整数据
创建类 CmdHelper
public class CmdHelper
{
private byte[] GetHeads()
{
return [0x0B, 0x0F];
}
private byte[] GetTails()
{
return [0x0C, 0x0A];
}
public byte[] BuilderCmd(ParamBase param)
{
return
[
.. GetHeads(),
.. param.ToCmd(),
.. GetTails(),
];
}
}
调用:
var cmdHelper = new CmdHelper();
var param = new MoveParam()
{
XPoint = 14,
YPoint = 14,
ZPoint = 14,
Axis = Enums.Axis.Axis_1,
Speed = 20,
StepNum = 1
};
var byteArr = cmdHelper.BuilderCmd(param);
foreach (var item in byteArr)
{
Console.Write(item.ToString("X2") + " ");
}
最后的打印结果为:
0B 0F 00 00 00 01 00 00 01 00 00 00 00 14 0E 00 00 00 0E 00 00 00 0E 00 00 00 0C 0A
如果后续在写其他命令,只需继承于 ParamBase 类,在对应的属性上使用 CmdProperty 特性即可