65.9K
CodeProject 正在变化。 阅读更多。
Home

CmdMessenger

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2014年12月17日

CPOL

6分钟阅读

viewsIcon

56734

downloadIcon

823

一篇关于全向 Arduino Yun 机器人和 CmdMessenger 的替代 .Net 实现的文章。

引言

我最近一直在构建一个全向机器人,并在此处记录。由于我不想让这篇文章过长,我将把文档分成几篇文章。

在第一篇文章中,我将讨论用于与机器人通信的通信协议。您可以在下面看到机器人的图片和视频: 

 

背景

我过去曾参与过许多需要串行通信的 Arduino 平台项目。当发送给 Arduino 的消息变得更复杂时,我经常在使用串行通信时遇到障碍。我开始寻找解决这个问题的方法,很快就在 Arduino playground 上发现了 CmdMessenger

来自库主页

引用

CmdMessenger 是一个用于 Arduino 平台(和 .NET/Mono 平台)的消息库。它使用串口作为传输层。要使用 CmdMessenger,我们定义一个命令标识符列表,然后为收到的消息附加回调/处理函数。

该库是我试图解决的串行通信问题的绝佳解决方案。然而,该协议的 .Net 实现不符合我的要求。现有的 .Net 库与使用串口作为传输层紧密耦合。由于 Raspberry Pi 和 Arduino Yun 等许多设备现在都支持 WiFi,我希望能够通过 TCP/IP 发送消息。

然后可以使用一个简单的 Python 脚本将通过 TCP/IP 发送的消息代理到 Arduino。请参阅下面的图表。

我正在使用一个名为 tcp_serial_redirect.py 的 Python 脚本,它作为守护进程在 Linux 设备上运行。我不会在本文中介绍配置设备的步骤。在 Arduino Yun 发布之前,我使用的是 Raspberry Pi 作为 Linux 设备,并与 Arduino Uno 配合使用。

协议

CmdMessenger 定义的协议简单且非常灵活。协议的格式如下:

字符串转义参数

由于字符串可能包含参数或命令分隔符,因此协议必须支持这些特殊字符的转义。特殊字符通过在其后面加上转义字符来转义。默认情况下,转义字符是反斜杠。例如:

  • 5,Hello,World; - 将显示为一个带有三个参数的命令。
  • 5,Hello\,World; - 将显示为一个带有两个参数的命令。 

所有这些都由库处理,因此客户端无需了解这些细节。

架构

以下类图显示了库的基本概述。

CmdMessenger

CmdMessenger 是库的主要入口点。

  • Start - 打开连接并开始处理传入命令。
  • Stop - 关闭连接并停止读取传入命令。
  • Send - 发送 ISendCommand 并阻塞直到收到响应。
  • SendAsync - 发送 ISendCommand 并返回 Taks<IRecievedCommand>。
  • Register - 允许方法或 ICommandObserver 订阅具有指定 ID 的传入命令。

IReceivedCommand

IReceivedCommand 提供了一个用于读取传入命令的接口。

  • ReadInt16 - 从命令中读取一个 16 位整数。
  • ReadBool - 从命令中读取一个布尔值,其中 1, 0 分别表示 true, false。
  • ReadInt32 - 从命令中读取一个 32 位整数。
  • ReadString - 从命令中读取一个字符串。

ReceivedCommand

ReceivedCommand 是 IReceivedCommand 的具体实现。客户端无需实现该接口。

ISendCommand

ISendCommand 提供了一个用于发送传出命令的接口。

SendCommand

SendCommand 提供了 ISendCommand 接口的具体实现。客户端可以直接使用该类,或者通过创建派生自该类的命令来扩展库。

ICmdComms

ICmdComms 为传输层提供了一个通用接口。

CmdComms

提供了从流中读取命令的通用实现。其他基于流的传输层可以从该基类派生。库提供了 ICmdComms 的两个实现:

  • SerialCmdClient
  • TcpCmdClient

简单演示

