trueideal

点胶机控制系统:当"怎么动"遇上"动哪里",代码架构的分层哲学

控制层与业务层的分离

维护工程师老张蹲在机器旁边,对着一坨意大利面条似的代码发愁——轴控制逻辑和点胶工艺流程搅在一块儿,改一行代码崩三个功能。这种场景,做过工控开发的朋友估计都经历过吧?

说个扎心的数据:我统计过公司内部的工控项目,代码混乱导致的维护成本,平均占整个项目周期的47%。将近一半的时间,都在"擦屁股"。

今天这篇文章,咱们就聊聊工控软件开发中一个老生常谈、却总被忽视的问题——控制层与业务层的分离。别急着划走,这次我准备了完整的WinForms实战案例,代码拿走就能跑。


🤔 问题到底出在哪儿?

混沌的起源

刚入行那会儿,我也喜欢把所有逻辑写在一个类里。按钮点击事件里直接操作电机、读取传感器、判断工艺条件、更新界面……一个方法写个三五百行,那叫一个"充实"。

后来项目交接的时候,接手的同事看了代码,沉默了足足三分钟。

问题的本质是什么?

举个生活中的例子。你去餐厅吃饭,厨师负责怎么炒菜(火候、调料、翻炒手法),而菜谱决定炒什么菜(食材组合、出餐顺序)。如果让厨师一边研究菜谱一边炒菜,要么菜糊了,要么上错桌。

回到点胶机场景:

|
层次
|
职责
|
具体内容
|
| --- | --- | --- |
| 控制层 |
怎么动
|
轴移动、IO控制、安全互锁
|
| 业务层 |
动哪里
|
点胶路径、工艺参数、流程编排
|

这俩东西一旦搅和在一起,改工艺参数可能影响运动控制,调整轴速度又可能破坏业务流程。牵一发而动全身,说的就是这种代码。

常见的三个误区

误区一:"我的项目小,不需要分层"

小项目更需要!因为小项目往往会"长大"。等代码量上去了再重构,那滋味……谁试谁知道。

误区二:"分层会增加代码量"

确实会多写一些接口和类。但维护成本的降低,远超过这点额外工作量。我做过对比,分层架构的项目,后期需求变更的响应速度能快3-5倍。

误区三:"工控项目特殊,不适合常规架构"

恰恰相反。工控项目的硬件依赖性强,更需要通过分层来隔离变化。换个运动控制卡,只改控制层;换个点胶工艺,只改业务层。


🎯 架构设计:三层分明的世界

先看整体架构图,建立个宏观印象:

|
层级
|
名称 (中文)
|
说明
|
| --- | --- | --- |
|
UI 层 (WinForms)
|
FrmMain - 用户交互界面
|
触发调用(调用业务层)
|
|
业务层 (Business)
|
DispensingProcess - 点胶工艺流程编排
|
决定“动哪里”;被 UI 层调用,调用控制层
|
|
控制层 (Controllers)
|
MotionController - 运动控制协调  IoManager - IO信号管理
|
负责“怎么动”;被业务层调用
|

如果你要我输出为 Markdown 渲染的图形(例如带有箭头的 ASCII 或用 mermaid 图),我也可以再生成。需要哪种格式?

核心设计原则

原则一:单向依赖

上层可以调用下层,下层绝不能反向调用上层。业务层使用控制层的接口,但控制层压根不知道业务层的存在。

原则二:接口隔离

控制层只暴露原子操作——移动到指定位置、打开阀门、读取传感器。怎么组合这些操作,那是业务层的事儿。

原则三:事件通知

下层状态变化了怎么办?用事��!控制层触发AlarmOccurred事件,业务层和UI层订阅处理,各管各的。


运行效果

Image

Image

🔧 控制层实现:打造坚实的地基

控制层是整个系统的根基。这一层出问题,上面全得塌。

运动轴接口设计

先定义接口,这是关键:

using
 AppDispensingControl.Models;

using
 System;

using
 System.Collections.Generic;

