_FYAW智能显示控制仪表的简单使用_串口通信

一、简介

该仪表可以实时显示位移传感器的测量值,并可设定阈值等。先谈谈简单的使用方法,通过说明书,我们可以知道长按SET键可以进入参数选择状态,按"↑""↓"可以选择该组参数的上一个或者下一个参数。

从参数一览中可以看到有不同组的参数,当我们第一次进入参数选择状态时会进入第一组参数,可以设置不同的阈值。只不过由于是数码管,显示字母时会用一些比较奇怪的表达,比如"5"其实就是"S",可以通过对照参数表,获取不同字母的显示。

如果想进入其他组参数,可以在第一组参数中,通过"↑"或"↓"找到最后一个oA,然后按"←"开始设置参数,当把4位数码管都设为1,即输入密码1111后,再按下SET确定,就可以解锁密码了。此时可以通过长按SET键切换到其他组参数。

更多功能可自行查看数据手册,不过要注意的是说明书中的参数并非全部与实际仪器一一对应,实际仪器有时会缺少一两个参数。

二、串口使用

串口使用的是RS485电平或者RS232,千万要注意的就是A、B的接线,不要接错。这个一般需要按照说明书的来,说明书上说"7"对应的是"B","8"对应的是"A",如果仪器上贴着的标签是相反的,那么可以先按照说明书上的接法,如果不行再按照仪器上的,不要忘记接地

接下来说说具体的串口通信,仪器默认波特率是9600,通信协议是Modbus-RTU。这里推荐使用Modbus-RTU协议。

这个通信过程并非是仪表主动不断地发送测量数据给上位机,而是需要你先发送相应命令给仪器,然后接收仪器数据。使用过程中倒是发现一些与说明书不同吻合的地方,比如读取测量值这一步,按理来说应答应如下,但实际过程中接收的是"01 04 04 42 47 3F 3F 3F 28",即多了一个04

不过当我们需要连续读取时就会发现单次发送实在是麻烦,现在可以有下面几种方法连续发下

1,使用llcom

这个串口助手可以写lua脚本,以实现自动发送数据,并读取数据保存

chenxuuu/llcom: 🛠功能强大的串口工具。支持Lua自动化处理、串口调试、WinUSB、串口曲线、TCP测试、MQTT测试、编码转换、乱码恢复等功能 (github.com)

下面是可以发送命令并把读取数据保存起来的lua脚本。只不过lua脚本很难运行什么GUI,自然就无法显示图表

Lua 复制代码
-- 发送数据中间间隔时间(单位ms)
local sendDelay = 100

-- 生成16进制数据"01 04 0000 0002 71CB"
local usartData = "01040000000271CB"
usartData=usartData:fromHex()

-- 获取当前日期和时间
local function get_current_datetime()
    local datetime = os.date("%Y%m%d_%H%M%S")
    return datetime
end

-- 生成带有日期和时间的文件名
local function generate_filename()
    local datetime = get_current_datetime()
    return "D:/Script/python/expr_com/data_log/log_" .. datetime .. ".txt"
end

-- 打开文件,如果文件不存在则创建,如果存在则覆盖
local filePath = generate_filename()
local file, err = io.open(filePath, "w")
if not file then
    -- 如果文件打开失败,输出错误信息
    print("无法打开文件: " .. err)
else
    print("文件成功打开: " .. filePath)
end

local value_str = ""

-- 发送数据的函数
apiSetCb("uart", function(data)
    -- 写入数据
    value_str = data:toHex():sub(7, 14)
    file:write(value_str .. "\n")  -- 添加换行符以便区分不同数据
    print(value_str)
end)

-- 循环发送任务
sys.taskInit(function()
    while true do
        -- 发送数据
        apiSendUartData(usartData)
        sys.wait(sendDelay)
    end
end)

-- 确保在脚本结束时关闭文件
--[[
atexit(function()
    if file then
        file:close()
        print("文件已关闭")
    end
end)
]]

2,使用Python脚本

使用Python脚本直接打开串口,然后发送命令并读取数据,需要注意的是下面脚本里指定了一个串口,你需要打开设备管理器来找到实际串口并修改脚本里的串口为实际串口号。同时注意波特率设置。

由于仪器发送的其实是浮点数据的实际表达,所以下面脚本就自动做了这个转换

可惜的是,使用Python后,无法很好地实时更新数据到图表中,下面也就没有添加这个功能。

python 复制代码
import serial
import struct
import time
import os
from datetime import datetime

# 配置串口
ser = serial.Serial(
    port='COM7',  # 根据实际情况修改端口号
    baudrate=115200,  # 波特率
    timeout=1  # 超时设置
)

# 要发送的16进制数据
send_data = bytes.fromhex('01040000000271CB')

# 确保 data_log 目录存在
log_dir = 'data_log'
os.makedirs(log_dir, exist_ok=True)

# 获取当前日期和时间
current_time = datetime.now().strftime('%Y%m%d_%H%M%S')
log_file_path = os.path.join(log_dir, f"log_{current_time}.csv")

