# 简历项目 **Repository Path**: wenfeifei/resume-projects ## Basic Information - **Project Name**: 简历项目 - **Description**: 介绍化工厂上位机项目 - **Primary Language**: C# - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-11-04 - **Last Updated**: 2026-11-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 化工装置监控上位机(含视觉检测工位) 个人项目 · 独立开发 · C# / .NET 10 / WPF 面向化工装置水压与温度回路的监控上位机:手写 Modbus RTU/TCP 协议栈、ISA-18.2 分级报警引擎、 自绘实时趋势、SQLite 历史数据与 CSV 导出。在此基础上接了一条**工业视觉检测链路** (读图 → 定位 → 测量 → 判定),测量结果与变送器读数走**同一条报警与历史管线**。 ![主界面](docs/截图/主界面-报警与趋势.png) > **关于项目来源** > > 本项目受实习期间在现场巡检时的观察启发,**实习结束后从零独立完成**。与实习公司所用的系统 > **无任何关系**:未使用任何公司代码、数据、图纸、寄存器表或阈值配置。 > > 开发环境没有真实硬件。工艺数据由**自写的从站模拟器**产生,检测图像由**自写的生成器**绘制; > 两者的共同点是:只替代传感器,不替代系统。协议栈本身是真实的——帧由真实的编解码器组帧与 > 解析,CRC 真的算,串口真的开着(用两个 USB-RS485 模块互连构成真实电气链路);视觉部分 > 测的是真实像素,毫米值来自真实标定,合格判定由同一个报警引擎做出。这一点在下面的 > 「已知不足」里也再次写明。 ## 它做什么 | 模块 | 说明 | |---|---| | 协议栈 | 手写 Modbus RTU / TCP:CRC16、0x01/0x02/0x03/0x04/0x06/0x10 组帧与解析、异常响应、粘包/半包重同步 | | 采集引擎 | 扫描周期、超时重试、断线自动重连与指数退避、点质量(Good/Uncertain/Bad)与原因分离 | | 报警引擎 | ISA-18.2 四态状态机、四级阈值、每级独立回差死区与防抖、报警确认、报警事件落库 | | 实时界面 | 数值面板、自绘趋势曲线(无第三方图表库)、报警板与事件日志 | | 历史数据 | SQLite 持久化(WAL)、时间范围查询、按桶聚合、CSV 导出(带 BOM,Excel 直接打开不乱码) | | 从站模拟器 | 一阶惯性工艺模型、四种现场故障注入、真实异常响应;可在真串口或内存回环上运行 | | 视觉检测 | 圆查找定位 + 直径测量(OpenCV Hough 变换)、单比例尺标定、边缘支持度评分;判定复用报警引擎 | ## 快速开始 ```bash # 1. 看界面(内存回环 + 从站模拟器,不需要任何硬件) dotnet run --project src/ChemMonitor.App # 2. 只看协议栈,不开界面 dotnet run --project src/ChemMonitor.Simulator -- selftest # 3. 跑测试 dotnet test ``` 打开界面后点「**触发工况扰动**」,可以看到温度缓慢爬进上上限报警带:卡片变红、`HH` 徽章亮起、 "报警未确认"、右侧报警板出现条目、事件日志记录、曲线同步爬升。 再点几次「**视觉检测**」,它按 合格件 → 超上限件 → 超下限件 循环检查(演示图集首次运行自动生成): 合格件在状态栏留下绿色判读,超差件则让「工件直径」卡片变红、报警板出现条目、历史库多出一行—— **走的是和温度报警完全相同的那条路径**。 **用两个 USB-RS485 模块互连跑真实串口链路**(模块 A-A、B-B 对插,两个 COM 口): ```bash # 终端 1:在 COM4 上扮演从站 dotnet run --project src/ChemMonitor.Simulator -- serial COM4 # 终端 2:在 COM3 上扮演主站探针,打印双向原始报文 dotnet run --project src/ChemMonitor.Simulator -- probe COM3 ``` `probe` 会列出四种字节序的解码结果——这是接真变送器时定位寄存器表和字节序的工具。 ## 架构 ``` WPF 界面 (MVVM) └── MainViewModel ──── 跨线程 marshal,BeginInvoke 从不让采集等 UI │ 采集引擎 (AcquisitionEngine) 报警引擎 (AlarmEngine) 扫描周期 / 重连 / 点质量 ISA-18.2 四态 / 死区 / 防抖 │ ▲ │ │ 视觉工位 (VisionInspectionPublisher) │ 读图 → 圆查找 → 标定 → 映射 │ │ │ └── CompositeMeasurementSink ────────┤ ├── AlarmEvaluatingSink ───────┘ ├── 历史库 SqliteHistorian(有界队列 + 后台批量提交) └── UI sink(Dispatcher.BeginInvoke) │ ModbusMaster ── 回复匹配 / 重试 / 请求串行化 / 状态栏计数器 │ IModbusFrameCodec (RTU | TCP) ── 帧封装差异只在这一层 │ ModbusFrameBuffer ── 粘包/半包/垃圾重同步/CRC 失败恢复 │ ITransport (Serial | Tcp | Loopback) ── 字节管道,三种实现 ``` 分层的核心目的:**PDU、帧封装、字节流三层分离**。同一套采集逻辑跑在 RS485、Modbus TCP 和内存 回环上,切换只改组合根的一行,采集引擎里没有任何一个 `if (isTcp)`。 视觉链路接在 `IMeasurementSink` 这个接缝上,而不是另起一条通道——这是它**没有改动报警引擎、 历史库和界面任何一行**的原因。视觉测点带一个占位寄存器地址(`MeasurementPoint` 要求有地址), 但**不进 Modbus 点位表**:采集引擎每轮会按表逐个读寄存器,视觉点进去就会被轮询到一个无意义的 地址。它们由 `VisionInspectionPublisher` 直接推进同一个 `CompositeMeasurementSink`。 技术细节见 [docs/协议层设计.md](docs/协议层设计.md) 与 [工业视觉检测/06-海康算子SDK不可用-调研记录.md](工业视觉检测/06-海康算子SDK不可用-调研记录.md)。 ## 技术要点 **CRC 低字节先发。** 经典请求 `01 03 00 00 00 0A` 的 CRC 是 `0xCDC5`,但帧尾是 `C5 CD`—— 低字节在前。写反会得到一个总线上看起来完全合理、但从站一律拒绝的帧。测试向量由一份独立的 Python 逐位实现生成后再交叉验证,不是凭记忆写的。 **字节序是每点可配的。** 32 位值跨两个寄存器有 ABCD / CDAB / BADC / DCBA 四种排法,厂家之间 没有统一。解错不会报错,只会给出一个看起来合理的错值——这比崩溃更难发现,所以它是配置项而不是常量。 **负温度必须走有符号解码。** 0.1℃ 缩放的变送器把 -0.5℃ 发成 `0xFFFB`,按 unsigned 读会得到 65531,在冬季的监控画面上显示成"6553.1℃"。 **报警死区与防抖是两件事。** 死区防的是"值停在阈值上反复越限/恢复"的抖动;防抖防的是"单次噪声 采样"触发报警。两者都需要,且在正确的方向上不对称:**升级立即,降级和清除要防抖**——安全不等待。 **报警状态机用 ISA-18.2 的四态**,而不是"越限=报警、恢复=清除"两态。区别在于 `UnacknowledgedNormal`(恢复正常未确认):一个上上限温度在没人看画面时来了又走,两态模型会把它 擦掉,四态模型会留在报警板上直到有人确认。 **视觉测的是像素,报的是毫米,中间隔着标定。** 映射时像素值保留为 `RawValue`、毫米值作为 `EngineeringValue`——和寄存器读数的"原始计数 / 工程值"完全同构。这样当直径看着不对时,"是刀具 测歪了还是标定标错了"当场就能分辨,不必重跑检测。 **判定不写在视觉层。** 合格与否是一次阈值比较,而阈值只有一个归属地:和温度、压力放在一起的 `AlarmLimits`。视觉层再写一套 pass/fail 就是第二个改动点,图纸一改就要改两处,而两处总会 有一天给出不一致的结论——其中错的那一处还没人看。 **不做有无检测的"最高分圆"。** 单个工位的 Hough 会返回多个候选,演示版按边缘支持度取最优; 真实产线不会这么做,而是用配方里给出的预期位置和半径把搜索钉住——"零件应该在哪儿"是已知的, 不该整幅图去找,而且画面里有两个真零件时"最优"是没有定义的。 **预模糊参数是量出来的,不是猜的。** 默认 7×7 / σ=2.0 来自一次扫描:对着故意不加光学模糊的 工件图,5×5 / σ=1.5 会把 280 px 的工件测成 291 px(11 px 误差,任何公差都扛不住),而 σ≥2.0 在 120–500 px 全尺寸段都稳在 1 px 内。只把核加宽、不抬 σ 是没用的(11×11 / σ=1.5 仍差 3.6 px)。 ## 测试 **705 个测试用例**,全套约 3 秒。 ```bash dotnet test ``` 测试向量不是凭记忆写的:CRC 的 15 个基准向量由一份独立的 Python 逐位实现生成,并与公开文献中的 经典帧交叉验证;`Crc16Tests` 另外穷举了 65536 个双字节输入,把查表实现和逐位实现对齐——查表抄错 一个字节藏不住。 除单元测试外还有:**8 项端到端集成测试**(真链路:主站 + RTU 编解码 + 回环传输 + 从站模拟器)、 **14 项报警状态机对抗测试**、**7 项量程与报警限值边界测试**。 视觉部分(25 项)跑**真图**:合成图上画的是已知圆心和直径,测试断言工具把它们恢复出来 (120–500 px 全尺寸段误差在 1 px 内);判定侧则把演示图集里的三个工件跑完整条链路,断言 合格件不报警、超上限件触发 `High`、超下限件触发 `Low`——**预期结论写死在工件定义里**, 而不是在测试里重算一遍公差,否则测试只会和被测代码一起错。 > 视觉测试会在临时目录渲染 PNG。系统盘紧张时可用 `CHEMMONITOR_TEST_TMP` 把临时目录指到数据盘。 ## 已知不足 这一栏是刻意保留的,它比"功能列表"更能说明问题在哪里。 - **未对接真实 PLC 或变送器。** 协议栈在真实串口上验证过(两个 USB-RS485 模块互连),但没有 接过真实仪表,因此没有经历过现场的真机联调与电磁干扰环境。 - **未做 7×24 长稳测试**,也没有看门狗守护。目前的验证是短时间的(分钟级到小时级)。 - **未实现 SOE(事件顺序记录)与操作留痕。** 报警事件精确到毫秒,但没有做站间时钟同步,因此 不能宣称具备 SOE 能力;操作员动作也只记录了确认人,没有完整的审计日志。 - **单机 SQLite,未考虑多客户端。** 没有历史数据归档与分级存储,保留策略是简单的时间阈值删除。 - **报警限值配置在代码里**,尚未做成可编辑的配置表。点位定义与报警限值在当前版本通过组合根装配。 - **趋势图是单 Y 轴。** 多条不同量纲的曲线共用一个轴时,小量程曲线会被压扁成一条贴底的直线, 所以演示里只放了温度。每序列独立缩放或双 Y 轴是明确的下一步。 - **历史查询只提供预设时间范围**(最近 1/8/24 小时、7 天),不支持任意起止时间输入。 - **界面层没有自动化测试。** WPF 的绑定错误是静默的(不抛异常),本项目靠实际运行并截图验证, 这部分没有回归测试保护;核心逻辑则全部有测试覆盖。 - **模拟器的工艺模型是演示用的简化模型。** 一阶惯性 + 噪声,参数非标定值;工况扰动会同时抬高 温度并压低压力,这在物理上并非严格自洽,只是为了在演示中同时看到两类报警。 - **视觉没有接真实相机。** 检测图像由程序绘制,只替代传感器。合成图不含镜头畸变、透视、 全场照度不均和运动模糊——工具只在这种更"干净"的图上调过参,真实产线上的表现没有验证过。 - **标定是单比例尺的**(mm/px,假设光轴垂直、畸变可忽略),不是完整的相机模型。工作距离、 镜头或工件高度一变,标定值就作废;需要跨视场保持精度时要上标定板解内参和畸变。 - **视觉只有一个工位、一种特征、固定不动的搜索窗口。** 没有 ROI 配方、没有模板匹配、 没有多工位坐标变换,检测也是手动触发的,没有接光电开关或 PLC 的触发信号。 - **Hough 候选圆按边缘支持度择优,而不是按配方定位。** 画面里出现两个工件时"最优"无定义, 这一点在演示里看不出来,但真实产线必须先用配方把位置钉住。 ## 目录结构 ``` src/ ChemMonitor.Core/ 与界面无关的全部逻辑 Protocol/ Modbus 协议栈(契约在顶层,实现分目录) Rtu/ Tcp/ 帧编解码 Framing/ 粘包/半包/重同步 Master/ 主站:回复匹配、重试、串行化 Transport/ Serial / Tcp / Loopback Simulation/ 从站模拟器 + 工艺信号发生器 Points/ 点位定义、寄存器块合并、缩放 Acquisition/ 采集引擎、点质量 Alarms/ ISA-18.2 报警引擎 Vision/ 视觉抽象:结果、标定、测点、映射、发布 Persistence/ SQLite 历史库、报警日志、CSV 导出 Diagnostics/ 时钟抽象 ChemMonitor.Vision.OpenCv/ 视觉实现:圆查找、合成图生成、演示图集 ChemMonitor.App/ WPF 界面(MVVM,含自绘趋势控件) ChemMonitor.Simulator/ 从站模拟器命令行(serial / probe / selftest) tests/ ChemMonitor.Core.Tests/ 680 个测试(协议、报警、历史、视觉映射) ChemMonitor.Vision.OpenCv.Tests/ 20 个测试(精度、鲁棒性、演示工位端到端) docs/ 协议层设计.md 协议层设计详解(分层理由、报文示例、字节序表) 截图/ 界面截图 ``` 视觉部分单独一个工程、而不是塞进 Core,是因为它是整个解决方案里唯一引入原生图像库的地方; 分开之后 Core 和它的 680 个测试都不依赖任何原生二进制。换视觉库就是在这个工程旁边再加一个, 再改组合根一行。 ## 技术栈 C# 13 · .NET 10 · WPF(MVVM,手写 `ViewModelBase` / `RelayCommand` / `AsyncRelayCommand`)· Microsoft.Data.Sqlite + Dapper · OpenCvSharp4(视觉)· xUnit 未使用任何 Modbus 库、图表库或 MVVM 框架——协议栈、趋势控件和 MVVM 基础设施都是手写的。 视觉部分用的是 OpenCV 的官方 .NET 绑定,圆查找算法本身没有自己实现。