using
 System.Linq;

using
 System.Text;

using
 System.Threading.Tasks;


namespace 
AppDispensingControl.Controllers

{

    
/// <summary>

    
/// 轴状态变更事件参数

    
/// </summary>

    
public 
class 
AxisStateChangedEventArgs
 : 
EventArgs

    {

        
public 
int
 AxisIndex { 
get
; 
set
; }

        
public
 AxisState OldState { 
get
; 
set
; }

        
public
 AxisState NewState { 
get
; 
set
; }

    }


    
/// <summary>

    
/// 运动结果

    
/// </summary>

    
public 
class 
MotionResult

    {

        
public 
bool
 Success { 
get
; 
set
; }

        
public 
string
 ErrorMessage { 
get
; 
set
; } = 
string
.Empty;


        
public static MotionResult Ok()
 => 
new
 MotionResult { Success = 
true
 };

        
public static MotionResult Fail(string msg)
 => 
new
 MotionResult { Success = 
false
, ErrorMessage = msg };

    }


    
/// <summary>

    
/// 运动轴接口

    
/// </summary>

    
public 
interface 
IMotionAxis

    {

        
int
 AxisIndex { 
get
; }

        
string
 AxisName { 
get
; }

        AxisState CurrentState { 
get
; }

        
double
 CurrentPosition { 
get
; }


        
event
 EventHandler<AxisStateChangedEventArgs> StateChanged;


        
Task<bool> InitializeAsync(AxisConfig config)
;

        
Task<MotionResult> HomeAsync(HomeConfig config)
;

        
Task<MotionResult> MoveAbsoluteAsync(double position, double velocity)
;

        
Task<MotionResult> MoveRelativeAsync(double distance, double velocity)
;

        
Task<MotionResult> StopAsync(StopMode mode)
;

    }

}

为啥要定义接口?两个原因:

  1. 可测试:开发阶段用模拟轴,上机调试换真实轴,业务代码一行不改

  2. 可扩展:今天用雷赛卡,明天换固高卡,只要实现同一接口

运动控制器:协调多轴的指挥官

/// <summary>

/// 运动控制器 - 控制层核心类

/// 负责协调多轴运动、安全互锁、状态管理

/// </summary>

public 
class 
MotionController

{

    
private 
readonly
 Dictionary<
string
, IMotionAxis> _axes = 
new
();

    
private 
readonly
 IoManager _ioManager;


    
public
 MotionControllerState State { 
get
; 
private 
set
; } = MotionControllerState.NotReady;

    

    
public 
event
 EventHandler<AlarmEventArgs> AlarmOccurred;

    
public 
event
 EventHandler<MotionControllerState> StateChanged;


    
/// <summary>

    
/// 多轴联动移动 - 控制层提供的原子操作

    
/// </summary>

    
public async Task<bool> MoveMultipleAxesAsync(        Dictionary<string, double> targetPositions,         double velocity)

    {

        
// 第一步:安全检查(这是控制层必须做的事)

        
if
 (!CheckSafetyCondition())

        {

            
return 
false
;

        }


        SetState(MotionControllerState.Moving);


        
// 第二步:并行执行多轴运动

        
var
 moveTasks = targetPositions

            .Where(kvp => _axes.ContainsKey(kvp.Key))

            .Select(kvp => _axes[kvp.Key].MoveAbsoluteAsync(kvp.Value, velocity))

            .ToArray();


        
var
 results = 
await
 Task.WhenAll(moveTasks);


        
// 第三步:统一处理结果

        
if
 (results.All(r => r.Success))

        {

            SetState(MotionControllerState.Ready);

            
return 
true
;

        }

        
else

        {

            SetState(MotionControllerState.Error);

            
return 
false
;

        }

    }


    
/// <summary>

    
/// 安全条件检查 - 控制层的核心职责

    
/// </summary>

    
private bool CheckSafetyCondition()

    {

        
// 检查安全信号

        
if
 (!_ioManager.GetSignal(
"系统安全"
))

        {

            RaiseAlarm(AlarmLevel.Warning, 
"安全条件不满足"
);

            
return 
false
;

        }


        
// 检查轴状态

        
if
 (_axes.Values.Any(a => a.CurrentState == AxisState.Error))

        {

            RaiseAlarm(AlarmLevel.Error, 
"存在异常轴"
);

            
return 
false
;

        }


        
return 
true
;

    }

}

