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

[OoB] 使用继电器、Arduino 和 .NET WinForms 射击彩弹标记器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2014年10月5日

CPOL

6分钟阅读

viewsIcon

14059

如何使用继电器、Arduino 和 .NET Winforms 射击彩弹标记器

我的第一个基于 Arduino 的项目是 带 C#、JS 和 HTML5 的声纳。现在我将继续“无聊之余”系列,搭建一个允许通过计算机发送命令来发射彩弹标记器(枪)的装置。:) 这次软件栈更简单——只有一个小的 Arduino 草图和一个不复杂的 WinForms (.NET/C#) 应用程序,但硬件需要一点电子知识。不过别担心,没什么太复杂的——我绝对不是这方面的专家…… 

Controlling paintball marker with laptop... Click to enlarge...

该项目围绕一个机电继电器展开。这种继电器基本上是一个电子控制开关,允许你通过从 Arduino 输出引脚发送信号来打开和关闭强大的设备。例如,你可以控制非常大的电机或灯泡。这个组件的妙处在于你可以控制外部电路——你的控制电路和你想要打开/关闭的设备之间没有物理连接。你可以将继电器用于需要巨大电流的设备,但你也可以控制更精密的设备,这正是我决定做的。我玩彩弹/速射,我碰巧拥有一款高端的电动气动标记器,名为 DM13。这种标记器有电子操作的扳机,并使用电磁阀来射击……我想:“如果我按下笔记本电脑上的回车键就能让这把枪每秒发射近 20 发子弹,那不是很酷吗?”……观看此视频以了解它是如何工作的:)

这是一份使用的硬件部件列表

元素 角色
Arduino Uno R3 通过继电器控制扳机并与 PC 通信
JZC-11F 005-IZ SPDT 继电器 通过闭合扳机电路模拟扳机拉动
P2N2222AG NPN 晶体管 提供电流以操作继电器 
1.2k 欧姆电阻 限制晶体管基极电流
绿色 LED 指示准备就绪状态
红色 LED 指示发射 
2x 330 欧姆电阻 限制通过二极管的电流
压电蜂鸣器 以不同音调指示读取/发射
SPST 开关 打开/关闭蜂鸣器
面包板和跳线或通用板 连接组件 

这是电路图

Circuit diagram... Click to enlarge...

连接到引脚 13的 LED 用于指示设备已准备就绪,连接到引脚 12的 LED 指示发射(扳机电路闭合)。LED 当然不直接连接到 Arduino,有电阻保护它们免受过电流。蜂鸣器在设备准备就绪时发出一个音调,在设备发射时发出另一个(更高)音调。开关的目的是在测试时让你的生活更轻松。蜂鸣器声音很快就会变得烦人,所以你可以将其关闭……

这些是无聊的部分,更有趣的东西在图表的左侧。引脚 2通过电阻连接到 NPN 晶体管的基极集电极连接到继电器线圈。需要晶体管是因为控制开关功能的线圈需要比 Arduino 输出引脚能提供的更多电源。在此电路中,晶体管不是用作放大器,而是用作开关。当引脚 2设置为高电平(基极-发射极电压 = 5V)时,晶体管达到完全导通状态,电流流过集电极使线圈通电。将高电平状态放在引脚 2上会导致继电器的COM公共)和NO常开)引脚之间建立连接。我用万用表(电阻模式)检查了我的标记器,我看到拉动扳机导致扳机开关的中间和下部引脚之间形成闭合电路。将一根电缆连接在开关的中间引脚COM引脚之间,另一根电缆连接在下部开关引脚常开引脚之间,就可以模拟扳机拉动。换句话说:就标记器而言,Arduino 引脚 2上的高电平状态等于扳机拉动*。这很简单,但正如你在视频中看到的,它确实效果很好!还有一件事:看看 JZC-11F 继电器右侧的图表——有一个信号二极管,它的作用是保护晶体管免受引脚 2设置为低电平(当继电器线圈的电源电压被移除时)时出现的电压尖峰。这种二极管用法被称为“飞轮”或“续流”……我首先在面包板上创建了这个电路,然后将其焊接到通用板上……请记住,有多种 Arduino 继电器扩展板可用,所以你实际上不必自己创建这种基于晶体管的电路。我这样做是因为我认为这是一种复习一些非常基本的电子知识的好方法。好了,硬件部分我们完成了!

现在是软件时间这个GitHub 仓库包含所有代码)!

这是完整的Arduino 草图

const byte fireRelayPin = 2;
const byte fireBuzzerPin = 11;
const byte fireLedPin = 12;
const byte readyLedPin = 13;

const byte readyToFireMessage = 6; // ASCII ACK
const byte fireCommand = 70; // ASCII F
const byte triggerPullDelayInMs = 30;
const byte fireBuzzerHz = 1000;
const byte readyBuzzerHz = 400;

void setup() {  
    pinMode(fireRelayPin, OUTPUT);
    pinMode(fireBuzzerPin, OUTPUT);
    pinMode(fireLedPin, OUTPUT);
    pinMode(readyLedPin, OUTPUT);
  
    Serial.begin(9600);
    
    tone(fireBuzzerPin, readyBuzzerHz);  
    digitalWrite(readyLedPin, HIGH); 

    Serial.write(readyToFireMessage);     
}