# 打开日志文件
with open(log_file_path, 'w') as log_file:
    log_file.write("Timestamp,Value\n")  # 写入表头

    try:
        while True:
            # 发送16进制数据
            ser.write(send_data)
            # 从串口读取9字节的数据,并提取中间的5-8位16进制数据
            data = ser.read(9)[3:7]

            # 将16进制数据转换为32位浮点数
            try:
                float_data = struct.unpack('>f', data)[0]  # '>f' 表示大端模式
                print(f"data: {float_data}")  # 打印浮点数

                # 获取当前时间戳
                timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

                # 写入日志文件
                log_file.write(f"{timestamp},{float_data}\n")
                log_file.flush()  # 立即写入文件

            except struct.error as e:
                print(f"Error unpacking data: {e}")

            # 每100毫秒(即每秒10次)更新一次
            time.sleep(0.1)

    except KeyboardInterrupt:
        print("Program terminated by user")

    finally:
        # 关闭串口
        ser.close()

CRC校验计算

补充一点,为了可以使用其他命令,需要计算16位CRC校验值。比如下面可以数据手册上的读取测量值命令的CRC校验值"01 04 0000 0002 71CB",其中71CB是16位校验值,在下面脚本中输入前面命令"01 04 0000 0002"即可

python 复制代码
import crcmod


def calculate_crc16(data):
    # 创建一个CRC16校验对象
    crc16_func = crcmod.mkCrcFun(0x18005, initCrc=0xFFFF, rev=True, xorOut=0x0000)

    # 将16进制字符串转换为字节
    byte_data = bytes.fromhex(data)

    # 计算CRC16校验码
    crc_value = crc16_func(byte_data)

    # 交换高低字节
    crc_value = ((crc_value & 0xFF) << 8) | ((crc_value & 0xFF00) >> 8)

    return crc_value


# 输入的16进制内容
hex_data = "01 04 0000 0002"

# 去除空格并计算CRC16
hex_data_no_spaces = hex_data.replace(" ", "")
crc16_result = calculate_crc16(hex_data_no_spaces)

# 打印结果
print(f"CRC16:{crc16_result:04X}")

8位16进制字符串转为32位浮点数据

python 复制代码
def hex_to_float(hex_str):
    # 将 16 进制字符串转换为 32 位无符号整数
    uint_value = int(hex_str, 16)
    # 将 32 位无符号整数转换为浮点数
    float_value = struct.unpack('!f', struct.pack('!I', uint_value))[0]
    return float_value

3,使用WPF

就功能而言,这个方法我是最满意的,可以自己定制化写一个专用的串口助手。不过它们各有缺陷,llcom虽然可以使用lua脚本做很多自动化处理,但无法显示图表。python虽然可以显示图表,但实时更新的效果并不好。使用WPF,虽然可以实现很多功能,但需要搭建Visual Studio环境,并且需要写不少代码。

由于时间限制,目前我只实现了简单的定时发送,转为浮点数据的功能。后续,我会添加显示图表的功能,记录日志的功能还没有做好,使用会出问题。并且我暂时并不打算在这上面花太多时间,所以没怎么考虑界面设计。

因此,界面很简单,使用起来也很简单。

XAML文件

XML 复制代码
<Window x:Class="expr_com.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="expr_com" Height="700" Width="1000">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <!-- 可用串口 -->
            <RowDefinition Height="Auto" />
            <!-- 波特率 -->
            <RowDefinition Height="Auto" />
            <!-- 打开串口按钮 -->
            <RowDefinition Height="Auto" />
            <!-- 发送数据 -->
            <RowDefinition Height="Auto" />
            <!-- 16进制复选框 -->
            <RowDefinition Height="Auto" />
            <!-- 16进制显示复选框 -->
            <RowDefinition Height="Auto" />
            <!-- 单次发送和定时循环发送按钮 -->
            <RowDefinition Height="Auto" />
            <!-- 周期(秒) -->
            <RowDefinition Height="Auto" />
            <!-- 记录数据复选框 -->
            <RowDefinition Height="Auto" />
            <!-- 处理数据复选框及参数 -->
            <RowDefinition Height="*" />
            <!-- 接收数据框 -->
            <RowDefinition Height="*" />
            <!-- 处理数据框 -->
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="10,10,10,0">
            <Label Content="可用串口:" Width="80" />
            <ComboBox Name="cmbPorts" SelectionChanged="cmbPorts_SelectionChanged" Width="150" />
        </StackPanel>

        <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="10,0,10,0">
            <Label Content="波特率:" Width="80" />
            <TextBox Name="txtBaudRate" Text="115200" Width="100" />
        </StackPanel>

        <Button Name="btnOpenPort" Content="打开串口" Click="btnOpenPort_Click" Width="120" Margin="10,0,10,10" Grid.Row="2" />

        <StackPanel Orientation="Horizontal" Grid.Row="3" Margin="10,0,10,0">
            <Label Content="发送数据:" Width="80" />
            <TextBox Name="txtSendData" Text="01040000000271CB" Width="200" />
        </StackPanel>

        <CheckBox Name="chkHex" Content="16进制" Margin="10,0,10,10" Grid.Row="4" />

        <CheckBox Name="chkHexDisplay" Content="16进制显示" Margin="10,0,10,10" Grid.Row="5" />

        <StackPanel Orientation="Horizontal" Grid.Row="6" Margin="10,0,10,0">
            <Button Name="btnSendOnce" Content="单次发送" Click="btnSendOnce_Click" Width="120" />
            <Button Name="btnStartLoopSend" Content="定时循环发送" Click="btnStartLoopSend_Click" Width="120" Margin="10,0,0,0" />
        </StackPanel>

        <StackPanel Orientation="Horizontal" Grid.Row="7" Margin="10,0,10,0">
            <Label Content="周期(秒):" Width="80" />
            <TextBox Name="txtPeriod" Text="0.1" Width="50" />
        </StackPanel>

        <CheckBox Name="chkLog" Content="记录数据" Margin="10,0,10,10" Grid.Row="8" />

        <StackPanel Orientation="Horizontal" Grid.Row="9" Margin="10,0,10,0">
            <CheckBox Name="chkProcessData" Content="处理数据" Margin="0,0,10,0" />
            <Label Content="起始位置:" Width="80" />
            <TextBox Name="txtStartIndex" Text="7" Width="50" Margin="0,0,10,0" />
            <Label Content="长度:" Width="50" />
            <TextBox Name="txtLength" Text="8" Width="50" />
        </StackPanel>

        <TextBox Name="txtReceivedData" IsReadOnly="True" Margin="10,0,10,10" Grid.Row="10" VerticalScrollBarVisibility="Auto" />

        <Grid Grid.Row="11" Margin="10,0,10,10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <TextBox Name="txtExtractedData" IsReadOnly="True" VerticalScrollBarVisibility="Auto" Grid.Column="0" />
            <TextBox Name="txtFloatData" IsReadOnly="True" VerticalScrollBarVisibility="Auto" Grid.Column="1" />
        </Grid>
    </Grid>
