Appearance
第一个小项目:串口设备监视工具
你的第一个上位机项目不用很复杂。目标是:用 WPF 做一个串口监视工具,能选择串口、打开/关闭、发送数据、在列表里显示收到的数据。
功能不多,但它覆盖了上位机开发最核心的能力:界面 + 通信 + 数据显示。做完它,你就知道一个上位机项目是怎么从零搭起来的。
开始前准备:没装好 Visual Studio 先看环境安装与版本选择;不熟悉波特率、数据位、校验位先看串口与 TCP 基础;想跟 AI 协作时能看清它改了什么,用Git 最小入门里的三条命令就够。
项目概览
这是一个贯穿式教学项目,让你跟着一条完整练习路径,从需求到可运行 Demo 做一个串口设备监控工具。这里为了教学会连续经历多个阶段;以后做真实项目中的一个明确小修改时,不需要机械重复整套流程。
阶段 1:整理需求 — 把模糊想法变成结构化需求,让 AI 帮忙发现遗漏阶段 2:项目设计 — 让 AI 输出模块划分和设计方案阶段 3:拆分任务 — 把大目标拆成 7 个可独立验证的小任务阶段 4:AI 实现 — 每一步提供任务卡,约束 AI 修改范围阶段 5:增加协议解析 — 从文本收发练习升级到一个自定义二进制协议示例阶段 6:增加实时曲线 — 数据显示层升级阶段 7:模拟 Bug 排查 — 练习给 AI 提供证据并验证修改
完成标准:能运行、能收发数据、能保存日志、能在 AI 辅助下排查 Bug。本教学项目使用虚拟串口和模拟数据,不代表真实生产环境或真实设备验收。
阶段 1:整理需求
目标:把"做个串口监视工具"这个模糊想法变成结构化需求。
人工任务:
- 写出你理解的工具功能:选串口、打开/关闭、发送数据、显示接收数据、保存日志
- 把需求发给 AI,让它帮忙整理和发现遗漏
AI 提示:
text
我正在设计一个 WPF 串口设备监控工具。请帮我整理需求,先不要写代码。
工具的基本功能:
- 选择串口和参数
- 打开/关闭串口
- 发送测试指令
- 接收并显示原始数据
- 保存接收数据到文件
- 显示连接状态
请帮我:
1. 整理成结构化需求清单
2. 发现我可能遗漏的功能
3. 列出每个功能的验收标准
4. 指出需要我补充的信息人工裁剪:AI 的需求候选未必全要
把上面的 AI 输出作为候选框架,逐项判断:
P0 · 第一版必须做
串口配置、打开/关闭、数据接收、数据发送、日志保存、连接状态、异常提示
P1 · 后续再做
协议解析、实时曲线、历史数据查询、导出功能
不做清单
用户登录、权限管理、云同步、报表中心、多设备管理、远程访问
提示:把"不做清单"记下来,后续让 AI 编码时作为约束输入——防止它自动补充已排除的功能。详见 从模糊想法到第一版需求。
验收:得到一份完整的需求清单,知道还需要确认什么(比如:需要虚拟串口工具、需要确认数据格式)。
阶段 2:项目设计
目标:确定软件的模块划分和设计方案。
AI 提示:
text
我的需求清单如下:[粘贴阶段1的产出]
请帮我做项目设计,先不要写代码:
1. 模块划分(界面层、通信层、数据层各包含什么)
2. 类结构建议(MainWindow、SerialService、DataLogger 等)
3. 提供两种实现方案并分析优缺点
4. 技术选型建议(WPF/.NET版本、是否需要 NuGet 包)验收:得到模块划分方案,确认技术选型。
阶段 3:拆分任务
目标:把项目拆成可独立验证的小任务。
AI 提示:
text
我的项目设计如下:[粘贴阶段2的产出]
请帮我拆分成可独立完成的小任务。每个任务要求:
1. 目标单一(一个任务只做一件事)
2. 可以独立编译和测试
3. 有明确的输入输出
4. 有验收条件
按以下顺序拆分:
1. 创建 WPF 项目和基本窗口
2. 完成串口配置区 UI
3. 完成串口打开/关闭功能
4. 完成数据发送功能
5. 完成数据接收和显示
6. 完成日志保存
7. 完成异常提示和状态显示验收:得到 7 个按顺序排列的任务卡。
阶段 4:AI 实现
现在拿到了 7 个任务卡,下面逐一实现。每完成一步都可以编译运行验证。
前置准备
- 安装 Visual Studio Community 2022,安装时勾选“.NET 桌面开发”工作负载。
- 安装虚拟串口工具(例如 com0com 或 Virtual Serial Port Driver),创建一对虚拟串口(如 COM1 ↔ COM2)。
- 准备一个串口调试助手(例如 SSCOM 或其它你熟悉的工具),用来模拟设备端收发数据。
第一步:创建 WPF 项目
打开 Visual Studio → 创建新项目 → 选择“WPF 应用程序” → 项目名称填 SerialMonitor → .NET 版本选 .NET 8.0(或你项目统一使用的受支持版本)。
创建完成后,你会看到一个只有空白窗口的项目。
这个示例使用 System.IO.Ports.SerialPort。现代 .NET 项目通常需要引用 System.IO.Ports NuGet 包。可以在 Visual Studio 的“管理 NuGet 程序包”中安装,也可以在项目目录执行:
powershell
dotnet add package System.IO.Ports选择与你的目标框架兼容的稳定版本即可。不要因为教程代码能识别 using System.IO.Ports; 就假设所有项目模板已经自动引用这个包。
第二步:设计界面
打开 MainWindow.xaml,把里面的内容替换成下面的布局:
xml
<Window x:Class="SerialMonitor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="串口监视工具" Height="500" Width="700">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 串口设置区 -->
<GroupBox Header="串口设置" Grid.Row="0" Margin="0,0,0,8">
<StackPanel Orientation="Horizontal" Margin="5">
<Label Content="串口:" VerticalAlignment="Center"/>
<ComboBox x:Name="cmbPorts" Width="100" Margin="5,0"/>
<Label Content="波特率:" VerticalAlignment="Center" Margin="10,0,0,0"/>
<ComboBox x:Name="cmbBaudRate" Width="100" Margin="5,0"
SelectedIndex="0">
<ComboBoxItem>9600</ComboBoxItem>
<ComboBoxItem>19200</ComboBoxItem>
<ComboBoxItem>38400</ComboBoxItem>
<ComboBoxItem>115200</ComboBoxItem>
</ComboBox>
<Button x:Name="btnOpen" Content="打开串口"
Width="80" Margin="15,0,5,0" Click="BtnOpen_Click"/>
<Button x:Name="btnClose" Content="关闭串口"
Width="80" Margin="5,0" Click="BtnClose_Click" IsEnabled="False"/>
</StackPanel>
</GroupBox>
<!-- 接收数据区 -->
<GroupBox Header="接收数据" Grid.Row="2" Margin="0,5,0,0">
<ListBox x:Name="lstReceived" FontFamily="Consolas" FontSize="13"/>
</GroupBox>
<!-- 发送区 -->
<GroupBox Header="发送数据" Grid.Row="3" Margin="0,8,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="txtSend" Grid.Column="0" Margin="5"/>
<Button x:Name="btnSend" Content="发送" Grid.Column="1"
Width="60" Margin="5" Click="BtnSend_Click"/>
</Grid>
</GroupBox>
</Grid>
</Window>这个界面分为三个区域:顶部选串口和波特率,中间大块显示接收到的数据,底部是发送区。
第三步:写串口操作代码
打开 MainWindow.xaml.cs,替换成下面的代码。这里故意使用 code-behind 保持第一项目简单;它是教学 Demo,不是在规定生产项目必须把通信都写进窗口类。
⚠️ 源码文件编码与串口文本编码是两件事:
.cs/.xaml保存为 UTF-8 可以避免源码中文字被写坏;串口收到的文本是否乱码,则取决于设备发送编码与SerialPort.Encoding是否一致。二进制协议不要先强行转成字符串。
csharp
using System.IO.Ports;
using System.Windows;
namespace SerialMonitor;
public partial class MainWindow : Window
{
private readonly SerialPort _serialPort = new();
public MainWindow()
{
InitializeComponent();
// 事件只订阅一次,避免某次 Open 失败后再次点击造成重复订阅。
_serialPort.DataReceived += SerialPort_DataReceived;
// 窗口结束时释放串口资源。真实项目还应结合自己的 Stop/Cancel 生命周期设计。
Closed += (_, _) =>
{
if (_serialPort.IsOpen)
_serialPort.Close();
_serialPort.Dispose();
};
RefreshPorts();
}
private void RefreshPorts()
{
cmbPorts.Items.Clear();
foreach (var port in SerialPort.GetPortNames())
{
cmbPorts.Items.Add(port);
}
if (cmbPorts.Items.Count > 0)
cmbPorts.SelectedIndex = 0;
}
private void BtnOpen_Click(object sender, RoutedEventArgs e)
{
if (cmbPorts.SelectedItem == null)
{
MessageBox.Show("请先选择一个串口");
return;
}
try
{
_serialPort.PortName = cmbPorts.SelectedItem.ToString()!;
_serialPort.BaudRate = int.Parse(
((ComboBoxItem)cmbBaudRate.SelectedItem).Content.ToString()!);
_serialPort.Open();
btnOpen.IsEnabled = false;
btnClose.IsEnabled = true;
cmbPorts.IsEnabled = false;
cmbBaudRate.IsEnabled = false;
AddLog($"串口 {_serialPort.PortName} 已打开");
}
catch (Exception ex)
{
MessageBox.Show($"打开串口失败:{ex.Message}");
}
}
private void BtnClose_Click(object sender, RoutedEventArgs e)
{
try
{
_serialPort.Close();
btnOpen.IsEnabled = true;
btnClose.IsEnabled = false;
cmbPorts.IsEnabled = true;
cmbBaudRate.IsEnabled = true;
AddLog($"串口 {_serialPort.PortName} 已关闭");
}
catch (Exception ex)
{
MessageBox.Show($"关闭串口失败:{ex.Message}");
}
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// DataReceived 在后台线程触发,不能直接修改 WPF UI 控件。
string data = _serialPort.ReadExisting();
Dispatcher.Invoke(() => AddLog($"收到:{data}"));
}
private void BtnSend_Click(object sender, RoutedEventArgs e)
{
if (!_serialPort.IsOpen)
{
MessageBox.Show("请先打开串口");
return;
}
try
{
string text = txtSend.Text;
_serialPort.Write(text);
AddLog($"发送:{text}");
txtSend.Clear();
}
catch (Exception ex)
{
MessageBox.Show($"发送失败:{ex.Message}");
}
}
private void AddLog(string message)
{
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
lstReceived.Items.Insert(0, $"[{timestamp}] {message}");
// 新消息插在索引 0,因此超过上限时删除列表末尾的最旧消息。
while (lstReceived.Items.Count > 500)
lstReceived.Items.RemoveAt(lstReceived.Items.Count - 1);
}
}这段代码的关键点
最容易踩的坑:UI 线程
串口的 DataReceived 事件是在后台线程触发的。如果直接在事件处理里操作 WPF 控件,可能触发跨线程异常。示例使用 Dispatcher.Invoke 切回 UI 线程。
这是最小教学写法。高频采集的生产项目还要考虑 UI 更新频率、队列、背压、取消和 Stop/Dispose 生命周期,不要把每一帧都同步阻塞地刷新界面。
ReadExisting() 读取的是文本,解码仍受 SerialPort.Encoding 影响。如果调试助手和程序需要收发中文或其它非 ASCII 文本,请让两端使用一致编码,并显式设置对应 Encoding。如果设备协议是二进制帧,应按字节读取和解析,而不是依赖 ReadExisting() 解决“乱码”。
第四步:运行和测试
- 确保虚拟串口工具已经创建一对串口(例如 COM1 ↔ COM2)。
- 打开串口调试助手,连接到 COM2。
- 运行 SerialMonitor,选择 COM1,点击“打开串口”。
- 在调试助手中发送 ASCII 测试文本,确认程序可以收到。
- 在程序中发送文本,确认调试助手可以收到。
- 关闭再重新打开串口,确认消息没有因为重复事件订阅而显示两次。
- 关闭窗口后确认串口资源已释放,调试助手/下一次启动可以重新占用端口。
如果这些步骤都通过,可以说明这个模拟环境下的文本收发 Demo 已跑通;不能因此推导真实设备协议已经验证。
第五步:让 AI 帮你扩展功能
基础版本跑通后,可以让 AI 帮你逐项加功能。这里仍然建议一个逻辑批次只改一个目标,方便判断是哪次修改引入问题:
“我有一个 WPF 串口监视工具,基础文本收发已经在虚拟串口环境跑通。现在请依次增加:十六进制显示/发送、清空接收区、接收数据保存、串口状态指示。每次只实现当前一项,保持已有行为,其余作为 NonGoals;完成后给出本项最小验证。”
这个模板的关键是:已有事实、当前目标、NonGoals、对应验证,而不是要求所有真实项目都固定一次只改一个文件。
可能遇到的问题
| 问题 | 可能原因 | 解决方向 |
|---|---|---|
| 串口列表为空 | 没有可用串口 | 安装虚拟串口工具,创建虚拟串口对 |
| 打开串口失败 | 串口被其他程序占用 / 参数不可用 | 关闭占用程序,核对端口与参数 |
| 收不到数据 | 串口参数不匹配 / 对端没发送 | 核对两端端口、波特率和发送动作 |
| 程序出现跨线程异常 | 后台回调直接修改 UI | 把 UI 更新调度回 Dispatcher |
| 中文或特殊字符乱码 | 两端文本编码不一致 | 显式统一 SerialPort.Encoding;二进制数据按 byte/HEX 处理 |
| 重开后同一消息显示多次 | DataReceived 被重复订阅 | 确认订阅 ownership,只订阅一次或严格成对解绑 |
这个项目验证了什么
做完阶段 4,你已经实际练习过:
- 用 XAML 搭建一个简单界面;
- 用 C# 打开、关闭和收发串口文本;
- 处理一次跨线程 UI 更新;
- 管理最基本的事件订阅和资源释放;
- 用 AI 扩展功能并根据现象排查问题;
- 区分“虚拟串口 Demo 通过”和“真实设备已经验证”。
阶段 5:增加协议解析
目标:在文本收发 Demo 上练习一个自定义二进制协议解析器。
真实项目并不都使用同一种协议:可能是自定义二进制帧、ASCII 文本、SCPI、Modbus,也可能由厂商 SDK 封装。本阶段只是为了练习 framing,故意选择下面这个自定义二进制示例;不要把它当成所有串口设备的通用格式。
AI 提示:
text
我的串口监视工具已经能收发文本。现在要新增一个“自定义二进制协议练习模式”。
本练习协议:
帧头(2B: 0xAA 0x55) + 长度(1B) + 命令(1B) + 数据(N字节) + 校验(1B: XOR)
请帮我:
1. 设计协议解析边界;
2. 针对这个流式协议处理半包和多帧连续到达;
3. 验证帧头、长度和本协议定义的 XOR 校验;
4. 保留原始字节用于日志/Replay;
5. 不破坏现有文本演示模式。练习:用虚拟串口发送符合本练习协议的二进制样本,再补一个截断帧、连续两帧和错误校验样本。这里验证的是这个自定义协议,不是在证明所有串口协议都必须有 CRC/粘包测试。
阶段 6:增加实时曲线
目标:在工具中增加数据可视化。
AI 提示:
text
我的串口监视工具已经能得到用于显示的数值。现在需要在界面上增加一个实时曲线图。
要求:
1. 显示最近 500 个数据点的曲线
2. X 轴固定窗口,新数据到达时自动滚动
3. 只影响界面显示,不修改通信和保存逻辑
4. 使用项目已有的图表控件(如果没有,请先推荐方案)
5. UI 更新不要阻塞接收路径练习:用模拟数据记录一段固定场景下的 UI 响应、点数上限和内存趋势。短时间观察正常只能证明当前场景,没有长稳证据时不要写“无内存泄漏”。
阶段 7:模拟 Bug 排查
目标:练习在出现问题时如何给 AI 提供证据并验证修改。
以下是 7 个可以人为制造或假设的练习场景,选择一个来排查:
Bug 1:串口关闭后仍有旧回调
现象:关闭后仍看到旧任务/回调继续处理。练习确认资源 ownership 和生命周期。
Bug 2:发送按钮无响应
现象:点击发送没反应。练习追按钮到实际发送入口的调用链。
Bug 3:下拉框不能选择
现象:串口下拉框点击没反应。练习查控件状态、数据源和 UI 线程。
Bug 4:日志文件不生成
现象:路径配置了但没有文件。练习查写入入口、路径权限和异常证据。
Bug 5:长时间运行界面变慢
现象:运行后逐渐卡顿。练习先建立可见点数、Dispatcher、CPU/内存基线。
Bug 6:重复订阅事件
人为把订阅放进每次 Open,再制造一次失败/重试,观察为何同一数据会重复显示。
Bug 7:内存持续增长
人为移除列表上限,观察 UI 集合无限增长,再恢复边界。
排查流程(以 Bug 1 为例):
- 准备现象和复现步骤,必要时保存日志。
- 先让 AI 做 L1 分析:列出已确认事实、1–3 个候选原因和下一条最能区分它们的证据(见 AI 自主程度)。
- 拿到证据后再选择修复,不根据第一条猜测直接改代码。
- 限制修改到当前 Root Cause 对应范围。
- 按改动影响做 V1/V2 focused 验证,不机械扩大成整个解决方案回归。
项目复盘
完成以上阶段后,花 10 分钟写一个简短复盘:
text
项目:串口设备监控工具
完成阶段:1-7
做得好的地方:
遇到的最大困难:
AI 帮了哪些忙:
AI 在哪些地方出错了:
哪些只在模拟环境验证:
哪些仍是 Unverified / HardwarePending:
下次会怎么改进:复盘完成后,你已经做完一次从需求、实现到模拟验证和问题调查的练习闭环。接下来可以进入完整项目工作流,理解真实项目为什么还需要更完整的设备边界、异常恢复和验收证据。
下一步
完成练习后,可以按学习路线深入更多专题,或进入完整项目工作流学习系统性开发方法。