源代码包含一个简单的演示应用程序,它演示了发送命令和处理其响应。演示显示了以下步骤:

  • CmdMessenger 首先与传输层一起实例化。
  • 注册一个命令处理程序来处理响应命令。
  • 调用 start 方法以打开与传输层的连接并开始处理命令。
  • 向 CmdMessenger 服务器发送一个命令,并等待直到服务器响应 CommandID 为 1。
 // Create an instance of CmdMessenger and provide an implementation of the transport layer.
 var cmdClient = new CmdMessenger.CmdMessenger(new TcpCmdClient("127.0.0.1", 5000));
 // Register one or more command handlers.
 cmdClient.Register(1, r => Console.WriteLine("Response received"));
 // Open the connection and begin processing incoming commands.
 cmdClient.Start();

 while (Console.ReadLine() != "x")
 {
       // Send a simple command with no arguments.
       cmdClient.Send(new SendCommand(0, 1));
 }

命令可以是单向或双向的。如果发送命令定义了 AckCommandID,则发送命令将阻塞,直到收到响应或达到超时。 

// Will block until the device responds with a commandID of 1
cmdClient.Send(new SendCommand(0, 1);

// Will return imedietly since no AckCommandID has been defiened.
cmdClient.Send(new SendCommand(0);

该库支持 C#5 的 SendAsync 方法。 

Task<IReceivedCommand> received = cmdClient.SendAsync(new SendCommand(0, 1);

// Do some other work

// Wait for the response 
await received;

机器人控制器应用程序

源代码包含一个用于控制机器人的 WPF 应用程序。该应用程序通过添加额外的抽象层进一步扩展了功能。

机器人控制器由四个组件组成:

  • CmdMessenger - CmdMessenger 库。
  • PiBot.Common - 一个类库,在 CmdMessenger 之上添加了一个抽象层。
  • ControlPad - 一个自定义控件(我可能会在后续文章中记录它)。
  • PiBot.Gui - 主应用程序。

PiBot.Common - 机器人命令

PiBot.Common 包含许多命令。我计划在某个阶段为机器人添加传感器和云台摄像机。我还没有添加这些功能,因此我只完整实现了几个命令。

  • SetMotorSpeed
  • SendMotorSpeed

PitBot.Gui - 应用程序

机器人控制器应用程序易于使用。

  • 从组合框中选择“地址 = 127.0.0.1,端口 5000”。
  • 点击连接,控制器将等待直到建立连接。
  • 建立连接后,控制板将启用,允许您点击控制方向。

由于您没有我的机器人,我创建了一个可以与机器人控制器应用程序一起使用的测试工具。

Arduino 代码

以下是 Arduino 代码的代码转储。 

要点

我正在使用 VEX 机器人公司的四个电机控制器来驱动机器人 Motor Controller 29。电机控制器需要 1ms 到 2ms 范围的 PWM 信号,其中 1ms 为全反转,2ms 为全正转,1.5ms 为中性。通过测试,我能够使用 Arduino Servo 库生成所需的 PWM 信号。我发现角度 40 给我全反转,角度 140 给我全正转,因此可用速度范围是 0 - 100。 

#include <CmdMessenger.h>
#include <Base64.h>
#include <Streaming.h>
#include <Servo.h>

char field_separator = ',';
char command_separator = ';';

CmdMessenger cmdMessenger = CmdMessenger(Serial, field_separator, command_separator);

Servo servo1;
Servo servo2;
Servo servo3;
Servo servo4;

enum
{
    //Request the device to send it's motor speed.
    GetMotorSpeed = 1,
    // Response message to a GetMotorSpeed message.
    GetMotorSpeedResponse,
    // Sets the motor speed on the device.
    SetMotorSpeed,
    // Message send when an external device changes the motor speed.
    SendMotorSpeed,
    // Request the current pan tilt position from the device.
    GetPanTiltPos,
    // Response message to a GetPanTiltPos message.
    GetPanTiltPosResponse,
    // Set the pan tilt position on the device.
    SetPanTiltPos,
    // Message sent when an external device modifies the pan tilt position.
    SendPanTiltPos,
    // Get the current value for a specified sensor.
    GetSensorValue,
    // Response message to a GetSensorValue.
    GetSensorValueResponse,
    //Set sensor value 
    SetSensorValue,
    // Message sent when a sensor value changes.
    SendSensorValue,
    // A fault has occurred.
    Fault
};

// Commands we send from the PC and want to receive on the Arduino
// We must define a callback function in our Arduino program for each entry in the list.
messengerCallbackFunction messengerCallbacks[] = 
{
    HandleGetMotorSpeed,
    unknownCmd,
    HandleSetMotorSpeed,
    unknownCmd,
    NULL
};

// ---------------------- C A L L B A C K  M E T H O D S ----------------------------------

void HandleGetMotorSpeed()
{
    int leftSpeedF  = servo1.read() - 40;
    int rightSpeedF = servo2.read() - 40;
    int leftSpeedR  = servo3.read() - 40;
    int rightSpeedR = servo4.read() - 40;
    
    String response = "Send pan tilt pos" + String(leftSpeedF) + "," + String(rightSpeedF) + 
                                        "," + String(leftSpeedR) + "," + String(leftSpeedR);
    
    char* stdStr = new char[response.length() + 1];
    response.toCharArray(stdStr,response.length());
  
    cmdMessenger.sendCmd(GetMotorSpeedResponse, "Get mototr speed");
}

void HandleSetMotorSpeed()
{
    int leftSpeedF  = cmdMessenger.readInt();
    int rightSpeedF = cmdMessenger.readInt();
    int leftSpeedR  = cmdMessenger.readInt();
    int rightSpeedR = cmdMessenger.readInt();
    
    cmdMessenger.sendCmd(SendMotorSpeed, "Send motor speed");
  
    servo1.write(40 + leftSpeedF); 
    servo2.write(40 + rightSpeedF); 
    servo3.write(40 + leftSpeedR); 
    servo4.write(40 + rightSpeedR); 
}

// ----------------------- D E F A U L T   C A L L B A C K S --------------------------------------

void unknownCmd()
{
    // Default response for unknown commands and corrupt messages
    cmdMessenger.sendCmd(Fault,"Unknown command");
}

// ------------------ E N D  C A L L B A C K  M E T H O D S ------------------


// ------------------ S E T U P ----------------------------------------------

void attach_callbacks(messengerCallbackFunction* callbacks)
{
    int i = 0;
    int offset = 1;
    while(callbacks[i])
    {
        cmdMessenger.attach(offset+i, callbacks[i]);
        i++;
    }
}

void setup()
{
    // Listen on the serial connection for messages from pc
    Serial.begin(57600);
    
    servo1.attach(8);
    servo2.attach(9);
    servo3.attach(10);
    servo4.attach(11); 
    
    // Pront line break between messages.
    cmdMessenger.print_LF_CR();
    
    cmdMessenger.attach(unknownCmd);
    
    attach_callbacks(messengerCallbacks);
    
}

// ------------------ E N D  S E T U P ----------------------------------

void loop()
{
    cmdMessenger.feedinSerialData();
  
}

关注点

支持二进制格式 (高效)

CmdMessenger 库的原始 C# 实现支持以二进制格式发送消息。我决定不在我的库中实现此功能。我认为协议的优点是消息可以使用简单的串行或 telnet 会话发送。消息可以使用简单的 ASCII 命令发送到设备。实现此功能会使库复杂化。以二进制形式发送消息的主要目的是减少延迟,这不是此库的主要目的。

源代码  

如果您想查看库的源代码和演示应用程序,可以在我的 Bit Bucket 网站上找到代码。 

https://bitbucket.org/chrism233/pibot

git 存储库地址如下:

https://chrism233@bitbucket.org/chrism233/pibot.git

历史

日期 变更
25/08/2014 初始发布。

 

© . All rights reserved.