</Window>

CS代码

cs 复制代码
using System;
using System.IO.Ports;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.IO;
using System.Timers;

namespace expr_com
{
    public partial class MainWindow : Window
    {
        private SerialPort? _serialPort; // 声明为可为 null 的类型
        private System.Timers.Timer? _timer; // 声告为可为 null 的类型

        public MainWindow()
        {
            InitializeComponent();
            LoadAvailablePorts();
            _serialPort = null; // 初始化为 null
            _timer = null; // 初始化为 null
        }

        private void LoadAvailablePorts()
        {
            cmbPorts.ItemsSource = SerialPort.GetPortNames();
        }

        private void cmbPorts_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (cmbPorts.SelectedItem != null)
            {
                txtBaudRate.Focus();
            }
        }

        private void btnOpenPort_Click(object sender, RoutedEventArgs e)
        {
            if (cmbPorts.SelectedItem == null)
            {
                MessageBox.Show("请选择一个串口。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            if (_serialPort == null || !_serialPort.IsOpen)
            {
                try
                {
                    _serialPort = new SerialPort(cmbPorts.SelectedItem.ToString(), int.Parse(txtBaudRate.Text))
                    {
                        ReadTimeout = 1000, // 增加读取超时时间
                        WriteTimeout = 500,
                        DataBits = 8,
                        StopBits = StopBits.One,
                        Parity = Parity.None
                    };

                    // 注册 DataReceived 事件
                    _serialPort.DataReceived += SerialPort_DataReceived;

                    _serialPort.Open();
                    btnOpenPort.Content = "关闭串口";

                    // 显示成功消息
                    txtReceivedData.AppendText("← 串口已打开。" + Environment.NewLine);
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"打开串口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
                    txtReceivedData.AppendText($"← 打开串口失败: {ex.Message}" + Environment.NewLine);
                }
            }
            else
            {
                _serialPort.DataReceived -= SerialPort_DataReceived; // 取消注册 DataReceived 事件
                _serialPort.Close();
                btnOpenPort.Content = "打开串口";

                // 显示成功消息
                txtReceivedData.AppendText("← 串口已关闭。" + Environment.NewLine);
            }
        }

        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                string data = _serialPort.ReadExisting();
                if (!string.IsNullOrEmpty(data))
                {
                    Dispatcher.Invoke(() =>
                    {
                        if (chkHexDisplay.IsChecked == true)
                        {
                            // 将接收到的数据转换为16进制字符串
                            byte[] bytes = Encoding.ASCII.GetBytes(data);
                            string hexData = BitConverter.ToString(bytes).Replace("-", " ");
                            txtReceivedData.AppendText($"→ {hexData}" + Environment.NewLine);

                            if (chkProcessData.IsChecked == true)
                            {
                                ProcessData(hexData);
                            }
                        }
                        else
                        {
                            txtReceivedData.AppendText($"→ {data}" + Environment.NewLine);
                        }

                        // 立即滚动到底部
                        txtReceivedData.ScrollToEnd();
                    });
                }
            }
            catch (Exception ex)
            {
                // 处理其他异常
                Dispatcher.Invoke(() =>
                {
                    txtReceivedData.AppendText($"→ 读取错误: {ex.Message}" + Environment.NewLine);
                });
            }
        }

        private void LogData(string data)
        {
            string logFilePath = Path.Combine("data_log", $"log_{DateTime.Now:yyyyMMdd_HHmmss}.csv");
            File.AppendAllText(logFilePath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss},{data}{Environment.NewLine}");
        }

        private async void btnSendOnce_Click(object sender, RoutedEventArgs e)
        {
            await SendDataAsync(false);
        }

