5.30:多VM解耦 X光检测(曝光)(func<CancellationToken,Task> scanLogic)

关于多 VM 解耦

多个 ViewModel 如果直接互相实例化调用,就是紧耦合,改一个模块会牵连其他模块,后期维护、新增功能极易出问题;

解决办法:用消息总线(Messenger)做发布 - 订阅通信,VM 之间互不依赖,只靠收发消息联动,实现解耦。

demo

new Random().Next(1, 80) == 1 随机故障模拟

1. 代码语法含义

Random.Next(min, max):生成一个 左闭右开 的随机整数,范围是 1 ~ 79(取不到 80);

==1:只有随机数恰好等于 1 时,条件判定为 true。

2. 触发概率

满足条件的概率 = 79/1≈1.27%,属于低概率随机触发。

3. 业务场景作用(工业模拟)

这段代码放在图像采集方法里,用来模拟工业现场偶发的硬件故障:

条件成立时,模拟「图像探测器断开、相机通信闪断」等非必现故障;

触发后自动记录故障日志、切换设备故障状态、执行整机急停;

目的是测试程序的异常捕获、故障处理能力,不用手动反复模拟故障,更贴近真实生产环境。

4. 设计目的

真实工业场景中,硬件故障大多是低概率偶发问题,这段代码可以验证你的程序:

能否捕获异常不崩溃;

能否自动进入故障状态并停机;

能否记录故障日志方便排查问题。

x光检测-曝光时长

