[OoB] 使用操纵杆和伺服电机移动网络摄像头(Arduino/SharpDX/WinForms)
使用操纵杆和伺服电机移动网络摄像头(Arduino/SharpDX/WinForms)
“无聊之余”系列的第三集时间到了。:) 之前有一个[OoB] 使用 Arduino、C#、JavaScript 和 HTML5 的声纳项目,以及一些关于射击彩弹的内容……这次,你将学习如何使用 Arduino 和 .NET 4.5 接收来自操纵杆的输入,并将其用于控制伺服电机,从而水平和垂直移动网络摄像头!
它是如何工作的?简短版本:操纵杆(在我的例子中是罗技 Extreme 3D Pro)通过 USB 连接到运行 Windows 7 的笔记本电脑。桌面应用程序(.NET 4.5/WinForms)使用 SharpDX 库(托管 DirectX API)从操纵杆获取位置信息。这些信息以数字和图形方式呈现在 UI 上(C# 5.0 async 在这里提供了帮助)。操纵杆位置随后被转换为所需的平移和倾斜伺服角度,并将该数据通过串口发送到 Arduino Uno。Arduino 接收数据,并借助其 Servo 库,命令伺服电机移动……
更详细的描述分为两部分。第一部分描述桌面应用程序,第二部分展示 Arduino 草图……
1. 桌面应用程序
程序的主要任务是读取操纵杆位置。这得益于 SharpDX 2.6.2.0 库,它封装了 DirectX API,使其可以方便地通过 C# 操作。应用程序中使用了 SharpDX
和 SharpDX.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
组件并调整了其 Enabled
和 Interval
属性,使应用程序每 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
库,以便创建 panServo
和 tiltServo
对象。这些对象(类型为 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:点击此处观看我最近一个项目的视频,该项目使用了平移和倾斜伺服电机来建造炮塔!