        private async void btnStartLoopSend_Click(object sender, RoutedEventArgs e)
        {
            if (_timer == null || !_timer.Enabled)
            {
                try
                {
                    double period = double.Parse(txtPeriod.Text);
                    _timer = new System.Timers.Timer(period * 1000); // 转换为毫秒
                    _timer.Elapsed += OnTimedEvent;
                    _timer.AutoReset = true;
                    _timer.Enabled = true;

                    btnStartLoopSend.Content = "停止循环发送";

                    // 显示启动消息
                    txtReceivedData.AppendText($"← 定时循环发送已启动,周期: {period} 秒。" + Environment.NewLine);
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"设置定时发送失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
                    txtReceivedData.AppendText($"← 设置定时发送失败: {ex.Message}" + Environment.NewLine);
                }
            }
            else
            {
                _timer.Enabled = false;
                _timer.Dispose();
                _timer = null;
                btnStartLoopSend.Content = "定时循环发送";

                // 显示停止消息
                txtReceivedData.AppendText("← 定时循环发送已停止。" + Environment.NewLine);
            }
        }

        private async void OnTimedEvent(object source, ElapsedEventArgs e)
        {
            try
            {
                await Dispatcher.InvokeAsync(async () =>
                {
                    await SendDataAsync(true);
                });
            }
            catch (Exception ex)
            {
                // 处理其他异常
                await Dispatcher.InvokeAsync(() =>
                {
                    txtReceivedData.AppendText($"← 发送错误: {ex.Message}" + Environment.NewLine);
                });
            }
        }

        private async Task SendDataAsync(bool isLoop)
        {
            try
            {
                if (_serialPort != null && _serialPort.IsOpen)
                {
                    string sendData = txtSendData.Text.Trim();
                    if (string.IsNullOrEmpty(sendData))
                    {
                        throw new InvalidOperationException("发送数据不能为空。");
                    }

                    if (chkHex.IsChecked == true)
                    {
                        byte[] hexData = Convert.FromHexString(sendData);
                        _serialPort.Write(hexData, 0, hexData.Length);
                    }
                    else
                    {
                        _serialPort.WriteLine(sendData);
                    }

                    // 显示发送数据
                    txtReceivedData.AppendText($"← {sendData}" + Environment.NewLine);

                    if (chkLog.IsChecked == true)
                    {
                        LogData(sendData);
                    }
                }
                else
                {
                    // 串口未打开,显示错误信息
                    txtReceivedData.AppendText("← 串口未打开,无法发送数据。" + Environment.NewLine);
                }
            }
            catch (Exception ex)
            {
                // 处理其他异常
                txtReceivedData.AppendText($"← 发送错误: {ex.Message}" + Environment.NewLine);
            }
        }

        private void ProcessData(string hexData)
        {
            try
            {
                // 从文本框中获取用户输入的起始位置和长度
                int startIndex = int.Parse(txtStartIndex.Text) - 1; // 减1是为了适应索引从0开始
                int length = int.Parse(txtLength.Text);

                // 去除空格
                string hexDataWithoutSpaces = hexData.Replace(" ", "");

                // 检查数据长度是否足够
                if (hexDataWithoutSpaces.Length >= startIndex + length)
                {
                    // 截取指定位置和长度的数据
                    string hexDataToProcess = hexDataWithoutSpaces.Substring(startIndex, length);

                    // 将16进制字符串转换为字节数组
                    byte[] bytes = Convert.FromHexString(hexDataToProcess);

                    // 检查字节顺序
                    if (BitConverter.IsLittleEndian)
                    {
                        // 如果系统是小端序,而数据是大端序,则需要反转字节顺序
                        Array.Reverse(bytes);
                    }

                    // 将字节数组转换为浮点数
                    float floatValue = BitConverter.ToSingle(bytes, 0);

                    // 追加结果
                    txtExtractedData.AppendText($"{hexDataToProcess}" + Environment.NewLine);
                    txtFloatData.AppendText($"{floatValue}" + Environment.NewLine);

                    // 立即滚动到底部
                    txtExtractedData.ScrollToEnd();
                    txtFloatData.ScrollToEnd();
                }
                else
                {
                    txtExtractedData.AppendText("数据长度不足,无法处理。" + Environment.NewLine);
                }
            }
            catch (FormatException ex)
            {
                txtExtractedData.AppendText($"格式错误: {ex.Message}" + Environment.NewLine);
            }
        }
    }
}

实际效果:

单次发送

循环发送

2024.11.21

添加了 LiveCharts组件,可以实现实时更新,并且图表的最大显示点数可以随时设置。同时添加了是否禁用动画选项,启用该选项可以显著提高流畅度。注意注意的是,这个图表的数据来源仅仅绑定了转换后的浮点数据,而非接收的原数据。

此时,单个文件的代码似乎有些挤了。

为了方便观察效果,创建了一个调试按钮,以100Hz的速度更新数据,禁用动画后,图像可以很流畅的滚动更新。

XAML:

