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

[OoB] 使用操纵杆和伺服电机移动网络摄像头(Arduino/SharpDX/WinForms)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2014年11月6日

CPOL

7分钟阅读

viewsIcon

23344

使用操纵杆和伺服电机移动网络摄像头(Arduino/SharpDX/WinForms)

“无聊之余”系列的第三集时间到了。:) 之前有一个[OoB] 使用 Arduino、C#、JavaScript 和 HTML5 的声纳项目,以及一些关于射击彩弹的内容……这次,你将学习如何使用 Arduino 和 .NET 4.5 接收来自操纵杆的输入,并将其用于控制伺服电机,从而水平和垂直移动网络摄像头!

  • 你可以在这里看到项目成果的视频:Vimeo
  • 此仓库包含所有代码:GitHub

Controlling servos with joystick... Click to enlarge...

它是如何工作的?简短版本:操纵杆(在我的例子中是罗技 Extreme 3D Pro)通过 USB 连接到运行 Windows 7 的笔记本电脑。桌面应用程序(.NET 4.5/WinForms)使用 SharpDX 库(托管 DirectX API)从操纵杆获取位置信息。这些信息以数字和图形方式呈现在 UI 上(C# 5.0 async 在这里提供了帮助)。操纵杆位置随后被转换为所需的平移和倾斜伺服角度,并将该数据通过串口发送到 Arduino Uno。Arduino 接收数据,并借助其 Servo 库,命令伺服电机移动……

更详细的描述分为两部分。第一部分描述桌面应用程序,第二部分展示 Arduino 草图……

1. 桌面应用程序

ServoJoy WinForms application... Click to enlarge...

程序的主要任务是读取操纵杆位置。这得益于 SharpDX 2.6.2.0 库,它封装了 DirectX API,使其可以方便地通过 C# 操作。应用程序中使用了 SharpDXSharpDX.DirectInput NuGet 包。这是一个包含监视操纵杆移动所需所有代码的类

using SharpDX.DirectInput;
using System;
using System.Linq;
using System.Threading;

namespace ServoJoyApp
{
    public class JoystickMonitor
    {
        private string _joystickName;

        public JoystickMonitor(string joystickName)
        {
            _joystickName = joystickName;
        }

        public void PollJoystick
		(IProgress<JoystickUpdate> progress, CancellationToken cancellationToken)
        {
            var directInput = new DirectInput();
            
            DeviceInstance device = directInput.GetDevices
		(DeviceType.Joystick, DeviceEnumerationFlags.AttachedOnly)
                                        .SingleOrDefault(x => x.ProductName == _joystickName);

            if (device == null)
            {
                throw new Exception(string.Format
			("No joystick with \"{0}\" name found!", _joystickName));
            }
            
            var joystick = new Joystick(directInput, device.InstanceGuid);
            
            joystick.Properties.BufferSize = 128;
            joystick.Acquire();

            while (!cancellationToken.IsCancellationRequested)
            {
                joystick.Poll();
                JoystickUpdate[] states = joystick.GetBufferedData();
                
                foreach (var state in states)
                {
                    progress.Report(state);                    
                }
            }
        }
    }
}

DirectInput 类允许我们获取操纵杆的访问权限。它的 GetDevices 方法用于查找具有特定名称的连接操纵杆。如果找到这样的设备,则会创建 Joystick 类的对象。Joystick 类具有 Poll 方法,该方法填充一个包含操纵杆状态信息的缓冲区。状态信息以 JoystickUpdate 结构的形式出现。这样的结构可用于确定按下了哪个按钮,或者例如在 y 轴上的当前位置。

这是读取 x 轴上当前操纵杆位置的示例

if (state.Offset == JoystickOffset.X)
{
      int xAxisPosition = state.Value;
}

位置保存在 Value 属性中,但在使用它之前,您必须检查该值的含义。这可以通过将 Offset 属性与所需的 JoystickOffset enum 值进行比较来完成。请参阅 JoystickOffset文档,了解您可以读取哪些类型的值。

前面介绍的 PollJoystick 方法具有以下签名

public void PollJoystick(IProgress<JoystickUpdate> progress, CancellationToken cancellationToken)

IProgress 泛型接口是在 .NET 4.5 中引入的,用于允许方法报告任务进度PollJoystick 方法使用它来通知程序的其余部分操纵杆状态的变化。这是通过 progress.Report(state) 调用完成的。第二个参数(类型为 CancellationToken)允许我们随时停止操纵杆轮询。当 CancellationToken 结构的 IsCancellationRequested 属性设置为 true 时,PollJoystick 方法会这样做。使用这些与 async 相关的东西来轮询操纵杆数据有必要吗?不,可以将操纵杆轮询循环直接放入按钮事件处理程序中,但这样所有工作都将在 UI 线程中执行,这将导致应用程序无响应!以下是您如何在现代 C# 中运行操纵杆监视的方法

private async void btnJoystickMonitorStart_Click(object sender, EventArgs e)
{
    try
    {
        btnJoystickMonitorStart.Enabled = false;
        btnJoystickMonitorStop.Enabled = true;

        var joystickMonitor = new JoystickMonitor(txtJoystickName.Text.Trim());

        _joystickMonitorCancellation = new CancellationTokenSource();
        var progress = new Progress<JoystickUpdate>(s => ProcessJoystickUpdate(s));
        await Task.Run(() => joystickMonitor.PollJoystick(progress, _joystickMonitorCancellation.Token),
                                _joystickMonitorCancellation.Token);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Oh no :(", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

请注意,按钮事件处理程序已用 async 关键字标记。在 PollJoystick 任务启动之前,会创建一个新的取消令牌,并将 ProcessJoystickUpdate 设置为异步任务进度通知的处理程序。完成此设置后,操纵杆监视任务将通过 await Task.Run 调用启动……

这是负责处理操纵杆状态变化的部分代码

private void ProcessJoystickUpdate(JoystickUpdate state)
{
    if (state.Offset == JoystickOffset.X)
    {
        int xAxisPercent = GetAxisValuePercentage(XAxisMax, state.Value);
        pnlXAxisPercent.Width = (int)xAxisPercent;
        lblXAxisPercent.Text = xAxisPercent + "%";
        lblXAxisValue.Text = state.Value.ToString();

        if (rbPanOnXAxis.Checked)
        {
            _panServoPosition = MapAxisValueToPanServoPosition(state.Value, XAxisMax);         
            lblPanServoPosition.Text = _panServoPosition.ToString();
        }
    }
	
	// ... more ...

如您所见,JoysticUpdate 结构用于确定 x 轴上的当前位置。UI 元素被更新,并计算所需的伺服位置……如果您仔细观察,您可能会想知道为什么存在 if (rbPanOnXAxis.Checked)。这是因为该应用程序允许用户决定平移(水平移动)伺服电机是应该绑定到 x 轴(左右摇杆移动)还是 zRotation 轴(通过扭动操纵杆手腕控制——并非所有操纵杆都有此功能)。

private byte MapAxisValueToPanServoPosition(double axisValue, double axisMax)
{            
    byte servoValue = (byte)Math.Round((axisValue / axisMax) * 
				(PanServoMax - PanServoMin) + PanServoMin);
    return chkPanInvert.Checked ? (byte)(PanServoMax - servoValue) : servoValue;
}

我的操纵杆位置值报告范围在 0 到 65535 之间,但对于我使用的伺服电机来说,只有 0 到 180 之间的数字才有意义。这就是为什么创建了上面展示的 MapAxisValueToPanServoPosition 方法……

好的,我们已经完成了操纵杆移动的检测!现在我们需要将所需的伺服位置发送到 Arduino。幸运的是,这非常简单,这得益于您可以在 WinForms 程序中使用的 SerialPort 组件。只需从工具箱中拖动此组件,然后使用以下代码控制与 Arduino 的连接(spArduino 是我给 SerialPort 组件的名称)

private void btnArduinoConnectionToggle_Click(object sender, EventArgs e)
{
    try
    {
        if (spArduino.IsOpen)
        {
            spArduino.Close();

            btnArduinoConnectionToggle.Text = "Connect";
        }
        else
        {
            spArduino.BaudRate = (int)nudBaudRate.Value;
            spArduino.PortName = txtPortName.Text;

            spArduino.Open();
            btnArduinoConnectionToggle.Text = "Disconnect";
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Oh no :(", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

在我的情况下,Arduino 可以通过 COM3 端口访问,9600 的波特率就足够了。没错——尽管设备通过 USB 线缆连接到 PC,但它可以通过COM 端口访问。

将伺服位置发送到 Arduino 真的很容易

private void tmServoPosition_Tick(object sender, EventArgs e)
{
    if (spArduino.IsOpen)
    {
        spArduino.Write(new byte[] 
		{ _panServoPosition, _tiltServoPosition, SerialPackagesSeparator }, 0, 3);
    }
}

spArduino.Write 调用用于向 Arduino 发送一个三字节数组。其中两个值用于请求的伺服位置,最后一个值用于分隔这些对,以便伺服控制程序始终能够区分平移和倾斜值。通过串行端口写入是在 Timer 组件的 Tick 方法中执行的。这次,我没有费心手动创建后台任务。我只是拖动了 Timer 组件并调整了其 EnabledInterval 属性,使应用程序每 10 毫秒与 Arduino 通信一次……

2. Arduino 草图

我们已经讨论了一个可以检测操纵杆移动并通过串行端口发送伺服位置请求的程序。现在是时候来一段能够实际强制伺服电机移动的微控制器软件了。这是全部代码

#include <Servo.h>  

const byte setupReadyLedPin = 8;
const byte panServoPin = 10;
const byte tiltServoPin = 12;
const byte separator = 255;

Servo panServo; 
Servo tiltServo; 

void setup() {  
    pinMode(setupReadyLedPin, OUTPUT);
         
    panServo.attach(panServoPin);   
    tiltServo.attach(tiltServoPin);   
    
    Serial.begin(9600); // Open connection with PC
    
    digitalWrite(setupReadyLedPin, HIGH);
}

void loop() {      
    if (Serial.available() > 2) {            
        byte panAngle = Serial.read();
        byte tiltAngle = Serial.read();
        byte thirdByte = Serial.read();
         
        if (panAngle != separator && tiltAngle != separator && thirdByte == separator) {         
            // Moving servos
            panServo.write(panAngle);
            tiltServo.write(tiltAngle);
        }
    }       
}

是的,就这么简单!包含了 Servo 库,以便创建 panServotiltServo 对象。这些对象(类型为 Servo)可以命令伺服电机移动到所需位置。这通过调用 write 方法并传入所需角度来完成,就像这样

panServo.write(panAngle);

然而,在此之前,必须将伺服电机分配给 Arduino 的输出引脚。这是通过在 setup 函数中调用 attach 方法来实现的。数字伺服电机通过以 20 毫秒间隔计算的 ON 脉冲持续时间来控制,1.5 毫秒的 ON 脉冲应该命令伺服电机移动到中间……但 Servo 库为您完成了所有繁重的工作,因此您无需手动创建正确的控制信号。您只需连接每个伺服电机都有的 3 根电缆。我拥有的伺服电机使用棕色电缆接地,红色电缆接正极,橙色电缆接控制信号。[OoB] 声纳项目使用单个微型伺服电机,因此所需的唯一电源是 USB 中包含的电源。这次,使用了两个伺服电机,因此您应该添加外部电源。我通过 Arduino Uno 板上的插头连接了 1A 交流/直流电源适配器,伺服电机运行得非常好。Arduino 有一个内置保险丝,可以保护 USB 端口免受过电流(这是一个可复位保险丝,不允许电流大于 500mA)……

与 .NET 应用程序的通信通过 Serial 类实现。首先,在 setup 函数中,通过 Serial.begin(9600) 调用建立连接。然后,在循环内部,使用 Serial.available 方法检查是否已从 PC 收到包含伺服位置请求的数据包。如果收到,则读取平移和倾斜伺服角度,并命令伺服电机移动。

这就是使用连接到电脑的操纵杆控制两个伺服电机所需的所有内容。:) 在我的项目中,我使用了带有平移和倾斜支架的 DGServo S3003 伺服电机来移动 A4tech PK-910H 网络摄像头,我对结果非常满意!在观看视频时,您可能会认为摄像头相对于操纵杆的移动量过大。但请记住,伺服电机可以在 180 度范围内移动,而我的操纵杆操作范围较小。这就是为什么微小的操纵杆移动会导致大幅度的摄像头摆动。尽管如此,我仍然能够以 1 度精度相当容易地控制伺服电机位置……

更新 2015-01-11点击此处观看我最近一个项目的视频,该项目使用了平移和倾斜伺服电机来建造炮塔!

© . All rights reserved.