在 C# 中继续之前等待串行消息

Waiting for a serial message before proceeding in C#

我目前正在研究 C# 和 arduino 项目之间的接口。我正在使用串行端口从 arduino 发送和接收消息,到目前为止一切正常。

但是我到了一个点,我想向 arduino 发送一个字符串并等待响应,直到我要发送下一个字符串。

我想我不能简单地在这里超时,因为我的 RichTextField 也将冻结并且无法从那里读取“OK*”消息。

我已经在网上阅读了一些其他解决方案,但由于我是 C# 的新手,我希望有人能为我指明正确的方向。

此致

//用完整的代码示例编辑了 post:

            using System;
        using System.Collections.Generic;
        using System.ComponentModel;
        using System.Data;
        using System.Drawing;
        using System.Linq;
        using System.Text;
        using System.Threading;
        using System.Threading.Tasks;
        using System.Windows.Forms;
        using System.IO;
        using System.IO.Ports;


        namespace interface_test
        {
            public partial class Form1 : Form
            {

                string serialDataIn;

                public Form1()
                {
                    InitializeComponent();

                    Enable_Console();

                    comboBox_baudRate.Text = "115200";
                    comboBox_comPort.Text = "COM3";
                    string[] portLists = SerialPort.GetPortNames();
                    comboBox_comPort.Items.AddRange(portLists);
                }

                        private void Form1_Load(object sender, EventArgs e)
                {

                }

                /* erase textbox contents */
                private void button_clearLog_Click(object sender, EventArgs e)
                {
                    richTextBox_receiveMsg.Text = "";
                }

                /* listening to the serial port */
                private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
                {
                    serialDataIn = serialPort1.ReadExisting();
                    this.Invoke(new EventHandler(ShowData));
                }

                /* process the serial data */
                public void ShowData(object sender, EventArgs e)
                {
                    string trimmedMsg = serialDataIn;
                    richTextBox_receiveMsg.Text += trimmedMsg;

                    if (trimmedMsg.Contains("Initialization done."))
                    {
                        button_detectCEM.Enabled = false;
                    }
                }

                /* open COM-port to arduino */
                private void button_openCom_Click(object sender, EventArgs e)
                {
                    try
                    {
                        serialPort1.PortName = comboBox_comPort.Text;
                        serialPort1.BaudRate = Convert.ToInt32(comboBox_baudRate.Text);
                        serialPort1.Open();
                        Enable_Console();

                        button_detectCEM.Enabled = true;
                    }
                    catch (Exception error)
                    {
                        MessageBox.Show(error.Message);
                    }
                }

                /* close COM-port to arduino */
                private void button_closeCom_Click(object sender, EventArgs e)
                {
                    if (serialPort1.IsOpen)
                    {
                        try
                        {
                            serialPort1.Close();

                            Enable_Console();
                        }
                        catch (Exception error)
                        {
                            MessageBox.Show(error.Message);
                        }
                    }
                }

                
                private void button_fileBrowser_Click(object sender, EventArgs e)
                {
                    openAndWrite();
                }

                public void openAndWrite()
                {
                    /* load a .txt file */
                        
                    OpenFileDialog openFileDialog1 = new OpenFileDialog();
                    openFileDialog1.InitialDirectory = "c:\";
                    openFileDialog1.Filter = "TXT files (*.txt)|*.txt";
                    openFileDialog1.FilterIndex = 0;
                    openFileDialog1.RestoreDirectory = true;
                    string selectedFile = "";

                    if (openFileDialog1.ShowDialog() == DialogResult.OK)
                    {
                        selectedFile = openFileDialog1.FileName;
                    }

                    /* parse file and store it in data[] */
                    byte[] data;

                    using (FileStream fsSource = new FileStream(selectedFile, FileMode.Open, FileAccess.Read))
                    {
                        data = new byte[fsSource.Length];
                        int numBytesToRead = (int)fsSource.Length;
                        int numBytesRead = 0;
                        while (numBytesToRead > 0)
                        {
                            // Read may return anything from 0 to numBytesToRead.
                            int n = fsSource.Read(data, numBytesRead, numBytesToRead);

                            // Break when the end of the file is reached.
                            if (n == 0)
                                break;

                            numBytesRead += n;
                            numBytesToRead -= n;
                        }
                        numBytesToRead = data.Length;
                    }

                for (int i = 0; i < BitConverter.ToInt32(blockLength, 0); i++)
                {
                    data[i] = data[offset + i];
                }

                    
                    /* write data block */
                        
                    offset = 0;

                    while (offset < data.Length)
                    {
                        byte[] dataString = new byte[6];
                        string blockMsg = "FFFFFF";

                        int start = offset;
                        offset += 6;

                        if (offset > data.Length) offset = data.Length;
                        for (int i = 0; i < 6; i++)
                        {
                            if ((start + i) < data.Length) dataString[i] = data[start + i];
                        }

                        blockMsg += BitConverter.ToString(dataString).Replace("-", string.Empty);
                        blockMsg += "*";


                        serialPort1.Write(blockMsg);
                            
                        //Wait for "OK*" before next Message...

                    }
                }
            }
        }
    }

