_C#_串口助手_字符串拼接缺失问题(未知原理)

最近使用WPF开发串口助手时,遇到一个很奇怪的问题,无论是主线程、异步还是多线程,当串口接收速度达到0.016s一次以上,就会发生字符串缺失问题并且很卡。而0.016s就一切如常,仿佛0.015s与0.016s是天堑之隔。
同一份代码放在多线程(主线程或异步)环境中:

由于是在测试,尝试过用索引下标的方式遍历数组

其中allData的定义为

cs 复制代码
List<string> allData = new List<string>();
cs 复制代码
                if (allData.Count > 0)
                {
                    RecvBuffer.Clear();
                    StringBuilder sb = new StringBuilder();
                    sb.Append(DateTime.Now);
                    sb.Append(">>");
                    Application.Current.Dispatcher.Invoke(() =>
                    {
                        for (int i = 0; i < allData.Count; i++)
                        {
                            RecvBuffer.Append(sb);
                            RecvBuffer.Append(allData[i]);
                        }
                        TextEditor?.AppendText(RecvBuffer.ToString());
                    });
                }

0.016s一次接收:

日期可以正常打印下来,而且打印的速度很快,整体显示很流畅

0.015s一次接收:

虽然串口发送脚本的速度只提高了一点点,但是日期时间几乎不打印,只有零星几个有,而且整个显示非常卡


按理来说这两个速度差别应该不大,不至于产生这种差别,后来又相继测试了多线程、异步、主线程的全速打印(直接放进while死循环里,拟定的固定字符串) ,但都可以正常显示日期时间。

于是只能怀疑到字符串本身的问题了,由于此前本人是学C/C++的,对C#的字符串原理还不甚了解,大概知道C#的字符串中的"\0"并非作为字符串的结尾。但此时的打印结果却让我对C#的字符串拼接原理产生了深深的困惑。

根据前面的全速打印等测试情况,如果把硬编码的字符串或者数字塞进StringBuilder是没有问题的,所以想知道是不是字符串本身的问题,于是我把allData里的数据先转为数字,再直接拼接字符串:

cs 复制代码
                if (allData.Count > 0)
                {
                    RecvBuffer.Clear();
                    StringBuilder sb = new StringBuilder();
                    sb.Append(DateTime.Now);
                    sb.Append(">>");
                    Application.Current.Dispatcher.Invoke(() =>
                    {
                        
                        for (int i = 0; i < allData.Count; i++)
                        {
                            RecvBuffer.Append(sb);
                            var value = float.TryParse(allData[i], out var val);
                            RecvBuffer.Append($"{val}\n");
                        }
                        TextEditor?.AppendText(RecvBuffer.ToString());
                    });
                }

虽然变得极其卡,但可以正常显示日期时间了

然而把字符串中的'\0'去掉之后并没有达到我的预期,依旧会很卡,且没有日期时间
查了许多资料,也问了AI很长时间,前前后后花了好几天,最终未能揭开其中的原理

不知道是队列的原因还是SerialPort中Read与ReadExisting的区别,或是字符串与字节数组的原因,亦或是多次接收的数据变为合并为一个数据,总之经过了下面变换,可用了,而且打印效率肉眼可见地提升,不再卡顿了。

cs 复制代码
// 原代码:

        // 在OnDataReceived中
        private void OnDataReceived(object? sender, SerialDataReceivedEventArgs e)
        {
            if (sender is SerialPort sp)
            {
                //ReceiveBytes += sp.BytesToRead;
                //++ReceiveNum;

                string data = sp.ReadExisting();
                _receiveQueue.Enqueue(data); // 将接收到的数据放入接收队列
                //_queueSemaphore.Release(); // 信号量释放,通知有新数据
                _resetEvent.Set(); // 通知读取任务开始处理数据
            }
        }

        // 读取任务
        private void ReadTask()
        {
            while (_isRunning)
            {
                _resetEvent.WaitOne(); // 等待数据接收事件
                if (!_isRunning)
                    break;

                List<string> allData = new List<string>();
                while (true)
                {
                    if (!(_receiveQueue.TryDequeue(out var data)))
                        break;

                    allData.Add(data);

                    if (allData.Count > 10240)
                        break;

                    Thread.Sleep(10); // 适当休眠,避免过度占用CPU
                }

                if (allData.Count > 0)
                {
                    Application.Current.Dispatcher.Invoke(() =>
                    {
                        foreach (var item in allData)
                        {
                            TextEditor?.AppendText($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]>> {item}");
                        }
                    });
                }
            }
        }
