我们直接聊工业上位机里的OEE计算。这几年只要做工厂数字化、设备联网、产线追溯,OEE就是一个绕不开的指标。我见过太多项目,报表做得花团锦簇,但底层数据一塌糊涂,算出来的OEE根本没人信。原因无外乎两点:一是对OEE的公式理解停留在PPT层面,二是上位机代码里的时间口径跟车间实际对不上。这篇文章我会把OEE从概念到上位机落地实现完整过一遍,重点放在数据建模、代码实现和现场调试这三个环节,全部基于我实际写过的项目代码,你可以直接拿去改。
1. OEE计算最容易翻车的不是公式,而是时间口径
OEE的公式本身极其简单,就是可用率、表现指数、质量指数的乘积,任何一个学过基础工业工程的人都能背出来。但真正到了上位机开发阶段,问题从来不出在乘法上,而是出在“公式里的每一项,你到底用的是哪个时间”。
什么叫可用率?教科书上写的是“设备实际运行时间除以计划生产时间”。但计划生产时间到底含不含午休?含不含换班交接的停机?含不含设备预热?现场工人的理解、设备工程师的理解、上位机代码里的理解,三个版本经常对不上。我遇到过一个客户,他们的设备中午停机一小时是常态,但PLC里根本没有午休停机的信号,上位机按连续运行去算,OEE直接虚高十几个点,最后车间主任拿着报表去质问生产经理,场面非常尴尬。
所以第一个原则是:OEE计算的时间基准,必须和车间的真实排班逻辑严格对齐。代码里不能默认“计划时间就是24小时”,更不能默认“设备只要有电就算计划时间”。
在实践中,我一般建议上位机获取以下三个层面的时间数据:
- 排班表时间:来自MES或手工配置,定义了每个班次的开始和结束,包括午休、晚休等休息时段的起止。
- 设备状态时间:来自PLC或设备控制器,定义了设备在任意时刻处于什么状态,是运行、待机、故障、维修还是停机。
- 产量与不良数:来自传感器计数、PLC计数或人工录入,定义了在某个时间段内实际生产了多少件、其中多少件不合格。
OEE计算的上位机实现,本质上就是把这三类数据按统一的时间轴做对齐,然后切分到每个班次、每个小时、每个工单上去聚合。这一步做扎实了,OEE的准确率就有了七成保障。
还有一个容易踩的坑是“时间片”的粒度。有的设备状态信号变化很快,一个故障可能只持续了十几秒,但如果你的上位机轮询周期是1分钟,这个故障就丢了。反过来,如果轮询周期是100毫秒,数据量又太大,存储和查询都会成为瓶颈。我一般建议:状态信号变化的采集用200到500毫秒的轮询或者直接走PLC的变位上报,聚合展示用1分钟的粒度,这样能兼顾实时性和存储开销。这个后面代码部分会细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 上位机OEE的完整数据模型:从设备信号到指标落库
聊完口径,接下来是数据结构。我见过不少团队的OEE代码写着写着就成了面条代码,几百行的if-else,把OEE的三个因子揉在一起算,最后连自己都说不清哪里出了问题。正确的做法是先建模,把“设备状态”“时间分段”“产量数据”拆成三层,各算各的,最后再做乘法。
2.1 设备状态模型:不要只盯着“运行”和“故障”
典型的工业上位机项目里,设备状态至少需要区分这么几类:运行(RUN)、待机(IDLE)、故障(FAULT)、维修(MAINT)、停机(DOWN)、离线(OFFLINE)。有的行业还会分得更细,比如半导体行业会有Process、Recipe Change、Hold等状态。
这些状态的核心价值在于:它们直接决定了可用率里“设备实际运行时间”的界定。在代码层面,我建议给每一个状态定义一个枚举,并且给每个枚举绑定一个“是否计入OEE分母/分子”的布尔属性。这样做的好处是:业务规则有调整时,只需要改配置,不用改算法。
拿C#上位机来举例:
csharp复制public enum DeviceState
{
[StateMapping(CountAsPlanned = true, CountAsRun = false)]
Idle = 0,
[StateMapping(CountAsPlanned = true, CountAsRun = true)]
Running = 1,
[StateMapping(CountAsPlanned = true, CountAsRun = false)]
Fault = 2,
[StateMapping(CountAsPlanned = true, CountAsRun = false)]
Maintenance = 3,
[StateMapping(CountAsPlanned = true, CountAsRun = false)]
Down = 4,
[StateMapping(CountAsPlanned = false, CountAsRun = false)]
Offline = 5
}
注意这里有个小细节:Offline状态默认不计入计划时间。为什么?因为设备断电或者彻底脱离产线的时间,不应该算进OEE的分母里。但有的工厂有不同意见,他们觉得只要设备在班次内,不管是否离线都该算计划时间。这个没有统一标准,关键看你们和客户约定的口径。所以我把StateMapping这个attribute做成可配置的,现场调的时候不用改代码,改配置文件就行。
2.2 时间分段:把一天切成可计算的片段
设备状态是瞬时的、不平滑的,而OEE是按班次聚合的。中间需要一层“时间分段”的转换,也就是把连续的状态流切分成一段段有明确起止时间、明确状态属性、明确归属班次的记录。
这张表我一般叫DeviceStateSegment,结构大致如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| SegmentId | bigint | 分段主键 |
| DeviceId | int | 设备ID |
| StartTime | datetime | 分段开始时间 |
| EndTime | datetime | 分段结束时间 |
| State | int | 状态枚举值 |
| DurationSeconds | int | 持续时间(秒) |
| ShiftId | int | 所属班次 |
| OrderId | string | 关联工单 |
为什么要有这么一张分段表?因为它把“信号的连续变化”转成了“业务可查询的离散记录”。报表要查某个班次某个设备的状态分布时,一
