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

C# 中带串口的蓝牙模拟

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (18投票s)

2008年6月11日

CPOL

6分钟阅读

viewsIcon

278756

downloadIcon

8555

本文介绍了一款使用串口模拟蓝牙连接的移动设备应用程序。

引言

我想用 C# 为移动设备编写一个蓝牙应用程序,该程序可以将消息从一个实例发送到另一个实例(一个聊天应用程序)。经过一番研究,我发现 C# 本身并不原生支持蓝牙。如果我想构建这样的应用程序,我必须购买一个蓝牙库。Franson Bluetools 是一个非常好的 C# 蓝牙库,只需 100 美元。鉴于我不想花任何钱,我必须从头开始编写我的应用程序。

我发现最简单的方法是使用串口来模拟蓝牙连接。

背景

此应用程序使用 .NET Compact Framework。要理解代码,您需要具备使用 SerialPort 类的基本知识,还需要知道如何从与应用程序运行的线程不同的辅助线程更新应用程序的 UI。

顾名思义,SerialPort 类在 C# 中用于串口通信。该类位于 System.IO.Ports 命名空间中。您可以像这样实例化一个该类型的对象

SerialPort inPort = new SerialPort("COM1",9600); 

然后像这样打开端口

inPort.Open();

要从串口读取一行,请使用以下代码

string mess=inPort.ReadLine(); 

要向串口写入一行,您可以使用以下代码

string message="Hello";
inPort.WriteLine(message);

要查找设备上可用的 COM 端口,可以使用

string[] ports=SerialPort.GetPortNames();

完成后,您可以像这样关闭连接

inPort.Close(); 

在进入下一节之前,您还需要了解的另一件事是如何从辅助线程更新用户界面。在 .NET 世界中,UI 只能由创建它的线程更新。如果您尝试从不同的线程更新 UI,将会抛出跨线程操作异常。要从不同的线程更新 UI,您需要使用 invoke() 方法。此方法使用委托来调用与调用了 invoke() 方法的对象所在的线程相同的线程上的函数。例如,假设您想从辅助线程更新窗体上的 TextBox。为此,您首先创建一个与您想要调用的函数签名相同的委托

public delegate void UpdateTextBox(string text);

然后,您创建更新 TextBox 的方法

private void UpdateText(string text)
{
    TextBox1.Text=text;
}

要更新文本框,您可以在辅助线程上调用以下行

TextBox1.invoke(new UpdateTextBox(UpdateText),textToWrite);

使用代码

鉴于我的应用程序通过串口连接到另一台移动设备,并且用于蓝牙通信的串口因设备而异,我使用 SettingsForm 窗体类来设置通信的 COM 端口(您可以通过转到“设置”->“连接”->“蓝牙”->“服务”,然后选择“串口”服务并单击“高级”来找到这些端口)。设置窗体如下所示

bth_serial_port/settingsform.png

设置窗体在其构造函数中初始化两个组合框,其中包含该设备所有可用的 COM 端口,如下所示

string[] ports = SerialPort.GetPortNames();
comboBox1.Items.Add("NO PORT SELECTED");
for (int i = 0; i < ports.Length; i++)
     comboBox1.Items.Add(ports[i]);
comboBox2.Items.Add("NO PORT SELECTED");
for (int i = 0; i < ports.Length; i++)
     comboBox2.Items.Add(ports[i]);

显示窗体时,用户选择入站和出站端口,然后单击“确定”。可以使用以下代码检索 COM 设置

public string GetInboundPort() 
{
    return (string)comboBox1.SelectedItem;
}
public string GetOutboundPort()
{
    return (string)comboBox2.SelectedItem;
}

只有在通过设置窗体设置了 COM 端口后,我们才能尝试连接。

应用程序的主窗体由 Form1 类表示,如下图所示

bth_serial_port/mainform.png

bth_serial_port/mainform2.png

如您所见,窗体具有非常简单的界面,由两个文本框(一个用于编写要发送的消息,另一个用于显示已发送和已接收消息的历史记录)、一个发送消息的按钮、一个显示当前状态信息的标签以及一个菜单组成。通过菜单,您可以设置 COM 端口,您可以连接到另一台设备,也可以断开与另一台设备的连接。

正如我之前所说,要设置 COM 端口,请单击“设置”菜单选项。此事件处理程序的代码如下

SettingsForm form = new SettingsForm(inboundPort, outboundPort);
if(form.ShowDialog() == DialogResult.OK)
{
    if(form.GetInboundPort().CompareTo("NO PORT SELECTED") == 0 ||
       form.GetOutboundPort().CompareTo("NO PORT SELECTED") == 0)
    {
        MessageBox.Show("The ports were not set properly.");
        lblStatus.Text = "Ports not set.";
        mnuConnect.Enabled = false;
    }
    else
    {
        inboundPort = form.GetInboundPort();
        outboundPort = form.GetOutboundPort();
        mnuConnect.Enabled = true; 
        lblStatus.Text = "Ports set.\r\nin:" + inboundPort +
                         "\r\nout:" + outboundPort + 
                         "\r\nWaiting to press Connect...";
    }
}

从这段代码可以看出,我首先创建 SettingsForm 类的实例并显示它。如果端口设置正确,我将设置 inboundPortoutboundPort 变量并启用“连接”菜单选项,否则我会显示一个消息框,指出端口未设置。