cs 复制代码
// 改进后:
        private void OnDataReceived(object? sender, SerialDataReceivedEventArgs e)
        {
            if (sender is SerialPort sp)
            {
                //ReceiveBytes += sp.BytesToRead;
                //++ReceiveNum;

                //string data = sp.ReadExisting();
                //_receiveQueue.Enqueue(data); // 将接收到的数据放入接收队列
                //_queueSemaphore.Release(); // 信号量释放,通知有新数据
                _resetEvent.Set(); // 通知读取任务开始处理数据
            }
        }


        public async void ReadTask()
        {
            const int MaxDataSize = 512;
            const int BatchSize = 1024; // 批量读取大小

            var allData = new List<byte>();
            while (true)
            {
                _resetEvent.WaitOne(); // 等待数据接收事件

                try
                {
                    while (allData.Count < MaxDataSize && _serialPort.BytesToRead > 0)
                    {
                        var bytesToRead = Math.Min(_serialPort.BytesToRead, BatchSize);
                        var buffer = new byte[bytesToRead];
                        _serialPort.Read(buffer, 0, bytesToRead);
                        allData.AddRange(buffer);

                        if (allData.Count >= MaxDataSize) break;

                        await Task.Delay(10); // 防止CPU占用过高,使用异步延迟
                    }
                }
                catch
                {
                    // 处理异常
                    allData.Clear();
                    continue;
                }

                if (allData.Count > 0)
                {
                    await LogMessageAsync(_serialPort.Encoding.GetString(allData.ToArray()), DateTime.Now.ToString());
                    allData.Clear(); // 清空数据列表
                }
            }
        }

        private async Task LogMessageAsync(string inputData, string timestamp)
        {
            // 使用StringBuilder提高效率
            var sb = new StringBuilder();
            sb.Append($"[{timestamp}] ");

            // 使用正则表达式替换换行符,并添加时间戳
            var logEntry = Regex.Replace(inputData, @"(\r\n|\r|\n)", $"\r\n{timestamp}>> ");
            sb.Append(logEntry);

            // 异步更新UI
            await Application.Current.Dispatcher.InvokeAsync(() =>
            {
                TextEditor?.AppendText(sb.ToString());
            });
        }

出于时间考量不再深究,于此留下痕迹。
2024.11.28:

**淦!**用这个可以流畅打印的函数试了之后发现一个不对劲的地方,那就是接收字节数过大,测了一下,发现Python脚本发送数据的真实速度是16359.5次/s,即0.00006s发送一次数据

千算万算没想到是python脚本的问题,不过这倒也解决了"0.015s"与0.016s一次差距悬殊的原因,并且也找到了能高性能显示的方案。真淦

下面是罪魁祸首

python 复制代码
import asyncio
import math
import random

import serial
import serial_asyncio


class VirtualSerialServer(asyncio.Protocol):
    def __init__(self, baudrate=115200, interval=0.1, frequency=1.0, amplitude=1.0, offset=0.0):
        self.transport = None
        self.baudrate = baudrate
        self.interval = interval
        self.frequency = frequency  # 正弦波频率(Hz)
        self.amplitude = amplitude  # 正弦波振幅
        self.offset = offset  # 正弦波偏移量
        self.time = 0.0  # 当前时间

    def connection_made(self, transport):
        self.transport = transport
        print(f'Virtual Serial Port connected with baudrate {self.baudrate}')
        asyncio.create_task(self.send_data_periodically())

    # def data_received(self, data):
    #     print(f'Received: {data.decode()}')
    #     # 回显接收到的数据
    #     self.transport.write(data)

    async def send_data_periodically(self):
        value = 0
        while True:
            # 生成100以内的随机整数
            # value = random.randint(0, 100)
            # 将整数转换为字符串格式
            value += 1
            data_to_send = f'{value}\n'.encode('utf-8')
            self.transport.write(data_to_send)
            # 打印数据
            # print(f'Sent: {value}')
            await asyncio.sleep(self.interval)


