上位机软件在和PMAC通信的时候,最常见的通讯手段是使用官方提供的类库PComm32(dll或者pcommserver.exe):
- 通过类库的SelectDevice方法选择设备;
- Open方法打开指定端口;
- 发送GetResponseEx()或者其它指令进行通信。
PMAC.SelectDevice(0, out int PMACDevice, out int SelectPmacSuccess);
if (SelectPmacSuccess)
{
PMAC.Open(PMACDevice, out bool OpenPmacSuccess);
if (OpenPmacSuccess)
{
//选定的运动控制器打开成功
PMAC.GetResponseEx(PMACDevice, "#1p#2p#3p#4p#5p#6p", true, out string pmacAnswer, out int pmacStatus);
Log(pmacAnswer);
}
else
{
//运动控制器打开失败
}
}
else
{
//选定的运动控制器无法通信
}
用这个组件的好处是所有方法都已经封装好了,不需要用户关心底层的通信细节,用户需要做的只是发送命令,读取返回值,非常便捷。不过在实际使用的时候,这个组件的表现并不是很稳定(初步怀疑运动控制器同一时间只能接受一个客户端连接),并且上位机环境是Linux的时候这个类库就无能为力了,所以最终还是自己用Socket实现了一遍通讯协议。
Delta Tau专门有开发手册讲述这部分内容,但说得非常简单,而相关的内容在网上一星半点都找不到,刚开始手里没有手册的时候着实浪费了不少精力去尝试通信。也正是这个原因,才会有这篇文章,希望可以帮到正好有需求的朋友。有些东西如果在网上随便找找就是一大堆,也就没必要再去把别人说过无数次的东西再拿出来复读一遍了。
Socket通信既可以用TCP也可以用UDP,默认端口是1025,整个过程和PComm Server一样,也是分为两步:发送,接收。发送的时候需要根据不同的方法构造该方法对应的数据包,然后读取响应即可完成一次请求。
包格式如下:
byte RequestType, //上传(pmac -> 上位机)0xC0,下载0x40
byte Request, //指令的类型
ushort wValue, //用于辅助的指令值,在某些指令中需要传入多个变量
ushort wIndex, //和wValue类似,大部分场景是0,某些指令中做标志用
ushort wLength, //bData部分的长度
byte[] bData //需要发送到控制器的实际命令
特别需要注意的是:wValue, wIndex, wLength需要使用大端字节序。
Request定义如下:
const byte VR_PMAC_SENDLINE = 0xb0;
const byte VR_PMAC_GETLINE = 0xb1;
const byte VR_PMAC_FLUSH = 0xb3;
const byte VR_PMAC_GETMEM = 0xb4;
const byte VR_PMAC_SETMEM = 0xb5;
const byte VR_PMAC_SENDCTRLCHAR = 0xb6;
const byte VR_PMAC_SETBIT = 0xba;
const byte VR_PMAC_SETBITS = 0xbb;
const byte VR_PMAC_PORT = 0xbe;
const byte VR_PMAC_GETRESPONSE = 0xbf;
const byte VR_PMAC_READREADY = 0xc2;
const byte VR_CTRL_RESPONSE = 0xc4;
const byte VR_PMAC_GETBUFFER = 0xc5;
const byte VR_PMAC_WRITEBUFFER = 0xc6;
const byte VR_PMAC_WRITEERROR = 0xc7;
const byte VR_FWDOWNLOAD = 0xcb;
const byte VR_IPADDRESS = 0xc0;
最重要的一条请求是VR_PMAC_GETRESPONSE,请求结构是:
byte RequestType = 0x40, //上传(pmac -> 上位机)0xC0,下载0x40
byte Request = 0xbf, //指令的类型
ushort wValue = 0, //用于辅助的指令值,在某些指令中需要传入多个变量
ushort wIndex = 0, //和wValue类似,大部分场景是0,某些指令中做标志用
ushort wLength, //bData部分的长度
byte[] bData //发送到控制器的实际命令
这个包的data部分的值是:40 bf 00 00 00 00 00 03 49 31 30,对照包结构可以识别出来bData的内容是I10,也就是发送了一条I10命令,wLength = 3也能和请求结构对应上。也就是说,只要我们用Socket发送一个byte[] {0x40, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x49, 0x31, 0x30},就实现了发送I10命令的功能,返回值即代表变量I10的值。
CTRL组合键的请求是VR_PMAC_SENDCTRLCHAR,包结构如下:
byte RequestType = 0x40, //上传(pmac -> 上位机)0xC0,下载0x40
byte Request = 0xb6, //指令的类型
ushort wValue, //字符ascii码 & 1f的结果
ushort wIndex = 0 //固定为0
关于请求VR_PMAC_SENDCTRLCHAR官方文档描述得并不是很详尽,没有常量VR_PMAC_SENDCTRLCHAR的定义(这个错误太低级了),也没有说wValue具体如何取值(实际应该是字符的ascii码和1f按位求与的结果,比如ctrl + K即为0x1b & 0x1f = 0x0b)。
DEMO:
public static class PMACMessenger
{
public const byte VR_PMAC_SENDLINE = 0xb0;
public const byte VR_PMAC_GETLINE = 0xb1;
public const byte VR_PMAC_FLUSH = 0xb3;
public const byte VR_PMAC_GETMEM = 0xb4;
public const byte VR_PMAC_SETMEM = 0xb5;
public const byte VR_PMAC_SENDCTRLCHAR = 0xb6;
public const byte VR_PMAC_SETBIT = 0xba;
public const byte VR_PMAC_SETBITS = 0xbb;
public const byte VR_PMAC_PORT = 0xbe;
public const byte VR_PMAC_GETRESPONSE = 0xbf;
public const byte VR_PMAC_READREADY = 0xc2;
public const byte VR_CTRL_RESPONSE = 0xc4;
public const byte VR_PMAC_GETBUFFER = 0xc5;
public const byte VR_PMAC_WRITEBUFFER = 0xc6;
public const byte VR_PMAC_WRITEERROR = 0xc7;
public const byte VR_FWDOWNLOAD = 0xcb;
public const byte VR_IPADDRESS = 0xc0;
public const string HOST = "192.6.94.5";//默认的控制器地址
public const string CMD_PMAC_READ = "#1p";//读取1号电机的位置
public const int PORT = 1025;
public static byte[] Send(byte[] data)
{
Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.BeginConnect(HOST, PORT, null, null);
DateTime begin = DateTime.Now;
while (true)
{
if (begin.AddSeconds(2) < DateTime.Now || client.Connected) break;
}
if (!client.Connected) return Encoding.ASCII.GetBytes("NETWORK_ERROR.");
client.Send(data);
begin = DateTime.Now;
byte[] bytes = new byte[1024];
bool recv = false;
int len = 0;
var task = Task.Run(() =>
{
len = client.Receive(bytes, 0, 1024, SocketFlags.None);
recv = true;
});
while (true)
{
if (begin.AddSeconds(5) < DateTime.Now || recv) break;
}
return len > 0 ? bytes.ToArray() : null;
}
public static byte[] Send(PMACPacket data) => Send(data.Data);
}
public class PMACPacket
{
public const byte RequestType = 0x40;
public byte Request { get; set; }
public ushort wValue { get; set; }
public ushort wIndex { get; set; }
public ushort wLength => (ushort)bData.Length;
public byte[] bData { get; set; }
public byte[] Data
{
get
{
IEnumerable<byte> bytes = new byte[] {
RequestType,
Request }.Concat(
BitConverter.GetBytes(wValue).Reverse()).Concat(
BitConverter.GetBytes(wIndex).Reverse()).Concat(
BitConverter.GetBytes(wLength).Reverse()).Concat(bData);
return bytes.ToArray();
}
}
public PMACPacket(byte req, ushort val, ushort index, byte[] data)
{
Request = req;
wValue = val;
wIndex = index;
bData = data;
}
}
var result = PMACMessenger.Send(
new PMACPacket(
PMACMessenger.VR_PMAC_GETRESPONSE,
0,
0,
Encoding.ASCII.GetBytes(strCmd)));
//发送strCmd命令