设置 COM 端口后,您可以通过单击“连接”菜单选项连接到设备。下面展示了此事件处理程序的代码

serialIn = new SerialPort(inboundPort);
serialOut = new SerialPort(outboundPort); 
serialIn.ReadTimeout = 1000;
serialOut.ReadTimeout = 1000; 
disconnectRequested = false;
try
{
    if(!serialIn.IsOpen)
    {
       lblStatus.Text = "Input port closed. Opening input port...";
        serialIn.Open();
    }
    if(!serialOut.IsOpen)
    {
        lblStatus.Text = "Output port closed. Opening output port...";
        serialOut.Open();
    }
    lblStatus.Text = "Ports opened. Starting the listener thread...";
    rcvThread = new Thread(new ThreadStart(ReceiveData)); 
    numThreads++;
    rcvThread.Start();
    lblStatus.Text = "Listener thread started.";
    btnSend.Enabled = true;
    lblStatus.Text="Connected.\r\nInbound:" + inboundPort + 
                   "\r\nOutbound:" + outboundPort;
    MessageBox.Show("Connected.");
    mnuConnect.Enabled = false;
    mnuSettings.Enabled = false;
    mnuDisconnect.Enabled = true;
}
catch (Exception ex)
{
    MessageBox.Show(ex.Message);
    lblStatus.Text = "error.\r\nPorts not set.";
    mnuConnect.Enabled = false;
}

首先,我实例化代表串口的两个变量(需要两个:一个用于入站,一个用于出站)。我还设置了超时值,以便应用程序在需要时可以正常退出。我将在稍后讨论这一点。

接下来,我验证端口是否已打开,如果未打开,则将其打开。在我测试该应用程序的 PDA 上,此时会显示所有附近设备的列表。您现在可以选择要与之通信的设备,然后打开端口。

打开端口后,将启动一个辅助线程。此线程用于从远程设备接收消息并在历史文本框中显示它们。启动线程后,应用程序的状态会更新,并且“断开连接”菜单项会被启用。

现在您可以编写消息并发送它们。下面展示了发送按钮事件处理程序的代码

try 
{
     serialOut.WriteLine(txtMess.Text);
     txtLog.Text += "you:" + txtMess.Text + "\r\n";
     txtMess.Text = "";
}
catch (Exception ex)
{
     MessageBox.Show(ex.Message);
}

该代码使用 SerialPort 类的 WriteLine() 函数发送消息。

正如我在上面几行提到的,消息是在辅助线程上接收的。此线程执行 ReceiveData() 函数。此函数在下面的代码中实现

private void ReceiveData()
{
     while(!closeRequested && !disconnectRequested)
     {
         try
         {
              string line = serialIn.ReadLine();
              if(line.CompareTo("quit$$$") == 0)
              {
                   disconnectRequested = true;
                   continue;
              }
              txtLog.Invoke(new updateText(UpdateText),line);
          }
          catch

          {}
     }
     if(closeRequested)
         closeMe();
     if(disconnectRequested)
         this.Invoke(new disconnectDel(onDisconnect));               
}

该函数使用一个 while 循环,只要用户不退出或断开连接,循环就会继续使用 SerialPort.ReadLine() 函数读取消息。读取消息后,通过使用 Invoke() 在 UI 线程上调用 UpdateText() 函数来更新 UI。

现在是时候解释为什么我为串口变量设置了超时值了。SerialPort 类的 ReadLine() 方法会阻塞,直到收到消息。为了让应用程序处理断开连接和关闭请求,它需要执行 ReadLine() 之后的代码行。通过将超时值设置为 1000 毫秒,在等待一秒钟而未收到消息后,该函数将抛出异常,然后 while 循环将开始新的迭代。这为应用程序提供了检查这两个变量并可能退出循环的机会。

如果用户单击“关闭”按钮,应用程序将调用 closeMe() 函数。此函数会减少打开的辅助线程的数量,并调用 Form.Close() 函数来关闭应用程序。Form.Close() 函数也必须使用 Invoke() 调用,因为它需要在与 UI 相同的线程上调用。

private void closeMe() 
{
     numThreads--;           
     this.Invoke(new closeDel(this.Close));
}

如果用户单击“断开连接”,将显示“quit$$$”消息以通知远程设备意图断开连接,并设置 disconnectRequired 标志。

try
{
    disconnectRequested = true;
    serialOut.WriteLine("quit$$$");
}
catch (Exception ex)
{
    MessageBox.Show(ex.Message);
}

如果 disconnectRequired 标志设置为 true,则辅助线程中的 while 循环将终止,并执行 onDisconnect() 方法。该方法实现如下

private void onDisconnect()
{
   serialIn.Close(); 
   serialOut.Close();
   mnuDisconnect.Enabled = false;
   mnuConnect.Enabled = false;
   mnuSettings.Enabled = true;
   lblStatus.Text = "Disconected.\r\nPorts not set.";
   numThreads--;
}

此函数关闭端口,启用“设置”按钮,并减少正在运行的辅助线程的数量。

另外,如果应用程序收到“quit$$$”消息,它将断开连接。

if(line.CompareTo("quit$$$") == 0) 
{
      disconnectRequested = true;
      continue;
}

这就是全部。正如您从这段代码中看到的,使用移动设备上的串口模拟蓝牙连接相对简单。您只需要检查正在使用的端口,然后启动应用程序。希望您喜欢这篇文章,也希望您能提供一些反馈。

© . All rights reserved.