async def main():
    # 固定波特率和时间间隔
    baudrate = 115200
    interval = 0.015  # 发送间隔(秒)

    # 使用实际的虚拟串口号,例如 'COM15'
    port = 'COM15'

    loop = asyncio.get_running_loop()
    try:
        server = await serial_asyncio.create_serial_connection(
            loop, lambda: VirtualSerialServer(baudrate, interval), url=port, baudrate=baudrate)
    except serial.serialutil.SerialException as e:
        print(f"Error opening serial port {port}: {e}")
        return

    await asyncio.Event().wait()  # 保持程序运行


if __name__ == '__main__':
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print('程序已终止。')

经过一些测试,发现在异步情况下,Python的定时执行容易出现很奇葩的问题,在我的例子中0.015s就是那个极限,0.016s及其以上却是正常的。

下面是经过改正且可以正常使用的代码

python 复制代码
import time
import serial


class VirtualSerialServer:
    def __init__(self, interval=0.015):
        self.interval = interval
        self.last_send_time = None  # 记录上次发送时间
        self.value = 0  # 初始化发送的数据值

    def start(self, port, baudrate):
        try:
            with serial.Serial(port, baudrate) as ser:
                print(f'Virtual Serial Port connected with interval {self.interval}')
                while True:
                    current_time = time.perf_counter()
                    if self.last_send_time is not None:
                        actual_interval = current_time - self.last_send_time
                        print(f'Sent: {self.value}, Actual interval: {actual_interval:.6f} seconds')  # 打印实际间隔

                    # 发送数据
                    self.value += 1
                    data_to_send = f'{self.value}\n'.encode('utf-8')
                    ser.write(data_to_send)

                    # 更新最后一次发送时间
                    self.last_send_time = current_time
                    # 等待直到下一个发送时间点
                    next_send_time = self.last_send_time + self.interval
                    while time.perf_counter() < next_send_time:
                        pass  # 空循环等待直到达到下一个发送时间点
        except serial.SerialException as e:
            print(f"Error opening serial port {port}: {e}")


if __name__ == '__main__':
    interval = 0.012  # 发送间隔(秒)
    port = 'COM15'  # 使用实际的虚拟串口号
    baudrate = 115200  # 固定波特率

    server = VirtualSerialServer(interval=interval)
    try:
        server.start(port, baudrate)
    except KeyboardInterrupt:
        print('程序已终止。')
相关推荐
慧都小妮子11 分钟前
Spire.PDF for .NET【页面设置】演示:旋放大 PDF 边距而不改变页面大小
pdf·c#·.net·spire.pdf·报表控件
△曉風殘月〆2 小时前
WPF从本地文件加载界面
wpf
△曉風殘月〆2 小时前
WPF中的VisualState(视觉状态)
wpf
码猩2 小时前
C#高速串口通讯方案winform实例
开发语言·单片机·c#
艾妮艾妮3 小时前
用函数实现模块化程序设计(七)--数组作为函数参数(排序算法)
c语言·算法·c#·排序算法
PHP代码4 小时前
C#是Unity 3D的默认语言,Unity 3D是一种领先的游戏引擎
开发语言·c++·c#
Tdm_8885 小时前
C# 线程--Thread类
java·开发语言·jvm·后端·微软·c#·asp.net
zaim16 小时前
计算机的错误计算(一百七十三)
java·c++·python·matlab·c#·php·ruby
海盗12346 小时前
WPF+LibVLC开发播放器-LibVLC在C#中的使用
开发语言·c#·wpf
suzh1qian6 小时前
Unity类银河战士恶魔城学习总结(P155 More example on audio effects更多的音效细节)
学习·游戏·unity·c#·游戏引擎