XML 复制代码
<Window x:Class="expr_com.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
        Title="MainWindow" Height="800" Width="1200">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid Grid.Column="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>

            <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="10,10,10,0">
                <Label Content="可用串口:" Width="80" />
                <ComboBox Name="cmbPorts" SelectionChanged="cmbPorts_SelectionChanged" Width="106" Height="19" />
            </StackPanel>

            <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="10,0,10,0">
                <Label Content="波特率:" Width="80" />
                <TextBox Name="txtBaudRate" Text="115200" Width="100" Height="18" />
            </StackPanel>

            <Button Name="btnOpenPort" Content="打开串口" Click="btnOpenPort_Click" Width="120" Margin="10,0,10,10" Grid.Row="2" />

            <StackPanel Orientation="Horizontal" Grid.Row="3" Margin="10,0,10,0">
                <Label Content="发送数据:" Width="80" />
                <TextBox Name="txtSendData" Text="01040000000271CB" Width="200" Height="18" />
            </StackPanel>

            <CheckBox Name="chkHex" Content="16进制" Margin="10,0,10,10" Grid.Row="4" />

            <CheckBox Name="chkHexDisplay" Content="16进制显示" Margin="10,0,10,10" Grid.Row="5" />

            <StackPanel Orientation="Horizontal" Grid.Row="6" Margin="10,0,10,0">
                <Button Name="btnSendOnce" Content="单次发送" Click="btnSendOnce_Click" Width="112" />
                <Button Name="btnStartLoopSend" Content="定时循环发送" Click="btnStartLoopSend_Click" Width="105" Margin="10,0,0,0" RenderTransformOrigin="1.418,0.603" />
            </StackPanel>

            <StackPanel Orientation="Horizontal" Grid.Row="7" Margin="10,0,10,0">
                <Label Content="周期(秒):" Width="80" />
                <TextBox Name="txtPeriod" Text="0.1" Width="50" Height="18" />
            </StackPanel>

            <CheckBox Name="chkLog" Content="记录数据" Margin="10,0,10,264" Grid.Row="8" Grid.RowSpan="3" />

            <TextBox Name="txtReceivedData" IsReadOnly="True" Margin="10,45,10,52" Grid.Row="9" VerticalScrollBarVisibility="Auto" Grid.RowSpan="2" />

            <StackPanel Orientation="Horizontal" Grid.Row="10" Margin="10,254,10,0" Height="23" VerticalAlignment="Top">
                <CheckBox x:Name="chkProcessData" Content="处理数据" Height="14" RenderTransformOrigin="0.804,0.788" Width="80" />
                <Label Content="起始位置:" Width="80" />
                <TextBox x:Name="txtStartIndex" Text="7" Width="50" Margin="0,0,10,0" Height="18" />
                <Label Content="长度:" Width="50" />
                <TextBox x:Name="txtLength" Text="8" Width="50" Height="18" />
            </StackPanel>

            <Grid Grid.Row="11" Margin="10,0,10,10">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBox Name="txtExtractedData" IsReadOnly="True" VerticalScrollBarVisibility="Auto" Grid.Column="0" />
                <TextBox Name="txtFloatData" IsReadOnly="True" VerticalScrollBarVisibility="Auto" Grid.Column="1" />
            </Grid>
        </Grid>

        <Grid Grid.Column="1" RenderTransformOrigin="-0.073,0.281">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>

            <CheckBox Name="chkShowChart" Content="显示图表" Margin="10,10,10,0" Grid.Row="0" Checked="chkShowChart_Checked" Unchecked="chkShowChart_Unchecked" />

            <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="10,0,10,0">
                <Label Content="最大显示点数:" Width="100" />
                <TextBox Name="txtMaxPoints" Text="100" Width="50" Height="18"  TextChanged="txtMaxPoints_TextChanged" />
                <CheckBox Name="chkDisableAnimation" Content="禁用动画"  Checked="chkDisableAnimation_Checked" Unchecked="chkDisableAnimation_Unchecked" />
                <!--<Button Name="btnDebug" Content="测试" Click="btnDebug_Click" Width="120" />-->
            </StackPanel>

            <lvc:CartesianChart Name="chart" Grid.Row="2" Margin="10" Visibility="Collapsed">
                <lvc:CartesianChart.AxisX>
                    <lvc:Axis MinValue="1" MaxValue="100" />
                </lvc:CartesianChart.AxisX>
                <lvc:CartesianChart.AxisY>
                    <lvc:Axis LabelFormatter="{Binding Formatter}"/>
                </lvc:CartesianChart.AxisY>
                <lvc:CartesianChart.Series>
                    <lvc:LineSeries Values="{Binding FloatValues}" Title="浮点数据" />
                </lvc:CartesianChart.Series>
            </lvc:CartesianChart>

            
        </Grid>
    </Grid>
</Window>

C#:

cs 复制代码
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO.Ports;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using LiveCharts;
using LiveCharts.Definitions.Charts;
using LiveCharts.Wpf;

namespace expr_com
{
    public partial class MainWindow : Window
    {
        private SerialPort? _serialPort;
        private System.Timers.Timer? _timer;
        private ObservableCollection<double> _floatValues = new ObservableCollection<double>();
        private int _maxPoints = 100;
        
        // 判断
        private bool isChartVisible = false;

        // 定义一个可绑定的数据集合
        public ChartValues<double> FloatValues { get; set; }

        // 调试用的
        // 定义一个静态变量
        private static float debugCount = 0;
        private System.Timers.Timer? _debugTimer=null;
        private Random random = new Random();

        public MainWindow()
        {
            InitializeComponent();
            LoadAvailablePorts();
            DataContext = this;
            _serialPort = null;
            _timer = null;
            txtMaxPoints.Text = _maxPoints.ToString();


            // 初始化数据集合
            FloatValues = new ChartValues<double>();
            // 将数据集合绑定到图表
            chart.Series = new SeriesCollection
            {
                new LineSeries
                {
                    Values = FloatValues,
                    Title = "浮点数据"
                }
            };

            // 初始化图表
            chart.Visibility = chkShowChart.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
        }