_exposureTime 完整解析(结合你的X光检测

_exposureTime 是 ViewModel 里的私有字段 ,配合公开属性 ExposureTime 实现界面双向绑定

  • 对应界面上的「曝光参数输入框/滑动条」,单位:毫秒(ms)
  • CanEditParam 控制:扫描运行中自动锁定不可修改,设备空闲时可自由调整

二、核心业务作用(模拟真实X光成像)

它用来模拟 X光发射机的曝光时长,成像逻辑对应现实工业场景:

曝光时间越长 → X光照射工件的时间越久 → 探测器采集的光子越多 → 生成的图像整体亮度越高;

曝光时间越短 → 图像整体偏暗,细节对比度更强。

类比日常拍照:曝光时间 = 相机快门时长,快门开得越久,照片越亮。

三、代码中的实际联动逻辑

ShowXrayImage 方法里,直接参与图像生成计算:

csharp 复制代码
// 基础灰度值 = 固定基准值 + 曝光时间换算的亮度偏移
int grayBase = 80 + ExposureTime / 10;
// 用曝光参数调整整张图像的亮度
using (HImage brightImage = rawImage.ScaleImage(1.0, (double)(grayBase - 128)))
  1. 把界面输入的曝光值,换算成图像灰度偏移量;
  2. 通过 ScaleImage 给整张图像做亮度偏移,真正实现"改曝光→图像亮度同步变化"
  3. 扫描日志同步记录曝光值,方便追溯工艺参数。

四、工业场景实际意义(面试/项目亮点)

  1. 工艺可控:不同厚度的工件,需要匹配不同曝光时间,薄工件短曝光避免过曝,厚工件长曝光保证穿透;
  2. 缺陷识别适配:曝光直接影响图像对比度,合适的曝光是缺陷识别准确的前提;
  3. 参数可追溯:每次扫描的曝光值会随日志保存,方便后续排查不良品原因。

五、权限设计(工控规范)

运行扫描时 CanEditParam = false,曝光参数输入框会变灰锁定:

  • 避免扫描过程中误改曝光,导致成像突变、检测结果失效;
  • 只有设备空闲/停止状态,才能修改曝光参数,符合工业设备安全规范。

一句话总结

_exposureTime 是控制X光图像亮度的核心工艺参数,既实现界面可视化调节,又和成像逻辑深度绑定,完全复刻真实工业X光机的使用逻辑。

RunScan(func<CancellationToken,Task> scanLogic)

总纲:

方法声明里写的token只是「预留接收口(占位符)」,没有实际停工能力;RunScan内部生成的token,才是本次任务唯一的「真实停工令牌」,必须传进去,才能让上层(停止/急停按钮)统一控制扫描任务叫停。


问题1:为什么必须把RunScan内部的token传给扫描逻辑?

1. 先分清两个token的本质
  1. 扫描逻辑里写的async token => { }:这是「形式参数」
    它只是一个预留的"接收停工指令的口子",就像外卖员提前留好的手机号,但此时还没有绑定任何订单,收不到任何真实的取消通知。
csharp 复制代码
 // 这里的token只是一个名字,没有任何实际的取消功能
   await RunScan(async token =>
   {
       await _scanAxis.MoveToAsync(50, 0, token);
   });
  1. RunScan内部的var token = _cts.Token:这是「实际参数」
    这是本次扫描任务,动态生成的、唯一的停工令牌 ,只有这个令牌,能响应你点击「停止/急停」的操作。

    csharp 复制代码
    _cts = new CancellationTokenSource();
    var 真实停工令牌 = _cts.Token; // 只有这个,能被上层调用Cancel()叫停
2. 传递的核心目的:统一管控
  • RunScan是全局调度中心 ,停止按钮、急停按钮最终调用的是_cts.Cancel()
  • 如果扫描逻辑自己生成token,调度中心无法控制它,点击停止按钮,这个扫描任务根本停不下来;
  • 只有把调度中心的真实令牌传给扫描逻辑,才能实现:上层一键叫停,所有扫描任务同步停工
生活化类比
  • 扫描逻辑的token = 外卖员预留的手机号(空接口)
  • RunScan的token = 本次外卖订单的专属取消码(真实权限)
  • 传递动作 = 平台把本次订单的取消码发给外卖员,外卖员才能收到平台的取消通知。

问题2:scanLogic明明是扫描方法,为什么比喻成「工人」?

1. 本质:scanLogic是「方法的引用(委托变量)」

scanLogic不是写死的代码,它是一个可以存放任意方法的容器 ,只要这个方法符合Func<CancellationToken, Task>的格式要求。

  • 存放点位扫描代码 → 这个容器里就是「点位扫描工人」
  • 存放连续扫描代码 → 这个容器里就是「连续扫描工人」
2. 工人的两个硬性能力(对应委托格式)
  1. 能接收停工指令 :方法必须能接收CancellationToken(工会代表);
  2. 后台干活可等待 :方法必须返回Task(后台任务工单)。

简单说:scanLogic里存的方法,就是符合招工要求的干活工人,工人负责具体业务,RunScan负责调度管控。


问题3:Func<CancellationToken, Task>为什么是「招工合同」?

Func是C#的委托类型,它的作用就是定义一份严格的格式合同 ,用来筛选能放进scanLogic容器的方法。

这份合同的两条核心条款:

  1. 入参条款 :应聘的方法,必须能接收1个CancellationToken类型的停工指令;
  2. 返回条款 :干完活之后,必须返回1个Task类型的后台工单,支持调度中心等待任务完成。

不符合合同的方法,编译器会直接报错,无法放进scanLogic里。


问题4:Task到底是什么?

Task可以理解为**「后台任务工单」**,它的核心作用:

  1. 标记这段代码是后台线程执行,不会卡死WPF界面;
  2. 支持用await等待工单完成,调度中心可以等扫描干完活,再执行资源释放、状态重置;
  3. 配合CancellationToken,支持中途撕单(任务取消)。

一句话:Task就是后台干活的凭证,有了它,才能异步执行、不卡界面、可等待、可叫停。


完整执行流程(从头到尾走一遍,彻底打通)

  1. 你点击「点位扫描」按钮 ,调用RunScan(扫描逻辑)
  2. 此时传入的async token => { 点位扫描代码 },只是一个带空接口的工人
  3. 进入RunScan调度中心
    • 新建_cts,生成本次任务专属的真实停工令牌
    • 把UI状态改为「运行中」,准备扫描;
  4. 调度中心下发令牌 :执行await scanLogic(真实停工令牌)
    • 此时工人(扫描逻辑)拿到真实令牌,开始后台执行点位扫描;
    • 工人干活期间,调度中心可以等待,也可以随时撕单(调用_cts.Cancel());
  5. 扫描完成/被取消:调度中心执行资源释放,重置状态。

极简记忆口诀

  1. 声明的token是空接口 ,RunScan的token是真令牌,必须传递才能统一管控;
  2. Func是招工合同 ,定死方法格式;scanLogic是干活工人,存具体业务代码;
  3. Task是后台工单,保证异步执行,不卡界面。

await scanLogic(token);

代码
csharp 复制代码
private async Task RunScan(Func<CancellationToken, Task> scanLogic)
{
    // 如果已有扫描任务在运行,直接退出,防止重复启动
    if (_cts != null) return;
    
    // 创建取消令牌(控制停止/急停)
    _cts = new CancellationTokenSource();
    var token = _cts.Token;
    
    try
    {
        // 更新UI:运行中、进度清零、打开X光
        Application.Current.Dispatcher.Invoke(() =>
        {
            Status = STATUS_RUNNING;
            Rate = 0;
            _isXrayOpen = true;
        });
        
        // ============== 核心代码 ==============
        await scanLogic(token);
    }
    // 后面是异常处理、收尾代码...
}

1. 方法作用:通用扫描总控制器

不管你点 点位扫描 还是 连续扫描 ,最终都会进这个方法

它负责:统一准备工作 + 统一收尾 + 统一异常处理

2. 参数 Func<CancellationToken, Task> scanLogic 是什么?

一个装着「具体扫描逻辑」的盒子

  • 点位扫描 → 盒子里装:移动到X坐标 → 采图
  • 连续扫描 → 盒子里装:轴持续移动 → 实时采图
3. 核心:await scanLogic(token); 到底干嘛?

一句话:

执行传入的「具体扫描任务」,并且等待它执行完成!

详细作用:
  1. 启动干活:执行点位/连续扫描的真正逻辑
  2. 等待完成:异步等待扫描结束,不卡死界面
  3. 传递开关 :把 token(停止/急停开关)传给扫描逻辑
  4. 统一调度:让所有扫描都走这套标准化流程

三、在项目中的真实流程
  1. 【点位扫描】
    → 把「移动坐标+采图」装进 scanLogic 盒子
  2. 【连续扫描】
    → 把「轴连续走+实时采图」装进 scanLogic 盒子
  3. 调用 RunScan
  4. RunScan 做准备(改状态、开X光)
  5. await scanLogic(token); → 执行真正的扫描
  6. 扫描结束/停止 → 统一收尾

四、await scanLogic(token);

= 执行具体的扫描任务(点位/连续) + 等待任务完成 + 支持急停/停止

整个X光扫描系统的 任务执行核心

相关推荐
czhc11400756632 小时前
6.1EtherCAT工业架构:软主站,分布式时钟DC,PDO实时通信
视觉·运控
czhc11400756633 天前
529: XYScanAxis()类
mvvm·视觉·运控
czhc11400756634 天前
528:Halcon图像控件 启动轴状态实时监控
mvvm·视觉·运控
czhc11400756635 天前
5.27 :工业设备检测模式与安全防护详解
安全·mvvm·视觉·运控
czhc11400756637 天前
525:检测完善
mvvm·视觉
学不懂飞行器7 天前
【电赛保姆级教程】电赛视觉怎么选?怎么调?从OpenMV到边缘计算硬核避坑指南(附高鲁棒通信源码)
人工智能·stm32·边缘计算·电赛·视觉
czhc11400756639 天前
523:MVVM 检测业务
视觉
czhc114007566310 天前
ICommand 视觉、运控522
视觉
czhc114007566311 天前
视觉521 halcon24.05 测试
视觉