Skip to content

Repository files navigation

CommandCenter

相机 + 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,启动后等几秒进程存活即冒烟通过。

发布(代码混淆,V2.14.31)

现场部署用混淆版,防止反编译拿到类名/方法名/设备 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)

⚠️ 上/下相机的 IP 与 FTP 目录是"安装位置相反配对":上相机(装在上面那台)真编号是 2、推到 \2; 它们(19.87.6.213/212cameraId 2/1、FTP 目录 \2/\1)在代码 CameraConfig.DefaultCameras()appconfig.jsoncameras 段里唯一维护,改动只动这两处。

可配置项

全部参数集中在 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[])

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~1270 合法; 同程序相邻已切过会自动跳过重发);未命中不切,保持相机当前程序。配置入口在"窗口/点位配置…"对话框。

PLC 通讯(plc{})

上位机是 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,V2.15.0)

字段 默认 白话说明
language zh-CN 界面语言:zh-CN=中文 / en-US=English。主界面标题栏【系统设置】按钮右侧的【语言切换按钮】可改(V2.15.1,外观与系统设置按钮一致),点击即切即存(热切换无需重启、立即写盘);非法/空值自动回落中文。仅界面文字翻译,日志保持中文;OK/NG/PLC/IP/SN 等专有名词不翻译

显示窗口(display{})

主界面中央是窗口矩阵,默认"自适应"铺排(勾选后行列输入框置灰):

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 标题栏"系统设置"按钮(生产现场可关防误改)

图像归档(image{})

相机拍照 = 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 服务器;图不到先查相机侧推图配置。

扫码枪(scanners[])

支持多台扫码枪,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):扫码枪读码失败时会按自身配置把错误字符串(如 ERRORER,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 是否为空

账号与安全(security{})

字段 默认 说明
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/*.jsonLogs/bin/obj/ 不入库; 改动后构建验证;通讯/握手/映射类改动必须同步 docs/CommandCenter.md 并记录 CHANGELOG.md

About

汇川PLC + 基恩士IV4相机 产线视觉上位机(.NET Framework 4.7.2 WinForms):PLC到位信号触发多相机TCP拍照判定、FTP取图归档、Modbus TCP寄存器读写、扫码枪SN、窗口/存图点位可视化配置

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages