✨ feat(file-check): 忽略无效扫码结果并完善汇报材料

This commit is contained in:
2026-10-22 16:41:57 +08:00
parent 07841fe4fe
commit 3238817185
10 changed files with 1776 additions and 328 deletions

View File

@@ -1,32 +0,0 @@
{
"permissions": {
"allow": [
"Bash(git:*)",
"Bash(printf '\\\\n---\\\\n')",
"Bash(dotnet new:*)",
"Bash(dotnet sln:*)",
"Bash(dotnet build:*)",
"Bash(dotnet add:*)",
"Bash(powershell -NoProfile -Command \"[Reflection.Assembly]::LoadFrom\\('C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll'\\).GetTypes\\(\\) | Where-Object { $_.Name -like '*Modbus*' -or $_.FullName -like '*Modbus*' } | Select-Object -ExpandProperty FullName\")",
"Bash(powershell -NoProfile -Command '[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\).GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\).GetMethods\\(\\) | Where-Object { $_.Name -in @\\(\"Connect\",\"ReadDiscrete\",\"Write\",\"ReadUInt16\"\\) } | ForEach-Object { $_.ToString\\(\\) }')",
"Bash(powershell -NoProfile -Command '$asm=[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\); $type=$asm.GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\); $type.GetMethods\\([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::DeclaredOnly\\) | ForEach-Object { $_.ToString\\(\\) }')",
"Bash(powershell -NoProfile -Command '$asm=[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\); $type=$asm.GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\); $type.GetInterfaces\\(\\) | ForEach-Object { $_.FullName }')",
"Bash(powershell -NoProfile -Command '$asm=[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\); $type=$asm.GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\); \"BASE: $\\($type.BaseType.FullName\\)\"; $type.GetMethods\\([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::Public\\) | Where-Object { $_.Name -match \"Connect|Close|Dispose|Disconnect|Open|Start|Stop|Release\" } | ForEach-Object { $_.DeclaringType.FullName + \" :: \" + $_.ToString\\(\\) }')",
"Bash(powershell -NoProfile -Command '$asm=[Reflection.Assembly]::LoadFrom\\(\"C:/Users/yunxiao.zhu/.nuget/packages/iotclient/1.0.42/lib/netstandard2.0/IoTClient.dll\"\\); $type=$asm.GetType\\(\"IoTClient.Clients.Modbus.ModbusTcpClient\"\\); $type.GetConstructors\\(\\) | ForEach-Object { $_.ToString\\(\\) }')",
"Read(//d D:/Dev/Codes/MFD_Solution/Axi_Omron/src/**)",
"Bash(dotnet restore:*)",
"Bash(systemctl status *)",
"Bash(service ssh *)",
"Bash(sshd -V)",
"Bash(apt list *)",
"Bash(dpkg -l)",
"Bash(sudo apt-get *)",
"Bash(sudo -n true)",
"Bash(mkdir -p /tmp/pcb)",
"Read(//tmp/**)",
"Bash(taskkill /F /IM \"AxiOmron.PcbCheck.exe\")",
"Bash(powershell -Command \"Stop-Process -Id 23292 -Force -ErrorAction SilentlyContinue\")",
"Bash(dotnet publish *)"
]
}
}

View File

