RS232上的消息原子发送和接收

5
我有一个奇怪的问题。
需要使用自定义协议与串行设备通信,协议如下,我使用标准ASCII简写。
1.程序发送SOH以开始传输 2.程序发送STX + 数据 + ETX数据包(我实际上一次发送SOH和此数据包) 3.设备发送ACK以确认已接收数据包 4.现在我的程序发送EOT以表示传输结束
如果消息是需要回答的请求,则设备将发送数据。由于SerialPort.DataReceived的工作方式,我必须手动将接收到的消息拼接起来。步骤如下:
1. 设备发送SOH+ STX+数据 2. 设备发送其他字节,以ETX结尾 3. 我的程序已完全接收数据包后发送ACK 4. 设备发送EOT
这基本上是它们之间的通信方式,不考虑丢失的数据包和NAKs等情况,但我不想让程序在我正在重新拼接接收到的消息时发送新消息。
我尝试使用Monitor.Enter()和Monitor.Exit(),但接收事件在不同的线程上调用,所以行不通。
我还尝试使用Semaphore,只有一个资源,开始发送或接收时调用semaphore.WaitOne(),在发送EOT和从设备接收EOT后调用semaphore.Release()。这也不太好用。
有更好的方法吗?

当你正在将先前的数据粘合在一起时,它如何接收更多的数据?最后一个数据包需要很长时间才能添加吗? - Ry-
如果您想等待此事务完成,那么使用DataReceived事件就没有意义了。直接调用Read()方法即可。不要忘记使用ReadTimeout。 - Hans Passant
@minitech,问题不在于我在粘贴时接收到更多数据,而是有时候当它正在接收数据包之间时,可能会通过同一条线路发出发送请求。这就是我想要避免的。 - Davio
3个回答

1
我会使用一个锁:

http://msdn.microsoft.com/en-gb/library/c5kehkcz(v=vs.71).aspx

你可以设置一个名为serialLock的全局对象,并使用它来确保在粘合片段时,任何发送线程都必须等待:
在接收方:
lock(serialLock)
{
//Glue data
}

在所有发送中:
lock(serialLock)
{
// send data
}

这将解决任何与不同线程等相关的问题。
我还会确保在释放锁之前,在粘合数据部分接收到完整的消息。也许为了使这一点清楚,您需要在DataReceived事件中更新线程安全的数据结构,并且在粘合数据部分中,您需要保持在此部分检查数据结构是否存在完整的消息,然后才能释放锁定。

我曾经使用过那个,但它基本上只是 Monitor.Enter 和 Monitor.Exit 的语法糖,正如我所指出的那样,这并不起作用。这是因为不同的线程试图获取和释放锁。数据是在不同的线程上接收的,这就是 DataReceived 的工作原理。 - Davio
问题在于发送完整消息分散在“发送、接收ACK、发送EOT”之间,因此我在发送和接收之间反复跳转。 - Davio
1
你不能在DataReceived事件中放置锁,你需要另一个线程将消息组合在一起。这不是一个简单的修复,你需要等待直到收到完整的消息才能发送下一条消息,这意味着你需要正确地管理锁定。 - TheKingDave

1

当我编写与串口上的GSM Modem通信的代码时,遇到了类似的问题。

通过利用System.Threading命名空间中的AutoResetEvent类,我能够开发出一个相当不错的解决方案。基本上,我让我的发送方法等待ACK信号。

这是一个框架代码。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO.Ports;

namespace Sample
{
    public class SocketWrapper
    {
        #region Static Variables

        private static AutoResetEvent _SendWaitHandle = new AutoResetEvent(false);

        #endregion

        #region Member Variables

        private object _SendLockToken = new object();

        #endregion

        #region Public Methods

        public void Write(byte[] data)
        {
            Monitor.Enter(_SendLockToken);
            try
            {
                // Reset Handle
                _SendWaitHandle.Reset();

                // Send Data
                // Your Logic

                // Wait for ACK
                if (_SendWaitHandle.WaitOne(1000)) // Will wait for 1000 miliseconds
                {
                    // ACK Received
                    // Send EOT
                }
                else
                {
                    // Timeout Occurred
                    // Your Logic To Handle Timeout
                }
            }
            catch (Exception)
            {

                throw;
            }
            finally
            {
                Monitor.Exit(_SendLockToken);
            }
        }
        #endregion

        #region Private Methods

        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            // When ACK is received call SET
            _SendWaitHandle.Set();

        }
        #endregion
    }
}

注意: 在DataReceived方法中,确保不要直接或间接调用Write方法,否则可能会导致死锁。始终使用BackgroundWorker或TPL在不同的线程上开始处理接收到的数据。

好的,这有助于发送消息,但我需要另一个技巧来接收它们。因为接收是分部分到达的,我不能只做lock(myLock) { doStuff(); } 我需要一些方法在多次DataReceived()操作中保持该锁。 - Davio
我还需要确保在接收时不会发送。 - Davio

1
我会定义一个 'SerialTxn' 类,其中包含请求数据、回复数据、编码/解码协议的状态机、超时、异常/错误消息、OnCompletion(SerialTxn thisTxn) 事件/委托以及其他完整事务所需的成员。然后我会创建一个 "SerialTxn" 实例,并加载请求数据等信息,将其排队(使用 BlockingCollection)到负责串口处理的一个线程中。该线程获取该实例,调用其方法来传输和接收数据,并在完成时调用 OnCompletion 事件/委托。这允许同步和异步交易-OnCompletion 事件或者可以信号一个事件(可能是 AutoResetEvent),等待的来源线程,或者它可以将回复排队返回给某个队列,当源线程希望时就可以处理它。
由于只有一个线程处理端口,因此没有任何重叠的tx/rx操作的机会。事务要么完全成功,要么将适当的异常或错误消息加载到 SerialTxn 中。如果发生错误,或者需要将数据发送到日志记录器、GUI 或其他地方,则所有相关数据都在一个对象中的一个位置,准备好被排队、BeginInvoked 或其他操作。我不喜欢在长时间操作过程中锁定...

“由于只有一个线程处理端口…” -- 完全同意,为每个资源使用专用线程可以消除争用和锁定的需求。这是使协作(而不是竞争/抢占式)系统高效工作的简单技巧。 - sawdust

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接