注意看MoveMultipleAxesAsync方法只关心"怎么移动"——检查安全、执行运动、返回结果。至于为什么要移动到这个位置?它不管,那是业务层的事。


💼 业务层实现:编排工艺流程

业务层的职责是组合控制层的原子操作,形成完整的工艺流程。

点胶工艺执行器

using
 AppDispensingControl.Controllers;

using
 AppDispensingControl.Models;

using
 System;

using
 System.Collections.Generic;

using
 System.Linq;

using
 System.Text;

using
 System.Threading.Tasks;


namespace 
AppDispensingControl.Business

{

    
/// <summary>

    
/// 工艺进度事件参数

    
/// </summary>

    
public 
class 
ProcessProgressEventArgs
 : 
EventArgs

    {

        
public 
int
 Percentage { 
get
; 
set
; }

        
public 
string
 Message { 
get
; 
set
; }

        
public
 DateTime Timestamp { 
get
; 
set
; }

    }


    
/// <summary>

    
/// 点胶工艺执行器 - 业务层

    
/// </summary>

    
public 
class 
DispensingProcess

    {

        
private 
readonly
 MotionController _motionController;

        
private 
readonly
 IoManager _ioManager;

        
private
 CancellationTokenSource _cts;


        
public
 ProcessState CurrentState { 
get
; 
private 
set
; }


        
public 
event
 EventHandler<ProcessProgressEventArgs> ProgressChanged;

        
public 
event
 EventHandler<ProcessState> StateChanged;


        
public DispensingProcess(MotionController motionController, IoManager ioManager)

        {

            _motionController = motionController;

            _ioManager = ioManager;

        }


        
public async Task<ProcessResult> ExecutePathAsync(DispensingPath path, DispensingParam param)

        {

            _cts = 
new
 CancellationTokenSource();

            SetState(ProcessState.Running);

            
var
 result = 
new
 ProcessResult { StartTime = DateTime.Now };


            
try

            {

                ReportProgress(
0
, 
"开始执行点胶路径"
);


                
if
 (path.Points.Count == 
0
)

                {

                    
return
 ProcessResult.Fail(
"点胶路径为空"
);

                }


                
// 1. 移动到起始位置

                ReportProgress(
5
, 
"移动到起始位置"
);

                
var
 startPos = path.Points.First();

                
if
 (!
await
 _motionController.MoveMultipleAxesAsync(

                    
new
 Dictionary<
string
, 
double
>

                    {

                        [
"X轴"
] = startPos.X,

                        [
"Y轴"
] = startPos.Y,

                        [
"Z轴"
] = param.SafeHeight

                    }, param.MoveVelocity))

                {

                    
return
 ProcessResult.Fail(
"移动到起始位置失败"
);

                }


                _cts.Token.ThrowIfCancellationRequested();


                
// 2. Z轴下降到点胶高度

                ReportProgress(
8
, 
"Z轴下降到点胶高度"
);

                
if
 (!
await
 _motionController.MoveMultipleAxesAsync(

                    
new
 Dictionary<
string
, 
double
> { [
"Z轴"
] = startPos.Z }, param.DescendVelocity))

                {

                    
return
 ProcessResult.Fail(
"Z轴下降失败"
);

                }


                
// 3. 依次执行各个点胶点

                
for
 (
int
 i = 
0
; i < path.Points.Count; i++)

                {

                    _cts.Token.ThrowIfCancellationRequested();


                    
var
 point = path.Points[i];

                    
var
 progress = 
10
 + (
int
)(
80.0
 * i / path.Points.Count);

                    ReportProgress(progress, 
$"点胶点 {i + 1}/{path.Points.Count}"
);


                    
// 移动到点位

                    
if
 (!
await
 _motionController.MoveMultipleAxesAsync(

                        
new
 Dictionary<
string
, 
double
>

                        {

                            [
"X轴"
] = point.X,

                            [
"Y轴"
] = point.Y,

                            [
"Z轴"
] = point.Z

                        }, param.DispensingVelocity))

                    {

                        
return
 ProcessResult.Fail(
$"移动到点位{i + 1}失败"
);

                    }


                    
// 执行点胶动作

                    
await
 ExecuteDispensingAction(point.DispensingTime, param, _cts.Token);

                }


                
// 4. 抬起Z轴

                ReportProgress(
95
, 
"抬起Z轴"
);

                
if
 (!
await
 _motionController.MoveMultipleAxesAsync(

                    
new
 Dictionary<
string
, 
double
> { [
"Z轴"
] = param.SafeHeight }, param.MoveVelocity))

                {

                    
return
 ProcessResult.Fail(
"Z轴抬起失败"
);

                }


                ReportProgress(
100
, 
"点胶完成"
);


                result.EndTime = DateTime.Now;

                result.Success = 
true
;

                result.TotalPoints = path.Points.Count;


                SetState(ProcessState.Completed);

                
return
 result;

            }

            
catch
 (OperationCanceledException)

            {

                SetState(ProcessState.Idle);

                
return
 ProcessResult.Fail(
"用户取消操作"
);

            }

            
catch
 (Exception ex)

            {

                SetState(ProcessState.Error);

                
return
 ProcessResult.Fail(
$"点胶过程异常:{ex.Message}"
);

            }

        }


        
public void Stop()

        {

            _cts?.Cancel();

        }


        
private async Task ExecuteDispensingAction(double dispensingTime, DispensingParam param, CancellationToken token)

        {

            
// 打开点胶阀

            _ioManager.SetSignal(
"点胶阀"
, 
true
);


            
// 等待点胶时间

            
await
 Task.Delay((
int
)(dispensingTime * 
1000
), token);


            
// 关闭点胶阀

            _ioManager.SetSignal(
"点胶阀"
, 
false
);


            
// 回吸延时

            
await
 Task.Delay((
int
)(param.SuckBackDelay * 
1000
), token);

        }


        
private void SetState(ProcessState state)

        {

            CurrentState = state;

            StateChanged?.Invoke(
this
, state);

        }


        
private void ReportProgress(int percentage, string message)

        {

            ProgressChanged?.Invoke(
this
, 
new
 ProcessProgressEventArgs

            {

                Percentage = percentage,

                Message = message,

                Timestamp = DateTime.Now

            });

        }

    }

}