@@ -1,14 +1,35 @@
# AxiOmron.PcbCheck 汇报文案
# AxiOmron.PcbCheck 汇报演讲稿
> 主题:3 个月,我从写 if-else 到 Vibe Coding<br>
> 主题:从初学者需求,到完整项目计划<br>
> 项目:AxiOmron.PcbCheck / PCB 目检放行上位机<br>
> 建议时长:10 分钟<br>
> 使用方式:本文件按网页汇报的 9 个页面整理;“屏显文案”用于页面内容,“讲述提示”用于口头汇报。
> 建议时长:10~12 分钟<br>
> 对应页面:[axiomron-pcbcheck-report-draft.html](./axiomron-pcbcheck-report-draft.html)<br>
> 内容来源:`docs/开发流程.txt`、`docs/纯plan.md`、当前项目实际工程
## 使用说明
这份文件不是简单的页面文案,而是现场可以直接使用的演讲稿:
- “屏显文案”对应 HTML 页面上看到的文字。
- “演讲主词”是建议直接讲出来的内容,可以按现场时间适当删减。
- “转场”用于从当前页面自然过渡到下一页。
- “操作/提示”用于现场演示,避免临场忘记重点。
- 全程只展示本地模拟和软件证据,不连接真实生产设备。
这次汇报的主线是:
```text
模糊需求 → 对话追问 → 补齐边界 → 完整计划 → 分阶段实现 → 测试验收
```
整场汇报不需要把代码讲得很深,重点是说明:我如何从一个初学者的功能描述,逐步学会定义问题、判断风险和验证结果。
---
## 01. 封面|3 个月,我从写 if-else 到 Vibe Coding
**预计时长:40 秒**
### 屏显文案
**3 个月,**<br>
@@ -18,250 +39,495 @@
用 AI 协作搭建一套 PCB 目检放行上位机:把 PLC、双扫码、文件校验、安灯和追溯记录,收束为一条可解释的现场闭环。
- 产品形态:WPF 工业上位机
- 架构基座:.NET 8 / MVVM
- 核心结果:可追溯放行闭环
- 技术基线:.NET 8 / MVVM
- 汇报重点:需求演进、调试方法、工程边界
### 讲述提示
### 演讲主词
三个月前,我遇到类似需求时,第一反应通常是继续加 if-else:扫码成功怎么办、文件找不到怎么办、PLC 超时又怎么办。现在我更关注如何先把现场规则拆成状态、契约和异常边界,再借助 AI 快速实现与验证。
大家好,今天我想分享的不是“AI 帮我生成了多少代码”,而是我如何从一个很简单的现场需求,逐步把它变成一份完整的项目计划,并最终做成一个可以运行的上位机项目。
今天分享的不是一个单独页面,而是我如何用 AI 协作,逐步搭建一套 PCB 目检放行上位机。
三个月前,遇到扫码、查文件、通知 PLC 这种需求时,我很容易直接进入 if-else:扫码成功怎么办,文件找不到怎么办,PLC 超时怎么办。
后来我逐渐发现,工业软件最重要的不是先写出一段代码,而是先把现场规则、设备边界、异常情况和验收方式问清楚。AI 可以加快实现,但方向和判断仍然需要我自己负责。
### 转场
所以我们先回到起点,看一下最开始的需求到底是什么样的。
---
## 02. 起点|让一块 PCB 安全地被放行
## 02. 初始需求|我只想让它先跑起来
**预计时长:55 秒**
### 屏显文案
**不是做一个界面,**<br>
**而是让一块 PCB 安全地被放行。**
**最开始,我只想让它**<br>
**先跑起来。**
> “扫码成功、文件存在、PLC 收到信号——这些局部成功,如何组成一个真正可追溯的业务成功?”
> “我需要写一个程序:PCB 到位后扫码,根据条码检查文件是否存在,再把结果返回给 PLC。”
| 关联对象 | 需要解决的问题 |
初版要求:
1. PCB 到位后开始处理。
2. 读取一个条码。
3. 根据条码检查对应文件是否存在。
4. 文件存在显示“正常”。
5. 文件不存在显示“异常”。
6. 页面显示当前状态、条码、处理结果和日志。
7. 保存最近的处理记录。
8. 先使用模拟数据,不连接真实设备。
9. 页面提供“正常”“异常”“设备故障”三种模拟情况。
### 演讲主词
这不是一个错误的需求,反而非常适合作为第一版。
它已经说清楚了业务主线:PCB 到位以后开始处理,读到条码,根据条码查文件,文件存在就是正常,不存在就是异常,同时页面要能显示状态和日志,而且一开始先用模拟数据。
对初学者来说,这已经足够启动一个原型了。问题是,它还没有进入现场工程的层次。
它没有说明 PLC 的哪个信号算到位,两把扫描枪到底怎样通信,文件具体怎样命名,也没有说明异常时如何恢复、结果保持多久、什么条件下才算真正完成。
所以我当时的第一版想法是:先把正常流程跑通,再用模拟按钮把正常、异常和设备故障演示出来。
### 转场
但是,当我准备真正开始写设备通信代码时,就会遇到一个问题:还缺哪些信息?
---
## 03. 第一次对话|还需要哪些信息才能开始写程序
**预计时长:1 分钟**
### 屏显文案
**真正开始写代码前,**<br>
**我先问:还缺什么?**
> “还需要哪些信息才能开始写程序?”
需要补齐四类事实:
| 类别 | 关键问题 |
| --- | --- |
| PLC | PCB 到位触发、忙碌、结果码、放行与复位,都需要明确握手语义。 |
| 双扫码枪 | 两路 TCP 返回可能成功、失败或不一致,不能靠单一分支草率处理。 |
| 文件校验 | 根据条码查找产品 ID 文件;找不到、重试耗尽和连接异常是不同结果。 |
| 现场追溯 | 异常要能报警、记录、回看;工程师需要看到过程,而不仅是最终 OK / NG。 |
| PLC | 型号、协议、点位、保持/脉冲、复位、应答、超时和异常恢复。 |
| 双扫描枪 | 通信角色、端口、结束符、编码、超时、双枪关系和断线行为。 |
| 文件规则 | 条码格式、文件名规则、匹配方式、目录类型、空文件和正在写入的处理。 |
| 程序与记录 | 界面、配置、日志、权限、模拟入口、部署方式和是否需要数据库。 |
### 讲述提示
### 演讲主词
这个项目的难点不在于“连上设备”,而在于这些设备的局部结果如何组成一个可靠的业务结论。
这一步的价值,不是让 AI 变得更会猜,而是让它少猜。
例如,扫码成功不代表可以放行;PLC 写成功也不代表产线已经完成放行;文件删除更不能发生在错误的时机。我要解决的是一条完整的现场闭环。
首先是 PLC。需要知道 PLC 的型号和通信协议,哪个地址代表 PCB 到位,哪些地址由上位机写入,信号是保持型还是脉冲型,PLC 有没有复位和业务应答。
然后是扫描枪。两把枪是同时触发还是有先后?它们是 TCP Server 还是 TCP Client?返回有没有结束符?如果没有结束符,程序怎么知道一条条码已经接收完成?
第三是文件规则。条码是完整文件名,还是要转换成文件名?文件不存在和目录无法访问,是不是同一种异常?文件正在生成时应该等待、重试还是直接失败?
最后是程序怎么使用。操作员需要看到什么,工程师需要哪些日志,哪些操作需要权限,模拟入口怎样保留,记录是否要长期保存。
这些问题如果不提前确认,代码可能在本地看起来正确,到了现场却因为一个点位、一个报文边界或一个异常分类而产生错误判断。
### 转场
当这些问题被逐项问出来,需求就开始从“功能列表”变成“工程契约”。
---
## 03. 架构|先搭骨架,再接设备
## 04. 需求完善|从功能描述到工程契约
**预计时长:1 分钟**
### 屏显文案
**把易变的设备放在边缘,**<br>
**把稳定的业务流程留在中心。**
**需求不再只是“做什么”,**<br>
**还要说清楚“边界在哪里”。**
核心:`WorkflowHostedService`
后台状态机负责单板生命周期;WPF 只订阅运行态,不阻塞 UI。
| 分类 | 组件 | 职责 |
| --- | --- | --- |
| 触发 | Omron PLC | 到位、复位、结果码、放行应答 |
| 输入 | 双 TCP 扫码枪 | 并发触发、结果汇总与不一致处理 |
| 校验 | SFTP / 本地文件 | 按条码检索、重试、路径安全边界 |
| 可观察性 | 安灯、CSV、日志 | 异常通知、结果追溯、监控与设置 |
架构原则:
- 接口隔离设备实现
- 后台处理耗时 I/O
- 配置不写死在流程里
- UI 只展示状态
### 讲述提示
我的第一步不是先做漂亮界面,而是把每一类设备抽象成接口。这样 PLC、扫码枪、文件查找和安灯都能被替换为模拟实现,流程服务只关心业务规则。
这一层架构也让 AI 能更好地协作:我可以让它先沿接口和实现追踪调用链,再只改动目标边界,而不是在 UI 或设备代码里直接塞逻辑。
---
## 04. 核心流程|从 if-else 到状态机
### 屏显文案
**代码不再问“下一个 if 是什么”,**<br>
**而是问“当前 PCB 处于什么状态”。**
1. **到位触发**:忽略重复上升沿,防止同一块板流程重入。
2. **扫码与校验**:汇总双扫码,再根据条码检索产品 ID 文件。
3. **结果与放行**:写入结果码,发出放行脉冲,等待 PLC 业务应答。
4. **完成与追溯**:清状态、记录 CSV、刷新界面,等待下一块板。
关键规则:
- 扫码失败:安灯 + NG 放行
- 文件不存在:重试耗尽后 NG 放行
- 系统异常:故障锁存,不自动放行
- 收到 PLC 应答后,才执行可选清理
### 讲述提示
这就是我从 if-else 思维切换到状态机思维的关键点。每一种异常都不是额外补一段判断,而是状态机中明确的一条路径。
这样做的好处是,任何一个现场问题都可以回到三个问题:当前状态是什么?允许的下一步是什么?失败后应该如何恢复或锁存?
---
## 05. 搭建过程|从方案到可运行系统
### 屏显文案
**从方案到测试,**<br>
**每一层都先解决一个真实问题。**
| 时间 | 阶段 | 内容 |
| --- | --- | --- |
| 04·16 | 先把现场语言画出来 | 梳理通信角色、PLC 点位、结果码、异常策略和状态机,不急着进入代码。 |
| 04·17 | 初始化 WPF 工程 | 建立 MVVM、Host/DI、日志与配置,让界面与业务流程先有干净的边界。 |
| 04·19 | 补齐可运维能力 | 管理员解锁、SFTP 探活、扫码启动探活、CSV 结果记录相继落地。 |
| 05·17 | 重构主放行流程 | 接入双扫码判定,把扫码、查文件、告警、放行统一为单板工作流。 |
| 06·10 | 把联调变得可观察 | 补齐 TCP 扫码连接测试与日志,让通信问题可以被定位,而不是被猜测。 |
### 讲述提示
这个项目不是一次性把所有功能堆出来,而是按风险逐层搭建:先确定规则,再建立骨架,然后补齐运维能力,最后持续让通信和异常更可观察。
AI 在这里的作用,是加快每一个小闭环的实现和验证,而不是跳过业务设计。
---
## 06. 场景演示|一块 PCB 从到位到放行
### 屏显文案
**用一个可控的模拟流程,演示“从到位到放行”。**
流程:
1. PLC 检测到 PCB 到位
2. 双扫码枪返回条码
3. 产品 ID 文件校验
4. 写结果并等待放行应答
5. 写入追溯记录,回到空闲
关键判断:
> “PLC 写入成功”不代表产线已确认放行。只有收到 PLC 应答,才能进入后续清理和完成记录。
补充规则:
- 双扫码任一成功即可继续;两者不一致时保留诊断信息。
- 文件未找到会受控重试;通信或认证异常进入系统故障,而非误放行。
- 放行脉冲有时长与应答超时;清理动作默认关闭,避免误删现场依据。
### 现场演示动作
1. 点击网页上的“播放模拟流程”。
2. 随着每一步高亮,口头说明它对应的现场动作。
3. 在第 4 步强调:此时不是“写了放行位就结束”,而是等待 PLC 给出业务应答。
4. 到第 5 步说明:完成记录和文件清理都依赖这一确认条件。
### 讲述提示
我不会在汇报现场连接真实 PLC 或 SFTP,而是用模拟器展示主流程。这种方式既能稳定演示完整业务链路,也能清楚地区分“本地逻辑验证”与“真实现场验收”。
---
## 07. AI 编程案例|先问清楚何时可以删
### 屏显文案
**AI 的价值不是直接补一行 `Delete(file)`,**<br>
**而是先问清楚“何时可以删”。**
需求:配置可选启用;收到 PLC 放行应答后,删除本轮读取到的产品 ID 文件。
AI 协作过程:
1. 追踪 `扫码 → 文件匹配 → ReleasePermit → PlcAckRelease` 的调用链。
2. 明确 FINS 写入成功,只证明协议操作成功;不能等同于 PLC 完成业务应答。
3. 保存精确命中路径;等待应答;先清放行脉冲;再由本地或 SFTP Provider 负责安全删除。
4. 验证:应答成功删除;应答超时不删;越界路径拒绝;删除失败不回滚已确认放行。
AI 的三个贡献:
- **读懂陌生系统**:快速在配置、接口、后台服务、设备实现和测试之间建立调用链。
- **把自然语言变成契约**:把“放行后删除”翻译成前置条件、超时行为、默认开关和失败后果。
- **让异常可验证**:用 Fake PLC / Fake 文件服务覆盖关键分支,让现场风险先在可控环境暴露。
### 讲述提示
这个案例最能说明我对 AI 的使用方式发生了变化。以前我可能直接让 AI 写删除文件的代码;现在我会先要求它追踪整条流程,再一起确认动作的前置条件与失败边界。
AI 帮我缩短了理解陌生系统和实现测试的时间,但业务责任仍然在我这里:什么时候删、删谁、失败后如何处理,必须由我结合现场规则确认。
---
## 08. 学习路径|从看懂到解决
### 屏显文案
**学习不是囤教程,**<br>
**而是不断缩短“看懂”到“解决”的距离。**
1. **看 5 个视频**:只抓住 WPF、依赖注入、后台服务、设备通信的基础概念。
2. **拆 1 个项目**:沿着一块 PCB 的生命周期,理解配置、状态、接口和日志为何存在。
3. **自己搭一个**:从界面骨架到 PLC、扫码、文件校验,持续用 AI 做代码阅读和小步实现。
4. **解决真实问题**:把“PLC 应答后再删文件”落成可配置、可测试、可追溯的工程规则。
关键转变:
> 从“让 AI 帮我生成代码”,升级为“我能给 AI 足够明确的业务上下文,并验证它给出的工程边界”。
### 讲述提示
我并不是等到学会所有技术才开始做项目,而是在真实问题里逐步学习。视频帮助我建立术语,拆项目帮助我理解结构,自己实现帮助我建立手感,真实问题则迫使我理解生产边界。
---
## 09. 总结|从代码到工程交付
### 屏显文案
**从 if-else 到 Vibe Coding,**<br>
**改变的不是“谁来写代码”。**
而是我开始能把一个模糊的现场需求,拆成:
- 设备契约
- 状态机
- 异常边界
- 可验证的交付
| 边界 | 内容 |
| 契约 | 需要明确的边界 |
| --- | --- |
| 已在本地覆盖 | 项目定向构建与工作流、文件相关测试可验证逻辑分支、接口隔离和异常行为。 |
| 仍需现场确认 | 真实 PLC 点位、SFTP 根目录与删除权限,必须在产线按实际设备与权限完成联调。 |
| PLC 契约 | 到位、复位、结果、应答、任务去重和确认超时。 |
| 扫描枪契约 | 双路并发、无结束符、总超时、首个有效结果和迟到数据。 |
| 文件契约 | 条码清洗、路径安全、文件存在、目录异常和结果分类。 |
| 应用契约 | 设备状态、当前任务、日志、统计、模拟场景和权限边界。 |
最后三点:
需求演进链路:
1. **技术深度**:不是“调用了多少 SDK”,而是能否说清状态、超时、失败与恢复。
2. **学习能力**:不是“看了多少教程”,而是能否把陌生系统拆成可验证的问题。
3. **业务结合**:不是“做了一个界面”,而是让现场每一次放行都可解释、可追溯。
`功能描述 → 条件边界 → 失败策略 → 验收证据`
### 演讲主词
需求完善以后,变化最大的地方是:每个功能都开始有前置条件和失败策略。
比如“把结果返回给 PLC”不再只是一句写入代码,而要继续问:结果保持多久?PLC 是否会给业务应答?确认超时怎么办?复位由谁触发?如果到位信号一直保持为真,是否会重复触发同一块 PCB?
“读取一个条码”也不再只是调用一次 Receive。要确认两把枪是否同时触发,是否允许任意一把成功,没有结束符时如何判断报文结束,迟到的数据是否会影响下一轮任务。
文件检查也要分清楚:文件存在、文件不存在、目录不可访问、路径越界、条码格式错误,这些不能全部压成一个简单的异常。
最终,需求被整理成一条链路:先描述功能,再补充条件边界,然后规定失败策略,最后为每个关键判断准备验收证据。
### 转场
当问题和边界都被整理出来以后,对话的结果就不再是一段零散的代码,而是一份可以执行的项目计划。
---
## 05. 完整项目计划|对话的终点是一份可执行的计划
**预计时长:1 分钟**
### 屏显文案
**对话的终点,**<br>
**是一份可以执行的计划。**
| 层面 | 最终形成的内容 |
| --- | --- |
| 目标 | 让 PCB 从到位、扫码、文件校验到 PLC 结果反馈,形成可解释、可追溯的业务闭环。 |
| 流程 | 到位触发 → 双枪扫描 → 条码校验 → 文件判断 → 结果保持 → 应答/复位 → 记录完成。 |
| 架构 | 当前项目落地为 .NET 8 + WPF;设备通信、流程编排、界面状态、配置和日志保持清晰边界。 |
| 测试与验收 | 单元测试和 Fake 设备先验证逻辑,再使用真实设备完成现场联调。 |
计划的价值:把“我想要一个程序”翻译成目标、状态、接口、阶段、测试和交付门槛。
### 演讲主词
最后形成的不是一张简单流程图,而是一份完整的项目计划。
第一层是目标:让一块 PCB 从到位、扫码、文件校验,到结果反馈和记录完成,形成一条能够解释、能够追溯的业务闭环。
第二层是流程:什么时候开始,什么时候扫描,什么时候查文件,结果什么时候写回,什么时候等待 PLC 应答,什么时候复位并进入下一轮。
第三层是架构:当前仓库的实际落地基线是 .NET 8 加 WPF。设备通信、流程编排、界面状态、配置和日志各自有清晰边界,便于模拟、测试和后续替换设备实现。
第四层是测试和验收:先用单元测试和 Fake 设备验证软件逻辑,再使用真实 PLC、扫描枪和文件服务做现场联调。
这里有一个需要主动说明的历史差异:早期计划文本里出现过 WinForms,但当前工程实际采用 WPF。计划不是一次写死的合同,事实确认后可以修正;重要的是把历史方案和最终落地方案区分清楚。
### 转场
有了目标、流程、架构和验收标准,下一步就是按风险顺序把大计划拆成多个小闭环。
---
## 06. 开发阶段|七个小闭环,逐步降低风险
**预计时长:55 秒**
### 屏显文案
**计划被拆成七个小闭环,**<br>
**每一步都有可以验证的产物。**
| 阶段 | 产物 |
| --- | --- |
| 01 需求与协议确认 | 点位、握手、报文、结果码和文件规则。 |
| 02 项目骨架 | WPF、配置、日志、依赖注入和基础状态。 |
| 03 PLC 通信 | 读写、超时、断线、重连和模拟接口。 |
| 04 双扫描枪 | 并发触发、无结束符、首个有效结果。 |
| 05 流程编排 | 任务去重、文件安全、结果保持和恢复。 |
| 06 状态界面 | 设备、任务、统计、日志和操作权限。 |
| 07 测试与交付 | 单测、Fake 流程、异常测试和现场验收。 |
### 演讲主词
这个项目不是一次性把所有功能堆出来,而是按风险顺序搭建。
第一阶段先确认事实,避免在错误协议或错误点位上继续开发。第二阶段建立项目骨架,让配置、日志、界面和业务流程有边界。第三、第四阶段分别处理 PLC 和双扫描枪通信,把设备问题尽量隔离在接口和服务里。
第五阶段才把到位、扫码、查文件、结果保持和复位串成完整流程。第六阶段把状态和结果展示给使用者。最后用单元测试、Fake 流程、异常测试和真实设备联调完成交付。
每个阶段都有可以检查的产物,所以遇到问题时,可以定位是需求没确认、通信没稳定、流程没定义,还是验收证据不足,而不是把所有问题都归结为“代码有 bug”。
### 转场
这也说明了 Vibe Coding 的真正价值:它可以加快每个小闭环,但不能替我们决定闭环是否成立。
---
## 07. Vibe Coding 上手难度|能跑不等于可靠
**预计时长:50 秒**
### 屏显文案
**上手不难,**<br>
**难的是从“能跑”走到“可靠”。**
| 阶段 | 需要的能力 |
| --- | --- |
| 做出原型 | 会描述需求、会运行项目,就能很快看到第一个结果。 |
| 做到可用 | 会拆小任务、补充上下文,并检查 AI 实际改了什么。 |
| 做到可靠 | 理解业务风险,验证断网、超时、错误输入和恢复路径。 |
> 它降低的是写代码的门槛,没有消除软件工程本身的难度。
### 演讲主词
从体验上说,Vibe Coding 的确降低了入门门槛。
会描述目标、会运行项目,就可以很快做出一个原型。再进一步,需要会拆小任务、补充上下文,并且确认 AI 实际修改了什么。真正做到可靠,则必须理解业务风险,主动验证断网、超时、错误输入和恢复路径。
工业软件最难的地方,往往不是正常时能不能跑,而是异常时会不会误判、重复触发、误放行,或者丢失现场证据。
所以我现在的理解是:AI 像一名速度很快的协作程序员,负责加速实现;我负责方向、边界和最终验收。
### 转场
下面看一下这份计划最终如何落到一个可以运行的项目界面和处理流程上。
---
## 08. 实际项目运行|程序真的运行起来了
**预计时长:1 分钟**
### 屏显文案
**这不是概念图,**<br>
**程序真的运行起来了。**
一块 PCB 的 5 步旅程:
1. PLC 通知 PCB 到位。
2. 两把扫码枪读取条码。
3. 根据条码检查产品文件。
4. 发送结果并等待确认。
5. 保存记录并回到空闲。
> 本地运行界面是软件证据,不等同于真实 PLC、扫描枪和文件服务现场验收。
### 现场操作
1. 展示 WPF 主界面中的设备状态、当前任务、文件校验、流程和日志。
2. 按页面右侧的 5 个步骤,说明一块 PCB 如何经过系统。
3. 重点停在第 4 步,说明写入结果以后还要等待 PLC 业务确认。
4. 强调当前是本地展示环境,没有连接真实生产设备。
### 演讲主词
到这里,前面讨论的计划已经对应到了实际工程中的界面和服务。
页面不是只显示一个最终的 OK 或 NG,而是把设备状态、当前任务、条码、文件检查、流程阶段和日志放在一起。这样发生异常时,工程师能够看到问题发生在哪一步,而不只是看到一个结果颜色。
这一页展示的是软件可以运行、流程可以观察,但它仍然不是现场验收证明。真实 PLC、真实扫描枪、真实文件服务和生产网络权限,还需要在现场单独验证。
### 转场
真正让我意识到工程边界的,是下面这个调试案例:信号明明写成功了,为什么文件仍然不能立即删除?
---
## 09. 真实调试案例|先问清楚何时可以删
**预计时长:1 分 10 秒**
### 屏显文案
**信号写成功了,**<br>
**为什么文件不能删?**
最初想法:
```text
写入放行信号 → 删除文件
```
调试后的流程:
```text
写入放行信号 → 等待 PLC 业务确认
→ 确认成功后,才按配置执行后续动作
```
> PLC 写入成功,不等于产线已经完成业务确认。
### 现场操作
1. 进入第 9 页。
2. 点击“收到 PLC 确认”,观察状态变成“业务已确认”,文件允许按配置处理。
3. 点击“等待确认超时”,观察状态变成“确认超时”,文件保持不动。
4. 对听众强调:写入成功和业务完成是两个不同层次的结果。
### 演讲主词
最初的想法很直接:写入放行信号成功以后,就把本轮文件删掉。
后来调试时发现,这里混淆了两个概念:协议操作成功,和业务动作已经完成。
这可以类比成发邮件。系统显示“发送成功”,只表示邮件已经交给发送系统,并不代表对方已经阅读,更不代表对方已经执行了邮件里的内容。
在这个项目里,FINS 写入成功只能说明上位机完成了一次通信操作。只有 PLC 返回业务确认,才能说明产线已经确认了放行结果。确认成功以后,才可以根据配置决定是否执行后续清理;如果确认超时,就必须保留文件,不继续执行危险动作。
这个案例让我改变了使用 AI 的方式。以前我可能会直接让 AI 补一行 `DeleteFile`;现在我会先让它追踪完整调用链,确认前置条件、超时行为、默认开关和失败后果。
### 转场
当问题变复杂时,我不会只把错误信息丢给 AI,而是使用一套固定的证据驱动调试方法。
---
## 10. 五步调试法|证据越完整,AI 越不容易靠猜
**预计时长:1 分钟**
### 屏显文案
**程序出错时,**<br>
**我用这套五步调试法。**
1. **先复现**:说明做了什么、期望什么、实际发生什么。
2. **给证据**:提供报错、日志、配置和最近的修改。
3. **讲流程**:先让 AI 解释调用过程,不急着修改。
4. **小步改**:一次只处理一个原因,修改后立即运行。
5. **正反测**:既测试成功,也测试超时、断网和错误输入。
常用提示:
> “先不要改代码。请根据日志找到调用流程,列出最可能的三个原因,以及如何逐个验证。”
### 演讲主词
我的调试方式可以概括成五步。
第一,先复现,把做了什么、期望什么、实际发生什么说清楚。第二,给证据,包括日志、异常、配置和最近的修改。第三,让 AI 先解释调用流程,不要一上来就修改代码。
第四,一次只改一个原因,修改以后立即运行。第五,不只测试成功场景,还要测试超时、断网、错误输入和功能开关关闭的情况。
我常用的提示词是:“先不要改代码,请根据日志找到调用流程,列出最可能的三个原因,以及如何逐个验证。”
这样做的好处是,每次修改都能回答三个问题:为什么改、改了什么、怎么证明改对了。
### 操作/提示
- 页面上的测试证据展示了 PLC 确认成功、确认超时和功能开关关闭三种结果。
- 不需要逐字讲出所有测试名称,只需说明“成功和失败都要验证”。
### 转场
方法解决了如何调试,边界解决的是哪些事情不能省、哪些结果不能被误读。
---
## 11. 工程边界与验收|这几条不能省
**预计时长:55 秒**
### 屏显文案
**Vibe Coding 很快,**<br>
**但这些不能省。**
| 规则 | 现场含义 |
| --- | --- |
| 任务要拆小 | 每一步都应该能运行、能观察、能撤回。 |
| 修改要看懂 | 确认改了哪些文件、为什么改、影响到哪里。 |
| 模拟不等于现场 | Fake 设备只能先验证逻辑,不能替代真实设备联调。 |
| 高风险动作默认关闭 | 删除、放行等动作必须有明确条件和可控开关。 |
交付边界:
- 异常可恢复:断线、超时、目录不可访问都有状态、日志和恢复策略。
- 过程可追溯:每一轮任务都能回看输入、判断、结果和失败原因。
- 现场才算验收:真实点位、通信边界、权限和连续生产稳定性必须确认。
### 演讲主词
这一页是我认为最重要的工程边界。
第一,任务要拆小,每一步都要能运行、能观察、能撤回。第二,修改要看懂,至少知道改了哪些文件、为什么改、影响了什么。
第三,模拟不等于现场。Fake PLC、Fake 扫描枪和本地文件可以帮助我们先验证逻辑,但不能替代真实设备、真实网络和真实权限。
第四,高风险动作默认关闭。删除文件、放行、清理现场依据,都要有明确条件、可控开关和失败策略。
所以交付标准不是“页面能打开”,而是异常可恢复、过程可追溯,并且真实设备完成现场验收。
### 被问到现场验证时
可以直接回答:
> 当前已有本地构建、定向测试和可控模拟流程证据;真实 PLC、扫描枪、文件服务、网络权限和连续生产稳定性仍需要现场联调验收,不能用模拟结果替代。
### 转场
最后用三句话总结这次从初始需求到项目计划的变化。
---
## 12. 总结|从一句话需求到可运行项目
**预计时长:50 秒**
### 屏显文案
**从一句话需求,**<br>
**到可验证的完整计划。**
最后再到一个能运行、能追溯、知道边界的项目。
- **会提问**:把需求里的空白和风险问出来。
- **会拆解**:把自然语言变成流程、阶段和验收标准。
- **会验证**:把本地证据与真实现场验收分开。
### 收尾讲述
AI 让开发速度更快,但它没有替代工程判断。真正的成长,是我开始能定义问题、判断边界、验证结果,并把一个模糊需求交付成可靠的现场流程。
这次最大的变化,不是 AI 替我写了多少代码,而是我开始能把一个模糊的现场需求,拆成设备契约、状态流程、异常边界、开发阶段和验收证据。
谢谢。
以前我更关注“能不能生成代码”,现在我更关注“问题有没有定义清楚、边界有没有说明白、结果有没有被验证”。
AI 让开发速度更快,但没有替代工程判断。真正的成长,是我开始能定义问题、判断风险、验证结果,并把一句话需求逐步变成一份完整计划和一个可以运行的项目。
谢谢大家。
---
## 汇报现场速记
## 问答备忘
- 用方向键或右下角“上一页 / 下一页”控制网页汇报。
- 第 6 页点击“播放模拟流程”,演示完整闭环。
- 不演示真实 PLC、真实 SFTP 或真实删除动作。
- 不展示具体现场 IP、账号、密码、完整生产路径或未确认的 PLC 点位。
- 被问到“是否现场验证”时,回答:本地逻辑与定向测试可验证;真实设备和权限仍需现场联调验收。
### 问:这套程序现在是不是已经连接真实 PLC 了?
答:本次汇报展示的是本地可控环境和软件运行证据,没有连接真实生产设备。真实 PLC 点位、通信边界、权限和连续生产稳定性仍需要现场联调验收。
### 问:AI 是不是已经替代了开发人员?
答:AI 加快了查找、实现和测试,但没有替代需求确认、风险判断和验收责任。尤其是放行、删除和复位这类动作,必须由业务人员确认规则。
### 问:为什么计划里出现过 WinForms,最后项目是 WPF?
答:WinForms 是早期计划中的技术选择,随着实际工程基线和项目结构确认,当前实现落地为 .NET 8 + WPF。这个变化被保留并明确说明,避免把历史方案误认为最终实现。
### 问:为什么不直接把正常流程做完再说?
答:正常流程可以快速做出原型,但工业现场真正容易出问题的是超时、断线、重复触发、结果未确认和权限异常。提前定义这些边界,可以减少后期返工和现场风险。
### 问:项目现在能不能直接上产线?
答:不能仅凭本地模拟结果判断可以上产线。需要完成真实 PLC、双扫描枪、文件服务、网络权限、异常恢复和连续生产稳定性验证后,才能按现场验收结果决定。
---
## 现场演示检查清单
### 开始前
- 确认浏览器打开的是 `docs/axiomron-pcbcheck-report-draft.html`。
- 确认页面能看到 12 个圆点和“下一页”按钮。
- 确认第 9 页的两个调试按钮可以点击。
- 不打开真实 PLC、真实扫描枪、真实文件服务器或生产目录。
### 演示中
- 第 2 页讲完初始需求后,停顿一下,强调“这已经能做原型,但还不能直接上现场”。
- 第 3~6 页是本次新增主线,要讲清“追问如何变成完整计划”。
- 第 8 页展示项目运行证据,但明确它不是现场验收。
- 第 9 页先点“收到 PLC 确认”,再点“等待确认超时”。
- 第 11 页主动说明模拟测试和现场验收的区别。
### 结束后
- 用“会提问、会拆解、会验证”收尾。
- 如果时间不足,优先保留第 2、3、5、9、11、12 页。
- 如果时间充足,再完整讲第 4、6、7、8、10 页。

