C# 如何使用反应式扩展解析来自串行端口的字符流?

C# 如何使用反应式扩展解析来自串行端口的字符流?,c#,serial-port,system.reactive,reactive-programming,C#,Serial Port,System.reactive,Reactive Programming,我需要解析来自测试仪器的串行数据流,这似乎是一个很好的反应式扩展应用程序 协议非常简单……每个“数据包”都是一个字母,后跟数字。每个数据包类型的数字位数是固定的,但在不同的数据包类型之间可能有所不同。e、 g …A1234B123456C12 我试图把它分解成一个可观察的字符串,例如 “A1234”“B123456”“C12” 本以为这很简单,但看不到实现这一点的明显方法(我对LINQ有一些经验,但我对Rx是新手) 这是我到目前为止的代码,它从串口的SerialDataReceived事件生成可

我需要解析来自测试仪器的串行数据流,这似乎是一个很好的反应式扩展应用程序

协议非常简单……每个“数据包”都是一个字母,后跟数字。每个数据包类型的数字位数是固定的,但在不同的数据包类型之间可能有所不同。e、 g

…A1234B123456C12

我试图把它分解成一个可观察的字符串,例如

“A1234”“B123456”“C12”

本以为这很简单,但看不到实现这一点的明显方法(我对LINQ有一些经验,但我对Rx是新手)

这是我到目前为止的代码,它从串口的SerialDataReceived事件生成可观察的字符

        var serialData = Observable
                            .FromEventPattern<SerialDataReceivedEventArgs>(SerialPortMain, "DataReceived")
                            .SelectMany(_ => 
                            {
                                int dataLength = SerialPortMain.BytesToRead;
                                byte[] data = new byte[dataLength];
                                int nbrDataRead = SerialPortMain.Read(data, 0, dataLength);

                                if (nbrDataRead == 0)
                                    return  new char[0]; 

                                 var chars = Encoding.ASCII.GetChars(data);

                                 return chars; 
                            });
var serialData=可观察
.FromEventPattern(SerialPortMain,“DataReceived”)
.SelectMany(=>
{
int dataLength=SerialPortMain.BytesToRead;
字节[]数据=新字节[dataLength];
int-nbrDataRead=SerialPortMain.Read(数据,0,数据长度);
如果(nbrDataRead==0)
返回新字符[0];
var chars=Encoding.ASCII.GetChars(数据);
返回字符;
});

如何将serialData转换为字符串的可观察值,其中每个字符串都是一个数据包?

令人惊讶地精细!我用两种方法解决了这个问题:

// helper method to get the packet length
public int GetPacketLength(char c)
{
    switch(c)
    {
        case 'A':
            return 5;
        case 'B':
            return 6;
        case 'C':
            return 7;
        default:
            throw new Exception("Unknown packet code");
    }
}
然后我们可以这样做:

// chars is a test IObservable<char> 
string[] messages = { "A1234", "B12345", "C123456" };
var serialPort = Enumerable.Range(1, 10).ToObservable();
var chars = serialPort.SelectMany((_, i) => messages[i % 3]);

var packets = chars.Scan(
    Tuple.Create(string.Empty, -1),
    (acc, c) =>
        Char.IsLetter(c)
            ? Tuple.Create(c.ToString(), GetPacketLength(c) - 1)
            : Tuple.Create(acc.Item1 + c, acc.Item2 - 1))
    .Where(acc => acc.Item2 == 0)
    .Select(acc => acc.Item1)
    .Subscribe(Console.WriteLine);
它可以这样使用:

// chars is a test IObservable<char> 
string[] messages = { "A1234", "B12345", "C123456" };
var serialPort = Enumerable.Range(1, 10).ToObservable();
var chars = serialPort.SelectMany((_, i) => messages[i % 3]);

var packets = chars.GetPackets().Subscribe(Console.WriteLine);
//chars是一个可观察的测试
字符串[]消息={“A1234”、“B12345”、“C123456”};
var serialPort=Enumerable.Range(1,10).ToObservable();
var chars=serialPort.SelectMany((uu,i)=>messages[i%3]);
var packets=chars.GetPackets().Subscribe(Console.WriteLine);

这里有一个略短的方法,与使用类似助手方法的方法风格相同:

public static bool IsCompletePacket(string s)
{
    switch (s[0])
    {
        case 'A':
            return s.Length == 5;
        case 'B':
            return s.Length == 6;
        case 'C':
            return s.Length == 7;
        default:
            throw new ArgumentException("Packet must begin with a letter");
    }
}
代码是:

var packets = chars
    .Scan(string.Empty, (prev, cur) => char.IsLetter(cur) ? cur.ToString() : prev + cur)
    .Where(IsCompletePacket);
扫描部分构建以字母结尾的字符串,例如:

A
A1
A12
A123
...

然后,
,其中
只选择长度正确的。本质上,它只是从James'中删除元组,并使用字符串长度。

以下是我对这个问题的看法

static IObservable<string> Packets(IObservable<char> source)
{
    return Observable.Create<string>(observer =>
    {
        var packet = new List<char>();
        Action emitPacket = () =>
        {
            if (packet.Count > 0)
            {
                observer.OnNext(new string(packet.ToArray()));
                packet.Clear();
            }
        };
        return source.Subscribe(
            c =>
            {
                if (char.IsLetter(c))
                {
                    emitPacket();
                }
                packet.Add(c);
            },
            observer.OnError,
            () =>
            {
                emitPacket();
                observer.OnCompleted();
            });
    });
}
数据包的实现就是:

static IObservable<string> Packets(IObservable<char> source)
{
    return source
        .GroupSequential(char.IsLetter)
        .Select(x => new string(x.ToArray()));
}
静态IObservable数据包(IObservable源)
{
返回源
.GroupSequential(char.isleter)
.Select(x=>newstring(x.ToArray());
}

谢谢,詹姆斯,这似乎起作用了。我同意你的第二种方法。虽然有点“unRx-ey”,但我同意它更容易理解。是的,这是我书中的赢家。我将IsCompletePacket中的默认大小写更改为返回false,因此现在它只忽略代码不支持的任何数据包。我喜欢这样的事实,即它适用于任何数据包长度。虽然-Rx对我来说是一种非常不同的思维方式,但在遵循逻辑时遇到了一些问题。@TomBushell“由内而外”的方法有助于理解它。发生的事情是,当有人订阅
数据包(源)
,实际上只是订阅
。1.每当
发出
字符c
时,我们检查当前数据包是否完成:如果完成,我们发出它并启动一个新数据包。我们总是将当前字符添加到当前数据包中。2.当
源发出错误时,我们只是转发它。3.当
源代码
完成时,我们确保发出最后一个数据包(如果有)。
static IObservable<IList<T>> GroupSequential<T>(
    this IObservable<T> source, Predicate<T> isFirst)
{
    return Observable.Create<T>(observer =>
    {
        var group = new List<T>();
        Action emitGroup = () =>
        {
            if (group.Count > 0)
            {
                observer.OnNext(group.ToList());
                group.Clear();
            }
        };
        return source.Subscribe(
            item =>
            {
                if (isFirst(item))
                {
                    emitGroup();
                }
                group.Add(item);
            },
            observer.OnError,
            () =>
            {
                emitGroup();
                observer.OnCompleted();
            });
    });
}
static IObservable<string> Packets(IObservable<char> source)
{
    return source
        .GroupSequential(char.IsLetter)
        .Select(x => new string(x.ToArray()));
}