        public event PropertyChangedEventHandler? PropertyChanged;

        private void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        // 加载可用端口
        private void LoadAvailablePorts()
        {
            cmbPorts.ItemsSource = SerialPort.GetPortNames();
        }

        // 选择串口
        private void cmbPorts_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (cmbPorts.SelectedItem != null)
            {
                txtBaudRate.Focus();
            }
        }

        // 打开端口
        private void btnOpenPort_Click(object sender, RoutedEventArgs e)
        {
            if (cmbPorts.SelectedItem == null)
            {
                MessageBox.Show("请选择一个串口。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            if (_serialPort == null || !_serialPort.IsOpen)
            {
                try
                {
                    _serialPort = new SerialPort(cmbPorts.SelectedItem.ToString(), int.Parse(txtBaudRate.Text))
                    {
                        ReadTimeout = 1000,
                        WriteTimeout = 500,
                        DataBits = 8,
                        StopBits = StopBits.One,
                        Parity = Parity.None
                    };

                    _serialPort.DataReceived += SerialPort_DataReceived;
                    _serialPort.Open();
                    btnOpenPort.Content = "关闭串口";
                    txtReceivedData.AppendText("← 串口已打开。" + Environment.NewLine);
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"打开串口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
                    txtReceivedData.AppendText($"← 打开串口失败: {ex.Message}" + Environment.NewLine);
                }
            }
            else
            {
                _serialPort.DataReceived -= SerialPort_DataReceived;
                _serialPort.Close();
                btnOpenPort.Content = "打开串口";
                txtReceivedData.AppendText("← 串口已关闭。" + Environment.NewLine);
            }
        }

        // 串口数据接收
        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                string data = _serialPort.ReadExisting();
                if (!string.IsNullOrEmpty(data))
                {
                    Dispatcher.Invoke(() =>
                    {
                        if (chkHexDisplay.IsChecked == true)
                        {
                            byte[] bytes = Encoding.ASCII.GetBytes(data);
                            string hexData = BitConverter.ToString(bytes).Replace("-", " ");
                            txtReceivedData.AppendText($"→ {hexData}" + Environment.NewLine);

                            if (chkProcessData.IsChecked == true)
                            {
                                ProcessData(hexData);
                            }
                        }
                        else
                        {
                            txtReceivedData.AppendText($"→ {data}" + Environment.NewLine);
                        }

                        txtReceivedData.ScrollToEnd();
                    });
                }
            }
            catch (Exception ex)
            {
                Dispatcher.Invoke(() =>
                {
                    txtReceivedData.AppendText($"→ 读取错误: {ex.Message}" + Environment.NewLine);
                });
            }
        }

        // 日志数据记录
        private void LogData(string data)
        {
            string logFilePath = System.IO.Path.Combine("data_log", $"log_{DateTime.Now:yyyyMMdd_HHmmss}.csv");
            System.IO.File.AppendAllText(logFilePath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss},{data}{Environment.NewLine}");
        }

        // 单次发送数据
        private async void btnSendOnce_Click(object sender, RoutedEventArgs e)
        {
            await SendDataAsync(false);
        }

        // 定时循环发送
        private async void btnStartLoopSend_Click(object sender, RoutedEventArgs e)
        {
            if (_timer == null || !_timer.Enabled)
            {
                try
                {
                    double period = double.Parse(txtPeriod.Text);
                    _timer = new System.Timers.Timer(period * 1000);
                    _timer.Elapsed += OnTimedEvent;
                    _timer.AutoReset = true;
                    _timer.Enabled = true;

                    btnStartLoopSend.Content = "停止循环发送";
                    txtReceivedData.AppendText($"← 定时循环发送已启动,周期: {period} 秒。" + Environment.NewLine);
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"设置定时发送失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
                    txtReceivedData.AppendText($"← 设置定时发送失败: {ex.Message}" + Environment.NewLine);
                }
            }
            else
            {
                _timer.Enabled = false;
                _timer.Dispose();
                _timer = null;
                btnStartLoopSend.Content = "定时循环发送";
                txtReceivedData.AppendText("← 定时循环发送已停止。" + Environment.NewLine);
            }
        }

        // 定时事件处理
        private async void OnTimedEvent(object? source, ElapsedEventArgs e)
        {
            try
            {
                await Dispatcher.InvokeAsync(async () =>
                {
                    await SendDataAsync(true);
                });
            }
            catch (Exception ex)
            {
                await Dispatcher.InvokeAsync(() =>
                {
                    txtReceivedData.AppendText($"← 发送错误: {ex.Message}" + Environment.NewLine);
                });
            }
        }

        // 异步发送数据
        private async Task SendDataAsync(bool isLoop)
        {
            try
            {
                if (_serialPort != null && _serialPort.IsOpen)
                {
                    string sendData = txtSendData.Text.Trim();
                    if (string.IsNullOrEmpty(sendData))
                    {
                        throw new InvalidOperationException("发送数据不能为空。");
                    }

                    if (chkHex.IsChecked == true)
                    {
                        byte[] hexData = Convert.FromHexString(sendData);
                        _serialPort.Write(hexData, 0, hexData.Length);
                    }
                    else
                    {
                        _serialPort.WriteLine(sendData);
                    }

                    txtReceivedData.AppendText($"← {sendData}" + Environment.NewLine);

                    if (chkLog.IsChecked == true)
                    {
                        LogData(sendData);
                    }
                }
                else
                {
                    txtReceivedData.AppendText("← 串口未打开,无法发送数据。" + Environment.NewLine);
                }
            }
            catch (Exception ex)
            {
                txtReceivedData.AppendText($"← 发送错误: {ex.Message}" + Environment.NewLine);
            }
        }

