20 KiB
20 KiB
YJH-MES 8Multi协议接入优化文档
版本: v1.0.30
更新日期: 2025-12-01
项目: YJH-MES (yjh-mes + mes-ui)
说明: 本文档基于项目实际代码编写,准确反映当前实现
一、项目概述
1.1 技术栈
| 层级 | 技术 | 说明 |
|---|---|---|
| 后端 | Spring Boot + MyBatis-Plus | yjh-mes模块 |
| 前端 | Vue 2 + Element UI | mes-ui模块 |
| 数据库 | MySQL 8.0 | device / device_data 表 |
1.2 协议类型
| 协议 | 数据格式 | 判断条件 | 设备号来源 |
|---|---|---|---|
| 8ADPRO | 帧格式 | 以+开头且以EEFF结尾 |
parts[0] |
| 8MULTI | JSON格式 | 以{开头且以}结尾 |
id字段后4位 |
二、数据上报接口
2.1 接口信息
POST /equipment/info/ingest/raw
Content-Type: text/plain
2.2 8ADPRO帧格式
+YAV:设备号,温度,电流,计数1,计数2,模拟1,模拟2,模拟3,模拟4,模拟5,模拟6,模拟7,数字1,数字2,数字3,数字4,数字5,数字6,EEFF
示例:
+YAV:1,25.5,3.2,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,EEFF
2.3 8MULTI JSON格式
{
"header": "+YAV",
"Card": "8multi",
"id": 101126010002,
"dt": 300,
"a1": 0.106,
"a2": 0.106,
"q1": 0.098,
"q2": 0.098,
"t1": 25.5,
"t2": 26.0,
"dht1": 65.0,
"dht2": 60.0,
"cn_reg": 1,
"f1": 50.00,
"f2": 50.00,
"c1": 5,
"c2": 3,
"do_reg": 0,
"reg1": 0.000,
"reg2": 0.000,
"tv": 0,
"save": 0,
"oee": 100,
"end": "EEFF"
}
三、8MULTI卡号ID格式
卡号ID共12位: 品牌1位 + 年份1位 + 月日4位 + 用户号2位 + 序号4位
示例: 101126010002
1 - 品牌前缀
0 - 年份(0=2025, 1=2026, 2=2027...)
1126 - 月日(11月26日)
01 - 用户号
0002 - 设备序号(后端使用后4位作为设备号)
四、8MULTI JSON字段映射
4.1 字段对照表
| JSON字段 | 数据库字段 | 含义 | 说明 |
|---|---|---|---|
| id | device_no | 卡号 | 取后4位作为设备号 |
| a1 | current1_value | 电流1 | 已换算实际值(A) |
| a2 | current2_value | 电流2 | 已换算实际值(A) |
| q1 | quality1_value | 质量1 | 已换算实际值 |
| q2 | quality2_value | 质量2 | 已换算实际值 |
| t1 | temperature_c | 温度1 | °C |
| t2 | analog4 | 温度2 | °C(预留) |
| dht1 | humidity | 湿度1 | % |
| dht2 | analog5 | 湿度2 | %(预留) |
| c1 | counter1 | 计数1 | 发送完清零 |
| c2 | counter2 | 计数2 | 发送完清零 |
| f1 | f1 | 测频1 | Hz |
| f2 | f2 | 测频2 | Hz |
| cn_reg | cn_reg | 屏幕状态 | 0-7 |
| do_reg | do_reg | 状态输出 | 位编码 |
| reg1 | reg1 | 从机数据1 | |
| reg2 | reg2 | 从机数据2 | |
| tv | tv | 显示 | 1有0无 |
| save | save_flag | 边缘存储 | 1有0无 |
| oee | oee | OEE计算 | 0或100 |
| dt | dt | 采样间隔 | 秒 |
4.2 cn_reg屏幕状态定义
| 值 | 含义 | 前端颜色 |
|---|---|---|
| 0 | 计划停机 | 灰色(info) |
| 1 | 正常工作 | 绿色(success) |
| 2 | 待机 | 黄色(warning) |
| 3 | 故障 | 红色(danger) |
| 4 | 在修 | 黄色(warning) |
| 5 | 缺人 | 黄色(warning) |
| 6 | 缺料 | 黄色(warning) |
| 7 | 清零 | 灰色(info) |
4.3 cn_reg自动判断逻辑(下位机执行)
cn_reg=0(计划停机): 3次都是 [(a1'<20%) & (a2'<20%)] || [(f1=0) & (f2=0)]
cn_reg=1(正常工作): 3次都是 (a1'>40%) || (a2'>40%) || (f1>0.1) || (f2>0.1)
cn_reg=2(待机) : 3次都是 (a1介于20-40%) || (a2介于20-40%)
cn_reg=3-7 : 人工设置(触摸屏点击)
4.4 OEE计算规则
下位机逻辑: if(cn_reg=1) oee=100; else oee=0;
上位机: 计算时间段内oee均值
4.5 上报频率
1. 开机上发一次(dt=0)
2. 开机60s内,10s发一次
3. 开机超过60s,按设置间隔发送(默认300s)
4. 计数值改变时上发(间隔至少60s,dt=60)
5. 不接计数器时300s发一次(dt=300)
五、数据库设计
5.1 device表扩展字段
-- 协议类型
ALTER TABLE `device` ADD COLUMN `protocol_type` VARCHAR(16) DEFAULT '8ADPRO'
COMMENT '协议类型(8ADPRO/8MULTI)';
-- 量程配置(用于超量程警告)
ALTER TABLE `device` ADD COLUMN `current1_range_start` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `current1_range_end` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `current2_range_start` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `current2_range_end` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality1_range_start` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality1_range_end` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality2_range_start` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality2_range_end` DECIMAL(10,3) DEFAULT NULL;
ALTER TABLE `device` ADD COLUMN `quality1_unit` VARCHAR(10) DEFAULT 'kg';
ALTER TABLE `device` ADD COLUMN `quality2_unit` VARCHAR(10) DEFAULT 'kg';
ALTER TABLE `device` ADD COLUMN `current_unit` VARCHAR(10) DEFAULT 'A';
ALTER TABLE `device` ADD COLUMN `voltage1` DECIMAL(10,3) DEFAULT NULL COMMENT '电压1(用于功率计算)';
ALTER TABLE `device` ADD COLUMN `voltage2` DECIMAL(10,3) DEFAULT NULL COMMENT '电压2(用于功率计算)';
ALTER TABLE `device` ADD COLUMN `counter1_baseline` INT DEFAULT 0 COMMENT '计数1清零基准';
ALTER TABLE `device` ADD COLUMN `counter2_baseline` INT DEFAULT 0 COMMENT '计数2清零基准';
-- 设备扩展信息
ALTER TABLE `device` ADD COLUMN `location` VARCHAR(255) DEFAULT NULL COMMENT '设备位置';
ALTER TABLE `device` ADD COLUMN `brand` VARCHAR(128) DEFAULT NULL COMMENT '设备品牌';
ALTER TABLE `device` ADD COLUMN `model` VARCHAR(128) DEFAULT NULL COMMENT '设备型号';
ALTER TABLE `device` ADD COLUMN `workshop_id` BIGINT DEFAULT NULL COMMENT '车间ID(关联md_workshop)';
ALTER TABLE `device` ADD COLUMN `section` VARCHAR(64) DEFAULT NULL COMMENT '工序';
5.2 device_data表扩展字段
-- 8Multi V2.8 专用字段
ALTER TABLE `device_data` ADD COLUMN `current1_value` DECIMAL(10,3) DEFAULT NULL COMMENT '电流1实际值';
ALTER TABLE `device_data` ADD COLUMN `current2_value` DECIMAL(10,3) DEFAULT NULL COMMENT '电流2实际值';
ALTER TABLE `device_data` ADD COLUMN `quality1_value` DECIMAL(10,3) DEFAULT NULL COMMENT '质量1实际值';
ALTER TABLE `device_data` ADD COLUMN `quality2_value` DECIMAL(10,3) DEFAULT NULL COMMENT '质量2实际值';
ALTER TABLE `device_data` ADD COLUMN `humidity` DECIMAL(10,3) DEFAULT NULL COMMENT '湿度';
ALTER TABLE `device_data` ADD COLUMN `f1` DECIMAL(10,2) DEFAULT NULL COMMENT '测频1(Hz)';
ALTER TABLE `device_data` ADD COLUMN `f2` DECIMAL(10,2) DEFAULT NULL COMMENT '测频2(Hz)';
ALTER TABLE `device_data` ADD COLUMN `cn_reg` INT DEFAULT NULL COMMENT '屏幕状态(0-7)';
ALTER TABLE `device_data` ADD COLUMN `do_reg` VARCHAR(16) DEFAULT NULL COMMENT '状态输出';
ALTER TABLE `device_data` ADD COLUMN `reg1` DECIMAL(12,3) DEFAULT NULL COMMENT '从机数据1';
ALTER TABLE `device_data` ADD COLUMN `reg2` DECIMAL(12,3) DEFAULT NULL COMMENT '从机数据2';
ALTER TABLE `device_data` ADD COLUMN `tv` INT DEFAULT NULL COMMENT '显示';
ALTER TABLE `device_data` ADD COLUMN `save_flag` INT DEFAULT NULL COMMENT '边缘存储';
ALTER TABLE `device_data` ADD COLUMN `oee` INT DEFAULT NULL COMMENT 'OEE计算值';
ALTER TABLE `device_data` ADD COLUMN `dt` INT DEFAULT NULL COMMENT '采样间隔(秒)';
ALTER TABLE `device_data` ADD COLUMN `power1` DECIMAL(12,3) DEFAULT NULL COMMENT '功率1';
ALTER TABLE `device_data` ADD COLUMN `power2` DECIMAL(12,3) DEFAULT NULL COMMENT '功率2';
5.3 计数器触发器(仅8ADPRO使用)
DROP TRIGGER IF EXISTS `update_counter_totals_on_insert`;
DELIMITER $$
CREATE TRIGGER `update_counter_totals_on_insert`
BEFORE INSERT ON `device_data`
FOR EACH ROW
BEGIN
DECLARE last_c1 BIGINT UNSIGNED DEFAULT 0;
DECLARE last_c2 BIGINT UNSIGNED DEFAULT 0;
DECLARE proto VARCHAR(16) DEFAULT '8ADPRO';
SELECT IFNULL(protocol_type, '8ADPRO') INTO proto FROM device WHERE id = NEW.device_id LIMIT 1;
-- 仅8ADPRO使用触发器累计
IF proto = '8ADPRO' OR proto IS NULL THEN
SELECT IFNULL(counter1_total, 0), IFNULL(counter2_total, 0) INTO last_c1, last_c2
FROM device_data WHERE device_id = NEW.device_id ORDER BY collected_at DESC LIMIT 1;
SET NEW.counter1_total = IF(NEW.counter1 > 0, last_c1 + NEW.counter1, last_c1);
SET NEW.counter2_total = IF(NEW.counter2 > 0, last_c2 + NEW.counter2, last_c2);
END IF;
END$$
DELIMITER ;
六、后端核心代码
6.1 文件清单
yjh-mes/src/main/java/cn/sourceplan/equipment/
├── controller/
│ └── EquipmentInfoController.java # 设备信息控制器
├── service/
│ ├── IEquipmentInfoService.java # 服务接口
│ ├── Multi8ProtocolService.java # 8Multi协议解析服务 ⭐
│ └── impl/
│ └── EquipmentInfoServiceImpl.java # 服务实现
├── domain/
│ ├── MesDevice.java # 设备实体
│ └── DeviceData.java # 设备数据实体
└── mapper/
├── MesDeviceMapper.java # 设备Mapper
└── DeviceDataMapper.java # 数据Mapper
6.2 协议判断入口 (EquipmentInfoServiceImpl.java)
@Override
@Transactional(rollbackFor = Exception.class)
public void ingestFrame(String frameRaw) {
if (StringUtils.isBlank(frameRaw)) throw new IllegalArgumentException("frame is empty");
String raw = frameRaw.trim();
// 8Multi: 以{开头且以}结尾(JSON格式)
if (raw.startsWith("{") && raw.endsWith("}")) {
log.info("检测到8Multi协议(JSON格式)");
multi8ProtocolService.parse8MultiJson(raw);
return;
}
// 8AdPro: 以+开头且以EEFF结尾(帧格式)
if (!raw.startsWith("+") || !raw.endsWith("EEFF")) {
throw new IllegalArgumentException("协议格式无效");
}
// ... 8ADPRO处理逻辑
}
6.3 8Multi JSON解析 (Multi8ProtocolService.java)
public void parse8MultiJson(String rawJson) {
JSONObject json = JSONUtil.parseObj(rawJson);
// 1. 提取设备号(ID后4位)
String idStr = json.getStr("id");
String lastFour = idStr.substring(idStr.length() - 4);
Integer deviceNo = Integer.valueOf(lastFour);
// 2. 查找或创建设备
MesDevice device = findOrCreateDevice(deviceNo);
// 3. 构建DeviceData
DeviceData data = new DeviceData();
data.setDeviceId(device.getId());
data.setCollectedAt(new Timestamp(System.currentTimeMillis()));
data.setFrameRaw(rawJson);
// 4. 字段映射(下位机已换算,直接存储)
data.setCurrent1Value(parseDecimal(json.getStr("a1")));
data.setCurrent2Value(parseDecimal(json.getStr("a2")));
data.setQuality1Value(parseDecimal(json.getStr("q1")));
data.setQuality2Value(parseDecimal(json.getStr("q2")));
data.setTemperatureC(parseDecimal(json.getStr("t1")));
data.setHumidity(parseDecimal(json.getStr("dht1")));
data.setCounter1(parseInt(json.getStr("c1")));
data.setCounter2(parseInt(json.getStr("c2")));
data.setF1(parseDecimal(json.getStr("f1")));
data.setF2(parseDecimal(json.getStr("f2")));
data.setCnReg(parseInt(json.getStr("cn_reg")));
data.setDoReg(json.getStr("do_reg"));
data.setReg1(parseDecimal(json.getStr("reg1")));
data.setReg2(parseDecimal(json.getStr("reg2")));
data.setTv(parseInt(json.getStr("tv")));
data.setSaveFlag(parseInt(json.getStr("save")));
data.setOee(parseInt(json.getStr("oee")));
data.setDt(parseInt(json.getStr("dt")));
// 5. 计数器累计
DeviceData lastData = deviceDataMapper.selectLastByDeviceId(device.getId());
Long counter1Total = (lastData != null ? lastData.getCounter1Total() : 0L);
Long counter2Total = (lastData != null ? lastData.getCounter2Total() : 0L);
if (data.getCounter1() != null && data.getCounter1() > 0) {
counter1Total += data.getCounter1();
}
if (data.getCounter2() != null && data.getCounter2() > 0) {
counter2Total += data.getCounter2();
}
data.setCounter1Total(counter1Total);
data.setCounter2Total(counter2Total);
// 6. 保存
deviceDataMapper.insert(data);
}
6.4 OEE均值查询 (DeviceDataMapper.java)
@Select({
"<script>",
"SELECT AVG(oee) FROM device_data ",
"WHERE device_id = #{deviceId} AND oee IS NOT NULL",
"<if test='from != null and from != \"\"'>",
" AND collected_at >= #{from}",
"</if>",
"<if test='to != null and to != \"\"'>",
" AND collected_at <= #{to}",
"</if>",
"</script>"
})
Double selectOeeAvg(@Param("deviceId") Long deviceId, @Param("from") String from, @Param("to") String to);
七、前端实现
7.1 文件位置
mes-ui/src/views/mes/equipment/info/index.vue # 设备监控页面
mes-ui/src/api/mes/equipment/info.js # API接口
7.2 API接口
// 获取设备OEE均值
export function getOeeAvg(deviceId, from, to) {
return request({
url: '/equipment/info/oee/avg',
method: 'get',
params: { deviceId, from, to }
})
}
7.3 协议类型判断显示
<!-- 8ADPRO设备显示 -->
<div v-if="!d.protocol_type || d.protocol_type === '8ADPRO'" class="metrics">
<!-- 温度、电流、计数器等 -->
</div>
<!-- 8MULTI设备显示 -->
<div v-else-if="d.protocol_type === '8MULTI'" class="metrics">
<!-- 电流1/2、质量1/2、温湿度、OEE、状态等 -->
</div>
7.4 cn_reg状态显示
// 状态文本
cnRegText(val) {
const map = {
0: '计划停机', 1: '正常工作', 2: '待机', 3: '故障',
4: '在修', 5: '缺人', 6: '缺料', 7: '清零'
}
return map[val] || '未知'
},
// 状态颜色
cnRegType(val) {
const map = {
0: 'info', 1: 'success', 2: 'warning', 3: 'danger',
4: 'warning', 5: 'warning', 6: 'warning', 7: 'info'
}
return map[val] || 'info'
}
7.5 超量程红色警告
// 判断是否超量程
isOutOfRange(value, rangeStart, rangeEnd) {
if (value === null || value === undefined) return false
if (rangeStart === null && rangeEnd === null) return false
if (rangeStart !== null && value < rangeStart) return true
if (rangeEnd !== null && value > rangeEnd) return true
return false
}
<!-- 超量程显示红色 -->
<span :style="{color: isOutOfRange(d.current1_value, d.current1RangeStart, d.current1RangeEnd) ? '#F56C6C' : ''}">
{{ fmt(d.current1_value, 'A', 2) }}
</span>
7.6 设备编辑表单扩展
新增字段:
- 设备位置 (location)
- 设备品牌 (brand)
- 设备型号 (model)
- 所属车间 (workshopId) - 从md_workshop表读取
- 所属工序 (section) - 从pro_process表读取
- 量程配置 (current1/2RangeStart/End, quality1/2RangeStart/End) - 用于超量程警告
八、废弃字段说明
8Multi V2.8协议中不再使用,但保留以兼容8ADPRO:
| 废弃字段 | 替代方案 | 说明 |
|---|---|---|
| current1_raw, current2_raw | current1_value | 下位机已换算 |
| quality1_raw, quality2_raw | quality1_value | 下位机已换算 |
| status_work/stop/fault/reset | cn_reg (0-7) | 统一用屏幕状态 |
| frequency1, frequency2 | f1, f2 | 使用新测频字段 |
| counter1_total_8multi | counter1_total | 统一使用 |
| relay_power/alarm, do_soft_start/stop | do_reg | 统一用状态输出 |
| slave_data1/2 | reg1, reg2 | 使用新从机数据字段 |
| touchscreen_data | tv | 使用新显示字段 |
九、设备OEE功能
9.1 OEE页面改造
将设备OEE页面与8ADPRO/8MULTI设备关联,不再使用车间设备。
文件清单:
mes-ui/src/views/mes/equipment/oeeGantt/index.vue # OEE时序图页面
mes-ui/src/api/mes/equipment/equipment.js # API接口
yjh-mes/.../controller/EquipmentController.java # 控制器
yjh-mes/.../service/impl/EquipmentServiceImpl.java # 服务实现
yjh-mes/.../mapper/EquipmentMapper.java # Mapper接口
yjh-mes/.../resources/mapper/equipment/EquipmentMapper.xml # SQL
9.2 OEE页面筛选功能
<!-- 协议类型筛选 -->
<el-select v-model="queryParams.deviceType" @change="onDeviceTypeChange">
<el-option label="全部" value="" />
<el-option label="8ADPRO" value="8ADPRO" />
<el-option label="8MULTI" value="8MULTI" />
</el-select>
<!-- 设备筛选 -->
<el-select v-model="queryParams.deviceId" filterable>
<el-option v-for="d in deviceList" :key="d.id" :label="d.name" :value="d.id" />
</el-select>
9.3 设备列表API
// EquipmentController.java
@GetMapping("/allDevices")
public AjaxResult listAllDevices(String deviceType) {
return success(equipmentService.listAllDevices(deviceType));
}
<!-- EquipmentMapper.xml -->
<select id="selectMesDevices" resultType="Map">
SELECT id, device_no, device_name, protocol_type
FROM device WHERE is_active = 1
<if test="deviceType != null and deviceType != ''">
AND protocol_type = #{deviceType}
</if>
ORDER BY device_no
</select>
9.4 OEE数据查询
<!-- EquipmentMapper.xml -->
<select id="selectMesDeviceOee" parameterType="Map" resultType="Map">
SELECT
d.id as equipment_id,
d.device_name as equipment_name,
d.device_no as equipment_number,
dd.collected_at as record_time,
dd.cn_reg as equipment_status,
dd.oee as oee_value,
d.protocol_type
FROM device d
LEFT JOIN device_data dd ON dd.device_id = d.id
WHERE d.is_active = 1
AND dd.collected_at IS NOT NULL
AND dd.collected_at >= #{map.beginDate}
AND dd.collected_at <= #{map.endDate}
<if test="map.deviceType != null and map.deviceType != ''">
AND d.protocol_type = #{map.deviceType}
</if>
<if test="map.deviceId != null and map.deviceId != ''">
AND d.id = #{map.deviceId}
</if>
ORDER BY d.id DESC, dd.collected_at DESC
</select>
9.5 OEE状态映射
cn_reg值映射到OEE图表状态:
| cn_reg值 | 含义 | OEE状态 | 颜色 |
|---|---|---|---|
| 1 | 正常工作 | 运行中 | 绿色(#67C23A) |
| 3 | 故障 | 故障 | 红色(#F56C6C) |
| 0,2,4,5,6,7 | 其他 | 停机 | 灰色(#909399) |
// EquipmentServiceImpl.java - 状态转换
Integer cnReg = ((Number)cnRegObj).intValue();
Integer status;
switch (cnReg) {
case 1: status = 0; break; // 正常工作 -> 运行中
case 3: status = 2; break; // 故障
default: status = 1; break; // 其他 -> 停机
}
9.6 前端调整
移除的功能:
- 设备卡片上的OEE显示
- 设备详情中的OEE时段均值查询
保留的功能:
- OEE时序图页面(独立页面)
- 设备类型/设备筛选
- 时序图和饼状图显示
- 设备状态统计卡片
十、计数器处理差异
| 协议 | counter1/2值 | 累计方式 | 触发器 |
|---|---|---|---|
| 8ADPRO | limitToBinary(>0→1) | 数据库触发器累加 | ✅使用 |
| 8MULTI | 原始增量值 | Java代码累加 | ❌不使用 |
十一、注意事项
- 8MULTI协议识别: 根据数据格式判断,JSON格式即为8MULTI
- 设备号提取: 8MULTI使用ID后4位作为设备号
- 量程配置: 8MULTI下位机已换算,量程配置仅用于超量程警告显示
- 计数器清零: 通过设置baseline实现软清零
- OEE页面: 仅显示8ADPRO/8MULTI设备,不显示车间设备
- OEE状态: 基于cn_reg字段判断(1=运行中,3=故障,其他=停机)
- 兼容性: 所有修改不影响8ADPRO原有功能