6

我有一个长度为 256 的缓冲区,它从蓝牙接收字节序列。我需要提取的实际数据包以 byte开头和结尾126。我想使用 LINQ 提取缓冲区中的最新数据包。

我现在正在做的是检查最后一个索引,126然后倒数直到我到达另一个126。也有一些陷阱,例如,两个相邻的数据包可能会导致两个字节126彼此相邻。

这是缓冲区的示例:

126 6 0 5 232 125 93 126 126 69 0
0 1 0 2 2 34 6 0 5 232 125
93 126 126 69 0 0 1 0 2 2 34
6 0 5 232 125 93 126 126 69 0 0
1 0 2 2 34 6 0 5 232 125 93
126 126 69 0 0

所以我掌握的信息是:

  • 数据包以 126 字节值开始和结束
  • 起始索引后的下一个字节的值为 69
  • 126 结束字节之前的最后 3 个字节是我知道如何计算的整个数据包的 CRC,因此在提取数据包后,我可以检查这个 CRC 以查看我是否有正确的数据包

所以最后我想要一个包含正确数据包的数组或列表。例如:

126 69  0  0   1   0   2   2   34  6   0   5   232 125 93 126

你能给我一个从缓冲区中提取这个数据包的快速解决方案吗?

这是我迄今为止尝试过的......它失败了,因为它不能真正返回我正在寻找的正确数据包:

var data = ((byte[])msg.Obj).ToList(); //data is the buffer 

byte del = 126; //delimeter or start/end byte
var lastIndex = data.LastIndexOf(del);
var startIndex = 0;
List<byte> tos = new List<byte>(); //a new list to store the result (packet)    

//try to figure out start index                            
if(data[lastIndex - 1] != del)
{
    for(int i = lastIndex; i > 0; i--)
    {
        if(data[i] == del)
        {
            startIndex = i;
        }
    }

    //add the result in another list
    for(int i = 0; i <= lastIndex - startIndex; i++)
    {
        tos.Add(data[i]);
    }

    string shit = string.Empty;

    foreach (var b in tos)
        shit += (int)b + ", ";

   //print result in  a textbox
    AddTextToLogTextView(shit + "\r\n");
}
4

5 回答 5

3

如果可以将以下两条规则应用于缓冲区,则使用 LINQ 可以在一行代码中完成:

  • 缓冲区包含至少一个由给定分隔符包围的完整包。
  • 每个数据包至少包含一个字节的数据。

这是代码:

var data = (byte[])msg.Obj;
byte delimiter = 126;

var packet = data.Reverse()
                 .SkipWhile(b => b != delimiter)
                 .SkipWhile(b => b == delimiter)
                 .TakeWhile(b => b != delimiter)
                 .Reverse();

(好吧,这不仅仅是一行,因为为了更好的可读性,我把它分成了多行。)

编辑:删除了对 Take(1) 的调用,因为它总是返回一个空序列。但是,结果不包含这种方式的分隔符。


以下是它的工作原理:

由于我们想找到最后一个数据包,我们可以反转数据:

var reversed = data.Reverse();

缓冲区可以以尚未完成的数据包结束。所以让我们跳过:

reversed = reversed.SkipWhile(b => b != delimiter);

reversed现在要么为空,要么以delimiter. 由于我们假设缓冲区总是包含至少一个完整的数据包,我们已经可以为我们的结果获取下一个字节,因为我们知道它是分隔符:

var packet = reversed.Take(1);

在序列中,我们现在可以跳过一个字节。如果我们找到的分隔符实际上是一个新数据包的开始,那么剩余的序列将以另一个分隔符开始,所以我们也必须跳过它:

reversed = reversed.Skip(1);
if (reversed.First() == delimiter)
{
    reversed.Skip(1);
}

由于我们知道一个数据包不能为空,因为它包含一个 3 字节的 CRC,我们可以这样写:

reversed = reversed.SkipWhile(b => b == delimiter);

现在实际数据如下:

packet = packet.Concat(reversed.TakeWhile(b => b != delimiter));
reversed = reversed.SkipWhile(b => b != delimiter);

下一个字节是标记数据包开始的分隔符:

packet = packet.Concat(reversed.Take(1));

最后要做的是再次反转结果:

packet = packet.Reverse();

也许你想把它放到一个方法中:

public IEnumerable<byte> GetPacket(byte[] data, byte delimiter)
{
    yield return delimiter;

    foreach (byte value in data.Reverse()
                               .SkipWhile(b => b != delimiter)
                               .SkipWhile(b => b == delimiter)
                               .TakeWhile(b => b != delimiter))
    {
        yield return value;
    }

    yield return delimiter;
}

您必须在此方法的返回值上调用 Reverse。


如果性能很重要,您可以在底层阵列上使用相同的算法。这样它会快大约 20 倍:

int end = data.Length - 1;
while (data[end] != delimiter)
    end--;

