相机 + PLC 现场命令中心上位机(Windows 桌面程序,.NET Framework 4.7.2 WinForms)。
一句话:PLC 告诉它"该扫码了" → 它扫 SN、派相机拍照 → 图片进窗口、判定结果写给 PLC → 等 PLC 说 "下一位"再循环。 现场操作员盯着大屏窗口矩阵看 OK/NG,改了型号直接点标题栏下拉切换。
┌──────────────────────────────────────────────────────────┐
│ 上位机 CommandCenter(本程序) │
│ │
│ ScannerService ──┐ │
│ (扫码枪读SN) │ │
│ ▼ │
│ ProductionCoordinator ◀──PLC 触发、给 PLC 回结果 │
│ (生产编排大脑,三通道状态机) │
│ │ │
│ KeyenceIV4Camera ─┘ ImageStore ──► 归档图片 + 窗口显示 │
└──────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ Modbus TCP(从站,502口) │ TCP 触发 │ TCP 无协议
│ │ + FTP 推图 │ 读码
汇川 PLC 上/下相机 扫码枪
(主站,指挥中心) (拍照) (扫SN)
| 项 | 内容 | 说明 |
|---|---|---|
| 框架 | .NET Framework 4.7.2 + WinForms | C# 7.3,非 .NET Core |
| PLC 通讯 | NModbus 3.0.83 | 上位机做 Modbus TCP 从站,监听 502 |
| 相机 | 基恩士 IV4 系列(TCP 无协议) | 发指令触发拍照、读 OK/NG;图片走 FTP 推图(jpeg + iv4p 双文件归档) |
| 扫码枪 | 基恩士 SR 系列(TCP 无协议 / 串口) | 默认 TCP,上位机连上后发 LON 开始读码 |
| 配置序列化 | Newtonsoft.Json | Config/appconfig.json,小驼峰命名 |
| 依赖部署 | 第三方 DLL 拷在 CommandCenter/libs/ |
csproj 直接引用,离线可编译,不依赖 NuGet |
工程按"界面 → 业务编排 → 通讯 → 存储"四层划分,层层单向依赖:
┌───────────────────────────────────────────────────────────────┐
│ 界面层 Views/ Controls/ │
│ MainForm 主窗体:标题栏 + 窗口矩阵 + 事件接线 │
│ SettingsForm 系统设置(管理员) │
│ WindowPointForm 窗口↔点位 + 点位→相机程序 可视化配置 │
│ DirTreeEditForm 存图目录结构可视化配置 │
│ LoginForm 账号登录 + 改密码 │
│ DeveloperModeForm 开发者模式(原名 DevTestForm:通讯验证 + 账号管理) │
│ CameraDisplayControl 单窗口控件:显示图 + OK/NG 徽标 │
├───────────────────────────────────────────────────────────────┤
│ 业务编排 Services/ │
│ ProductionCoordinator 生产流程大脑(三通道状态机,核心) │
├───────────────────────────────────────────────────────────────┤
│ 通讯 Services/ │
│ PlcService Modbus TCP 从站(请求/结果握手) │
│ KeyenceIV4Camera 相机 TCP 触发 + 读判定 + FTP 取图 │
│ ScannerTcp/SerialService 扫码枪 TCP/串口,自动触发 + 收码 │
│ ConnectionMonitor 后台心跳 + 断线自动重连 + 边沿日志 │
├───────────────────────────────────────────────────────────────┤
│ 存储 Utils/ Models/ │
│ ConfigStore appconfig.json 读写 + 旧配置自动补齐 │
│ ImageStore 监听 FTP 新图、归档双格式、定期清理过期 │
│ SecurityUtil 密码 SHA-256 哈希 + 记住密码 DPAPI │
│ AppConfig 全部可配置项模型 │
└───────────────────────────────────────────────────────────────┘
通讯层全部坚持一条铁律(曾为此冻过界面):UI 线程不做网络 IO,轮询/连接/读写一律后台线程 + 强制超时 + 完成后回 UI 刷新,保证拨了不可达的 IP 界面也不卡。
PLC 写 40001=1(扫码请求)
▼ (V2.14.28)收到请求即清空窗口图 + 标题栏计数清零——扫码=新检测件,重新统计本件检测点
扫码枪读 SN ──► 标题栏序列号更新(也可手动【人工补录】)
▼ SN 到位,PLC 写 40002/40003=点位号(相机拍照请求)
按"型号 + 点位"查相机程序表 → 发 PW,nnn 切相机程序 → 发 T2 拍照 + 读 OK/NG
▼ 判定一出结果立刻写回 PLC(40005/40006=1 OK / 2 NG),PLC 不用等取图
并行异步:相机 FTP 推图 → 上位机取最新 jpeg+iv4p → 归档到存图目录 → 窗口矩阵显示
▼
等 PLC 复位请求 → 上位机复位结果 → 进入下一轮
三拍握手(PLC 主站 ↔ 上位机从站,一进一出对账,防止残值误当新结果):
PLC(主站) 上位机(从站)
│ ① 写请求 ≠ 0 │
├─ 40001 扫码请求 / 40002/40003 相机 →┤ 读到请求,开工
│ │ 扫码/拍照/判定
│ ② 读结果 │ ④ 复位结果 = 0
◀─ 40004 扫码结果 / 40005/40006 相机 ──┤
│ (1=OK, 2=NG, 3=点位禁用跳过) │
│ ③ 读到结果后复位请求 = 0 │
├──────────────────────────────────→ │ 看到请求清零 → 复位结果 → 下一拍
CommandCenter/
├── Views/ 界面窗体(MainForm / SettingsForm / LoginForm / DeveloperModeForm /
│ WindowPointForm / DirTreeEditForm / SerialInputForm…)
│ ※ 静态布局在 *.Designer.cs 里可视化维护;动态部分(窗口矩阵、状态灯)
│ 在业务文件里运行时生成
├── Controls/ 自绘控件(CameraDisplayControl 相机窗 + OK/NG 徽标等)
├── Services/ 通讯与业务编排(ProductionCoordinator / PlcService / KeyenceIV4Camera /
│ ImageStore / ScannerService / ScannerTcpService / ConnectionMonitor)
├── Models/ 配置模型(AppConfig 全量配置 / WindowData 单轮数据)
├── Utils/ ConfigStore(JSON 读写)/ SecurityUtil / LogHelper(按天日志)
├── libs/ 本地第三方 DLL(NModbus / Newtonsoft.Json)
├── docs/ 文档中心(见文末)
└── Config/ 运行时配置 appconfig.json(不入库,首次启动自动生成)
& "D:\Program Files\Microsoft Visual Studio\18\Enterprise\MSBuild\Current\Bin\MSBuild.exe" `
CommandCenter/CommandCenter.csproj /p:Configuration=Debug /p:Platform=AnyCPU /t:Build /nologo /v:m /m
产物:CommandCenter\bin\Debug\CommandCenter.exe,启动后等几秒进程存活即冒烟通过。
现场部署用混淆版,防止反编译拿到类名/方法名/设备 IP/寄存器号/相机指令等业务细节:
& ".\CommandCenter\build-obfuscated.ps1"
- 流程:Release 构建 → Obfuscar 混淆 → 补第三方 dll + config → 启动冒烟。
- 产物:
CommandCenter\bin\Obfuscated\,整个目录拷去现场即可运行。 - 混淆只改名字/字符串(类名变 A.a 乱码、字符串运行时解密),不改功能;
Models配置模型除外—— 属性名必须保留(appconfig.json字段名依赖它),保证现场旧配置可无缝升级。 - 混淆版 PDB 失配、无法断点调试;现场/开发排查问题请用 Debug 版 +
Logs/日志。 bin\Obfuscated\Mapping.txt是"原名↔混淆名"对照表,仅内部反查崩溃栈用,勿随安装包发放。- 依赖工具
tools\Obfuscar\Obfuscar.Console.exe已随仓库入库(离线可用,策略同libs/)。
生产现场的接线关系(默认值,都可在系统设置里改):
生产现场(19.87.6.x 网段)
汇川 PLC ──────Modbus TCP──▶ 上位机 [监听 0.0.0.0:502,从站]
▲ ▲ │
上位机连相机/枪(主动)──────────┤ │
│ │ 相机拍照后把图 FTP 推到上位机目录
上相机 19.87.6.213 ◀──TCP 触发────┤ │ ─────────► D:\IV存图\2
下相机 19.87.6.212 ◀──TCP 触发────┤ │ ─────────► D:\IV存图\1
扫码枪 19.87.6.100:9004 ◀─TCP─────┘ (上位机监听落盘目录取图,无需装 FTP 服务器)
| 设备 | 地址 | 关键点 |
|---|---|---|
| 汇川 PLC(主站) | 由 PLC 主动连上位机 502 | 上位机只监听,不主动连 PLC |
| 上相机(基恩士 IV4) | 19.87.6.213:8500 |
真编号 cameraId=2,拍照推图到 D:\IV存图\2;PLC 通道 40002/40005 |
| 下相机(基恩士 IV4) | 19.87.6.212:8500 |
真编号 cameraId=1,拍照推图到 D:\IV存图\1;PLC 通道 40003/40006 |
| 扫码枪(基恩士 SR) | 19.87.6.100:9004 |
TCP 无协议,连上后发 LON 开始读码;串口枪可选(默认 COM3/115200/1/None) |
\2;
它们(19.87.6.213/212、cameraId 2/1、FTP 目录 \2/\1)在代码 CameraConfig.DefaultCameras() 和
appconfig.json 的 cameras 段里唯一维护,改动只动这两处。
全部参数集中在 Config/appconfig.json(首次启动自动按默认值生成,kebab 命名的节点如下):
appconfig.json 顶层结构
├── productModel 当前运营型号(默认 "U171")
├── productModels 全部型号候选(默认 ["U171","Z121"])
├── language 界面语言(V2.15.0,默认 "zh-CN",可选 "en-US")
├── cameras[] 相机表(每台:IP/端口/编号/FTP目录/PLC通道/点位程序表)
├── plc{} 从站监听 + 寄存器地址 + 型号→PLC序号映射
├── display{} 显示矩阵行列/自适应开关/OK NG 颜色与显隐开关
├── image{} 存图根目录/目录层级/保留天数/时间戳开关
├── scanners[] 扫码枪表(TCP 与串口各自独立成行)
└── security{} 管理员 + 开发者账号
更换现场配置优先走"系统设置"界面(保存即热生效),不手改 json。下面按业务分组说明。
cameras 是数组,每台相机一条。触发拍照由 PLC 请求点位号 → 上位机按"型号+点位"查程序表 → 切程序 →
T2 拍照 完成。一台相机 = 一个 FTP 目录,所有点位图混放,上位机取图时扫目录取"修改时间最新"的一对
(文件名不保证固定编号),归档后删除 FTP 源文件(中转即删)。
| 字段 | 默认(上相机 | 下相机) | 白话说明 |
|---|---|---|---|
name |
上相机 | 下相机 | 显示名,也当 {相机} 目录层名 |
|
cameraId |
2 | 1 | 相机真编号 = 存图目录号,窗口↔点位反查的关联键 | |
ipAddress |
19.87.6.213 | 19.87.6.212 | TCP 触发 IP | |
commandPort |
8500 | 触发指令端口 | |
ftpUploadDir |
D:\IV存图\2 | D:\IV存图\1 | 相机 FTP 推图的落盘目录,上位机监听它取图 | |
plcRequestAddress |
2 | 3 | 该相机拍照请求寄存器(协议号 = 值 + 40000,即 40002/40003) | |
plcResultAddress |
5 | 6 | 该相机结果寄存器(协议 40005/40006);0 = 未配置通道、不参与轮询 | |
triggerCommand |
T1 | 仅触发拍照,不读判定 | |
triggerAndReadCommand |
T2 | 触发 + 读判定(主用) | |
readResultCommand |
RT | 单独读判定值 | |
outputFormat |
空 | OF,nn 输出格式,恰好 2 位数字,配错会让该相机触发失败 |
|
imageWaitMs |
10000 | 发完 T2 后等 FTP 图的最长时间 | |
timeoutMs / responseTimeoutMs |
3000 / 5000 | 指令收发超时 | |
stationPrograms / modelStationPrograms |
见下 | 点位→相机程序号映射表 |
点位→程序号映射(每种产品型号一张表):现场是"28 个窗口点位由两台相机分工拍"(不是每台拍全部),
且各相机程序库互相独立、同一台相机在不同型号下点位也不一样(如 U171 上相点位 120 用 P000P012、
Z121 上相点位 126 用 P013P028)。所以按 相机 + 型号 分表:
modelStationPrograms:
[{ modelName: "U171", programs: [{ stationNo: 1, programNo: 0 }, …] }, ← 上/下相机各一张
{ modelName: "Z121", programs: [{ stationNo: 1, programNo: 13 }, …] }]
触发前按本轮点位查表:命中先发 PW,nnn 切到该点位程序再 T2(程序号 0~127,0 合法;
同程序相邻已切过会自动跳过重发);未命中不切,保持相机当前程序。配置入口在"窗口/点位配置…"对话框。
上位机是 Modbus TCP 从站,监听本机端口等 PLC 主站连入。寄存器表(值 = DataStore 索引, 协议号 = 索引 + 40000,配置里填索引即可,如协议 40002 就填 2):
| 寄存器 | 方向 | 含义 | 取值 |
|---|---|---|---|
| 40001 | PLC→上位机 | 扫码请求 | 1=请求扫码、0=无 |
| 40002 | PLC→上位机 | 上相机拍照请求 | 1~255 = 点位号 |
| 40003 | PLC→上位机 | 下相机拍照请求 | 1~255 = 点位号 |
| 40004 | 上位机→PLC | 扫码结果 | 0=复位、1=OK、2=NG(V2.14.33:PLC 拿到 2 死等人工补录,补录后上位机改写成 1) |
| 40005 | 上位机→PLC | 上相机结果 | 1=OK、2=NG、3=点位禁用跳过 |
| 40006 | 上位机→PLC | 下相机结果 | 同上 |
| 40007 | 上位机→PLC | 型号序号 | 查 plc.modelIndexes,默认 Z121=1、U171=2 |
| 40008~40012 | 上位机→PLC | 型号 ASCII 字符串 | 每寄存器 2 字符、高字节在前、最多 10 字符 |
关键字段:ipAddress(监听 IP,0.0.0.0=所有网卡)、port(502)、unitId(1,需与 PLC 主站一致)、
scanRequestAddress/scanResultAddress(默认 1/4)、productModelIndexAddress(7)/
productModelAddress(8)productModelLen(5)、modelIndexes(型号→序号映射)。
几个重要的行为约定:
- 上电初始化复位:从站一建站成功,先把 40004~40006 结果寄存器全写 0,防断电重启残留旧值被当新结果。
- 相机结果"判定即写":相机 OK/NG 一判出来马上写 40005/40006,不等 FTP 取图归档(图传输会让 PLC 白等几百毫秒~2 秒);图缺失只影响显示和存图,判定不回退。
- 通道释放闸门:相机通道要等"PLC 已复位请求 且 拍照任务完全结束"才释放,防止下一拍请求并发 起新任务导致同相机"取图/删源混图"。
- 当前型号:主界面标题栏型号下拉直切(候选 = 预置 U171/Z121 ∪ 配置
productModels)。 切换立即生效:写盘 + 按新型号查程序表 + 下发 PLC(40007 序号 + 40008~40012 字符串)+ 窗口重排, 设备连接一路复用不断开。 - 型号集合的增删:在系统设置 PLC 区"产品型号配置…"弹窗维护——表格左列"选中"勾选框 + 右上角
【删除选中】按钮支持多选/单选删除(序号、型号名称全居中显示),新增直接在最下行填;确定写回
plc.modelIndexes;新增型号名自动并入productModels候选,标题栏/点位配置立刻可选。 - 窗口总数与"窗口↔点位"铺排随型号走(见下),所以换型号要连"型号点位表"一并对齐。
| 字段 | 默认 | 白话说明 |
|---|---|---|
language |
zh-CN |
界面语言:zh-CN=中文 / en-US=English。主界面标题栏【系统设置】按钮右侧的【语言切换按钮】可改(V2.15.1,外观与系统设置按钮一致),点击即切即存(热切换无需重启、立即写盘);非法/空值自动回落中文。仅界面文字翻译,日志保持中文;OK/NG/PLC/IP/SN 等专有名词不翻译 |
主界面中央是窗口矩阵,默认"自适应"铺排(勾选后行列输入框置灰):
U171 默认铺排(4×7=28 格,点位 24 个 → 窗口 25~28 为空窗口)
┌────┬────┬────┬────┬────┬────┬────┐
│上·1│上·2│上·3│上·4│上·5│上·6│上·7│ ← 窗口 1~20 = 上相机点位 1~20
├────┼────┼────┼────┼────┼────┼────┤
│上·8│… │上·14│ │ │ │ │
├────┼────┼────┼────┼────┼────┼────┤
│上·15…上·20│ │ │ │ │ │
├────┼────┼────┼────┼────┼────┼────┤
│下·1│下·2│下·3│下·4│空 │空 │空 │ ← 窗口 21~24 = 下相机点位 1~4
└────┴────┴────┴────┴────┴────┴────┘
- 窗口总数:自适应 = 各相机当前位置数表条目和(U171=20+4=24、Z121=26+3=29);不勾自适 = 手填 行×列(放不下自动补行),点位不够的格子 = 空窗口(照样建窗占满显示区,只是不接图)。
- 自动铺排规则:列最多 7,遍历列取"行列和最小、并列列多者优先",窗口尽量放大占满 (1→1×1、4→2×2、7→2×4、28→4×7)。行数超高时窗口区自动出竖直滚动条。
- 窗口↔点位映射:默认"前上相机后下相机"铺排;在"窗口/点位配置…"里可按型号手动编辑点位 /
交换位置 / 恢复默认(结果按型号存
display.windowPointMaps),被禁用的点位跟窗口走(交换时 禁用标志一起搬)。 - 存图点位 = 相机点位号(不是窗口序号):上下相机点位号各自从 1 起会重复,靠存图目录里的
{相机}层隔开。
| 开关字段 | 默认 | 效果 |
|---|---|---|
autoFit |
true | 行列是否自动铺排 |
rows / columns |
4 / 7 | 非自适应下的手填行列 |
okColorName / ngColorName |
Green / Red | OK=绿、NG=红(现场习惯,可改) |
windowOkNgVisible |
true | 窗口右下角 OK/NG 徽标;只随开关走(V2.14.26 还原)——开着就显示,未判定时默认绿 OK、拿到判定按结果变色 |
windowIndexVisible |
true | 窗口左上角编号 |
windowToolTipVisible |
true | 悬停"双击放大/还原"气泡 |
titleOkNgHighlight |
true | 标题栏 OK/NG 计数用实心色块高亮 |
showProductModel / showSerialNumber / showTotalCount / showOkCount / showNgCount |
true | 标题栏各项显隐 |
showSettingsButton |
true | 标题栏"系统设置"按钮(生产现场可关防误改) |
相机拍照 = jpeg + iv4p 两个文件(iv4p 是基恩士私有复盘格式,原样归档)。程序监听取图目录,取到
最新一对就归档:源文件名 + "_" + 时间戳(如 0084.jpeg → 0084_20260814_164022_461.jpeg,
防同点位重复拍照互相覆盖),随后删掉 FTP 源文件。
E:\Images\ ← saveRootDir(存图根目录)
└── 2026年08月14日\ ← {年月日}(一个整体目录名,不是年/月/日三层)
└── SN-0001\ ← {SN}
├── 上相机\ ← {相机}(把两台相机隔开,防点位号重复互相覆盖)
│ └── OK\ ← {OKNG}(按判定的 OK/NG 分到两个并列目录)
│ └── 0084_20260814_164022_461.jpeg + 同名 .iv4p
└── 下相机\
└── NG\
subDirs:目录层级列表,每级只能是一层名字/规则({年月日}/{SN}/{相机}/{OKNG}), 不要把整条路径模板粘成一级(会一层套一层嵌套,加载时旧坏配置会自动拆层修复)。keepDays:存图保留天数(默认 30,0 = 不自动清理);后台每天清一次过期目录,只动保存根目录 下的过期目录,绝不动相机 FTP 取图目录;根目录误配成盘符根(如E:\)时自动放弃防误删整盘。fileTimestampSuffix:归档文件名是否带时间戳后缀(默认 true)。- FTP 取图目录(
ftpUploadDir)由相机侧(基恩士工程师在 IV Navigator 里配)推图,上位机只需 监听落盘目录,不用装任何 FTP 服务器;图不到先查相机侧推图配置。
支持多台扫码枪,TCP(默认,基恩士 SR TCP/IP 无协议)与串口两种,设置页里两张表分开配。 任何一台扫到码都更新当前序列号;扫地枪漏读/没枪时点标题栏【人工补录】手动输入。
| 字段(TCP 行) | 默认 | 说明 |
|---|---|---|
enabled |
true | 是否启用 |
ipAddress / port |
19.87.6.100 / 9004 | TCP 地址 |
triggerCommand |
LON | 连上/重连后自动发一条该指令才开始读码,发时自动补 \r\n;留空=不发送(枪设成上电自动读码) |
ignoreScanTexts |
ERROR,ERR,NG,NOREAD | 读码失败/状态文本过滤名单(V2.14.30):扫码枪读码失败时会按自身配置把错误字符串(如 ERROR、ER,READ,00)当条码推送,命中名单的行不当序列号(不显示、不存图),并把扫码结果写 2 通知 PLC (V2.14.33:PLC 拿到 2 死等人工补录,补录后上位机覆盖成 1;不等 30s 超时)。逗号分隔、忽略大小写;精确匹配不误伤同前缀真码,以 * 结尾的项按前缀匹配。留空=不过滤 |
| 字段(串口行) | 默认 | 说明 |
|---|---|---|
portName / baudRate |
COM3 / 115200 | 串口号 / 波特率 |
stopBits |
"1" | 停止位,存字符串 "1"/"15"/"2" |
parity |
None | 校验位,存枚举名 None/Odd/Even/Mark/Space |
ignoreScanTexts |
ERROR,ERR,NG,NOREAD | 同 TCP 行的读码失败文本过滤 |
mode 缺省一律按 "Tcp"(现场就是 TCP 枪);scanners 段为空时启动自动补一台启用 TCP 枪
(19.87.6.100:9004/LON)——排查"网络通但程序不连枪"先看 scanners 是否为空。
| 字段 | 默认 | 说明 |
|---|---|---|
adminEnabled |
true | 点"系统设置"是否要登录(生产建议开,防误改配置) |
adminUser / adminPasswordHash |
admin / admin123 的哈希 | 管理员,密码只存 SHA-256 哈希、不存明文 |
devEnabled / devUser / devPasswordHash |
true / dev / dev123 的哈希 | 开发者,登录进开发者模式窗体(原名功能测试:通讯验证 + 账号管理) |
| 记住密码 | — | 勾选后用 Windows DPAPI 加密存 %LOCALAPPDATA%\CommandCenter\(绑定本机用户,拷走无效);管理员/开发者记录互斥,登录任一方会清掉另一方 |
管理员密码在登录对话框里改(验证原密码 → 新密码两次一致且 ≥6 位 → 保存);开发者密码不提供在线 修改(改哈希需用工具算好写配置)。
| 文档 | 用途 |
|---|---|
docs/CommandCenter.md |
主文档:用户操作手册、系统总览与设备清单、扫码枪/相机/PLC 通讯详情与寄存器定义、计数结果流转、IP 参数速查、版本演进 |
docs/上位机操作说明书.md |
现场操作员版说明书:开机启动、界面认识、日常操作、人工补录、扫码枪异常处理、常见问题速答(通俗易懂,面向非技术操作员) |
docs/上位机PLC通信接口定义文档.md |
PLC 侧对接视角的通讯接口定义 |
docs/上位机通讯封装范式.md |
通讯架构技术总结(连接/心跳/重连/UI 解耦范式,跨项目可复用) |
代码约定(给接手的开发者):文件 UTF-8;配置
Config/*.json、Logs/、bin/、obj/不入库; 改动后构建验证;通讯/握手/映射类改动必须同步docs/CommandCenter.md并记录CHANGELOG.md。