View File

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 294 KiB

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#f4f7fb" />
<title>3 个月,我从写 if-else 到 Vibe Coding</title>
<title>从初学者需求到完整项目计划 · AxiOmron PCBCheck</title>
<style>
:root {
--ink: #10233f;
@@ -765,6 +765,248 @@
font-weight: 800;
}
.requirement-layout,
.question-layout,
.contract-layout,
.plan-layout,
.phase-layout {
display: grid;
gap: clamp(24px, 4vw, 60px);
align-items: center;
}
.requirement-layout { grid-template-columns: .72fr 1.28fr; }
.question-layout { grid-template-columns: .72fr 1.28fr; }
.contract-layout,
.plan-layout,
.phase-layout { grid-template-columns: 1fr; }
.requirement-intro,
.question-intro {
align-self: center;
}
.quote-card {
margin-top: 24px;
padding: 22px 24px;
border-left: 5px solid var(--orange);
border-radius: 0 16px 16px 0;
color: var(--ink);
background: var(--orange-soft);
box-shadow: 0 10px 28px rgba(216, 107, 24, .08);
font-size: clamp(17px, 1.25vw, 20px);
font-weight: 700;
line-height: 1.65;
}
.small-note {
margin: 18px 0 0;
color: var(--muted);
font-size: 14px;
line-height: 1.6;
}
.requirement-grid,
.question-grid,
.contract-grid,
.plan-grid,
.phase-grid,
.boundary-row {
display: grid;
gap: 12px;
}
.requirement-grid { grid-template-columns: repeat(3, 1fr); }
.question-grid,
.contract-grid,
.plan-grid { grid-template-columns: repeat(2, 1fr); }
.phase-grid { grid-template-columns: repeat(4, 1fr); }
.boundary-row { grid-template-columns: repeat(3, 1fr); margin-top: 18px; }
.requirement-card,
.question-card,
.contract-card,
.plan-card,
.phase-card,
.boundary-card {
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255, 255, 255, .94);
box-shadow: 0 10px 28px rgba(28, 58, 90, .06);
}
.requirement-card {
min-height: 92px;
padding: 16px;
}
.requirement-card strong,
.question-card strong,
.contract-card strong,
.plan-card strong,
.phase-card strong,
.boundary-card strong {
display: block;
margin-bottom: 7px;
color: var(--ink);
font-size: 15px;
line-height: 1.35;
}
.requirement-card span,
.question-card p,
.contract-card p,
.plan-card p,
.phase-card p,
.boundary-card p {
margin: 0;
color: var(--ink-soft);
font-size: 13px;
line-height: 1.5;
}
.requirement-card .card-number,
.phase-card .card-number {
display: block;
margin-bottom: 10px;
color: var(--primary);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 800;
letter-spacing: .08em;
}
.question-intro .quote-card {
border-left-color: var(--primary);
color: var(--primary-dark);
background: var(--primary-soft);
}
.question-card,
.contract-card,
.plan-card {
min-height: 148px;
padding: 20px;
}
.question-card::before,
.contract-card::before,
.plan-card::before {
display: block;
width: 30px;
height: 4px;
margin-bottom: 18px;
border-radius: 999px;
background: var(--primary);
content: "";
}
.question-card:nth-child(2)::before,
.contract-card:nth-child(2)::before,
.plan-card:nth-child(2)::before { background: var(--teal); }
.question-card:nth-child(3)::before,
.contract-card:nth-child(3)::before,
.plan-card:nth-child(3)::before { background: var(--orange); }
.question-card:nth-child(4)::before,
.contract-card:nth-child(4)::before,
.plan-card:nth-child(4)::before { background: #7b61a8; }
.card-kicker {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
.contract-flow {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 9px;
margin-top: 22px;
color: var(--primary-dark);
font-size: 13px;
font-weight: 800;
}
.contract-flow span {
padding: 9px 12px;
border: 1px solid #abd8ed;
border-radius: 999px;
background: var(--primary-soft);
}
.contract-flow i {
color: var(--muted);
font-style: normal;
}
.plan-card {
min-height: 168px;
}
.plan-card strong { font-size: 20px; }
.plan-card p { font-size: 15px; line-height: 1.6; }
.plan-foot,
.phase-foot {
margin: 20px auto 0;
color: var(--ink-soft);
font-size: clamp(15px, 1.2vw, 18px);
line-height: 1.6;
text-align: center;
}
.phase-card {
min-height: 134px;
padding: 17px;
}
.phase-card strong { font-size: 15px; }
.phase-card p { font-size: 12px; }
.phase-card:nth-child(2) { background: #f1faf8; }
.phase-card:nth-child(3) { background: #fff8f1; }
.phase-card:nth-child(4) { background: #f6f2fb; }
.phase-card:nth-child(5) { background: #f3f8fc; }
.phase-card:nth-child(6) { background: #f5faf8; }
.phase-card:nth-child(7) { background: #fff8f1; }
.phase-foot {
padding: 13px 18px;
border-left: 4px solid var(--teal);
background: var(--teal-soft);
}
.boundary-card {
padding: 17px 18px;
}
.boundary-card strong { font-size: 16px; }
.boundary-card p { font-size: 13px; }
.boundary-card:nth-child(2) { border-top: 4px solid var(--teal); }
.boundary-card:nth-child(3) { border-top: 4px solid var(--orange); }
.boundary-note {
margin-top: 20px;
padding: 15px 18px;
border: 1px solid #abd8ed;
border-radius: 14px;
color: var(--primary-dark);
background: var(--primary-soft);
font-size: 15px;
line-height: 1.6;
}
.rule-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@@ -886,13 +1128,20 @@
.project-layout,
.project-evidence-layout,
.debug-layout,
.finish-layout { grid-template-columns: 1fr; }
.finish-layout,
.requirement-layout,
.question-layout { grid-template-columns: 1fr; }
.compare-grid { grid-template-columns: 1fr; }
.compare-arrow { width: auto; height: 30px; transform: rotate(90deg); }
.level-grid { grid-template-columns: 1fr; }
.method-grid { grid-template-columns: repeat(2, 1fr); }
.evidence-row { grid-template-columns: 1fr; }
.rule-grid { grid-template-columns: 1fr; }
.requirement-grid,
.question-grid,
.contract-grid,
.plan-grid,
.phase-grid { grid-template-columns: repeat(2, 1fr); }
.brand-mark { top: 22px; }
.slide-footer span:first-child { display: none; }
.slide-footer { justify-content: flex-end; }
@@ -903,7 +1152,13 @@
h2 { font-size: clamp(32px, 9vw, 46px); }
.method-grid,
.logic-compare,
.scenario-buttons { grid-template-columns: 1fr; }
.scenario-buttons,
.requirement-grid,
.question-grid,
.contract-grid,
.plan-grid,
.phase-grid,
.boundary-row { grid-template-columns: 1fr; }
.flow-item { grid-template-columns: 42px 1fr; }
.flow-item span:last-child { display: none; }
.slide-footer { display: none; }
@@ -930,6 +1185,22 @@
.test-row { padding-top: 6px; padding-bottom: 6px; }
.rule-grid { margin-top: 20px; }
.rule-card { padding: 17px; }
.requirement-card { min-height: 78px; padding: 13px; }
.question-card,
.contract-card,
.plan-card { min-height: 126px; padding: 16px; }
.question-card::before,
.contract-card::before,
.plan-card::before { margin-bottom: 12px; }
.plan-layout { gap: 14px; }
.plan-layout h2 { margin-bottom: 8px; }
.plan-card { min-height: 112px; padding: 14px; }
.plan-card::before { margin-bottom: 9px; }
.plan-card strong { font-size: 17px; }
.plan-card p { font-size: 13px; line-height: 1.45; }
.plan-foot { margin-top: 8px; font-size: 14px; line-height: 1.35; }
.phase-card { min-height: 108px; padding: 13px; }
.phase-foot { margin-top: 14px; padding: 10px 13px; font-size: 14px; }
.slide-footer { margin-top: 16px; }
}
@@ -950,7 +1221,7 @@
<div class="brand-mark">AI PROGRAMMING · FIELD CASE</div>
<main class="deck" id="deck" aria-live="polite">
<section class="slide is-active" data-slide="0" aria-label="第 1 页:封面">
<section class="slide is-active" data-slide="0" data-title="封面" aria-label="第 1 页:封面">
<div class="slide-body cover-grid">
<div>
<p class="eyebrow">3 MONTHS · ONE REAL PROJECT</p>
@@ -960,39 +1231,140 @@
<aside class="cover-card" aria-label="汇报信息">
<span class="label">CASE STUDY</span>
<strong>AxiOmron<br>PCBCheck</strong>
<div class="meta-row"><span>项目</span><b>工业上位机</b></div>
<div class="meta-row"><span>重点</span><b>上手 · 调试 · 边界</b></div>
<div class="meta-row"><span>时长</span><b>8–10 MIN</b></div>
</aside>
</div>
<footer class="slide-footer"><span>一段真实的 AI 编程实践</span><span class="slide-index">01 / 08</span></footer>
<div class="meta-row"><span>项目</span><b>工业上位机</b></div>
<div class="meta-row"><span>重点</span><b>上手 · 调试 · 边界</b></div>
<div class="meta-row"><span>时长</span><b>10–12 MIN</b></div>
</aside>
</div>
<footer class="slide-footer"><span>一段真实的 AI 编程实践</span><span class="slide-index">01 / 12</span></footer>
</section>
<section class="slide" data-slide="1" aria-label="第 2 页:什么是 Vibe Coding">
<section class="slide" data-slide="1" data-title="初始需求" aria-label="第 2 页:初始需求">
<div class="slide-body requirement-layout">
<div class="requirement-intro">
<p class="eyebrow">01 / THE FIRST REQUEST</p>
<h2>最开始,我只想让它<br><span class="orange">先跑起来</span>。</h2>
<div class="quote-card">“我需要写一个程序:PCB 到位后扫码,根据条码检查文件是否存在,再把结果返回给 PLC。”</div>
<p class="small-note">这就是《开发流程.txt》里的最初版本:目标很清楚,但工程边界还没有被说出来。</p>
</div>
<div class="requirement-grid" aria-label="初始需求清单">
<article class="requirement-card"><span class="card-number">01</span><strong>PCB 到位后开始处理</strong><span>收到触发,进入本轮任务。</span></article>
<article class="requirement-card"><span class="card-number">02</span><strong>读取一个条码</strong><span>拿到 PCB 身份信息。</span></article>
<article class="requirement-card"><span class="card-number">03</span><strong>检查对应文件</strong><span>根据条码查找指定文件。</span></article>
<article class="requirement-card"><span class="card-number">04</span><strong>文件存在显示正常</strong><span>给出 OK 结果。</span></article>
<article class="requirement-card"><span class="card-number">05</span><strong>文件不存在显示异常</strong><span>给出 NG 结果。</span></article>
<article class="requirement-card"><span class="card-number">06</span><strong>页面显示当前状态</strong><span>看到条码、结果和日志。</span></article>
<article class="requirement-card"><span class="card-number">07</span><strong>保存最近处理记录</strong><span>之后可以回看过程。</span></article>
<article class="requirement-card"><span class="card-number">08</span><strong>先使用模拟数据</strong><span>不一开始连接真实设备。</span></article>
<article class="requirement-card"><span class="card-number">09</span><strong>模拟正常、异常、故障</strong><span>页面提供可控演示入口。</span></article>
</div>
</div>
<footer class="slide-footer"><span>初版需求:先把业务主线讲清楚</span><span class="slide-index">02 / 12</span></footer>
</section>
<section class="slide" data-slide="2" data-title="第一次对话" aria-label="第 3 页:第一次对话">
<div class="slide-body question-layout">
<div class="question-intro">
<p class="eyebrow">02 / ASK BEFORE BUILDING</p>
<h2>真正开始写代码前,<br>我先问:<span class="accent">还缺什么?</span></h2>
<div class="quote-card">“还需要哪些信息才能开始写程序?”</div>
<p class="small-note">这一步把“能不能写”变成“哪些事实还没有确认”。</p>
</div>
<div class="question-grid" aria-label="第一次对话中需要补齐的信息">
<article class="question-card"><span class="card-kicker">01 · PLC</span><strong>触发和握手怎么定义?</strong><p>型号、协议、点位、保持/脉冲、复位、应答、超时和异常恢复。</p></article>
<article class="question-card"><span class="card-kicker">02 · SCANNERS</span><strong>两把扫码枪怎么返回?</strong><p>IP、端口、通信角色、结束符、编码、超时、双枪关系和断线行为。</p></article>
<article class="question-card"><span class="card-kicker">03 · FILE RULES</span><strong>什么样的文件算通过?</strong><p>条码格式、文件名规则、精确/模糊匹配、目录类型、空文件和正在写入的处理。</p></article>
<article class="question-card"><span class="card-kicker">04 · APP & RECORDS</span><strong>程序要怎样被使用和追溯?</strong><p>界面、配置、日志、记录、权限、模拟入口、部署方式和是否需要数据库。</p></article>
</div>
</div>
<footer class="slide-footer"><span>好的协作不是马上给答案,而是先把问题问完整</span><span class="slide-index">03 / 12</span></footer>
</section>
<section class="slide" data-slide="3" data-title="需求逐步完善" aria-label="第 4 页:需求逐步完善">
<div class="slide-body contract-layout">
<div>
<p class="eyebrow">03 / TURN QUESTIONS INTO CONTRACTS</p>
<h2>需求不再只是“做什么”,<br>还要说清楚<span class="teal">“边界在哪里”</span>。</h2>
</div>
<div class="contract-grid" aria-label="需求完善后的四类工程契约">
<article class="contract-card"><span class="card-kicker">PLC CONTRACT</span><strong>到位、复位、结果、应答</strong><p>定义一轮任务什么时候开始、什么时候结束,以及长保持信号如何避免重复触发。</p></article>
<article class="contract-card"><span class="card-kicker">SCANNER CONTRACT</span><strong>双路并发、超时、报文边界</strong><p>不把一次 TCP Receive 想当然地当成完整条码,明确无结束符和迟到结果的处理。</p></article>
<article class="contract-card"><span class="card-kicker">FILE CONTRACT</span><strong>路径安全、结果分类</strong><p>把条码清洗、文件存在、目录不可访问和路径越界区分成可追踪的结果。</p></article>
<article class="contract-card"><span class="card-kicker">APP CONTRACT</span><strong>状态可见、过程可回看</strong><p>把设备状态、当前任务、日志、统计和模拟场景放进可观察的界面闭环。</p></article>
</div>
<div class="contract-flow" aria-label="需求演进链路">
<span>功能描述</span><i>→</i><span>条件边界</span><i>→</i><span>失败策略</span><i>→</i><span>验收证据</span>
</div>
</div>
<footer class="slide-footer"><span>每补充一个边界,现场就少一个靠猜的地方</span><span class="slide-index">04 / 12</span></footer>
</section>
<section class="slide" data-slide="4" data-title="完整项目计划" aria-label="第 5 页:完整项目计划">
<div class="slide-body plan-layout">
<div>
<p class="eyebrow">04 / THE COMPLETE PROJECT PLAN</p>
<h2>对话的终点,<br>是一份<span class="accent">可以执行的计划</span>。</h2>
</div>
<div class="plan-grid" aria-label="完整项目计划的四个层面">
<article class="plan-card"><span class="card-kicker">01 · GOAL</span><strong>目标</strong><p>让 PCB 从到位、扫码、文件校验到 PLC 结果反馈,形成一条可解释、可追溯的业务闭环。</p></article>
<article class="plan-card"><span class="card-kicker">02 · WORKFLOW</span><strong>流程</strong><p>到位触发 → 双枪扫描 → 条码校验 → 文件判断 → 结果保持 → 应答/复位 → 记录完成。</p></article>
<article class="plan-card"><span class="card-kicker">03 · ARCHITECTURE</span><strong>架构</strong><p>当前项目落地为 .NET 8 + WPF;设备通信、流程编排、界面状态和配置日志保持清晰边界。</p></article>
<article class="plan-card"><span class="card-kicker">04 · ACCEPTANCE</span><strong>测试与验收</strong><p>单元测试和 Fake 设备先验证逻辑,再用真实 PLC、扫描枪和文件服务完成现场联调。</p></article>
</div>
<p class="plan-foot"><strong>计划的价值:</strong>把“我想要一个程序”翻译成目标、状态、接口、阶段、测试和交付门槛。</p>
</div>
<footer class="slide-footer"><span>完整计划不是凭空出现的,而是对话逐项补齐后的结果</span><span class="slide-index">05 / 12</span></footer>
</section>
<section class="slide" data-slide="5" data-title="开发阶段路线图" aria-label="第 6 页:开发阶段路线图">
<div class="slide-body phase-layout">
<div>
<p class="eyebrow">05 / BUILD IN RISK ORDER</p>
<h2>计划被拆成七个小闭环,<br>每一步都有<span class="teal">可以验证的产物</span>。</h2>
</div>
<div class="phase-grid" aria-label="七个开发阶段">
<article class="phase-card"><span class="card-number">01</span><strong>需求与协议确认</strong><p>点位、握手、报文、结果码和文件规则。</p></article>
<article class="phase-card"><span class="card-number">02</span><strong>项目骨架</strong><p>WPF、配置、日志、依赖注入和基础状态。</p></article>
<article class="phase-card"><span class="card-number">03</span><strong>PLC 通信</strong><p>读写、超时、断线、重连和模拟接口。</p></article>
<article class="phase-card"><span class="card-number">04</span><strong>双扫描枪</strong><p>并发触发、无结束符、首个有效结果。</p></article>
<article class="phase-card"><span class="card-number">05</span><strong>流程编排</strong><p>任务去重、文件安全、结果保持和恢复。</p></article>
<article class="phase-card"><span class="card-number">06</span><strong>状态界面</strong><p>设备、任务、统计、日志和操作权限。</p></article>
<article class="phase-card"><span class="card-number">07</span><strong>测试与交付</strong><p>单测、Fake 流程、异常测试和现场验收。</p></article>
</div>
<p class="phase-foot">计划不是一次写死:事实确认后可以调整实现,但每次调整都要回到边界、证据和验收标准。</p>
</div>
<footer class="slide-footer"><span>AI 加快每个小闭环,我负责决定闭环是否成立</span><span class="slide-index">06 / 12</span></footer>
</section>
<section class="slide" data-slide="6" data-title="Vibe Coding 上手难度" aria-label="第 7 页:Vibe Coding 上手难度">
<div class="slide-body">
<p class="eyebrow">01 / WHAT IS VIBE CODING</p>
<h2>改变的不是“要不要思考”,<br>而是<span class="accent">把时间花在哪里</span>。</h2>
<div class="compare-grid">
<article class="compare-card">
<span class="mini-label">过去</span>
<h3>自己翻译成每一行代码</h3>
<p>需求 → 查资料 → 写代码 → 运行 → 修改</p>
<p class="eyebrow">06 / HOW HARD IS IT</p>
<h2>上手不难,<br>难的是从“能跑”走到<span class="orange">“可靠”</span>。</h2>
<div class="level-grid">
<article class="level-card">
<span class="level-badge">门槛低</span>
<h3>做出原型</h3>
<p>会描述需求、会运行项目,就能很快看到第一个结果。</p>
</article>
<div class="compare-arrow" aria-hidden="true">→</div>
<article class="compare-card is-now">
<span class="mini-label">现在</span>
<h3>先描述目标,再检查结果</h3>
<p>讲清需求 → AI 实现 → 我来验证 → 继续迭代</p>
<article class="level-card">
<span class="level-badge">需要方法</span>
<h3>做到可用</h3>
<p>要会拆小任务、补充上下文,并检查 AI 实际改了什么。</p>
</article>
<article class="level-card">
<span class="level-badge">仍需专业判断</span>
<h3>做到可靠</h3>
<p>需要理解业务风险,并验证断网、超时和错误输入等异常场景。</p>
</article>
</div>
<div class="analogy"><strong>一个简单比喻:</strong>像和一名速度很快的程序员结对工作——AI 负责加速,我负责方向与验收。</div>
<p class="level-footer"><strong>它降低的是写代码的门槛,</strong>没有消除软件工程本身的难度。</p>
</div>
<footer class="slide-footer"><span>Vibe Coding 不是一句话生成全部,而是持续对话与验证</span><span class="slide-index">02 / 08</span></footer>
<footer class="slide-footer"><span>有代码基础,上手会更快;没有代码基础,也可以从小工具开始</span><span class="slide-index">07 / 12</span></footer>
</section>
<section class="slide" data-slide="2" aria-label="第 3 页:项目案例">
<section class="slide" data-slide="7" data-title="实际项目运行" aria-label="第 8 页:实际项目运行">
<div class="slide-body">
<p class="eyebrow">02 / THE PROJECT</p>
<p class="eyebrow">07 / THE PROJECT RUNS</p>
<h2>这不是概念图,<br><span class="teal">程序真的运行起来了</span>。</h2>
<div class="project-evidence-layout">
<figure class="app-shot-card">
@@ -1014,39 +1386,13 @@
</div>
</div>
</div>
<footer class="slide-footer"><span>运行界面是本地证据,不等同于真实 PLC / SFTP 现场验收</span><span class="slide-index">03 / 08</span></footer>
<footer class="slide-footer"><span>运行界面是本地证据,不等同于真实 PLC / SFTP 现场验收</span><span class="slide-index">08 / 12</span></footer>
</section>
<section class="slide" data-slide="3" aria-label="第 4 页:上手难度">
<div class="slide-body">
<p class="eyebrow">03 / HOW HARD IS IT</p>
<h2>上手不难,<br>难的是从“能跑”走到<span class="orange">“可靠”</span>。</h2>
<div class="level-grid">
<article class="level-card">
<span class="level-badge">门槛低</span>
<h3>做出原型</h3>
<p>会描述需求、会运行项目,就能很快看到第一个结果。</p>
</article>
<article class="level-card">
<span class="level-badge">需要方法</span>
<h3>做到可用</h3>
<p>要会拆小任务、补充上下文,并检查 AI 实际改了什么。</p>
</article>
<article class="level-card">
<span class="level-badge">仍需专业判断</span>
<h3>做到可靠</h3>
<p>需要理解业务风险,并验证断网、超时和错误输入等异常场景。</p>
</article>
</div>
<p class="level-footer"><strong>它降低的是写代码的门槛,</strong>没有消除软件工程本身的难度。</p>
</div>
<footer class="slide-footer"><span>有代码基础,上手会更快;没有代码基础,也可以从小工具开始</span><span class="slide-index">04 / 08</span></footer>
</section>
<section class="slide" data-slide="4" aria-label="第 5 页:真实调试案例">
<section class="slide" data-slide="8" data-title="真实调试案例" aria-label="第 9 页:真实调试案例">
<div class="slide-body debug-layout">
<div>
<p class="eyebrow">04 / A REAL DEBUGGING CASE</p>
<p class="eyebrow">08 / A REAL DEBUGGING CASE</p>
<h2>信号写成功了,<br>为什么<span class="orange">文件不能删</span>?</h2>
<div class="logic-compare" aria-label="修改前后的代码逻辑对比">
<article class="logic-panel is-wrong">
@@ -1080,12 +1426,12 @@ if (ackReceived)
</div>
</section>
</div>
<footer class="slide-footer"><span>调试结论:协议成功 ≠ 业务完成</span><span class="slide-index">05 / 08</span></footer>
<footer class="slide-footer"><span>调试结论:协议成功 ≠ 业务完成</span><span class="slide-index">09 / 12</span></footer>
</section>
<section class="slide" data-slide="5" aria-label="第 6 页:五步调试法">
<section class="slide" data-slide="9" data-title="五步调试法" aria-label="第 10 页:五步调试法">
<div class="slide-body">
<p class="eyebrow">05 / DEBUG WITH AI</p>
<p class="eyebrow">09 / DEBUG WITH AI</p>
<h2>程序出错时,<br>我用这套<span class="accent">五步调试法</span>。</h2>
<div class="method-grid">
<article class="step-card"><span class="step-number">01</span><h3>先复现</h3><p>说明做了什么、期望什么、实际发生什么。</p></article>
@@ -1104,37 +1450,43 @@ if (ackReceived)
</section>
</div>
</div>
<footer class="slide-footer"><span>证据越完整,AI 越不容易靠猜</span><span class="slide-index">06 / 08</span></footer>
<footer class="slide-footer"><span>证据越完整,AI 越不容易靠猜</span><span class="slide-index">10 / 12</span></footer>
</section>
<section class="slide" data-slide="6" aria-label="第 7 页:注意事项">
<section class="slide" data-slide="10" data-title="工程边界与验收" aria-label="第 11 页:工程边界与验收">
<div class="slide-body">
<p class="eyebrow">06 / FOUR THINGS TO REMEMBER</p>
<h2>Vibe Coding 很快,<br>但这四条<span class="orange">不能省</span>。</h2>
<p class="eyebrow">10 / BOUNDARIES & ACCEPTANCE</p>
<h2>Vibe Coding 很快,<br>但这些<span class="orange">不能省</span>。</h2>
<div class="rule-grid">
<article class="rule-card"><span class="rule-no">01</span><div><h3>任务要拆小</h3><p>每一步都应该能运行、能观察、能撤回。</p></div></article>
<article class="rule-card"><span class="rule-no">02</span><div><h3>修改要看懂</h3><p>至少确认改了哪些文件、为什么改、影响到哪里。</p></div></article>
<article class="rule-card"><span class="rule-no">03</span><div><h3>本地不等于现场</h3><p>模拟测试不能替代真实设备、真实网络和实际权限。</p></div></article>
<article class="rule-card"><span class="rule-no">03</span><div><h3>模拟不等于现场</h3><p>Fake 设备只能先验证逻辑,不能替代真实 PLC、扫描枪和文件服务。</p></div></article>
<article class="rule-card"><span class="rule-no">04</span><div><h3>高风险动作默认关闭</h3><p>删除、付款、放行等动作必须有明确条件和可控开关。</p></div></article>
</div>
<div class="boundary-row" aria-label="交付边界">
<article class="boundary-card"><strong>异常可恢复</strong><p>断线、超时、目录不可访问,都要有状态、日志和恢复策略。</p></article>
<article class="boundary-card"><strong>过程可追溯</strong><p>每一轮任务都能回看输入、判断、结果和失败原因。</p></article>
<article class="boundary-card"><strong>现场才算验收</strong><p>真实点位、通信边界、权限和连续生产稳定性必须现场确认。</p></article>
</div>
<div class="boundary-note"><strong>交付门槛:</strong>本地构建与测试通过只是软件证据;真实设备和现场权限通过,才是生产验收。</div>
</div>
<footer class="slide-footer"><span>AI 可以给建议,但业务责任和最终判断仍然属于人</span><span class="slide-index">07 / 08</span></footer>
<footer class="slide-footer"><span>AI 可以给建议,但业务责任和最终判断仍然属于人</span><span class="slide-index">11 / 12</span></footer>
</section>
<section class="slide" data-slide="7" aria-label="第 8 页:总结">
<section class="slide" data-slide="11" data-title="总结" aria-label="第 12 页:总结">
<div class="slide-body finish-layout">
<div>
<p class="eyebrow">07 / TAKEAWAY</p>
<h2>上手不难,<br>真正的能力是<span class="accent">会验证</span>。</h2>
<p class="finish-statement">AI 没有让我不再需要技术,而是让我把更多时间放在理解问题、判断风险和验证结果上。</p>
<p class="eyebrow">11 / TAKEAWAY</p>
<h2>从一句话需求,<br>到<span class="accent">可验证的完整计划</span>。</h2>
<p class="finish-statement">AI 没有替我完成工程判断,而是帮我更快地提问、拆解、实现和验证,最后把一个模糊需求变成能运行、能追溯、知道边界的项目。</p>
</div>
<div class="takeaway-list">
<article class="takeaway-card"><b>会描述</b><span>把目标、条件和期望结果讲清楚</span></article>
<article class="takeaway-card"><b>会调试</b><span>用日志和测试逐步排除原因</span></article>
<article class="takeaway-card"><b>有边界</b><span>知道什么时候不能完全相信 AI</span></article>
<article class="takeaway-card"><b>会提问</b><span>把需求里的空白和风险问出来</span></article>
<article class="takeaway-card"><b>会拆解</b><span>把自然语言变成流程、阶段和验收标准</span></article>
<article class="takeaway-card"><b>会验证</b><span>把本地证据与真实现场验收分开</span></article>
</div>
</div>
<footer class="slide-footer"><span>谢谢 · AxiOmron.PcbCheck</span><span class="slide-index">08 / 08</span></footer>
<footer class="slide-footer"><span>谢谢 · AxiOmron.PcbCheck</span><span class="slide-index">12 / 12</span></footer>
</section>
</main>
@@ -1156,6 +1508,21 @@ if (ackReceived)
const scenarioResult = document.getElementById('scenarioResult');
let activeSlideIndex = 0;
/**
* 根据页面数量统一更新页脚编号和无障碍页面标题。
* @returns {void} 只同步每个页面的展示元数据,不返回数据。
*/
function syncSlideMetadata() {
const totalLabel = String(slides.length).padStart(2, '0');
slides.forEach((slide, index) => {
const pageNumber = String(index + 1).padStart(2, '0');
const title = slide.dataset.title || `第 ${index + 1} 页`;
const indexLabel = slide.querySelector('.slide-index');
if (indexLabel) indexLabel.textContent = `${pageNumber} / ${totalLabel}`;
slide.setAttribute('aria-label', `第 ${index + 1} 页:${title}`);
});
}
/**
* 创建汇报页面圆点导航,并为每个圆点绑定跳转行为。
* @returns {void} 只更新页面导航结构,不返回数据。
@@ -1247,6 +1614,7 @@ if (ackReceived)
if (event.key === 'End') setSlide(slides.length - 1);
});
syncSlideMetadata();
createDots();
setSlide(0);
</script>

104
docs/开发流程.txt Normal file
View File

@@ -0,0 +1,104 @@
1. PCB 到位后开始处理;
2. 读取一个条码;
3. 根据条码检查对应文件是否存在;
4. 文件存在显示“正常”;
5. 文件不存在显示“异常”;
6. 页面显示当前状态、条码、处理结果和日志;
7. 保存最近的处理记录;
8. 先使用模拟数据,不连接真实设备;
9. 页面上增加按钮,让我可以模拟“正常”“异常”“设备故障”三种情况。
我需要写一个程序完成下面的功能,
跟欧姆龙的plc通信,等待plc传递过来信号,通知我pcb板到位了.我触发两个网络的扫描枪扫描pcb板的二维码
根据条码检查对应指定文件夹里的文件是否存在,然后把结果返回给plc.
还需要哪些信息才能开始写程序?
主人,这个功能可以开始设计,但要先确认以下信息,尤其是 PLC 点位和握手流程。
PLC 信息PLC 型号:CP、CJ、CS、NJ、NX 等
通信协议:FINS/TCP、FINS/UDP、EtherNet/IP 或 OPC UA
PLC IP、端口、网络号、节点号
“PCB 到位”信号地址和数据类型
上位机需要写回的地址:扫描中
扫描成功
文件存在
文件不存在
条码错误
扫描超时
双枪条码不一致
PLC 对信号的要求:保持型、脉冲型,还是需要上位机清零
完整握手时序:触发、响应、复位、超时、异常恢复
最好提供 PLC 点位表或梯形图截图。不能只凭历史地址开发,现场需确认真实点位。
两把网络扫描枪信息品牌和型号
通信方式:TCP Server、TCP Client、UDP,还是串口转网口
每把扫描枪的 IP 和端口
条码结束符:CR、LF、CRLF 或无结束符
条码编码:ASCII、UTF-8 等
扫描超时时间
两把枪的作用:是否同时扫描同一个 PCB
是否分别扫描正反面或两个位置
两个条码是否必须一致
两把枪是否有先后顺序
扫描失败、重复扫描、断线后的处理方式
最好提供扫描枪通信协议手册和一组实际原始报文。
条码与文件规则PCB 二维码示例
条码长度、格式、是否区分大小写
条码如何转换成文件名或文件夹名
文件的完整路径规则
文件扩展名,例如 .csv、.json、.xml、.jpg
文件夹是本地目录、共享目录,还是 SFTP
文件存在的判断条件:精确文件名
模糊匹配
必须匹配一个文件
匹配多个文件也算成功
文件存在但为空、损坏或正在写入时如何处理
是否需要在 PLC 确认后删除或移动文件
返回 PLC 的结果定义
请明确每个结果对应的 PLC 值,例如:
结果 PLC 值
空闲 0
扫描中 1
文件存在/OK 2
文件不存在/NG 3
条码不一致 4
通信异常 5
还需要确认结果是写入一个字、多个 bit,还是写入字符串。
程序形态使用 WPF 还是 WinForms
目标 .NET 版本
是否需要操作员界面
是否需要显示 PLC、两把扫描枪和文件服务器状态
是否需要配置界面
日志保存位置和保留天数
是否需要开机自启动
是否部署为普通桌面程序还是 Windows Service
是否需要数据库保存扫描记录
我建议默认采用:
WPF + .NET 8
PLC、扫描枪、文件检查分别封装成独立通信服务
后台任务监听 PLC,不阻塞界面
所有 IP、端口、地址、超时和文件规则放入配置文件
先用模拟 PLC、模拟扫描枪和临时目录验证流程,再进行现场 PLC/扫描枪联调
最少需要提供的资料
你可以先给我这些,我就能开始搭建第一版:
PLC 型号、IP、通信协议
PLC 点位表或触发/返回时序
两把扫描枪型号、IP、端口
两把扫描枪实际扫描结果示例
PCB 条码示例
文件夹路径示例和文件名示例
OK/NG 返回值定义
希望使用 WPF 还是 WinForms
已有欧姆龙项目经验表明,PLC 网络写入成功不等于 PLC 业务确认,因此握手中必须明确应答信号、超时和复位流程;本地模拟验证也不能替代真实 PLC、扫描枪和文件服务器验收。
主人 任务完成了

682
docs/纯plan.md Normal file
View File

@@ -0,0 +1,682 @@
# 欧姆龙 PLC PCB 条码文件判定系统项目计划
## 1. 项目目标
开发一个 Windows WinForms 上位机程序,实现:
1. 通过 FINS/TCP 与欧姆龙 PLC 通信。
2. 等待 PLC 发出 PCB 到位信号。
3. 同时触发两把 TCP Server 扫描枪。
4. 两把扫描枪分别扫描 PCB 正面和反面,任意一把返回有效条码即可。
5. 根据条码检查本地目录中的 `<条码>.txt` 文件。
6. 将检查结果返回 PLC。
7. 通过 WinForms 界面显示 PLC、扫描枪、文件检查和当前任务状态。
8. 对通信超时、断线、重复触发和异常情况进行处理并记录日志。
## 2. 当前需求基线
### PLC 通信
| 项目 | 配置 |
|---|---|
| 协议 | Omron FINS/TCP |
| PLC IP | `192.168.2.100` |
| 端口 | `9600` |
| UnitAddress | `0` |
| SourceNodeAddress | `0` |
| EndianFormat | `ABCD` |
| PLC 区域 | D 区 |
### PLC 点位
| 名称 | 地址 | 类型 | 初步用途 |
|---|---:|---|---|
| PcbArrived | D101 | Bool | PCB 到位信号 |
| PlcReset | D102 | Bool | PLC 复位本轮流程 |
| PlcAckRelease | D103 | Bool | PLC 确认放行结果 |
| PcBusy | D51 | Bool | 上位机忙碌状态 |
| ReleasePermit | D52 | Bool | 是否允许放行 |
| ResultCode | D401 | Word | 扫描和文件检查结果 |
初版按 D 区字值 `0/1` 处理 Bool:
```text
0 = False
1 = True
```
如果 PLC 实际使用 `D101.00` 这类 bit 位,需要在开发前调整 FINS 读写实现。
### 扫描枪
| 项目 | 扫描枪 1 | 扫描枪 2 |
|---|---|---|
| IP | `192.168.2.13` | `192.168.2.14` |
| 端口 | `2001` | `2001` |
| 设备角色 | PCB 正面 | PCB 反面 |
| 通信模式 | TCP Server | TCP Server |
| 上位机角色 | TCP Client | TCP Client |
| 触发指令 | `T` | `T` |
| 返回结束符 | 无 | 无 |
| 超时时间 | 2000 ms | 2000 ms |
### 文件检查
```text
根目录:D:\test
文件规则:D:\test\<条码>.txt
```
例如条码为:
```text
PCB202608040001
```
检查文件:
```text
D:\test\PCB202608040001.txt
```
## 3. 业务流程
```mermaid
flowchart TD
A["等待 D101 到位信号"] --> B{"D101 是否为 True"}
B -- 否 --> A
B -- 是 --> C["生成任务号并锁存本轮触发"]
C --> D["写入 D51 = 1,进入扫描中"]
D --> E["并行连接两把扫描枪并发送 T"]
E --> F{"是否收到有效条码"}
F -- 是 --> G["记录第一条有效条码"]
F -- 否且未超时 --> E
F -- 超时 --> H["写入扫描超时结果"]
G --> I["检查 D:\\test\\条码.txt"]
I --> J{"文件是否存在"}
J -- 是 --> K["D401 写入通过结果,D52 = 1"]
J -- 否 --> L["D401 写入不通过结果,D52 = 0"]
K --> M["等待 PLC 确认或复位"]
L --> M
H --> M
M --> N{"D102 PlcReset 是否为 True"}
N -- 否 --> M
N -- 是 --> O["清除 D51、D52 和本轮结果"]
O --> P["等待 D101 恢复 False"]
P --> A
```
## 4. 详细握手策略
### 等待到位
- 程序持续读取 `D101`。
- `D101=True` 时只创建一次任务。
- 在本轮任务结束前,即使 `D101` 一直保持 True,也不能重复触发扫描。
- 程序重启时,如果发现 `D101` 已经为 True,默认不立即重复处理,等待 PLC 先复位到 False。
### 扫描阶段
- 设置 `D51=1`。
- 两把扫描枪并行发送 `T`。
- 任意一把返回非空条码后结束等待。
- 另一把扫描枪的迟到结果记录为本轮无效数据,不参与下一轮。
- 两把扫描枪都无返回时,2000 ms 后判定扫描超时。
### 文件判断阶段
- 对条码进行去除空白、长度检查和非法文件名字符检查。
- 禁止条码产生 `..`、路径分隔符或越过 `D:\test` 的路径。
- 使用精确路径检查 `<根目录>\<条码>.txt`。
- 文件存在即判定为允许放行。
- 文件不存在即判定为不允许放行。
### 结果保持阶段
- `D401` 结果写入后保持。
- 文件存在时 `D52=1`。
- 文件不存在或扫描失败时 `D52=0`。
- `D51` 在本轮任务期间保持为 1。
- 收到 `D102=True` 后清除本轮输出。
- `D103` 用于记录 PLC 是否确认放行结果;其是否作为必须等待的条件,需要 PLC 程序最终确认。
## 5. 建议结果编码
结果编码必须最终与 PLC 程序确认,建议如下:
| ResultCode | 含义 |
|---:|---|
| 0 | 空闲 |
| 1 | 扫描中 |
| 2 | 文件存在,允许放行 |
| 3 | 文件不存在,不允许放行 |
| 4 | 两把扫描枪均超时 |
| 5 | 扫描枪通信异常 |
| 6 | 条码为空或格式错误 |
| 7 | PLC 通信异常 |
| 8 | PLC 复位 |
| 9 | 系统异常 |
建议 `ReleasePermit` 只表达一个含义:
```text
D52 = 1:允许放行
D52 = 0:不允许放行
```
具体 `ResultCode` 用于区分“不存在文件”“扫描超时”“通信异常”等原因。
## 6. 系统架构
建议采用分层结构:
```text
src/
PcbCheck.Desktop/
PcbCheck.Application/
PcbCheck.Domain/
PcbCheck.Devices/
PcbCheck.Infrastructure/
tests/
PcbCheck.Tests/
PcbCheck.IntegrationTests/
docs/
architecture.md
plc-protocol.md
scanner-protocol.md
deployment.md
acceptance-checklist.md
```
### PcbCheck.Desktop
负责:
- WinForms 窗体
- 当前状态显示
- 设备连接状态显示
- 当前条码显示
- 文件检查结果显示
- 任务耗时和最近结果显示
- 错误提示
- 手动连接、断开和复位操作
界面层不能直接拼装 FINS 报文或操作 TCP 流。
### PcbCheck.Application
负责:
- PCB 检测流程编排
- PLC 到位触发处理
- 双扫描枪并行扫描
- 条码选择策略
- 文件检查调用
- PLC 结果写回
- 超时和异常状态处理
- 任务去重和复位控制
核心流程建议命名为:
```text
PcbInspectionWorkflow
```
### PcbCheck.Domain
负责:
- 任务状态
- 设备状态
- 扫描结果
- 文件检查结果
- PLC 结果编码
- 业务异常类型
该层不引用 WinForms、TCP、FINS、文件系统或第三方 SDK。
### PcbCheck.Devices
负责:
- FINS/TCP PLC 客户端
- TCP 扫描枪客户端
- Fake PLC
- Fake 扫描枪
- 设备连接状态和重连机制
建议接口:
```text
IFinsPlcClient
ITcpScannerClient
IIndustrialDevice
```
### PcbCheck.Infrastructure
负责:
- 本地文件检查
- 配置加载
- 日志
- 任务记录
- 文件路径安全校验
- 系统时间和基础设施封装
## 7. 扫描枪无结束符处理方案
扫描枪返回不带结束符,不能简单地认为一次 TCP `Receive` 就是一条完整条码,因为 TCP 可能出现:
- 一条报文拆成多次接收;
- 多条报文合并接收;
- 返回后保持连接不断开。
因此开发前必须确认扫描枪的实际行为:
### 优先方案
扫描枪返回条码后主动关闭本次 TCP 连接。程序读取到连接关闭即认为一条数据完成。
### 备用方案
如果扫描枪保持长连接,则使用:
- 最大报文长度限制;
- 接收缓冲区;
- 字节间空闲超时;
- 2000 ms 总超时;
- 收到数据后短暂等待后判定报文结束。
该参数需要配置化,不能写死在界面代码中。
## 8. WinForms 界面计划
### 设备状态区
显示:
- PLC:已连接、连接中、断开、故障
- 扫描枪 1:已连接、断开、超时
- 扫描枪 2:已连接、断开、超时
- 本地目录:存在、不可访问
### 当前任务区
显示:
- 当前流程状态
- 当前任务号
- PCB 到位状态
- 当前条码
- 条码来源:正面/反面
- 文件完整路径
- 文件是否存在
- ResultCode
- ReleasePermit
- 本轮耗时
### 统计区
显示:
- 总扫描数
- 通过数量
- 不通过数量
- 扫描超时数量
- 扫描枪通信异常数量
- PLC 通信异常数量
- 最近一次任务时间
### 操作区
建议提供:
- 连接设备
- 断开设备
- 清除报警
- 手动复位
- 打开日志目录
- 打开文件根目录
- 进入配置页面
手动写 PLC 点位的功能应限制权限,避免普通操作员误操作生产信号。
## 9. 配置文件计划
所有通信参数和业务参数放入配置文件,不硬编码在程序中。
```json
{
"Plc": {
"Host": "192.168.2.100",
"Port": 9600,
"UnitAddress": 0,
"SourceNodeAddress": 0,
"EndianFormat": "ABCD",
"MemoryArea": "D",
"Inputs": {
"PcbArrived": 101,
"PlcReset": 102,
"PlcAckRelease": 103
},
"Outputs": {
"PcBusy": 51,
"ReleasePermit": 52
},
"Registers": {
"ResultCode": 401
}
},
"Scanners": [
{
"Name": "FrontScanner",
"Host": "192.168.2.13",
"Port": 2001,
"Trigger": "T",
"TimeoutMs": 2000,
"Encoding": "ASCII"
},
{
"Name": "BackScanner",
"Host": "192.168.2.14",
"Port": 2001,
"Trigger": "T",
"TimeoutMs": 2000,
"Encoding": "ASCII"
}
],
"FileCheck": {
"RootPath": "D:\\test",
"FileNamePattern": "{Barcode}.txt",
"IgnoreCase": false
},
"Workflow": {
"WaitPlcReset": true,
"RequireReleaseAck": false,
"ClearResultAfterReset": true
}
}
```
`RequireReleaseAck` 在 PLC 握手确认后再决定是否启用,默认关闭。
## 10. 日志要求
每一轮 PCB 任务生成唯一任务号,日志至少记录:
- 任务号
- 开始时间和结束时间
- PLC 地址和读写结果
- 到位信号变化
- 两把扫描枪触发内容
- 两把扫描枪原始返回内容
- 选中的条码和来源设备
- 文件检查路径和结果
- ResultCode
- ReleasePermit
- PlcAckRelease
- PlcReset
- 每个步骤耗时
- 异常消息和通信错误
日志按日期滚动保存,例如:
```text
logs\
2026-08-04\
application.log
communication.log
task.log
```
## 11. 异常和恢复策略
### PLC 断线
- 状态显示为 PLC 断开。
- 停止触发扫描。
- 进入自动重连。
- 重连成功后重新读取当前状态。
- 不重复执行已经完成的任务。
### 扫描枪断线
- 单把扫描枪断线时,仍允许另一把扫描枪继续工作。
- 两把均断线时,任务判定为通信异常。
- 支持自动重连。
- 记录具体设备名称和异常原因。
### PLC 到位信号长时间保持
- 通过任务状态和任务锁避免重复触发。
- 本轮流程未复位前不再次发送 `T`。
- 复位后要求 `D101` 先恢复 False,再允许下一轮触发。
### 文件目录不可访问
- 不判定为普通“文件不存在”。
- 返回独立的系统异常码。
- 界面显示目录访问错误。
- 日志记录完整路径和系统异常。
### 程序重启
- 启动时先连接 PLC 和扫描枪。
- 如果 `D101=True`,默认等待 PLC 复位,不自动重扫。
- 如果存在上一次未完成任务,记录为中断任务,不自动伪造结果。
## 12. 开发阶段
### 阶段一:需求和协议确认
确认:
- D 区 Bool 是字值 0/1 还是 bit;
- `D103 PlcAckRelease` 的真实含义;
- `D102 PlcReset` 的触发和清除时序;
- `D401` 结果编码;
- 扫描枪返回后是否断开 TCP;
- 扫描枪编码格式;
- 条码最大长度和合法字符;
- 两把枪同时返回时的处理规则。
交付物:
- PLC 点位表
- PLC 握手时序图
- 扫描枪报文说明
- 结果编码表
- 文件规则说明
### 阶段二:项目骨架
实现:
- WinForms 工程
- 分层项目结构
- 配置加载
- 日志系统
- 依赖注入
- 领域状态和结果模型
- 基础状态界面
### 阶段三:FINS/TCP 通信
实现:
- FINS/TCP 连接
- D 区 Bool 读取
- D 区 Bool 写入
- D401 结果写入
- 连接超时
- 读写超时
- 断线重连
- 原始通信日志
- Fake PLC 测试实现
### 阶段四:双扫描枪通信
实现:
- 两个 TCP Client
- 自动连接和重连
- 同时发送 `T`
- 无结束符报文接收
- 2000 ms 超时
- 首个有效结果选择
- 空结果和异常处理
- Fake 扫描枪测试实现
### 阶段五:文件检查和流程编排
实现:
- 条码清洗
- 文件名安全校验
- `D:\test\<条码>.txt` 精确检查
- PLC 信号与扫描枪流程串联
- 结果保持
- PLC 复位
- 任务去重
- 异常恢复
### 阶段六:WinForms 状态界面
实现:
- PLC 状态
- 两把扫描枪状态
- 文件目录状态
- 当前任务显示
- 结果和统计显示
- 日志查看入口
- 报警显示
- 手动复位和连接控制
### 阶段七:测试和交付
实现:
- 单元测试
- Fake 设备流程测试
- 通信异常测试
- 现场联调
- 发布包
- 配置模板
- 操作说明书
- 维护说明书
- 验收记录
## 13. 测试计划
### 单元测试
覆盖:
- ResultCode 映射
- 文件路径拼接
- 条码非法字符校验
- 文件存在和不存在
- D101 长保持防重复
- PLC 复位流程
- 扫描超时
- 首个有效条码选择
- 两把扫描枪同时返回
- 连接断开和重连
### 协议测试
覆盖:
- FINS/TCP 建连
- D 区读取
- D 区写入
- D401 数值写入
- ABCD 字节序
- 错误响应
- 超时响应
- PLC 断电重连
### 模拟流程测试
覆盖:
1. 正常扫描,文件存在。
2. 正常扫描,文件不存在。
3. 正面扫描成功,反面无结果。
4. 反面扫描成功,正面无结果。
5. 两把均无结果。
6. 一把断线,另一把成功。
7. 两把均断线。
8. D101 长时间保持 True。
9. 扫描过程中收到 D102。
10. PLC 结果确认。
11. 程序重启时 D101 为 True。
12. 文件目录不存在或无权限。
### 现场测试
硬件测试必须在现场或真实设备环境中执行,模拟测试不能替代现场验收。
现场至少验证:
- PLC 实际 IP 和 FINS 节点;
- D 区真实点位;
- 真实 Bool 表达方式;
- 真实扫描枪触发报文;
- 真实返回报文边界;
- 两把扫描枪同时触发;
- PLC 复位和放行确认;
- 连续生产 30~60 个 PCB 的稳定性。
## 14. 交付标准
满足以下条件才算完成:
- Release 构建成功。
- 单元测试全部通过。
- Fake PLC 和 Fake 扫描枪流程测试通过。
- 所有设备操作均有超时和取消机制。
- UI 线程无阻塞通信。
- PLC 断线能够显示并重连。
- 扫描枪断线能够显示并恢复。
- D101 长保持不会重复扫描。
- 文件检查路径不可越界。
- 结果能够正确保持和复位。
- 日志能够定位每一轮任务。
- 配置文件不包含硬编码生产密码。
- 完成真实 PLC、扫描枪和本地目录现场验收。
## 15. 预计开发周期
在 PLC 和扫描枪可及时提供测试的前提下:
| 阶段 | 预计时间 |
|---|---:|
| 需求和协议确认 | 0.5~1 天 |
| 项目骨架和配置 | 0.5~1 天 |
| FINS/TCP 通信 | 1~2 天 |
| 双扫描枪通信 | 1~2 天 |
| 流程和文件判断 | 1 天 |
| WinForms 界面 | 1~2 天 |
| 测试和修复 | 1~2 天 |
| 现场联调 | 1~2 天 |
预计开发周期为 6~10 个工作日,现场联调时间取决于 PLC 点位、扫描枪协议和设备可用情况。
## 16. 当前开发门槛
正式编写现场通信代码前,必须最终确认:
1. D101、D102、D103、D51、D52 是否使用 D 字的 0/1。
2. 扫描枪返回后是否主动断开 TCP。
3. `D401` 的正式结果编码。
4. `D103 PlcAckRelease` 的正式握手含义。
5. 文件不存在时是否直接结束,还是继续等待另一把扫描枪。
6. PLC 程序是否要求 `D51`、`D52` 和 `D401` 保持到 `D102` 复位。
7. 真实条码编码和最大长度。
本计划采用设备接口隔离、后台异步通信、超时与重连、Fake 设备测试和真实设备验收分离的方式,便于先完成可测试的软件闭环,再进行现场通信确认。

View File

@@ -1,4 +1,5 @@
using AxiOmron.PcbCheck.Models;
using AxiOmron.PcbCheck.Utils;
namespace AxiOmron.PcbCheck.Services.Implementations;
@@ -18,17 +19,20 @@ public static class DualScannerResultMerger
ArgumentNullException.ThrowIfNull(first);
ArgumentNullException.ThrowIfNull(second);
if (first.IsSuccess && second.IsSuccess)
bool firstHasUsableBarcode = HasUsableBarcode(first);
bool secondHasUsableBarcode = HasUsableBarcode(second);
if (firstHasUsableBarcode && secondHasUsableBarcode)
{
return MergeBothSucceeded(first, second);
}
if (first.IsSuccess)
if (firstHasUsableBarcode)
{
return CreateSuccess("扫码枪1", first, second);
}
if (second.IsSuccess)
if (secondHasUsableBarcode)
{
return CreateSuccess("扫码枪2", second, first);
}
@@ -84,6 +88,18 @@ public static class DualScannerResultMerger
};
}
/// <summary>
/// 判断扫码结果是否同时满足成功标记和有效条码要求。
/// </summary>
/// <param name="result">待判断的扫码结果。</param>
/// <returns>结果标记成功且条码不是空值或 <c>noRead</c> 时返回 <see langword="true"/>。</returns>
private static bool HasUsableBarcode(ScanOperationResult result)
{
return result.IsSuccess
&& !string.IsNullOrWhiteSpace(result.Barcode)
&& !BarcodeCleaner.IsNoReadResponse(result.Barcode);
}
/// <summary>
/// 构造包含两路扫码原始信息的追溯文本。
/// </summary>

View File

@@ -122,14 +122,17 @@ public sealed class TcpDualScannerService : IScannerService
string rawMessage = await ReadResponseAsync(stream, _options.ReadTimeoutMs, cancellationToken).ConfigureAwait(false);
string barcode = BarcodeCleaner.Clean(rawMessage);
if (string.IsNullOrEmpty(barcode))
bool isNoReadResponse = BarcodeCleaner.IsNoReadResponse(barcode);
if (string.IsNullOrEmpty(barcode) || isNoReadResponse)
{
return new ScanOperationResult
{
IsSuccess = false,
DeviceConnected = true,
RawMessage = rawMessage,
ErrorMessage = $"{endpoint.Name} 返回空字符串或仅包含控制字符。",
ErrorMessage = isNoReadResponse
? $"{endpoint.Name} 返回 noRead,未读到有效条码。"
: $"{endpoint.Name} 返回空字符串或仅包含控制字符。",
DurationMs = stopwatch.ElapsedMilliseconds
};
}

View File

@@ -33,6 +33,8 @@ internal static class StringEscapeHelper
/// </summary>
internal static class BarcodeCleaner
{
private const string NoReadToken = "noread";
/// <summary>
/// 去除条码中的首尾空白与控制字符。
/// </summary>
@@ -56,6 +58,16 @@ internal static class BarcodeCleaner
return builder.ToString().Trim();
}
/// <summary>
/// 判断扫码枪返回值是否表示未读到条码。
/// </summary>
/// <param name="value">已清洗或待判断的扫码枪返回值。</param>
/// <returns>返回值为不区分大小写的 <c>noRead</c> 时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
public static bool IsNoReadResponse(string? value)
{
return string.Equals(value?.Trim(), NoReadToken, StringComparison.OrdinalIgnoreCase);
}
}
/// <summary>

View File

@@ -35,6 +35,35 @@ public sealed class DualScannerResultMergerTests
Assert.Contains("扫码枪2", result.RawMessage);
}
/// <summary>
/// 第一把扫码枪返回 noRead、第二把扫码枪返回有效条码时,应舍弃 noRead 并使用第二把扫码枪结果。
/// </summary>
[Fact]
public void Merge_ShouldUseSecondBarcode_WhenFirstScannerReturnsNoRead()
{
var first = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "noRead",
RawMessage = "noRead"
};
var second = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-SECOND",
RawMessage = "PCB-SECOND"
};
ScanOperationResult result = DualScannerResultMerger.Merge(first, second);
Assert.True(result.IsSuccess);
Assert.Equal("PCB-SECOND", result.Barcode);
Assert.Contains("使用扫码枪2", result.RawMessage);
Assert.DoesNotContain("使用扫码枪1", result.RawMessage);
}
/// <summary>
/// 两把扫码枪都成功但内容不一致时,应优先使用扫码枪1条码并保留不一致说明。
/// </summary>