while (data[end] == delimiter)
    end--;

int start = end;
while (data[start] != delimiter)
    start--;

byte[] result = new byte[end - start + 2];  // +2 to include delimiters
Array.Copy(data, start, result, 0, result.Length);
于 2013-04-12T16:31:00.030 回答
3

解决方案

我已经准备了三种可能的从输入缓冲区中获取最后一个数据包的解决方案:

使用 LINQ

public static byte[] GetLastPacketUsingLINQ(byte[] input, byte delimiter)
{
    var part = input.Reverse()
                    .SkipWhile(i => i != delimiter)
                    .SkipWhile(i => i == delimiter)
                    .TakeWhile(i => i != delimiter)
                    .Reverse();

    return (new byte[] { delimiter }).Concat(part).Concat(new byte[] { delimiter }).ToArray();
}

使用string.Split

public static byte[] GetLastPacketUsingString(byte[] input, byte delimiter)
{
    var encoding = System.Text.Encoding.GetEncoding("iso-8859-1");
    string inputString = encoding.GetString(input);
    var parts = inputString.Split(new[] { (char)delimiter }, StringSplitOptions.RemoveEmptyEntries);

    return encoding.GetBytes((char)delimiter + parts[parts.Length - 2] + (char)delimiter);
}

使用while循环和索引器

public static byte[] GetLastPacketUsingIndexers(byte[] input, byte delimiter)
{
    int end = input.Length - 1;
    while (input[end--] != delimiter) ;

    int start = end - 1;
    while (input[start--] != delimiter) ;

    var result = new byte[end - start];
    Array.Copy(input, start + 1, result, 0, result.Length);
    return result;
}

表现

我还进行了一些非常简单的性能测试。结果如下:

LINQ version result:
126 69 0 0 1 0 2 2 34 6 0 5 232 125 93 126

String version result:
126 69 0 0 1 0 2 2 34 6 0 5 232 125 93 126

Indexers version result:
126 69 0 0 1 0 2 2 34 6 0 5 232 125 93 126

LINQ version time: 64ms (106111 ticks)
String version time: 2ms (3422 ticks)
Indexers version time: 1ms (2359 ticks)

结论

如您所见,这里最简单的也是最好的。

您可能认为 LINQ 可以解决所有问题,但有时最好手动编写更简单的解决方案,而不是使用 LINQ 方法。

于 2013-04-14T13:28:17.293 回答
1

实际上有多种方法可以解决您的问题,最简单的想法是检测双126(0x7e),与 CRC 等其他事情无关。

这个概念的基本实现是这样的

  • 代码很简单

    var list=new List<byte[]>();
    int i=0, j=0;
    for(; i<data.Length; ++i)
        if(i>0&&0x7e==data[i]&&0x7e==data[i-1]) {
            list.Add(data.Skip(j).Take(i-j).ToArray());
            j=i;
        }
    list.Add(data.Skip(j).Take(i-j).ToArray());
    

基于我在 C# 中对 Konami Code 的旧答案,它甚至用于解决这个问题:在 c# 中记录击键时键入特殊字符时显示双字符

  • 带有序列检测器的代码

    public partial class TestClass {
        public static void TestMethod() {
            var data=(
                new[] { 
                        126, 6, 0, 5, 232, 125, 93, 126, 
                        126, 69, 0, 0, 1, 0, 2, 2, 34, 6, 0, 5, 232, 125, 93, 126, 
                        126, 69, 0, 0, 1, 0, 2, 2, 34, 6, 0, 5, 232, 125, 93, 126, 
                        126, 69, 0, 0, 1, 0, 2, 2, 34, 6, 0, 5, 232, 125, 93, 126, 
                        126, 69, 0, 0 
                    }).Select(x => (byte)x).ToArray();
    
            var list=new List<List<byte>>();
    
            foreach(var x in data) {
                if(list.Count<1||SequenceCapturer.Captured((int)x))
                    list.Add(new List<byte>());
    
                list.Last().Add(x);
            }
    
            foreach(var byteList in list)
                Debug.Print("{0}", byteList.Select(x => x.ToString("x2")).Aggregate((a, b) => a+"\x20"+b));
        }
    }
    
    public class SequenceCapturer {
        public int Count {
            private set;
            get;
        }
    
        public int[] Sequence {
            set;
            get;
        }
    
        public bool Captures(int value) {
            for(var i=Sequence.Length; i-->0; ) {
                if(Sequence[i]!=value) {
                    if(0==i)
                        Count=0;
    
                    continue;
                }
    
                if(Count!=i)
                    continue;
    
                ++Count;
                break;
            }
    
            var x=Sequence.Length==Count;
            Count=x?0:Count;
            return x;
        }
    
        public SequenceCapturer(int[] newSequence) {
            Sequence=newSequence;
        }
    
        public SequenceCapturer()
            : this(new[] { 0x7e, 0x7e }) {
        }
    
        public static bool Captured(int value) {
            return Instance.Captures(value);
        }
    
        public static SequenceCapturer Instance=new SequenceCapturer();
    }
    

