上位机软件在和PMAC通信的时候,最常见的通讯手段是使用官方提供的类库PComm32(dll或者pcommserver.exe):

  1. 通过类库的SelectDevice方法选择设备;
  2. Open方法打开指定端口;
  3. 发送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命令