void loop() {
   if (Serial.available()) {
        byte data = Serial.read(); 
        
        if (data == fireCommand) {
            pullTrigger();
            delay(triggerPullDelayInMs);  
            releaseTrigger();
        }
    }  
}

void pullTrigger() {
    digitalWrite(fireLedPin, HIGH);    
    digitalWrite(fireRelayPin, HIGH);  
    tone(fireBuzzerPin, fireBuzzerHz);     
}

void releaseTrigger() {
    digitalWrite(fireLedPin, LOW);    
    digitalWrite(fireRelayPin, LOW);  
    tone(fireBuzzerPin, readyBuzzerHz);  

    Serial.write(readyToFireMessage);     
}

一点也不复杂。:) 首先,通常(良好)的实践是为引脚编号和其他有用值创建常量。然后是一个`setup`方法,它配置引脚模式,初始化串行连接(用于与 PC 通信),并打开“就绪”LED。还有一个对`tune`函数的调用。`tune`用于通过使蜂鸣器中的压电元件以指定频率振动来生成声音。在`loop`函数中,程序等待 PC 通过串行端口发送的数据,并检查此数据是否表示发射命令(ASCII 字母“F”)。如果是,则模拟扳机拉动,然后稍作延迟并释放扳机。`digitalWrite(fireRelayPin, HIGH);`这行代码使彩弹枪发射。如果你仔细观察,你会注意到`setup`和`releaseTrigger`函数中都有`Serial.write(readyToFireMessage);`调用。这些是为了让计算机知道 Arduino 已准备好接收第一个发射命令或已准备好处理新命令。

这是用于控制彩弹标记器的 .NET 4.5 WinForms 应用程序

Command application... Click to enlarge...

按下“Fire”按钮一次会使标记器发射一发,按住它会使其连续发射。每秒发射多少子弹当然取决于`triggerPullDelayInMs`值、继电器速度、标记器设置(我的 DM13 设置为半自动模式,上限约为 20bps)、彩弹装填器速度等。只有当“Device is ready to fire”显示“YES”且“Safe”复选框未选中时才能射击。

这是上面显示的`CommandWindow`背后的代码(已删除一些特别无聊的行)

using System;
using System.Drawing;
using System.IO.Ports;
using System.Threading;
using System.Windows.Forms;

namespace PbFireApp
{
    public partial class CommandWindow : Form
    {
        private const byte ReadyToFireMessage = 6; // ASCII ACK
        private const byte FireCommand = 70; // ASCII F

        private bool _isReadyToFire;
        private bool IsReadyToFire
        {
            get
            {
                return _isReadyToFire;
            }
            set
            {
                _isReadyToFire = value;
                 SetIsReadyToFireLabel();
            }
        }

        public CommandWindow()
        {
            InitializeComponent();
            
            IsReadyToFire = false;
            spArduino.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
        }

        private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
        {
            byte data = (byte)spArduino.ReadByte();
            IsReadyToFire = data == ReadyToFireMessage;          
        }

        private void Fire()
        {
            if (!chkSafe.Checked && IsReadyToFire)
            {
                IsReadyToFire = false;
                spArduino.Write(new byte[] { FireCommand }, 0, 1);
            }
        }

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

                    btnConnect.Text = "Connect";                
                    gbFire.Enabled = false;
                }
                else
                {
                    spArduino.BaudRate = (int)nudBaudRate.Value;
                    spArduino.PortName = txtPortName.Text;

                    spArduino.Open();

                    btnConnect.Text = "Disconnect";            
                    gbFire.Enabled = true;
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), 
                "Oh no :(", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
		
        // ... more ...

        delegate void SetIsReadyToFireLabelCallback();

        private void SetIsReadyToFireLabel()
        {
            if (lblIsReadyToFire.InvokeRequired)
            {
                SetIsReadyToFireLabelCallback d = 
                new SetIsReadyToFireLabelCallback(SetIsReadyToFireLabel);
                Invoke(d, new object[] { });
            }
            else
            {
                lblIsReadyToFire.Text = IsReadyToFire ? "YES" : "NO";
                lblIsReadyToFire.ForeColor = IsReadyToFire ? Color.Orange : Color.Black;
            }
        }
    }
}

这个应用程序非常简单,所以我决定将所有代码放入窗体的 cs 文件中……`SerialPort`组件用于与 Arduino 通信(我在声纳项目帖子中写了更多关于串行通信的内容)。当向 Arduino 发送“`F`”命令(“Fire”的缩写,ASCII 码 70)时,彩弹标记器被指示发射。这行代码负责它: 

spArduino.Write(new byte[] { FireCommand }, 0, 1);

当从 Arduino 收到 ACK (确认,ASCII 6)消息时,`DataReceivedHandler` 方法用于将 `IsReadyToFire` 属性设置为 `true`。

除了`SetIsReadyToFireLabel`方法,代码很明显。为什么需要它呢?当`IsReadyToFire`属性设置时,`Label`控件的颜色和文本会改变。这可能发生在`DataReceivedHandler`执行时。我们不能直接从`SerialPort.DataReceived`事件处理程序更改 UI 元素,因为这会导致`InvalidOperationException`,并显示类似这样的消息:“`跨线程操作无效:从创建控件的线程以外的线程访问控件“lblIsReadyToFire”。`”。

就这样,第二个“无聊之余”项目完成。:) 

1. 直接控制电磁阀应该是可能的,但这会更复杂,并且可能会损坏我宝贵的 DM13……

© . All rights reserved.