        // 处理数据,把16进制数据转换为浮点数
        private void ProcessData(string hexData)
        {
            try
            {
                int startIndex = int.Parse(txtStartIndex.Text) - 1;
                int length = int.Parse(txtLength.Text);

                string hexDataWithoutSpaces = hexData.Replace(" ", "");

                if (hexDataWithoutSpaces.Length >= startIndex + length)
                {
                    string hexDataToProcess = hexDataWithoutSpaces.Substring(startIndex, length);
                    byte[] bytes = Convert.FromHexString(hexDataToProcess);

                    if (BitConverter.IsLittleEndian)
                    {
                        Array.Reverse(bytes);
                    }

                    float floatValue = BitConverter.ToSingle(bytes, 0);

                    // 提取数据并显示
                    txtExtractedData.AppendText($"{hexDataToProcess}" + Environment.NewLine);
                    txtFloatData.AppendText($"{floatValue}" + Environment.NewLine);

                    txtExtractedData.ScrollToEnd();
                    txtFloatData.ScrollToEnd();

                    // 添加数据到图表
                    if (chkShowChart.IsChecked == true)
                    {
                        AddDataPoint(floatValue);
                    }
                }
                else
                {
                    txtExtractedData.AppendText("数据长度不足,无法处理。" + Environment.NewLine);
                }
            }
            catch (FormatException ex)
            {
                txtExtractedData.AppendText($"格式错误: {ex.Message}" + Environment.NewLine);
            }
        }

        // 添加数据点到画布上
        private void AddDataPoint(double value)
        {
            FloatValues.Add(value);

            // 如果数据点过多,可以移除旧的数据点以保持图表整洁
            if (FloatValues.Count > int.Parse(txtMaxPoints.Text))
            {
                FloatValues.RemoveAt(0);
            }
        }


        // 是否选择显示图表
        private void chkShowChart_Checked(object sender, RoutedEventArgs e)
        {
            chart.Visibility = Visibility.Visible;
            isChartVisible = true;
        }
        private void chkShowChart_Unchecked(object sender, RoutedEventArgs e)
        {
            chart.Visibility = Visibility.Collapsed;
            isChartVisible = false;
        }

        // 最大显示点数
        private void txtMaxPoints_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (int.TryParse(txtMaxPoints.Text, out int maxPoints))
            {
                _maxPoints = maxPoints;
                // 由于程序初始化时会主动运行一次本事件,此时只要访问chart.Visibility就会卡死
                if (isChartVisible)
                {
                    SetXTicks(_maxPoints);
                }
            }
            else
            {
                txtMaxPoints.Text = _maxPoints.ToString();
                txtReceivedData.AppendText($"Error:请勿输入非法数据\"{_maxPoints}\"" + Environment.NewLine);
            }



        }

        // 是否禁用动画
        private void chkDisableAnimation_Checked(object sender, RoutedEventArgs e)
        {
            chart.DisableAnimations = true;
        }
        private void chkDisableAnimation_Unchecked(object sender, RoutedEventArgs e)
        {
            chart.DisableAnimations = false;
        }


         测试程序
        //private void btnDebug_Click(object sender, RoutedEventArgs e)
        //{
        //    if (_debugTimer == null)
        //    {
        //        _debugTimer = new System.Timers.Timer(0.01 * 1000);
        //        _debugTimer.Elapsed += DebugTimedEvent;
        //        _debugTimer.AutoReset = true;
        //    }

        //    if (_debugTimer.Enabled)
        //    {
        //        _debugTimer.Enabled = false;
        //        _debugTimer.Dispose();
        //        _debugTimer = null;
        //    }
        //    else
        //    {
        //        _debugTimer.Enabled = true;
        //    }

        //    // 禁用动画后可以显著提高速度
        //    // 检查当前的渲染模式
        //    var currentRenderMode = RenderOptions.ProcessRenderMode;
        //    Console.WriteLine($"Current Render Mode: {currentRenderMode}");
        //    txtReceivedData.AppendText($"Error:请勿输入非法数据\"{currentRenderMode}\"" + Environment.NewLine);

        //}

        //private void DebugSendData()
        //{
        //    debugCount = random.Next(100);
        //    AddDataPoint(debugCount);
        //}

         定时事件处理
        //private async void DebugTimedEvent(object? source, ElapsedEventArgs e)
        //{
        //    try
        //    {
        //        await Dispatcher.InvokeAsync(DebugSendData);
        //    }
        //    catch (Exception ex)
        //    {
        //        await Dispatcher.InvokeAsync(() =>
        //        {
        //            txtReceivedData.AppendText($"← Debug错误: {ex.Message}" + Environment.NewLine);
        //        });
        //    }
        //}


        // 设置X轴数量
        private void SetXTicks(int tickCount)
        {
            chart.AxisX.Clear();
            chart.AxisX.Add(new Axis
            {
                MinValue = 1,
                MaxValue = tickCount,
            });
        }

        // 硬件加速暂时还没搞定
        // <CheckBox Name = "chkHardwareAcceleration" Content="启用硬件加速"  Checked="chkHardwareAcceleration_Checked" Unchecked="chkHardwareAcceleration_Unchecked" />
         硬件加速
        //private void chkHardwareAcceleration_Checked(object sender, RoutedEventArgs e)
        //{
        //    EnableHardwareAcceleration(chart);

        //    // 设置渲染模式
        //   // RenderOptions.ProcessRenderMode = RenderMode.Default;
        //}

        //private void chkHardwareAcceleration_Unchecked(object sender, RoutedEventArgs e)
        //{
        //    DisableHardwareAcceleration(chart);

        //}

        //private void EnableHardwareAcceleration(UIElement element)
        //{
        //    RenderOptions.SetBitmapScalingMode(element, BitmapScalingMode.HighQuality);
        //    RenderOptions.SetEdgeMode(element, EdgeMode.Unspecified);
        //    RenderOptions.SetClearTypeHint(element, ClearTypeHint.Enabled);
        //}

        //private void DisableHardwareAcceleration(UIElement element)
        //{
        //    RenderOptions.SetBitmapScalingMode(element, BitmapScalingMode.LowQuality);
        //    RenderOptions.SetEdgeMode(element, EdgeMode.Aliased);
        //    RenderOptions.SetClearTypeHint(element, ClearTypeHint.Auto);
        //}
    }
}