或者,如果您想在 Linq 中完整地编写它,您可能想尝试以下方法。你甚至不需要使用ListpacketArray直接给你一个字节数组。

s 旨在将let代码分成几行,否则它将是一行中的极长语句。如果您认为一条线是最好的,那么我会的。

  • 代码packetArray

    var packetArray=(
        from sig in new[] { new byte[] { 0x7e, 0x7e } }
        let find=new Func<byte[], int, IEnumerable<byte>>((x, i) => x.Skip(i).Take(sig.Length))
        let isMatch=new Func<IEnumerable<byte>, bool>(sig.SequenceEqual)
        let filtered=data.Select((x, i) => 0==i||isMatch(find(data, i-1))?i:~0)
        let indices=filtered.Where(i => ~0!=i).Concat(new[] { data.Length }).ToArray()
        from index in Enumerable.Range(1, indices.Length-1)
        let skipped=indices[index-1]
        select data.Skip(skipped).Take(indices[index]-skipped).ToArray()).ToArray();
    
  • 输出代码

    foreach(var byteArray in packetArray)
        Debug.Print("{0}", byteArray.Select(x => x.ToString("x2")).Aggregate((a, b) => a+"\x20"+b));
    

但是,即使在相同的解决方案概念中,也会有我之前提到的各种方法。我强烈建议不要涉及诸如 CRC 之类的附加条件,这可能会使事情变得更加复杂。

于 2013-04-05T13:11:39.150 回答
0

由于您正在寻找最后一个数据包,因此反转 byte[] 并查找第一个数据包要容易得多。您的两个数据包分隔符不仅仅是 126。它们是 126、69 的开头和 126、126 的结尾,除非数据包的结尾是收到的最后一个字节,这使得结尾分隔符为 126。

我建议使用与此类似的方法:

public static byte[] GetMessage(byte[] msg)
    {
        //Set delimiters
        byte delimit = 126;
        byte startDelimit = 69;

        //Reverse the msg so we can find the last packet
        List<byte> buf = msg.Reverse().ToList();

        //set indices to impossible values to check for failures
        int startIndex = -1;
        int endIndex = -1;
        //loop through the message
        for (int i = 0; i < buf.Count - 1; i++)
        {
            //find either a double 126, or 126 as the last byte (message just ended)
            if (buf[i] == delimit && (buf[i + 1] == delimit || i == 0))
            {
                if (i == 0)
                {
                    startIndex = i;
                    i++;
                }
                else
                {
                    startIndex = i + 1;
                    i += 2;
                }
                continue;
            }
            //Only process if we've found the start index
            if (startIndex != -1)
            {
                //check if the byte is 69 followed by 126
                if (buf[i] == startDelimit && buf[i + 1] == delimit)
                {
                    endIndex = i + 1;
                    break;
                }
            }
        }
        //make sure we've found a message
        if (!(startIndex == -1 || endIndex==-1))
        {
            //get the message and reverse it to be the original packet
            byte[] revRet = new byte[endIndex - startIndex];
            Array.Copy(buf.ToArray(), startIndex, revRet, 0, endIndex - startIndex);

            return revRet.Reverse().ToArray();
        }
        return new byte[1];
    }

我不完全确定副本的索引是否完全正确,但这应该是它的要点。

于 2013-04-17T15:34:02.630 回答
0

由于您可能会收到不完整的数据,因此您必须存储最后一个不完整的缓冲区。

这是示例案例,首次接收:

126,   6, 0,   5,  232, 125, 93,  126, 126, 69,  0, 
0,   1,   0,   2,   2,   34,  6  , 0 ,  5 ,  232, 125, 
93,  126, 126, 69,  0,   0,   1 ,  0,   2,   2,   34, 
6,   0,   5,   232, 125, 93,  126, 126, 69,  0,   0 ,
1,   0,  2,   2,   34,  6,   0,   5,   232, 125, 93, 
126, 126, 69,  0,   0

第二个流:

69,  0,   0 , 1,   0,  2,   2,   34,  6,   0, 126

和代码:

    List<byte> lastBuf = new List<byte>();

    List<byte[]> Extract(byte[] data, byte delim)
    {
        List<byte[]> result = new List<byte[]>();

        for (int i = 0; i < data.Length; i++)
        {
            if (lastBuf.Count > 0)
            {
                if(data[i] == delim)
                {
                    result.Add(lastBuf.ToArray());
                    lastBuf.Clear();
                }
                else
                {
                    lastBuf.Add(data[i]);
                }
            }
            else 
            { 
                if(data[i] != 126)
                {
                    lastBuf.Add(data[i]);
                }
            }
        }

        return result;
    }

结果 :数据结果

于 2013-04-19T02:13:21.077 回答