有几种方法可以解决这个问题。我的方法是利用 TaskCompletionSource class and a lambda function. Here's a rough example, you'll need to make the method containing this code async,并可能让它 return 一个任务来一直等待它在链上。

var tcs = new TaskCompletionSource<string>();

serialPort1.DataReceived += (s, e) =>
{
   SerialPort sp = (SerialPort)s;
   string newData = sp.ReadExisting();
   tcs.SetResult(newData);
}

serialPort1.Write(Msg);
var dataFromPort = await tcs.Task;

我不在 IDE,也无法访问您的所有代码,因此如果没有更广泛的上下文,很难确切知道您可能还需要什么。但是我过去做过一些 Arduino 的工作,这就是我解决问题的方法。

EDIT :: 考虑一下,您可能(首先测试)运行 遇到问题,因为该事件没有取消订阅并且代码在循环中 运行ning。 IF 原来是个问题,你可以利用 C# 的 Local Functions 来处理事件订阅,像这样:

var tcs = new TaskCompletionSource<string>();

void localHandler(object s, SerialDataReceivedEventArgs e)
{
    serialPort1.DataReceived -= localHandler;
    SerialPort sp = (SerialPort)s;
    string newData = sp.ReadExisting();
    tcs.SetResult(newData);
};

serialPort1.DataReceived += localHandler
serialPort1.Write(Msg);
var dataFromPort = await tcs.Task;

它可能看起来有点奇怪,但它肯定更干净 (IMO) 然后需要跟踪实例变量并手动管理线程。

EDIT2 :: 在我坐在电脑前测试过,如果我正确理解您的要求,这应该可以满足您的所有需求。

这可能会带您走向正确的方向。

在这种情况下,分离从显示更新线程接收到的数据可能很有用。由于接收到的数据是一个事件,它将通过自己的线程进入,可能来自某个组件 - 在本例中为串行端口。

窗体及其上的所有显示对象,运行 在单个线程上,由窗体拥有。

您可以将计时器用作后台线程,以便在收到的内容和您想要的内容之间实现飞跃 do/display。

例如,一种方法是使用并发队列和定时器来轮询队列的状态。

    using System.Collections.Concurrent;

    private ConcurrentQueue<string> dataIn = new ConcurrentQueue<string>();
    private System.Threading.Timer timer = new System.Threading.Timer(CheckQue,null,TimeSpan.FromSeconds(1),TimeSpan.FromSeconds(1));
    private void CheckQue(object state)
    {
        while(dataIn.TryDequeue(out var data))
        {
            ShowData(data);

            if (data.Contains("OK*"))
            {
                //Do Something
                //Better yet raise an event that does something
               Msg += BitConverter.ToString(dataString).Replace("-", string.Empty);
               Msg += "*";

               serialPort1.Write(Msg);
            }
        }
    }

然后修改你上面的代码,在收到数据时加入到队列中

    private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
    {
        dataIn.Enqueue(SerialPort1.ReadExisting());
    }

并且由于您使用的是 windows 表单,因此您需要确保在更新表单时不会进行 cross-threaded 操作,方法是像这样将更新编组到表单的线程中。

    public static void ShowData(string data)
    {
        if (this.InvokeRequired)
        {
            var del = new MethodInvoker(() => ShowData(data));
            try
            {
                this.Invoke(del);
                return;
            }
            catch
            { }
        }

        richTextBox_receiveMsg.Text += data;

    }

根据您修改后的问题进行编辑: 要修改发送文件块,您可以添加另一个 Que

private ConcurrentQueue<string> dataOut = new ConcurrentQueue<string>();

然后当你读取文件时,做你需要的任何处理并将最终输出放入队列并通过发送第一个块完成。

        public void openAndWrite()
        {
        /* load a .txt file */

        .
        .
        .

           while (offset < data.Length)
           {
            .
            .
            
            blockMsg += BitConverter.ToString(dataString).Replace("-", string.Empty);
            blockMsg += "*";

            dataOut.Enqueue(blockMsg);
           }
           
          SendBlock();
        }

SendBlock 看起来像这样

    private void SendBlock()
    {
        if(dataOut.TryDequeue(out var data))
        {
            serialPort1.Write(data);
        }
    }

然后只需修改计时器处理程序以根据需要发送块

    private void CheckQue(object state)
    {
        while (dataIn.TryDequeue(out var data))
        {
            ShowData(data);

            if (data.Contains("OK*"))
            {
                SendBlock();
            }
        }
    }