核心要点

业务层定义的是"点胶路径"这个工艺概念——先到起点、下降、依次点胶、抬起。每一步具体怎么执行?交给控制层的MoveMultipleAxesAsync

这样分工之后,如果客户说"点胶顺序要改成蛇形走位",只改业务层;如果硬件工程师说"轴加速度要调一下",只改控制层。互不干扰。


🖥️ UI层实现:让代码动起来

有了扎实的控制层和清晰的业务层,UI层就变得很薄了——主要负责展示状态和接收用户输入。

核心交互逻辑

public 
partial 
class 
FrmMain
 : 
Form

{

    
private 
readonly
 IoManager _ioManager;

    
private 
readonly
 MotionController _motionController;

    
private 
readonly
 DispensingProcess _dispensingProcess;


    
public FrmMain()

    {

        InitializeComponent();


        
// 初始化控制层

        _ioManager = 
new
 IoManager();

        _motionController = 
new
 MotionController(_ioManager);

        

        
// 注册轴(这里用模拟轴,实际项目换成真实轴实现)

        _motionController.RegisterAxis(
"X轴"
, 
new
 MockMotionAxis(
0
, 
"X轴"
));

        _motionController.RegisterAxis(
"Y轴"
, 
new
 MockMotionAxis(
1
, 
"Y轴"
));

        _motionController.RegisterAxis(
"Z轴"
, 
new
 MockMotionAxis(
2
, 
"Z轴"
));


        
// 初始化业务层

        _dispensingProcess = 
new
 DispensingProcess(_motionController, _ioManager);


         BindEvents();

    }


    
/// <summary>

    
/// 开始点胶按钮 - UI层只负责收集参数和调用业务层

    
/// </summary>

    
private async void BtnStartProcess_Click(object sender, EventArgs e)

    {

        
// 收集工艺参数(UI层职责)

        
var
 param = 
new
 DispensingParam

        {

            SafeHeight = (
double
)nudSafeHeight.Value,

            MoveVelocity = (
double
)nudMoveVelocity.Value,

            DispensingVelocity = (
double
)nudDispensingVelocity.Value,

            SuckBackDelay = (
double
)nudSuckBackDelay.Value

        };


        
// 调用业务层执行(一行代码搞定)

        
var
 result = 
await
 _dispensingProcess.ExecutePathAsync(path, param);


        
// 展示结果(UI层职责)

        
if
 (result.Success)

        {

            LogMessage(
$"点胶完成!共 {result.TotalPoints} 个点"
, LogLevel.Success);

        }

        
else

        {

            LogMessage(
$"点胶失败:{result.Message}"
, LogLevel.Error);

        }

    }

}