2024.11.24

有些昼伏夜出了哈。前面单个文件终究还是有点乱,尝试了以一下MVVM,有些麻烦,最后还是尽量采用MVVM模式。现在添加了保存日志、发送自动换行、刷新串口功能,打开日志目录,波特率、停止位等属性也加了,同时优化了整个界面布局,增加了一些错误检测。当长度输入"-1"时则不进行截取,取全长。

虽然界面现在依旧简陋,但基本功能已经完备了,可以把接收到的普通浮点数据打印在图表上。不过性能嘛,就有些值得考虑考虑了。

由于功能实现得并不多,可以当成一个简陋的框架去使用,也可以当成一个练手的小项目。目前准备发到github和gitcode上

github:ichliebedich-DaCapo/ZQcom (github.com)

gitcode:ZQcom - GitCode

这是初始界面

这是定时发送数据时

这是打开图表后,100个点,如果以0.01s更新频率,那还是有一些卡的,可能与我使用序列类型和事件聚合器有关,这俩占用还挺大的。有时候禁用动画或许会更流畅一些

这是日志保存,唯一麻烦的是打开日志目录那个功能会被360识别为"危险"(360弹出警告后无法本地截屏),不过好在可以启用360的开发者模式

好吧,忘记加清屏了。现补上,同时测了一下stm32f407无延时发送串口数据,结果是这样的。太快了以至于时间戳都卡没了,后续看看能不能解决

在不使用\r\n的情况下打开图表后就很不正常,可能因为我把图表添加数据点的函数放在了接收数据里。虽然我使用了事件聚合器,把直接调用换成了发布事件,但查了一下,事件聚合器默认是同步的。有空再试一下异步的或者缓存之类的方法

补上了滤除换行的功能,从图中滚动框的长短也可以看出,数据处理框和图表都远远慢于接收框

当单片机每1ms发送一串数据时,不启用处理数据功能,结果还挺好的(除了开头的十个数据)

还有一些测试准备做,太困了,先睡一觉再说
添加了"禁用时间戳"和"强制处理",测试了一下,发现100Hz的输入频率基本是可以的,虽然会有一些数据叠在一块了

把处理数据换成异步(生产者-消费者模式),并且使用队列,现在图表不会阻塞接收数据框了,下图是1000Hz(单片机设置的是延时1ms发送一次,实际上没有那么快)

添加了数据统计和FFT,现在先这样了,感觉够我目前使用了

相关推荐
LunarCod9 小时前
Linux驱动开发快速入门——字符设备驱动(直接操作寄存器&设备树版)
linux·驱动开发·设备树·嵌入式·c/c++·字符设备驱动
stone519514 小时前
鸿蒙系统ubuntu开发环境搭建
c语言·ubuntu·华为·嵌入式·harmonyos
飞凌嵌入式18 小时前
飞凌嵌入式旗下教育品牌ElfBoard与西安科技大学共建「科教融合基地」
嵌入式硬件·学习·嵌入式·飞凌嵌入式
飞凌嵌入式1 天前
飞凌嵌入式T113-i开发板RISC-V核的实时应用方案
人工智能·嵌入式硬件·嵌入式·risc-v·飞凌嵌入式
blessing。。1 天前
I2C学习
linux·单片机·嵌入式硬件·嵌入式
网易独家音乐人Mike Zhou2 天前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
jjyangyou3 天前
物联网核心安全系列——智能汽车安全防护的重要性
算法·嵌入式·产品经理·硬件·产品设计
FreakStudio4 天前
全网最适合入门的面向对象编程教程:59 Python并行与并发-并行与并发和线程与进程
python·单片机·嵌入式·面向对象·电子diy·电子计算机
憧憬一下5 天前
UART硬件介绍
arm开发·嵌入式硬件·串口·嵌入式·linux驱动开发