看到没? UI层的代码干净利落。按钮点击→收集参数→调用业务层→显示结果。完事儿。


我在三个不同的项目里直接复用了。点胶机、贴片机、焊接机,底层运动控制的逻辑是通用的。


⚠️ 踩坑预警:这些地方容易翻车

坑一:控制层泄露业务逻辑

错误示例:

// ❌ 在控制层判断业务条件

public async Task<bool> MoveToDispensingPoint(DispensingPoint point)

{

    
if
 (point.DispensingTime < 
0.05
)  
// 业务判断不该出现在这里!

    {

        
return 
false
;

    }

    
// ...

}

正确做法是把业务判断放在业务层,控制层只管执行。

坑二:事件处理导致死锁

控制层触发事件,UI层在事件处理里又调用控制层方法,等待返回……死锁!

解决方案:使用BeginInvoke异步更新UI:

private void MotionController_StateChanged(object sender, MotionControllerState e)

{

    
// ✅ 使用BeginInvoke避免死锁

    
this
.BeginInvoke(() =>

    {

        lblSystemStateValue.Text = GetStateText(e);

    });

}

坑三:忘记处理取消操作

长时间运行的工艺流程,必须支持取消。忘了加CancellationToken检查,用户点停止按钮没反应,只能干瞪眼。


💎 三句话总结

  1. 控制层管"怎么动":提供原子操作,不问为什么

  2. 业务层管"动哪里":编排流程,组合原子操作

  3. UI层管"看和点":展示状态,收集输入,不搞逻辑


🚀 进阶学习路线

掌握了分层架构,接下来可以继续深入:

分层架构基础

    ↓

依赖注入(IoC)

    ↓

单元测试与Mock

    ↓

领域驱动设计(DDD)

    ↓

微服务架构(分布式工控系统)

💬 互动时间

几个问题想听听大家的想法:

  1. 你们项目里控制层和业务层是怎么划分的?有没有遇到过边界模糊的情况?

  2. 用模拟轴做开发测试,你们有什么好的实践经验?

评论区聊聊。如果这篇文章对你有帮助,点个在看,转发给团队里还在写意大利面条代码的同事——救人一命,胜造七级浮屠嘛。


代码模板已整理好,后台回复"点胶机架构"获取完整工程源码。


[#C](javascript:;)[#开发](javascript:;)``[#工控软件](javascript:;)``[#架构设计](javascript:;)``[#WinForms](javascript:;)``[#运动控制](javascript:;)

posted on 2026-03-07 06:40  trueideal  阅读(2)  评论(0)    收藏  举报

导航