package com.mes.interaction.vehicle.handler;
|
|
import com.mes.device.entity.DeviceConfig;
|
import com.mes.device.entity.DeviceStatus;
|
import com.mes.device.service.DeviceConfigService;
|
import com.mes.device.service.DeviceGroupRelationService;
|
import com.mes.device.service.DevicePlcOperationService;
|
import com.mes.device.service.GlassInfoService;
|
import com.mes.device.service.DeviceStatusService;
|
import com.mes.device.vo.DeviceGroupVO;
|
import com.mes.device.vo.DevicePlcVO;
|
import com.mes.interaction.BaseDeviceLogicHandler;
|
import com.mes.interaction.vehicle.coordination.VehicleStatusManager;
|
import com.mes.interaction.vehicle.model.VehiclePosition;
|
import com.mes.interaction.vehicle.model.VehicleState;
|
import com.mes.interaction.vehicle.model.VehicleStatus;
|
import com.mes.interaction.vehicle.model.VehicleTask;
|
import com.mes.s7.enhanced.EnhancedS7Serializer;
|
import com.mes.s7.provider.S7SerializerProvider;
|
import com.mes.service.PlcDynamicDataService;
|
import com.mes.task.model.TaskExecutionContext;
|
import com.mes.interaction.workstation.scanner.handler.HorizontalScannerLogicHandler;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.stereotype.Component;
|
|
import javax.annotation.PreDestroy;
|
import java.util.*;
|
import java.util.concurrent.*;
|
|
/**
|
* 大车设备逻辑处理器
|
* 所有大车设备实例共享这个处理器
|
* 集成多实例状态管理和协调功能
|
*
|
* @author huang
|
* @since 2025-11-21
|
*/
|
@Slf4j
|
@Component
|
public class LoadVehicleLogicHandler extends BaseDeviceLogicHandler {
|
|
private final GlassInfoService glassInfoService;
|
|
@Autowired
|
private VehicleStatusManager statusManager;
|
|
@Autowired(required = false)
|
private DeviceConfigService deviceConfigService;
|
|
@Autowired(required = false)
|
private DeviceGroupRelationService deviceGroupRelationService;
|
|
@Autowired(required = false)
|
private DeviceStatusService deviceStatusService;
|
|
@Autowired(required = false)
|
private PlcDynamicDataService plcDynamicDataService;
|
|
@Autowired(required = false)
|
private S7SerializerProvider s7SerializerProvider;
|
|
// MES字段列表(进片和出片共用同一套协议)
|
// 根据协议,使用带数字后缀的字段名(1-6对应6个玻璃位置)
|
// 支持读取所有6个玻璃的信息
|
private static final List<String> MES_FIELDS = Arrays.asList(
|
"mesSend", "mesConfirm",
|
// 玻璃1-6的ID
|
"mesGlassId1", "mesGlassId2", "mesGlassId3", "mesGlassId4", "mesGlassId5", "mesGlassId6",
|
// 玻璃1-6的尺寸
|
"mesWidth1", "mesWidth2", "mesWidth3", "mesWidth4", "mesWidth5", "mesWidth6",
|
"mesHeight1", "mesHeight2", "mesHeight3", "mesHeight4", "mesHeight5", "mesHeight6",
|
"mesThickness1", "mesThickness2", "mesThickness3", "mesThickness4", "mesThickness5", "mesThickness6",
|
// 玻璃1-6的起始位置和目标位置
|
"start1", "start2", "start3", "start4", "start5", "start6",
|
"target1", "target2", "target3", "target4", "target5", "target6"
|
);
|
|
// 监控线程池:用于定期检查大车状态并协调卧转立设备
|
private final ScheduledExecutorService stateMonitorExecutor = Executors.newScheduledThreadPool(5, r -> {
|
Thread t = new Thread(r, "VehicleStateMonitor");
|
t.setDaemon(true);
|
return t;
|
});
|
|
// 空闲监控线程池:用于保持plcRequest=1
|
private final ScheduledExecutorService idleMonitorExecutor = Executors.newScheduledThreadPool(3, r -> {
|
Thread t = new Thread(r, "VehicleIdleMonitor");
|
t.setDaemon(true);
|
return t;
|
});
|
|
// 任务监控线程池:用于监控任务执行和状态切换
|
private final ScheduledExecutorService taskMonitorExecutor = Executors.newScheduledThreadPool(5, r -> {
|
Thread t = new Thread(r, "VehicleTaskMonitor");
|
t.setDaemon(true);
|
return t;
|
});
|
|
// 记录正在监控的设备:deviceId -> 监控任务
|
private final Map<String, ScheduledFuture<?>> monitoringTasks = new ConcurrentHashMap<>();
|
|
// 记录空闲监控任务:deviceId -> 空闲监控任务
|
private final Map<String, ScheduledFuture<?>> idleMonitoringTasks = new ConcurrentHashMap<>();
|
|
// 记录任务监控任务:deviceId -> 任务监控任务
|
private final Map<String, ScheduledFuture<?>> taskMonitoringTasks = new ConcurrentHashMap<>();
|
|
// 记录已协调的设备:deviceId -> 已协调的state字段集合(避免重复协调)
|
private final Map<String, List<String>> coordinatedStates = new ConcurrentHashMap<>();
|
|
// 记录当前任务:deviceId -> 任务信息
|
private final Map<String, MesTaskInfo> currentTasks = new ConcurrentHashMap<>();
|
|
@Autowired
|
public LoadVehicleLogicHandler(
|
DevicePlcOperationService devicePlcOperationService,
|
@Qualifier("deviceGlassInfoService") GlassInfoService glassInfoService) {
|
super(devicePlcOperationService);
|
this.glassInfoService = glassInfoService;
|
}
|
|
@Override
|
public String getDeviceType() {
|
return DeviceConfig.DeviceType.LOAD_VEHICLE;
|
}
|
|
@Override
|
protected DevicePlcVO.OperationResult doExecute(
|
DeviceConfig deviceConfig,
|
String operation,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
String deviceId = deviceConfig.getDeviceId();
|
log.info("执行大车设备操作: deviceId={}, deviceName={}, operation={}",
|
deviceId, deviceConfig.getDeviceName(), operation);
|
|
// 1. 检查这个设备实例的状态(对于需要状态检查的操作)
|
if (needsStateCheck(operation)) {
|
VehicleStatus status = statusManager.getVehicleStatus(deviceId);
|
if (status != null && !status.isAvailable()) {
|
// 对于 feedGlass 操作,如果状态是 EXECUTING,允许继续执行
|
// 因为定时器可能会重复调用,或者需要等待任务完成
|
if ("feedGlass".equals(operation) && status.isExecuting()) {
|
log.debug("车辆 {} 当前状态为 EXECUTING,但允许继续执行 feedGlass(定时器重复调用)", deviceId);
|
// 允许继续执行,不返回错误
|
} else {
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message(String.format("车辆 %s (%s) 当前状态为 %s,无法执行操作 %s",
|
deviceConfig.getDeviceName(),
|
deviceId,
|
status.getState(),
|
operation))
|
.build();
|
}
|
}
|
}
|
|
// 2. 标记为执行中(对于需要状态管理的操作)
|
if (needsStateManagement(operation)) {
|
statusManager.updateVehicleStatus(deviceId, deviceConfig.getDeviceName(), VehicleState.EXECUTING);
|
|
// 创建任务信息
|
VehicleTask task = createVehicleTask(deviceConfig, operation, params, logicParams);
|
statusManager.setVehicleTask(deviceId, task);
|
}
|
|
try {
|
// 3. 执行原有逻辑
|
DevicePlcVO.OperationResult result;
|
switch (operation) {
|
case "feedGlass":
|
result = handleFeedGlass(deviceConfig, params, logicParams);
|
break;
|
case "triggerRequest":
|
result = handleTriggerRequest(deviceConfig, params, logicParams);
|
break;
|
case "triggerReport":
|
result = handleTriggerReport(deviceConfig, params, logicParams);
|
break;
|
case "reset":
|
result = handleReset(deviceConfig, params, logicParams);
|
break;
|
case "clearGlass":
|
case "clearPlc":
|
case "clear":
|
result = handleClearGlass(deviceConfig, params, logicParams);
|
break;
|
case "checkStateAndCoordinate":
|
result = handleCheckStateAndCoordinate(deviceConfig, params, logicParams);
|
break;
|
case "startIdleMonitor":
|
result = handleStartIdleMonitor(deviceConfig, params, logicParams);
|
break;
|
case "stopIdleMonitor":
|
result = handleStopIdleMonitor(deviceConfig);
|
break;
|
case "checkMesTask":
|
result = handleCheckMesTask(deviceConfig, params, logicParams);
|
break;
|
case "checkMesOutboundTask":
|
// 出片任务也使用同一套协议,通过handleCheckMesTask处理
|
result = handleCheckMesTask(deviceConfig, params, logicParams);
|
break;
|
case "startTaskMonitor":
|
result = handleStartTaskMonitor(deviceConfig, params, logicParams);
|
break;
|
case "stopTaskMonitor":
|
result = handleStopTaskMonitor(deviceConfig);
|
break;
|
case "setOnlineState":
|
result = handleSetOnlineState(deviceConfig, params, logicParams);
|
break;
|
case "checkMesConfirm":
|
result = checkMesConfirm(deviceConfig, logicParams);
|
break;
|
case "markBroken":
|
result = handleMarkBroken(deviceConfig, params, logicParams);
|
break;
|
default:
|
log.warn("不支持的操作类型: {}", operation);
|
result = DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("不支持的操作: " + operation)
|
.build();
|
}
|
|
return result;
|
|
} catch (Exception e) {
|
log.error("执行大车设备操作异常: deviceId={}, operation={}", deviceId, operation, e);
|
// 发生异常时,将状态设置为错误
|
if (needsStateManagement(operation)) {
|
statusManager.updateVehicleStatus(deviceId, VehicleState.ERROR);
|
}
|
throw e;
|
} finally {
|
// 4. 执行完成后恢复为空闲状态(对于需要状态管理的操作)
|
if (needsStateManagement(operation)) {
|
// 注意:这里不立即设置为IDLE,因为实际执行可能需要时间
|
// 真正的状态更新应该在任务完成后通过回调或状态查询来更新
|
// 这里先保持EXECUTING状态,等待外部确认完成后再更新
|
log.debug("操作执行完成,保持执行中状态,等待外部确认: deviceId={}", deviceId);
|
}
|
}
|
}
|
|
/**
|
* 判断操作是否需要状态检查
|
*/
|
private boolean needsStateCheck(String operation) {
|
// 所有操作都需要检查状态,除了查询类操作和特定内部检查
|
if ("query".equals(operation) || "status".equals(operation)) {
|
return false;
|
}
|
if ("checkMesConfirm".equals(operation)) {
|
return false;
|
}
|
return true;
|
}
|
|
/**
|
* 判断操作是否需要状态管理
|
*/
|
private boolean needsStateManagement(String operation) {
|
// feedGlass 需要状态管理,其他操作根据实际情况
|
return "feedGlass".equals(operation);
|
}
|
|
/**
|
* 创建车辆任务信息
|
*/
|
private VehicleTask createVehicleTask(
|
DeviceConfig deviceConfig,
|
String operation,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
VehicleTask task = new VehicleTask();
|
task.setTaskId(generateTaskId(deviceConfig.getDeviceId()));
|
task.setTaskName("大车设备-" + operation);
|
task.setOperation(operation);
|
|
// 从参数中提取位置信息
|
String positionCode = (String) params.get("positionCode");
|
Integer positionValue = (Integer) params.get("positionValue");
|
|
if (positionCode != null || positionValue != null) {
|
VehiclePosition position = new VehiclePosition(positionCode, positionValue);
|
task.getPlannedPath().setStartPosition(position);
|
task.getPlannedPath().setEndPosition(position);
|
}
|
|
// 从配置中获取速度(如果有)
|
Object speedObj = logicParams != null ? logicParams.get("vehicleSpeed") : null;
|
Double speed = null;
|
if (speedObj != null) {
|
if (speedObj instanceof Double) {
|
speed = (Double) speedObj;
|
} else if (speedObj instanceof Integer) {
|
speed = ((Integer) speedObj).doubleValue();
|
} else if (speedObj instanceof Number) {
|
speed = ((Number) speedObj).doubleValue();
|
} else {
|
try {
|
speed = Double.parseDouble(String.valueOf(speedObj));
|
} catch (NumberFormatException e) {
|
log.warn("无法解析vehicleSpeed: {}", speedObj);
|
}
|
}
|
}
|
if (speed != null) {
|
task.setSpeed(speed);
|
task.calculateEstimatedEndTime();
|
}
|
|
task.setParameters(new HashMap<>(params));
|
|
return task;
|
}
|
|
/**
|
* 生成任务ID
|
*/
|
private String generateTaskId(String deviceId) {
|
return "TASK_" + deviceId + "_" + System.currentTimeMillis();
|
}
|
|
/**
|
* 处理玻璃上料操作
|
*/
|
private DevicePlcVO.OperationResult handleFeedGlass(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
// 从逻辑参数中获取配置(从 extraParams.deviceLogic 读取)
|
Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", 6000);
|
Integer glassGap = getLogicParam(logicParams, "glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm
|
Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true); // 自动上料:是否自动触发上料请求(写入plcRequest=1)
|
|
// 从运行时参数中获取数据(从接口调用时传入)
|
List<GlassInfo> glassInfos = extractGlassInfos(params);
|
if (glassInfos.isEmpty()) {
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("未提供有效的玻璃信息")
|
.build();
|
}
|
|
String positionCode = (String) params.get("positionCode");
|
Integer positionValue = (Integer) params.get("positionValue");
|
Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoFeed);
|
|
List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity, glassGap,
|
deviceConfig.getDeviceId());
|
if (plannedGlasses == null) {
|
// 玻璃没有长度时返回null表示错误
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("玻璃信息缺少长度数据,无法进行容量计算。请检查MES程序是否正确提供玻璃长度。")
|
.build();
|
}
|
if (plannedGlasses.isEmpty()) {
|
// 装不下,通知卧转立扫码设备暂停
|
notifyScannerPause(params, true);
|
log.warn("大车设备装不下,已通知卧转立扫码暂停: deviceId={}, glassCount={}, vehicleCapacity={}",
|
deviceConfig.getId(), glassInfos.size(), vehicleCapacity);
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("当前玻璃尺寸超出车辆容量,无法装载,已通知卧转立扫码暂停")
|
.build();
|
}
|
|
// 装得下,确保卧转立扫码继续运行
|
notifyScannerPause(params, false);
|
|
// 继续执行原有逻辑
|
|
// 构建写入数据
|
Map<String, Object> payload = new HashMap<>();
|
|
// 写入玻璃ID
|
int plcSlots = Math.min(plannedGlasses.size(), 6);
|
for (int i = 0; i < plcSlots; i++) {
|
String fieldName = "plcGlassId" + (i + 1);
|
payload.put(fieldName, plannedGlasses.get(i).getGlassId());
|
}
|
payload.put("plcGlassCount", plcSlots);
|
|
// 写入位置信息:PLC侧期望的是 MES 编号(如1001/1002),而不是位置映射后的格子值
|
Integer plcPosition = null;
|
if (positionValue != null) {
|
// 如果调用方直接传了数值,则认为这是MES编号,直接写入
|
plcPosition = positionValue;
|
} else if (positionCode != null) {
|
// 尝试将位置代码解析为数字(例如 "900" -> 900)
|
try {
|
plcPosition = Integer.parseInt(positionCode.trim());
|
} catch (NumberFormatException ignore) {
|
// 非数字编码时,不写入inPosition,由PLC或后续逻辑自行处理
|
}
|
}
|
if (plcPosition != null) {
|
payload.put("inPosition", plcPosition);
|
}
|
|
// 自动触发请求字
|
if (triggerRequest != null && triggerRequest) {
|
payload.put("plcRequest", 1);
|
}
|
|
String operationName = "大车设备-玻璃上料";
|
if (positionCode != null) {
|
operationName += "(" + positionCode + ")";
|
}
|
|
log.info("大车设备玻璃上料: deviceId={}, glassCount={}, position={}, plannedGlassIds={}",
|
deviceConfig.getId(), plcSlots, positionCode, plannedGlasses);
|
|
// 写入PLC,让大车开始装玻璃
|
DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
|
deviceConfig.getId(), payload, operationName);
|
|
// 如果执行成功,更新位置信息到状态
|
if (Boolean.TRUE.equals(result.getSuccess())) {
|
VehicleStatus status = statusManager.getOrCreateVehicleStatus(
|
deviceConfig.getDeviceId(), deviceConfig.getDeviceName());
|
if (positionCode != null || positionValue != null) {
|
VehiclePosition position = new VehiclePosition(positionCode, positionValue);
|
status.setCurrentPosition(position);
|
}
|
|
// 仅在“非多设备任务”场景下,才启动大车自身的自动状态监控和 MES 任务监控
|
boolean inMultiDeviceTask = params != null && params.containsKey("_taskContext");
|
if (!inMultiDeviceTask) {
|
// 启动自动状态监控,当 state=1 时自动协调卧转立设备
|
startStateMonitoring(deviceConfig, logicParams);
|
|
// 从 PLC/MES 创建正式任务并启动监控的逻辑,保留给独立 MES 场景使用
|
// 多设备任务场景下,这部分交由 TaskExecutionEngine 统一编排
|
}
|
}
|
|
return result;
|
}
|
|
/**
|
* 处理触发请求操作
|
*/
|
private DevicePlcVO.OperationResult handleTriggerRequest(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcRequest", 1);
|
|
log.info("大车设备触发请求: deviceId={}", deviceConfig.getId());
|
return devicePlcOperationService.writeFields(
|
deviceConfig.getId(),
|
payload,
|
"大车设备-触发请求"
|
);
|
}
|
|
/**
|
* 处理触发汇报操作
|
*/
|
private DevicePlcVO.OperationResult handleTriggerReport(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcReport", 1);
|
|
log.info("大车设备触发汇报: deviceId={}", deviceConfig.getId());
|
return devicePlcOperationService.writeFields(
|
deviceConfig.getId(),
|
payload,
|
"大车设备-触发汇报"
|
);
|
}
|
|
/**
|
* 处理重置操作
|
*/
|
private DevicePlcVO.OperationResult handleReset(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcRequest", 0);
|
payload.put("plcReport", 0);
|
// 清空state1~6,避免遗留状态导致误判
|
for (int i = 1; i <= 6; i++) {
|
payload.put("state" + i, 0);
|
}
|
payload.put("onlineState", Boolean.TRUE);
|
|
log.info("大车设备重置: deviceId={}", deviceConfig.getId());
|
|
DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
|
deviceConfig.getId(),
|
payload,
|
"大车设备-重置"
|
);
|
|
// 重置时,清除任务并恢复为空闲状态,停止监控
|
if (Boolean.TRUE.equals(result.getSuccess())) {
|
statusManager.clearVehicleTask(deviceConfig.getDeviceId());
|
statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE);
|
stopStateMonitoring(deviceConfig.getDeviceId());
|
handleStopTaskMonitor(deviceConfig);
|
handleStopIdleMonitor(deviceConfig);
|
updateDeviceOnlineStatus(deviceConfig, true);
|
} else {
|
// 即便重置失败,也尝试停止内部监控,避免任务取消后仍然反复写PLC
|
stopStateMonitoring(deviceConfig.getDeviceId());
|
handleStopTaskMonitor(deviceConfig);
|
handleStopIdleMonitor(deviceConfig);
|
}
|
|
return result;
|
}
|
|
/**
|
* 设置联机状态
|
* @param deviceConfig 设备配置
|
* @param params 参数,可包含 onlineState(1=联机,0=脱机)
|
* @param logicParams 逻辑参数
|
* @return 操作结果
|
*/
|
private DevicePlcVO.OperationResult handleSetOnlineState(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
// 从参数中获取联机状态值,默认为true(联机)
|
boolean onlineState = true;
|
if (params != null && params.containsKey("onlineState")) {
|
Object stateObj = params.get("onlineState");
|
if (stateObj instanceof Boolean) {
|
onlineState = (Boolean) stateObj;
|
} else if (stateObj instanceof Number) {
|
onlineState = ((Number) stateObj).intValue() != 0;
|
} else if (stateObj instanceof String) {
|
try {
|
String str = ((String) stateObj).trim();
|
if ("true".equalsIgnoreCase(str)) {
|
onlineState = true;
|
} else if ("false".equalsIgnoreCase(str)) {
|
onlineState = false;
|
} else {
|
onlineState = Integer.parseInt(str) != 0;
|
}
|
} catch (NumberFormatException e) {
|
log.warn("解析onlineState失败,使用默认值true: deviceId={}, value={}",
|
deviceConfig.getId(), stateObj);
|
}
|
}
|
}
|
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("onlineState", onlineState);
|
|
String stateText = onlineState ? "联机" : "脱机";
|
log.info("大车设备设置联机状态: deviceId={}, onlineState={} ({})",
|
deviceConfig.getId(), onlineState, stateText);
|
|
DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
|
deviceConfig.getId(),
|
payload,
|
"大车设备-设置联机状态(" + stateText + ")"
|
);
|
|
if (Boolean.TRUE.equals(result.getSuccess())) {
|
updateDeviceOnlineStatus(deviceConfig, onlineState);
|
}
|
return result;
|
}
|
|
/**
|
* 清空PLC中的玻璃数据
|
*/
|
private DevicePlcVO.OperationResult handleClearGlass(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
Map<String, Object> payload = new HashMap<>();
|
|
int slotCount = getLogicParam(logicParams, "glassSlotCount", 6);
|
if (slotCount <= 0) {
|
slotCount = 6;
|
}
|
|
List<String> slotFields = resolveGlassSlotFields(logicParams, slotCount);
|
for (String field : slotFields) {
|
payload.put(field, "");
|
}
|
|
payload.put("plcRequest", 0);
|
payload.put("plcReport", 0);
|
payload.put("onlineState", Boolean.TRUE);
|
|
if (params != null && params.containsKey("positionValue")) {
|
payload.put("inPosition", params.get("positionValue"));
|
} else if (params != null && Boolean.TRUE.equals(params.get("clearPosition"))) {
|
payload.put("inPosition", 0);
|
}
|
|
log.info("清空大车设备PLC玻璃数据: deviceId={}, clearedSlots={}", deviceConfig.getId(), slotFields.size());
|
|
DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
|
deviceConfig.getId(),
|
payload,
|
"大车设备-清空玻璃数据"
|
);
|
|
// 同步清空state1~6等动态字段(在动态数据区中)
|
clearDynamicTaskStates(deviceConfig);
|
|
// 清空后,恢复为空闲状态,停止监控
|
if (Boolean.TRUE.equals(result.getSuccess())) {
|
statusManager.clearVehicleTask(deviceConfig.getDeviceId());
|
statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE);
|
stopStateMonitoring(deviceConfig.getDeviceId());
|
handleStopTaskMonitor(deviceConfig);
|
handleStopIdleMonitor(deviceConfig);
|
updateDeviceOnlineStatus(deviceConfig, true);
|
} else {
|
// 写入失败也尝试停止监控,避免任务取消后仍旧运行
|
stopStateMonitoring(deviceConfig.getDeviceId());
|
handleStopTaskMonitor(deviceConfig);
|
handleStopIdleMonitor(deviceConfig);
|
}
|
|
return result;
|
}
|
|
private void updateDeviceOnlineStatus(DeviceConfig deviceConfig, boolean online) {
|
if (deviceStatusService == null || deviceConfig == null || deviceConfig.getId() == null) {
|
return;
|
}
|
try {
|
String status = online ? DeviceStatus.Status.ONLINE : DeviceStatus.Status.OFFLINE;
|
deviceStatusService.updateDeviceOnlineStatus(deviceConfig.getId(), status);
|
} catch (Exception e) {
|
log.warn("同步设备在线状态到数据库失败: deviceId={}, online={}, error={}",
|
deviceConfig.getDeviceId(), online, e.getMessage());
|
}
|
}
|
|
private List<String> resolveGlassSlotFields(Map<String, Object> logicParams, int fallbackCount) {
|
List<String> fields = new ArrayList<>();
|
if (logicParams != null) {
|
Object slotFieldConfig = logicParams.get("glassSlotFields");
|
if (slotFieldConfig instanceof List) {
|
List<?> configured = (List<?>) slotFieldConfig;
|
for (Object item : configured) {
|
if (item != null) {
|
String fieldName = String.valueOf(item).trim();
|
if (!fieldName.isEmpty()) {
|
fields.add(fieldName);
|
}
|
}
|
}
|
}
|
}
|
|
if (fields.isEmpty()) {
|
for (int i = 1; i <= fallbackCount; i++) {
|
fields.add("plcGlassId" + i);
|
}
|
}
|
return fields;
|
}
|
|
@Override
|
public String validateLogicParams(DeviceConfig deviceConfig) {
|
Map<String, Object> logicParams = parseLogicParams(deviceConfig);
|
|
// 验证必填参数
|
Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", null);
|
if (vehicleCapacity == null || vehicleCapacity <= 0) {
|
return "车辆容量(vehicleCapacity)必须大于0";
|
}
|
|
Integer glassGap = getLogicParam(logicParams, "glassGap", null);
|
if (glassGap != null && glassGap < 0) {
|
return "玻璃间隔(glassGap)不能为负数";
|
}
|
|
return null; // 验证通过
|
}
|
|
@Override
|
public String getDefaultLogicParams() {
|
Map<String, Object> defaultParams = new HashMap<>();
|
defaultParams.put("vehicleCapacity", 6000);
|
defaultParams.put("glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm
|
defaultParams.put("autoFeed", true); // 自动上料:是否自动触发上料请求(写入plcRequest=1),默认true
|
defaultParams.put("maxRetryCount", 5);
|
|
// MES任务相关配置
|
defaultParams.put("vehicleSpeed", 1.0); // 车辆速度(格/秒,grid/s),默认1格/秒
|
defaultParams.put("minRange", 1); // 最小运动距离(格子)
|
defaultParams.put("maxRange", 100); // 最大运动距离(格子),例如100格
|
defaultParams.put("homePosition", 0); // 初始位置(格子)
|
defaultParams.put("idleMonitorIntervalMs", 2000); // 空闲监控间隔(毫秒)
|
defaultParams.put("taskMonitorIntervalMs", 1000); // 任务监控间隔(毫秒)
|
defaultParams.put("mesConfirmTimeoutMs", 30000); // MES确认超时(毫秒)
|
|
|
// gridPositionMapping: 格子编号到位置的映射表(可选)
|
// 如果不配置,则格子编号直接作为位置值
|
Map<String, Integer> gridPositionMapping = new HashMap<>();
|
defaultParams.put("gridPositionMapping", gridPositionMapping);
|
|
Map<String, Integer> positionMapping = new HashMap<>();
|
positionMapping.put("POS1", 1);
|
positionMapping.put("POS2", 2);
|
defaultParams.put("positionMapping", positionMapping);
|
|
try {
|
return objectMapper.writeValueAsString(defaultParams);
|
} catch (Exception e) {
|
log.error("生成默认逻辑参数失败", e);
|
return "{}";
|
}
|
}
|
|
@SuppressWarnings("unchecked")
|
private List<GlassInfo> extractGlassInfos(Map<String, Object> params) {
|
List<GlassInfo> result = new ArrayList<>();
|
Object rawGlassInfos = params.get("glassInfos");
|
if (rawGlassInfos instanceof List) {
|
List<?> list = (List<?>) rawGlassInfos;
|
for (Object item : list) {
|
GlassInfo info = convertToGlassInfo(item);
|
if (info != null) {
|
result.add(info);
|
}
|
}
|
}
|
|
if (result.isEmpty()) {
|
List<String> glassIds = (List<String>) params.get("glassIds");
|
if (glassIds != null && !glassIds.isEmpty()) {
|
// 从数据库查询玻璃尺寸
|
Map<String, Integer> lengthMap = glassInfoService.getGlassLengthMap(glassIds);
|
for (String glassId : glassIds) {
|
Integer length = lengthMap.get(glassId);
|
result.add(new GlassInfo(glassId, length));
|
}
|
log.debug("从数据库查询玻璃尺寸: glassIds={}, lengthMap={}", glassIds, lengthMap);
|
}
|
}
|
return result;
|
}
|
|
private GlassInfo convertToGlassInfo(Object source) {
|
if (source instanceof GlassInfo) {
|
return (GlassInfo) source;
|
}
|
if (source instanceof Map) {
|
Map<?, ?> map = (Map<?, ?>) source;
|
Object id = map.get("glassId");
|
if (id == null) {
|
id = map.get("id");
|
}
|
if (id == null) {
|
return null;
|
}
|
Integer length = parseLength(map.get("length"));
|
if (length == null) {
|
length = parseLength(map.get("size"));
|
}
|
return new GlassInfo(String.valueOf(id), length);
|
}
|
if (source instanceof String) {
|
return new GlassInfo((String) source, null);
|
}
|
return null;
|
}
|
|
private Integer parseLength(Object value) {
|
if (value instanceof Number) {
|
return ((Number) value).intValue();
|
}
|
if (value instanceof String) {
|
try {
|
return Integer.parseInt((String) value);
|
} catch (NumberFormatException ignored) {
|
}
|
}
|
return null;
|
}
|
|
/**
|
* 规划玻璃装载
|
* @param source 源玻璃列表
|
* @param vehicleCapacity 车辆容量(mm)
|
* @param glassGap 玻璃之间的物理间隔(mm),默认200mm
|
* @param deviceId 设备ID(用于日志)
|
* @return 规划后的玻璃列表,如果玻璃没有长度则返回null(MES未提供长度数据)
|
*/
|
private List<GlassInfo> planGlassLoading(List<GlassInfo> source,
|
int vehicleCapacity,
|
int glassGap,
|
String deviceId) {
|
List<GlassInfo> planned = new ArrayList<>();
|
int usedLength = 0;
|
int capacity = Math.max(vehicleCapacity, 1);
|
int gap = Math.max(glassGap, 0); // 确保间隔不为负数
|
|
for (GlassInfo info : source) {
|
Integer glassLength = info.getLength();
|
|
// 如果玻璃没有长度,说明MES未提供,直接报错
|
if (glassLength == null || glassLength <= 0) {
|
log.error("玻璃[{}]缺少长度数据,无法进行容量计算。deviceId={},请检查MES程序是否正确提供玻璃长度。",
|
info.getGlassId(), deviceId);
|
return null;
|
}
|
|
int length = glassLength;
|
|
if (planned.isEmpty()) {
|
// 第一块玻璃,不需要间隙
|
planned.add(info.withLength(length));
|
usedLength = length;
|
continue;
|
}
|
|
// 后续玻璃需要考虑间隙:玻璃长度 + 间隙
|
int requiredLength = length + gap;
|
if (usedLength + requiredLength <= capacity) {
|
planned.add(info.withLength(length));
|
usedLength += requiredLength; // 包含间隙
|
} else {
|
// 装不下了,停止添加
|
break;
|
}
|
}
|
|
log.debug("玻璃装载规划: deviceId={}, total={}, planned={}, usedLength={}, capacity={}, glassGap={}",
|
deviceId, source.size(), planned.size(), usedLength, capacity, gap);
|
|
return planned;
|
}
|
|
/**
|
* 启动状态监控
|
* 定期检查大车的 state1~6,当检测到 state=1 时自动协调卧转立设备
|
*/
|
private void startStateMonitoring(DeviceConfig deviceConfig, Map<String, Object> logicParams) {
|
String deviceId = deviceConfig.getDeviceId();
|
|
// 如果已经在监控,先停止旧的监控任务
|
stopStateMonitoring(deviceId);
|
|
// 获取监控配置
|
Integer monitorIntervalMs = getLogicParam(logicParams, "stateMonitorIntervalMs", 1000);
|
Integer monitorTimeoutMs = getLogicParam(logicParams, "stateMonitorTimeoutMs", 300000);
|
if (monitorIntervalMs == null || monitorIntervalMs <= 0) {
|
monitorIntervalMs = 1000;
|
}
|
if (monitorTimeoutMs == null || monitorTimeoutMs <= 0) {
|
monitorTimeoutMs = 300000;
|
}
|
final int finalMonitorIntervalMs = monitorIntervalMs;
|
final int finalMonitorTimeoutMs = monitorTimeoutMs;
|
|
// 初始化已协调状态记录
|
coordinatedStates.put(deviceId, new CopyOnWriteArrayList<>());
|
|
// 记录监控开始时间
|
final long startTime = System.currentTimeMillis();
|
|
// 启动监控任务
|
ScheduledFuture<?> future = stateMonitorExecutor.scheduleWithFixedDelay(() -> {
|
try {
|
// 检查超时
|
if (System.currentTimeMillis() - startTime > finalMonitorTimeoutMs) {
|
log.info("大车状态监控超时,停止监控: deviceId={}, timeout={}ms", deviceId, finalMonitorTimeoutMs);
|
stopStateMonitoring(deviceId);
|
return;
|
}
|
|
// 检查车辆是否还在执行任务
|
VehicleStatus status = statusManager.getVehicleStatus(deviceId);
|
if (status == null || status.getState() != VehicleState.EXECUTING) {
|
log.debug("大车状态已改变,停止监控: deviceId={}, state={}",
|
deviceId, status != null ? status.getState() : "null");
|
stopStateMonitoring(deviceId);
|
return;
|
}
|
|
// 执行状态检查和协调
|
checkAndCoordinateState(deviceConfig);
|
|
} catch (Exception e) {
|
log.error("大车状态监控异常: deviceId={}", deviceId, e);
|
}
|
}, finalMonitorIntervalMs, finalMonitorIntervalMs, TimeUnit.MILLISECONDS);
|
|
monitoringTasks.put(deviceId, future);
|
log.info("已启动大车状态监控: deviceId={}, interval={}ms, timeout={}ms",
|
deviceId, finalMonitorIntervalMs, finalMonitorTimeoutMs);
|
}
|
|
/**
|
* 停止状态监控
|
*/
|
private void stopStateMonitoring(String deviceId) {
|
ScheduledFuture<?> future = monitoringTasks.remove(deviceId);
|
if (future != null && !future.isCancelled()) {
|
future.cancel(false);
|
log.debug("已停止大车状态监控: deviceId={}", deviceId);
|
}
|
coordinatedStates.remove(deviceId);
|
}
|
|
/**
|
* 检查大车状态并协调卧转立设备(内部方法,由监控线程调用)
|
*/
|
private void checkAndCoordinateState(DeviceConfig deviceConfig) {
|
String deviceId = deviceConfig.getDeviceId();
|
List<String> alreadyCoordinated = coordinatedStates.get(deviceId);
|
if (alreadyCoordinated == null) {
|
alreadyCoordinated = new CopyOnWriteArrayList<>();
|
coordinatedStates.put(deviceId, alreadyCoordinated);
|
}
|
|
try {
|
// 读取 state1~6 字段
|
List<String> stateFields = Arrays.asList("state1", "state2", "state3", "state4", "state5", "state6");
|
Map<String, Object> stateValues = new HashMap<>();
|
|
// 从 PLC 读取状态字段
|
DevicePlcVO.StatusInfo statusInfo = devicePlcOperationService.readStatus(deviceConfig.getId());
|
if (statusInfo == null || statusInfo.getFieldValues() == null) {
|
return;
|
}
|
|
for (String field : stateFields) {
|
Object value = statusInfo.getFieldValues().get(field);
|
if (value != null) {
|
stateValues.put(field, value);
|
}
|
}
|
|
// 检查是否有任何一个 state 为 1,且尚未协调过
|
List<String> newStateOneFields = new ArrayList<>();
|
for (String field : stateFields) {
|
Integer stateValue = parseInteger(stateValues.get(field));
|
if (stateValue != null && stateValue == 1 && !alreadyCoordinated.contains(field)) {
|
newStateOneFields.add(field);
|
}
|
}
|
|
if (newStateOneFields.isEmpty()) {
|
return; // 没有新的 state=1,无需协调
|
}
|
|
log.info("检测到大车新的 state=1: deviceId={}, stateFields={}", deviceId, newStateOneFields);
|
|
// 查找同组的卧转立设备
|
List<DeviceConfig> transferDevices = findTransferDevicesInSameGroup(deviceConfig);
|
if (transferDevices.isEmpty()) {
|
log.warn("未找到同组的卧转立设备: deviceId={}", deviceId);
|
// 即使找不到设备,也标记为已协调,避免重复检查
|
alreadyCoordinated.addAll(newStateOneFields);
|
return;
|
}
|
|
// 将每个卧转立设备的 plcRequest 置 0
|
boolean allSuccess = true;
|
for (DeviceConfig transferDevice : transferDevices) {
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcRequest", 0);
|
|
DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
|
transferDevice.getId(),
|
payload,
|
"大车自动协调-清空卧转立请求"
|
);
|
|
if (Boolean.TRUE.equals(result.getSuccess())) {
|
log.info("已自动清空卧转立设备 plcRequest: vehicleDeviceId={}, transferDeviceId={}, transferDeviceName={}, stateFields={}",
|
deviceId, transferDevice.getId(), transferDevice.getDeviceName(), newStateOneFields);
|
} else {
|
log.warn("自动清空卧转立设备 plcRequest 失败: vehicleDeviceId={}, transferDeviceId={}, message={}",
|
deviceId, transferDevice.getId(), result.getMessage());
|
allSuccess = false;
|
}
|
}
|
|
// 标记为已协调(无论成功与否,避免重复协调)
|
if (allSuccess) {
|
alreadyCoordinated.addAll(newStateOneFields);
|
log.info("大车状态协调完成: deviceId={}, coordinatedStateFields={}", deviceId, newStateOneFields);
|
}
|
|
} catch (Exception e) {
|
log.error("检查大车状态并协调卧转立设备异常: deviceId={}", deviceId, e);
|
}
|
}
|
|
/**
|
* 检查大车状态并协调卧转立设备(手动调用接口)
|
* 当 state1~6 中任何一个变为 1(上车完成)时,将同组卧转立设备的 plcRequest 置 0
|
*/
|
private DevicePlcVO.OperationResult handleCheckStateAndCoordinate(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
try {
|
// 读取 state1~6 字段
|
List<String> stateFields = Arrays.asList("state1", "state2", "state3", "state4", "state5", "state6");
|
Map<String, Object> stateValues = new HashMap<>();
|
|
// 从 PLC 读取状态字段
|
DevicePlcVO.StatusInfo statusInfo = devicePlcOperationService.readStatus(deviceConfig.getId());
|
if (statusInfo != null && statusInfo.getFieldValues() != null) {
|
for (String field : stateFields) {
|
Object value = statusInfo.getFieldValues().get(field);
|
if (value != null) {
|
stateValues.put(field, value);
|
}
|
}
|
}
|
|
// 检查是否有任何一个 state 为 1
|
boolean hasStateOne = false;
|
List<String> stateOneFields = new ArrayList<>();
|
for (String field : stateFields) {
|
Integer stateValue = parseInteger(stateValues.get(field));
|
if (stateValue != null && stateValue == 1) {
|
hasStateOne = true;
|
stateOneFields.add(field);
|
}
|
}
|
|
if (!hasStateOne) {
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("当前无 state=1 的状态,无需协调")
|
.build();
|
}
|
|
log.info("检测到大车 state=1: deviceId={}, stateFields={}",
|
deviceConfig.getId(), stateOneFields);
|
|
// 查找同组的卧转立设备
|
List<DeviceConfig> transferDevices = findTransferDevicesInSameGroup(deviceConfig);
|
if (transferDevices.isEmpty()) {
|
log.warn("未找到同组的卧转立设备: deviceId={}", deviceConfig.getId());
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("未找到同组的卧转立设备,跳过协调")
|
.build();
|
}
|
|
// 将每个卧转立设备的 plcRequest 置 0
|
List<DevicePlcVO.OperationResult> results = new ArrayList<>();
|
for (DeviceConfig transferDevice : transferDevices) {
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcRequest", 0);
|
|
DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
|
transferDevice.getId(),
|
payload,
|
"大车协调-清空卧转立请求"
|
);
|
results.add(result);
|
|
if (Boolean.TRUE.equals(result.getSuccess())) {
|
log.info("已清空卧转立设备 plcRequest: transferDeviceId={}, transferDeviceName={}",
|
transferDevice.getId(), transferDevice.getDeviceName());
|
} else {
|
log.warn("清空卧转立设备 plcRequest 失败: transferDeviceId={}, message={}",
|
transferDevice.getId(), result.getMessage());
|
}
|
}
|
|
boolean allSuccess = results.stream()
|
.allMatch(r -> Boolean.TRUE.equals(r.getSuccess()));
|
|
return DevicePlcVO.OperationResult.builder()
|
.success(allSuccess)
|
.message(String.format("已协调 %d 个卧转立设备,成功: %d",
|
transferDevices.size(),
|
results.stream().mapToInt(r -> Boolean.TRUE.equals(r.getSuccess()) ? 1 : 0).sum()))
|
.build();
|
|
} catch (Exception e) {
|
log.error("检查大车状态并协调卧转立设备异常: deviceId={}", deviceConfig.getId(), e);
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("协调异常: " + e.getMessage())
|
.build();
|
}
|
}
|
|
/**
|
* 查找同组内的卧转立设备
|
*/
|
private List<DeviceConfig> findTransferDevicesInSameGroup(DeviceConfig vehicleDevice) {
|
List<DeviceConfig> transferDevices = new ArrayList<>();
|
|
if (deviceGroupRelationService == null || deviceConfigService == null) {
|
log.warn("DeviceGroupRelationService 或 DeviceConfigService 未注入,无法查找同组设备");
|
return transferDevices;
|
}
|
|
try {
|
// 获取大车所属的设备组
|
List<DeviceGroupVO.GroupInfo> groups = deviceGroupRelationService.getDeviceGroups(vehicleDevice.getId());
|
if (groups.isEmpty()) {
|
log.debug("大车设备未加入任何设备组: deviceId={}", vehicleDevice.getId());
|
return transferDevices;
|
}
|
|
// 遍历所有设备组,查找卧转立设备
|
for (DeviceGroupVO.GroupInfo group : groups) {
|
List<DeviceGroupVO.DeviceInfo> groupDevices = deviceGroupRelationService.getGroupDevices(group.getId());
|
for (DeviceGroupVO.DeviceInfo deviceInfo : groupDevices) {
|
// 检查是否为卧转立设备
|
if (DeviceConfig.DeviceType.WORKSTATION_TRANSFER.equals(deviceInfo.getDeviceType())) {
|
DeviceConfig transferDevice = deviceConfigService.getDeviceById(deviceInfo.getId());
|
if (transferDevice != null) {
|
transferDevices.add(transferDevice);
|
}
|
}
|
}
|
}
|
|
log.debug("找到同组卧转立设备: vehicleDeviceId={}, transferDeviceCount={}",
|
vehicleDevice.getId(), transferDevices.size());
|
|
} catch (Exception e) {
|
log.error("查找同组卧转立设备异常: vehicleDeviceId={}", vehicleDevice.getId(), e);
|
}
|
|
return transferDevices;
|
}
|
|
private Integer parseInteger(Object value) {
|
if (value instanceof Number) {
|
return ((Number) value).intValue();
|
}
|
if (value == null) {
|
return null;
|
}
|
try {
|
return Integer.parseInt(String.valueOf(value));
|
} catch (NumberFormatException e) {
|
return null;
|
}
|
}
|
|
private String parseString(Object value) {
|
return value == null ? null : String.valueOf(value).trim();
|
}
|
|
private static class GlassInfo {
|
private final String glassId;
|
private final Integer length;
|
|
GlassInfo(String glassId, Integer length) {
|
this.glassId = glassId;
|
this.length = length;
|
}
|
|
public String getGlassId() {
|
return glassId;
|
}
|
|
public Integer getLength() {
|
return length;
|
}
|
|
public GlassInfo withLength(Integer newLength) {
|
return new GlassInfo(this.glassId, newLength);
|
}
|
|
@Override
|
public String toString() {
|
return glassId;
|
}
|
|
@Override
|
public int hashCode() {
|
return Objects.hash(glassId, length);
|
}
|
|
@Override
|
public boolean equals(Object obj) {
|
if (this == obj) return true;
|
if (obj == null || getClass() != obj.getClass()) return false;
|
GlassInfo other = (GlassInfo) obj;
|
return Objects.equals(glassId, other.glassId) && Objects.equals(length, other.length);
|
}
|
}
|
|
/**
|
* 启动空闲监控(没有任务时,plcRequest一直保持为1)
|
*/
|
private DevicePlcVO.OperationResult handleStartIdleMonitor(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
String deviceId = deviceConfig.getDeviceId();
|
|
// 停止旧的监控任务
|
handleStopIdleMonitor(deviceConfig);
|
|
// 获取监控间隔
|
Integer monitorIntervalMs = getLogicParam(logicParams, "idleMonitorIntervalMs", 2000); // 默认2秒
|
|
// 启动监控任务
|
ScheduledFuture<?> future = idleMonitorExecutor.scheduleWithFixedDelay(() -> {
|
try {
|
// 检查是否有任务在执行
|
VehicleStatus status = statusManager.getVehicleStatus(deviceId);
|
if (status != null && status.getState() == VehicleState.EXECUTING) {
|
// 有任务在执行,不设置plcRequest
|
return;
|
}
|
|
// 检查是否有待处理的进片或出片任务
|
if (plcDynamicDataService != null && s7SerializerProvider != null) {
|
EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
|
if (serializer != null) {
|
// 检查进片任务
|
Map<String, Object> mesData = plcDynamicDataService.readPlcData(
|
deviceConfig, MES_FIELDS, serializer);
|
Integer mesSend = parseInteger(mesData != null ? mesData.get("mesSend") : null);
|
|
// 进片和出片共用mesSend字段,只需检查一次
|
// 如果有待处理的任务,不设置plcRequest(等待任务处理)
|
if (mesSend != null && mesSend == 1) {
|
log.debug("大车空闲监控: deviceId={}, 检测到待处理任务(mesSend=1),不设置plcRequest", deviceId);
|
return;
|
}
|
}
|
}
|
|
// 没有任务,保持plcRequest=1
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcRequest", 1);
|
|
devicePlcOperationService.writeFields(deviceConfig.getId(), payload, "大车空闲监控-保持请求");
|
log.debug("大车空闲监控: deviceId={}, 保持plcRequest=1", deviceId);
|
|
} catch (Exception e) {
|
log.error("大车空闲监控异常: deviceId={}", deviceId, e);
|
}
|
}, monitorIntervalMs, monitorIntervalMs, TimeUnit.MILLISECONDS);
|
|
idleMonitoringTasks.put(deviceId, future);
|
log.info("已启动大车空闲监控: deviceId={}, interval={}ms", deviceId, monitorIntervalMs);
|
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("空闲监控已启动")
|
.build();
|
}
|
|
/**
|
* 停止空闲监控
|
*/
|
private DevicePlcVO.OperationResult handleStopIdleMonitor(DeviceConfig deviceConfig) {
|
String deviceId = deviceConfig.getDeviceId();
|
ScheduledFuture<?> future = idleMonitoringTasks.remove(deviceId);
|
if (future != null && !future.isCancelled()) {
|
future.cancel(false);
|
log.info("已停止大车空闲监控: deviceId={}", deviceId);
|
}
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("空闲监控已停止")
|
.build();
|
}
|
|
/**
|
* 检查MES任务(当mesSend=1时,读取MES参数并创建任务)
|
* 进片和出片共用同一套协议字段,通过位置信息判断任务类型
|
*/
|
private DevicePlcVO.OperationResult handleCheckMesTask(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
if (plcDynamicDataService == null || s7SerializerProvider == null) {
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("PlcDynamicDataService或S7SerializerProvider未注入")
|
.build();
|
}
|
|
String deviceId = deviceConfig.getDeviceId();
|
EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
|
if (serializer == null) {
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("获取PLC序列化器失败")
|
.build();
|
}
|
|
try {
|
// 检查是否已有任务在执行中
|
MesTaskInfo existingTask = currentTasks.get(deviceId);
|
if (existingTask != null) {
|
log.debug("设备已有任务在执行中,跳过检查MES任务: deviceId={}", deviceId);
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("任务执行中,无需重复检查MES任务")
|
.data(Collections.singletonMap("waiting", false))
|
.build();
|
}
|
|
// 读取MES字段(进片和出片共用)
|
Map<String, Object> mesData = plcDynamicDataService.readPlcData(
|
deviceConfig, MES_FIELDS, serializer);
|
if (mesData == null || mesData.isEmpty()) {
|
log.warn("读取MES字段失败: deviceId={}, mesData为空或null", deviceId);
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("读取MES字段失败")
|
.build();
|
}
|
|
// 解析mesSend
|
Integer mesSend = parseInteger(mesData.get("mesSend"));
|
|
if (mesSend == null || mesSend == 0) {
|
Map<String, Object> waitData = new HashMap<>();
|
waitData.put("completed", false);
|
waitData.put("waiting", true);
|
waitData.put("waitingReason", "mesSend=0");
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("等待MES发送请求(mesSend=0)")
|
.data(waitData)
|
.build();
|
}
|
|
// mesSend=1,记录日志
|
log.info("检测到mesSend=1,开始读取MES任务信息: deviceId={}", deviceId);
|
|
// mesSend=1,读取所有玻璃的任务参数(支持1-6个玻璃)
|
List<GlassTaskInfo> glasses = new ArrayList<>();
|
boolean isOutbound = false; // 将在处理第一个有效玻璃时确定
|
|
// 读取所有6个玻璃的信息
|
for (int i = 1; i <= 6; i++) {
|
String glassId = parseString(mesData.get("mesGlassId" + i));
|
if (glassId == null || glassId.isEmpty()) {
|
continue; // 跳过空的玻璃ID
|
}
|
|
Integer startSlot = parseInteger(mesData.get("start" + i));
|
Integer targetSlot = parseInteger(mesData.get("target" + i));
|
Integer width = parseInteger(mesData.get("mesWidth" + i));
|
Integer height = parseInteger(mesData.get("mesHeight" + i));
|
Integer thickness = parseInteger(mesData.get("mesThickness" + i));
|
|
log.debug("读取玻璃{}信息: deviceId={}, glassId={}, startSlot={}, targetSlot={}, width={}, height={}, thickness={}",
|
i, deviceId, glassId, startSlot, targetSlot, width, height, thickness);
|
|
if (startSlot == null || targetSlot == null) {
|
log.warn("玻璃{}信息不完整,跳过: glassId={}, startSlot={}, targetSlot={}",
|
i, glassId, startSlot, targetSlot);
|
continue;
|
}
|
|
// 对于第一个有效玻璃,判断是进片还是出片任务
|
if (glasses.isEmpty()) {
|
isOutbound = isOutboundTask(startSlot, logicParams);
|
}
|
|
// 位置映射
|
Integer startPosition;
|
if (isOutbound) {
|
// 出片任务:startSlot是格子编号,需要映射到实际位置
|
startPosition = mapOutboundPosition(startSlot, logicParams);
|
} else {
|
// 进片任务:startSlot是卧转立编号,通过positionMapping映射
|
startPosition = mapPosition(startSlot, logicParams);
|
}
|
|
// targetSlot统一通过positionMapping映射
|
Integer targetPosition = mapPosition(targetSlot, logicParams);
|
|
if (startPosition == null || targetPosition == null) {
|
log.warn("玻璃{}位置映射失败,跳过: glassId={}, startSlot={}, targetSlot={}",
|
i, glassId, startSlot, targetSlot);
|
continue;
|
}
|
|
// 创建玻璃任务信息
|
GlassTaskInfo glassInfo = new GlassTaskInfo();
|
glassInfo.glassId = glassId;
|
glassInfo.startSlot = startSlot;
|
glassInfo.targetSlot = targetSlot;
|
glassInfo.startPosition = startPosition;
|
glassInfo.targetPosition = targetPosition;
|
glassInfo.width = width;
|
glassInfo.height = height;
|
glassInfo.thickness = thickness;
|
glasses.add(glassInfo);
|
}
|
|
if (glasses.isEmpty()) {
|
// 记录详细的MES数据,帮助调试
|
log.warn("MES未提供有效的玻璃信息: deviceId={}, mesSend={}, mesData={}",
|
deviceId, mesSend, mesData);
|
Map<String, Object> waitData = new HashMap<>();
|
waitData.put("completed", false);
|
waitData.put("waiting", true);
|
waitData.put("waitingReason", "glassInfoMissing");
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("MES未提供有效的玻璃信息(mesSend=1但未找到有效的glassId、startSlot、targetSlot)")
|
.data(waitData)
|
.build();
|
}
|
|
// 读取当前位置
|
Integer currentPosition = getCurrentPosition(deviceConfig, logicParams);
|
|
// 使用第一个玻璃的位置计算时间(所有玻璃使用相同的路径)
|
GlassTaskInfo firstGlass = glasses.get(0);
|
TimeCalculation timeCalc = calculateTime(
|
currentPosition, firstGlass.startPosition, firstGlass.targetPosition, logicParams);
|
|
// 创建任务信息
|
MesTaskInfo taskInfo = new MesTaskInfo();
|
taskInfo.glasses = glasses;
|
taskInfo.currentPosition = currentPosition;
|
taskInfo.gotime = timeCalc.gotime;
|
taskInfo.cartime = timeCalc.cartime;
|
taskInfo.createdTime = System.currentTimeMillis();
|
taskInfo.isOutbound = isOutbound;
|
|
// 从配置中读取破损玻璃索引(用于测试场景)
|
// 配置格式:brokenGlassIndices: [0, 2] 表示第1个和第3个玻璃应该破损
|
@SuppressWarnings("unchecked")
|
List<Integer> brokenIndices = getLogicParam(logicParams, "brokenGlassIndices", null);
|
if (brokenIndices != null && !brokenIndices.isEmpty()) {
|
taskInfo.brokenGlassIndices = new ArrayList<>();
|
for (Integer index : brokenIndices) {
|
if (index != null && index >= 0 && index < glasses.size()) {
|
taskInfo.brokenGlassIndices.add(index);
|
}
|
}
|
if (!taskInfo.brokenGlassIndices.isEmpty()) {
|
log.info("任务创建时标记破损玻璃: deviceId={}, brokenIndices={}",
|
deviceId, taskInfo.brokenGlassIndices);
|
}
|
}
|
|
currentTasks.put(deviceId, taskInfo);
|
|
// 清空plcRequest(表示已接收任务)
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcRequest", 0);
|
plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
|
log.info("已清空plcRequest=0: deviceId={}", deviceId);
|
|
// 更新车辆状态为执行中
|
statusManager.updateVehicleStatus(deviceId, deviceConfig.getDeviceName(), VehicleState.EXECUTING);
|
|
// 启动任务监控
|
handleStartTaskMonitor(deviceConfig, params, logicParams);
|
log.info("已启动任务监控: deviceId={}", deviceId);
|
|
String taskType = isOutbound ? "出片" : "进片";
|
String glassIds = glasses.stream()
|
.map(g -> g.glassId)
|
.collect(java.util.stream.Collectors.joining(","));
|
log.info("MES{}任务已创建: deviceId={}, glassCount={}, glassIds=[{}], 起始位置={}格, 目标位置={}格, 距离{}格->{}格, gotime={}ms({}秒), cartime={}ms({}秒)",
|
taskType, deviceId, glasses.size(), glassIds,
|
firstGlass.startPosition, firstGlass.targetPosition,
|
Math.abs(firstGlass.startPosition - currentPosition),
|
Math.abs(firstGlass.targetPosition - firstGlass.startPosition),
|
timeCalc.gotime, timeCalc.gotime / 1000.0, timeCalc.cartime, timeCalc.cartime / 1000.0);
|
|
// 构建详细的步骤提示信息
|
StringBuilder stepMessage = new StringBuilder();
|
stepMessage.append("检测到MES任务(mesSend=1),已清空请求字(plcRequest=0)");
|
stepMessage.append(";开始计算时间,预计到达起始位置耗时").append(timeCalc.gotime / 1000.0).append("秒");
|
stepMessage.append(",运输到目标位置耗时").append(timeCalc.cartime / 1000.0).append("秒");
|
if (!isOutbound) {
|
stepMessage.append(";进片任务:等待玻璃上车后,将告知卧转立设备");
|
}
|
|
Map<String, Object> successData = new HashMap<>();
|
successData.put("waiting", false);
|
successData.put("taskStarted", true);
|
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message(stepMessage.toString())
|
.data(successData)
|
.build();
|
|
} catch (Exception e) {
|
log.error("检查MES任务异常: deviceId={}", deviceId, e);
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("处理异常: " + e.getMessage())
|
.build();
|
}
|
}
|
|
/**
|
* 判断是否为出片任务
|
* 通过startSlot判断:
|
* - 如果startSlot在positionMapping中,且不在大理片笼格子范围内,则是进片任务
|
* - 如果startSlot不在positionMapping中,或在大理片笼格子范围内,则是出片任务
|
*/
|
private boolean isOutboundTask(Integer startSlot, Map<String, Object> logicParams) {
|
if (startSlot == null) {
|
return false;
|
}
|
|
// 方法1:检查startSlot是否在positionMapping中(卧转立编号)
|
@SuppressWarnings("unchecked")
|
Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
|
if (positionMapping.containsKey(String.valueOf(startSlot))) {
|
// startSlot在positionMapping中,说明是卧转立编号,是进片任务
|
return false;
|
}
|
|
// 方法2:检查startSlot是否在大理片笼格子范围内
|
// 通过查找同组的大理片笼设备,检查格子范围
|
// 这里简化处理:如果startSlot不在positionMapping中,且是数字,可能是格子编号
|
// 可以通过配置指定格子编号范围,或者通过查找同组设备判断
|
|
// 方法3:通过配置指定车辆运动格子范围(兼容旧配置outboundSlotRanges)
|
@SuppressWarnings("unchecked")
|
List<Integer> vehicleSlotRange = getLogicParam(logicParams, "vehicleSlotRange", null);
|
if (vehicleSlotRange == null || vehicleSlotRange.isEmpty()) {
|
// 兼容旧配置
|
vehicleSlotRange = getLogicParam(logicParams, "outboundSlotRanges", null);
|
}
|
if (vehicleSlotRange != null && !vehicleSlotRange.isEmpty()) {
|
// 如果配置了车辆运动格子范围,检查startSlot是否在范围内
|
// 例如:[1, 101] 表示车辆只能在格子1~101之间运动
|
if (vehicleSlotRange.size() >= 2) {
|
int minSlot = vehicleSlotRange.get(0);
|
int maxSlot = vehicleSlotRange.get(1);
|
if (startSlot >= minSlot && startSlot <= maxSlot) {
|
return true;
|
}
|
}
|
}
|
|
// 默认:如果startSlot不在positionMapping中,且是较小的数字(可能是格子编号),判断为出片
|
// 这里可以根据实际需求调整判断逻辑
|
// 暂时:如果startSlot不在positionMapping中,判断为出片任务
|
return true;
|
}
|
|
|
/**
|
* 映射出片源位置(格子编号转换为实际位置)
|
* 通过查找同组的大理片笼设备配置,将格子编号转换为位置
|
*/
|
private Integer mapOutboundPosition(Integer gridNumber, Map<String, Object> logicParams) {
|
if (gridNumber == null) {
|
return null;
|
}
|
|
// 方法1:如果配置了格子到位置的映射表
|
@SuppressWarnings("unchecked")
|
Map<String, Integer> gridPositionMapping = getLogicParam(logicParams, "gridPositionMapping", new HashMap<>());
|
Integer position = gridPositionMapping.get(String.valueOf(gridNumber));
|
if (position != null) {
|
return position;
|
}
|
|
// 方法2:格子编号直接作为位置(如果格子编号就是位置值)
|
// 例如:格子1对应位置1格,格子52对应位置52格
|
// 这里可以根据实际需求调整映射逻辑
|
|
// 暂时直接使用格子编号作为位置
|
log.debug("使用格子编号作为位置: gridNumber={}", gridNumber);
|
return gridNumber;
|
}
|
|
/**
|
* 位置映射:将MES给的编号(如900/901)转换为实际位置值(如100/500)
|
*/
|
private Integer mapPosition(Integer slotNumber, Map<String, Object> logicParams) {
|
if (slotNumber == null) {
|
return null;
|
}
|
|
// 从配置中获取位置映射表
|
@SuppressWarnings("unchecked")
|
Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
|
|
// 查找映射
|
Integer position = positionMapping.get(String.valueOf(slotNumber));
|
if (position != null) {
|
return position;
|
}
|
|
// 如果没有配置映射,尝试直接使用编号
|
log.warn("位置映射未找到: slotNumber={}, 使用编号作为位置值", slotNumber);
|
return slotNumber;
|
}
|
|
/**
|
* 获取当前位置
|
*/
|
private Integer getCurrentPosition(DeviceConfig deviceConfig, Map<String, Object> logicParams) {
|
// 从状态管理器获取
|
VehicleStatus status = statusManager.getVehicleStatus(deviceConfig.getDeviceId());
|
if (status != null && status.getCurrentPosition() != null) {
|
return status.getCurrentPosition().getPositionValue();
|
}
|
|
// 从配置中获取默认位置
|
return getLogicParam(logicParams, "homePosition", 0);
|
}
|
|
/**
|
* 时间计算:根据速度、当前位置、目标位置计算gotime和cartime
|
* 速度单位:格/秒(grid/s)
|
* 位置和距离单位:格子(grid)
|
*/
|
private TimeCalculation calculateTime(Integer currentPos, Integer startPos,
|
Integer targetPos, Map<String, Object> logicParams) {
|
// 验证车辆运动格子范围
|
@SuppressWarnings("unchecked")
|
List<Integer> vehicleSlotRange = getLogicParam(logicParams, "vehicleSlotRange", null);
|
if (vehicleSlotRange == null || vehicleSlotRange.isEmpty()) {
|
// 兼容旧配置
|
vehicleSlotRange = getLogicParam(logicParams, "outboundSlotRanges", null);
|
}
|
if (vehicleSlotRange != null && vehicleSlotRange.size() >= 2) {
|
int minSlot = vehicleSlotRange.get(0);
|
int maxSlot = vehicleSlotRange.get(1);
|
// 验证startPos和targetPos是否在允许的范围内
|
if (startPos != null && (startPos < minSlot || startPos > maxSlot)) {
|
log.warn("起始位置 {} 超出车辆运动格子范围 [{}, {}]", startPos, minSlot, maxSlot);
|
}
|
if (targetPos != null && (targetPos < minSlot || targetPos > maxSlot)) {
|
log.warn("目标位置 {} 超出车辆运动格子范围 [{}, {}]", targetPos, minSlot, maxSlot);
|
}
|
}
|
|
// 获取速度(格/秒,grid/s)
|
// 这里不能直接用 Double 泛型,否则当配置里是 Integer 时会出现
|
// java.lang.Integer cannot be cast to java.lang.Double 的异常
|
Object speedObj = getLogicParam(logicParams, "vehicleSpeed", 1.0);
|
Double speed = null;
|
if (speedObj instanceof Double) {
|
speed = (Double) speedObj;
|
} else if (speedObj instanceof Integer) {
|
speed = ((Integer) speedObj).doubleValue();
|
} else if (speedObj instanceof Number) {
|
speed = ((Number) speedObj).doubleValue();
|
} else {
|
try {
|
speed = Double.parseDouble(String.valueOf(speedObj));
|
} catch (Exception ignore) {
|
}
|
}
|
if (speed == null || speed <= 0) {
|
speed = 1.0; // 默认1格/秒
|
}
|
|
// 获取运动距离范围(格子)
|
Integer minRange = getLogicParam(logicParams, "minRange", 1);
|
Integer maxRange = getLogicParam(logicParams, "maxRange", 100);
|
|
// 计算gotime:从当前位置到起始位置的时间(毫秒)
|
// 公式:时间(ms) = 距离(格) / 速度(格/秒) * 1000
|
long gotime = 0;
|
if (currentPos != null && startPos != null) {
|
int distance = Math.abs(startPos - currentPos); // 距离(格子)
|
// 限制在范围内
|
distance = Math.max(minRange, Math.min(maxRange, distance));
|
gotime = (long) (distance / speed * 1000); // 转换为毫秒
|
}
|
|
// 计算cartime:从起始位置到目标位置的时间(毫秒)
|
// 公式:时间(ms) = 距离(格) / 速度(格/秒) * 1000
|
long cartime = 0;
|
if (startPos != null && targetPos != null) {
|
int distance = Math.abs(targetPos - startPos); // 距离(格子)
|
// 限制在范围内
|
distance = Math.max(minRange, Math.min(maxRange, distance));
|
cartime = (long) (distance / speed * 1000); // 转换为毫秒
|
}
|
|
return new TimeCalculation(gotime, cartime);
|
}
|
|
/**
|
* 启动任务监控(监控state状态切换和任务完成)
|
*/
|
private DevicePlcVO.OperationResult handleStartTaskMonitor(
|
DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
|
String deviceId = deviceConfig.getDeviceId();
|
|
// 停止旧的监控任务
|
handleStopTaskMonitor(deviceConfig);
|
|
MesTaskInfo taskInfo = currentTasks.get(deviceId);
|
if (taskInfo == null) {
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("没有正在执行的任务")
|
.build();
|
}
|
|
// 获取监控间隔
|
Integer monitorIntervalMs = getLogicParam(logicParams, "taskMonitorIntervalMs", 1000); // 默认1秒
|
|
// 启动监控任务
|
ScheduledFuture<?> future = taskMonitorExecutor.scheduleWithFixedDelay(() -> {
|
try {
|
monitorTaskExecution(deviceConfig, taskInfo, logicParams);
|
} catch (Exception e) {
|
log.error("任务监控异常: deviceId={}", deviceId, e);
|
}
|
}, monitorIntervalMs, monitorIntervalMs, TimeUnit.MILLISECONDS);
|
|
taskMonitoringTasks.put(deviceId, future);
|
log.info("已启动任务监控: deviceId={}, interval={}ms", deviceId, monitorIntervalMs);
|
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("任务监控已启动")
|
.build();
|
}
|
|
/**
|
* 监控任务执行
|
*/
|
private void monitorTaskExecution(DeviceConfig deviceConfig,
|
MesTaskInfo taskInfo,
|
Map<String, Object> logicParams) {
|
|
if (plcDynamicDataService == null || s7SerializerProvider == null) {
|
return;
|
}
|
|
String deviceId = deviceConfig.getDeviceId();
|
EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
|
if (serializer == null) {
|
return;
|
}
|
|
try {
|
// 读取state1~6
|
List<String> stateFields = Arrays.asList("state1", "state2", "state3", "state4", "state5", "state6");
|
Map<String, Object> stateValues = new HashMap<>();
|
|
DevicePlcVO.StatusInfo statusInfo = devicePlcOperationService.readStatus(deviceConfig.getId());
|
if (statusInfo != null && statusInfo.getFieldValues() != null) {
|
for (String field : stateFields) {
|
Object value = statusInfo.getFieldValues().get(field);
|
if (value != null) {
|
stateValues.put(field, value);
|
}
|
}
|
}
|
|
// 根据时间计算更新state状态
|
long currentTime = System.currentTimeMillis();
|
long elapsed = currentTime - taskInfo.createdTime;
|
|
// 计算状态切换时间点
|
long state1Time = taskInfo.gotime; // 到达起始位置,上车完成
|
long state2Time = taskInfo.gotime + taskInfo.cartime; // 到达目标位置,运输完成
|
|
// 获取超时时间配置(默认:超过预期完成时间的200%视为超时未完成)
|
Double timeoutRatio = getLogicParam(logicParams, "state3TimeoutRatio", 2.0);
|
long state3TimeoutTime = (long) (state2Time * timeoutRatio); // 超时时间点
|
|
// 更新state状态(每个玻璃对应一个state)
|
int glassCount = taskInfo.glasses.size();
|
boolean hasStateOne = false; // 标记是否有state变为1
|
boolean hasStateTwo = false; // 标记是否有state变为2
|
String currentStepDesc = ""; // 当前步骤描述
|
|
for (int i = 0; i < glassCount && i < 6; i++) {
|
String stateField = "state" + (i + 1);
|
Object currentValue = stateValues.get(stateField);
|
Integer currentState = parseInteger(currentValue);
|
|
// 如果当前state已经是3(未完成)或8(破损),跳过
|
if (currentState != null && (currentState == 3 || currentState == 8)) {
|
continue;
|
}
|
|
// 优先检查是否标记为破损(state=8)
|
// 检查任务信息中是否标记了该玻璃为破损
|
if (taskInfo.brokenGlassIndices != null && taskInfo.brokenGlassIndices.contains(i)) {
|
updateStateIfNeeded(deviceConfig, serializer, stateValues, stateField, 8, taskInfo);
|
log.info("玻璃标记为破损: deviceId={}, stateField={}, glassIndex={}",
|
deviceConfig.getDeviceId(), stateField, i);
|
continue;
|
}
|
|
// 检查超时未完成(state=3)
|
if (elapsed >= state3TimeoutTime && (currentState == null || currentState < 2)) {
|
updateStateIfNeeded(deviceConfig, serializer, stateValues, stateField, 3, taskInfo);
|
log.warn("任务超时未完成: deviceId={}, stateField={}, elapsed={}ms, expectedTime={}ms",
|
deviceConfig.getDeviceId(), stateField, elapsed, state2Time);
|
continue;
|
}
|
|
// 正常状态更新
|
if (elapsed >= state1Time && elapsed < state2Time) {
|
// state应该为1(上车完成)
|
boolean stateChanged = updateStateIfNeeded(deviceConfig, serializer, stateValues, stateField, 1, taskInfo);
|
if (stateChanged) {
|
hasStateOne = true;
|
currentStepDesc = "玻璃已上车(state=1),正在运输到目标位置";
|
} else if (currentState != null && currentState == 1) {
|
currentStepDesc = "玻璃已上车(state=1),正在运输到目标位置";
|
}
|
} else if (elapsed >= state2Time) {
|
// state应该为2(运输完成)
|
boolean stateChanged = updateStateIfNeeded(deviceConfig, serializer, stateValues, stateField, 2, taskInfo);
|
if (stateChanged) {
|
hasStateTwo = true;
|
currentStepDesc = "玻璃已到达目标位置(state=2),等待MES确认";
|
} else if (currentState != null && currentState == 2) {
|
currentStepDesc = "玻璃已到达目标位置(state=2),等待MES确认";
|
}
|
} else {
|
// 还在前往起始位置
|
currentStepDesc = "正在前往起始位置,预计耗时" + (state1Time / 1000.0) + "秒";
|
}
|
}
|
|
// 当state变为1时(玻璃上车完成),清空卧转立设备的plcRequest(只执行一次)
|
if (hasStateOne && !taskInfo.transferPlcRequestCleared) {
|
clearTransferPlcRequest(deviceConfig, logicParams);
|
taskInfo.transferPlcRequestCleared = true; // 标记已清空,避免重复执行
|
if (!taskInfo.isOutbound) {
|
currentStepDesc = "玻璃已上车(state=1),已告知卧转立设备(plcRequest=0),正在运输到目标位置";
|
}
|
}
|
|
// 检查是否所有state都>=2,如果是则给MES汇报
|
if (elapsed >= state2Time && allStatesCompleted(stateValues, glassCount)) {
|
reportToMes(deviceConfig, serializer, taskInfo, logicParams);
|
// 记录MES确认开始等待的时间(只记录一次)
|
if (taskInfo.mesConfirmStartTime == null) {
|
taskInfo.mesConfirmStartTime = System.currentTimeMillis();
|
currentStepDesc = "玻璃已到达目标位置(state=2),已发送汇报(plcReport=1),等待MES确认";
|
}
|
}
|
|
// 保存当前步骤描述到任务信息中,供checkMesConfirm使用
|
if (!currentStepDesc.isEmpty()) {
|
taskInfo.currentStepDesc = currentStepDesc;
|
}
|
|
} catch (Exception e) {
|
log.error("监控任务执行异常: deviceId={}", deviceId, e);
|
}
|
}
|
|
/**
|
* 更新state状态(如果需要)
|
* @return 是否发生了状态变化(从非目标状态变为目标状态)
|
*/
|
private boolean updateStateIfNeeded(DeviceConfig deviceConfig,
|
EnhancedS7Serializer serializer,
|
Map<String, Object> currentStates,
|
String stateField,
|
int targetState,
|
MesTaskInfo taskInfo) {
|
|
// 检查当前state值
|
Object currentValue = currentStates.get(stateField);
|
Integer currentState = parseInteger(currentValue);
|
|
// 如果当前state小于目标state,则更新
|
// 注意:如果当前state已经是3(未完成)或8(破损),不再更新
|
if (currentState != null && (currentState == 3 || currentState == 8)) {
|
log.debug("任务状态已为异常状态,不再更新: deviceId={}, stateField={}, currentState={}, targetState={}",
|
deviceConfig.getDeviceId(), stateField, currentState, targetState);
|
return false;
|
}
|
|
if (currentState == null || currentState < targetState) {
|
// 实际写入PLC的state字段
|
try {
|
Map<String, Object> payload = new HashMap<>();
|
payload.put(stateField, targetState);
|
plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
|
|
log.info("任务状态已更新到PLC: deviceId={}, stateField={}, currentState={}, targetState={}",
|
deviceConfig.getDeviceId(), stateField, currentState, targetState);
|
// 返回true表示状态发生了变化
|
return true;
|
} catch (Exception e) {
|
log.error("写入PLC state字段失败: deviceId={}, stateField={}, targetState={}, error={}",
|
deviceConfig.getDeviceId(), stateField, targetState, e.getMessage());
|
return false;
|
}
|
}
|
return false;
|
}
|
|
/**
|
* 清空卧转立设备的plcRequest(当大车state=1时,表示玻璃已上车)
|
*/
|
private void clearTransferPlcRequest(DeviceConfig deviceConfig, Map<String, Object> logicParams) {
|
try {
|
// 查找同组的卧转立设备
|
List<DeviceConfig> transferDevices = findTransferDevicesInSameGroup(deviceConfig);
|
if (transferDevices.isEmpty()) {
|
log.debug("未找到同组的卧转立设备,跳过清空plcRequest: deviceId={}", deviceConfig.getId());
|
return;
|
}
|
|
// 将每个卧转立设备的 plcRequest 置 0
|
for (DeviceConfig transferDevice : transferDevices) {
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcRequest", 0);
|
|
DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
|
transferDevice.getId(),
|
payload,
|
"大车state=1自动清空卧转立请求"
|
);
|
|
if (Boolean.TRUE.equals(result.getSuccess())) {
|
log.info("已自动清空卧转立设备 plcRequest: vehicleDeviceId={}, transferDeviceId={}, transferDeviceName={}",
|
deviceConfig.getId(), transferDevice.getId(), transferDevice.getDeviceName());
|
} else {
|
log.warn("自动清空卧转立设备 plcRequest 失败: vehicleDeviceId={}, transferDeviceId={}, message={}",
|
deviceConfig.getId(), transferDevice.getId(), result.getMessage());
|
}
|
}
|
} catch (Exception e) {
|
log.error("清空卧转立设备plcRequest异常: deviceId={}", deviceConfig.getId(), e);
|
}
|
}
|
|
/**
|
* 检查是否所有state都已完成(>=2)
|
*/
|
private boolean allStatesCompleted(Map<String, Object> stateValues, int glassCount) {
|
// 只检查实际有玻璃的state字段
|
for (int i = 1; i <= glassCount && i <= 6; i++) {
|
String stateField = "state" + i;
|
Object value = stateValues.get(stateField);
|
Integer state = parseInteger(value);
|
if (state == null || state < 2) {
|
return false;
|
}
|
}
|
return glassCount > 0;
|
}
|
|
/**
|
* 给MES汇报
|
*/
|
private void reportToMes(DeviceConfig deviceConfig,
|
EnhancedS7Serializer serializer,
|
MesTaskInfo taskInfo,
|
Map<String, Object> logicParams) {
|
|
try {
|
// 设置汇报字
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcReport", 1);
|
plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
|
|
String taskType = taskInfo.isOutbound ? "出片" : "进片";
|
String glassIds = taskInfo.glasses.stream()
|
.map(g -> g.glassId)
|
.collect(java.util.stream.Collectors.joining(","));
|
log.info("已给MES汇报({}任务): deviceId={}, glassCount={}, glassIds=[{}]",
|
taskType, deviceConfig.getDeviceId(), taskInfo.glasses.size(), glassIds);
|
|
// 多设备任务场景下,不在这里阻塞等待MES确认,由任务引擎定时调用checkMesConfirm
|
} catch (Exception e) {
|
log.error("给MES汇报异常: deviceId={}", deviceConfig.getDeviceId(), e);
|
}
|
}
|
|
/**
|
* 检查MES确认状态(供任务引擎周期性调用)
|
* 返回OperationResult.data中的 completed 标志表示是否已确认完成
|
*/
|
public DevicePlcVO.OperationResult checkMesConfirm(DeviceConfig deviceConfig,
|
Map<String, Object> logicParams) {
|
if (plcDynamicDataService == null || s7SerializerProvider == null) {
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("PlcDynamicDataService或S7SerializerProvider未注入")
|
.build();
|
}
|
EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
|
if (serializer == null) {
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("获取PLC序列化器失败")
|
.build();
|
}
|
|
String deviceId = deviceConfig.getDeviceId();
|
MesTaskInfo taskInfo = currentTasks.get(deviceId);
|
|
// 如果没有任务记录,优先尝试补偿性地检查一次MES任务(避免因时序问题一直noTask)
|
if (taskInfo == null) {
|
log.info("检查MES确认时未找到任务记录,尝试补偿检查MES任务: deviceId={}", deviceId);
|
try {
|
DevicePlcVO.OperationResult checkResult =
|
handleCheckMesTask(deviceConfig, Collections.emptyMap(), logicParams);
|
if (Boolean.TRUE.equals(checkResult.getSuccess())) {
|
taskInfo = currentTasks.get(deviceId);
|
if (taskInfo != null) {
|
log.info("补偿检查MES任务成功,已创建任务记录: deviceId={}", deviceId);
|
} else {
|
log.info("补偿检查MES任务执行成功但未创建任务记录: deviceId={}, message={}",
|
deviceId, checkResult.getMessage());
|
}
|
} else {
|
log.warn("补偿检查MES任务失败: deviceId={}, message={}", deviceId, checkResult.getMessage());
|
}
|
} catch (Exception e) {
|
log.warn("补偿检查MES任务异常: deviceId={}, error={}", deviceId, e.getMessage());
|
}
|
}
|
|
// 补偿后仍然没有任务记录,说明MES尚未提供可执行任务
|
if (taskInfo == null) {
|
Map<String, Object> waitData = new HashMap<>();
|
waitData.put("completed", false);
|
waitData.put("waiting", true);
|
waitData.put("waitingReason", "noTask");
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("等待MES发送请求(mesSend=1)")
|
.data(waitData)
|
.build();
|
}
|
|
// 获取MES确认超时配置(默认30秒)
|
Integer mesConfirmTimeoutMs = getLogicParam(logicParams, "mesConfirmTimeoutMs", 30000);
|
|
Map<String, Object> data = new HashMap<>();
|
try {
|
// 如果尚未向MES汇报(plcReport=1),无需检查确认
|
if (taskInfo.mesConfirmStartTime == null) {
|
data.put("completed", false);
|
data.put("waiting", true);
|
data.put("waitingReason", "waitingReport");
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("大车任务执行中,尚未汇报,无需检查确认")
|
.data(data)
|
.build();
|
}
|
|
// 检查超时
|
long waitTime = System.currentTimeMillis() - taskInfo.mesConfirmStartTime;
|
if (waitTime > mesConfirmTimeoutMs) {
|
log.warn("MES确认超时: deviceId={}, waitTime={}ms, timeout={}ms",
|
deviceId, waitTime, mesConfirmTimeoutMs);
|
data.put("completed", false);
|
data.put("timeout", true);
|
data.put("waitTime", waitTime);
|
|
// 超时视为任务失败:清理任务状态并停止监控,避免继续累加等待时间
|
try {
|
clearTaskStates(deviceConfig, serializer);
|
} catch (Exception e) {
|
log.warn("MES确认超时时清空任务状态失败: deviceId={}, error={}", deviceId, e.getMessage());
|
}
|
statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.ERROR);
|
statusManager.clearVehicleTask(deviceConfig.getDeviceId());
|
currentTasks.remove(deviceConfig.getDeviceId());
|
handleStopTaskMonitor(deviceConfig);
|
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message(String.format("MES确认超时: 等待时间%dms,超时时间%dms", waitTime, mesConfirmTimeoutMs))
|
.data(data)
|
.build();
|
}
|
|
Object confirmValue = plcDynamicDataService.readPlcField(
|
deviceConfig, "mesConfirm", serializer);
|
Integer confirm = parseInteger(confirmValue);
|
boolean completed = confirm != null && confirm == 1;
|
data.put("completed", completed);
|
|
if (completed) {
|
// MES已确认,清空state和汇报字
|
clearTaskStates(deviceConfig, serializer);
|
|
// 任务完成,恢复为空闲状态
|
statusManager.updateVehicleStatus(
|
deviceConfig.getDeviceId(), VehicleState.IDLE);
|
statusManager.clearVehicleTask(deviceConfig.getDeviceId());
|
|
// 移除任务记录(如果有)
|
currentTasks.remove(deviceConfig.getDeviceId());
|
|
// 停止任务监控
|
handleStopTaskMonitor(deviceConfig);
|
|
// 恢复plcRequest=1(可以接收新任务)
|
Map<String, Object> payload = new HashMap<>();
|
payload.put("plcRequest", 1);
|
plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
|
|
log.info("MES任务已确认完成: deviceId={}", deviceConfig.getDeviceId());
|
String taskType = taskInfo.isOutbound ? "出片" : "进片";
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message(String.format("%s任务完成:MES已确认(mesConfirm=1),已清空state和汇报字,大车空闲(plcRequest=1),可以等待下次任务", taskType))
|
.data(data)
|
.build();
|
}
|
|
// 构建详细的等待提示信息
|
String waitMessage = "等待MES确认中(mesConfirm=0)";
|
if (taskInfo.currentStepDesc != null && !taskInfo.currentStepDesc.isEmpty()) {
|
waitMessage = taskInfo.currentStepDesc + ";" + waitMessage;
|
} else if (taskInfo.mesConfirmStartTime != null) {
|
long waitMillis = System.currentTimeMillis() - taskInfo.mesConfirmStartTime;
|
waitMessage = String.format("等待MES确认中(mesConfirm=0),已等待%.1f秒", waitMillis / 1000.0);
|
}
|
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message(waitMessage)
|
.data(data)
|
.build();
|
} catch (Exception e) {
|
log.error("检查MES确认状态异常: deviceId={}", deviceConfig.getDeviceId(), e);
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("检查MES确认状态异常: " + e.getMessage())
|
.data(data)
|
.build();
|
}
|
}
|
|
/**
|
* 设置玻璃为破损状态(state=8)
|
* 用于测试场景,模拟玻璃破损
|
*/
|
private DevicePlcVO.OperationResult handleMarkBroken(DeviceConfig deviceConfig,
|
Map<String, Object> params,
|
Map<String, Object> logicParams) {
|
String deviceId = deviceConfig.getDeviceId();
|
MesTaskInfo taskInfo = currentTasks.get(deviceId);
|
if (taskInfo == null) {
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("没有正在执行的任务")
|
.build();
|
}
|
|
// 从参数中获取要标记为破损的玻璃索引(0-based)
|
// 支持单个索引或索引列表
|
List<Integer> brokenIndices = new ArrayList<>();
|
Object brokenIndexObj = params.get("glassIndex");
|
if (brokenIndexObj != null) {
|
if (brokenIndexObj instanceof List) {
|
@SuppressWarnings("unchecked")
|
List<Object> indexList = (List<Object>) brokenIndexObj;
|
for (Object idx : indexList) {
|
Integer index = parseInteger(idx);
|
if (index != null && index >= 0 && index < taskInfo.glasses.size()) {
|
brokenIndices.add(index);
|
}
|
}
|
} else {
|
Integer index = parseInteger(brokenIndexObj);
|
if (index != null && index >= 0 && index < taskInfo.glasses.size()) {
|
brokenIndices.add(index);
|
}
|
}
|
}
|
|
if (brokenIndices.isEmpty()) {
|
return DevicePlcVO.OperationResult.builder()
|
.success(false)
|
.message("未指定有效的玻璃索引")
|
.build();
|
}
|
|
// 标记为破损
|
if (taskInfo.brokenGlassIndices == null) {
|
taskInfo.brokenGlassIndices = new ArrayList<>();
|
}
|
for (Integer index : brokenIndices) {
|
if (!taskInfo.brokenGlassIndices.contains(index)) {
|
taskInfo.brokenGlassIndices.add(index);
|
}
|
}
|
|
// 立即写入PLC的state字段
|
EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
|
if (serializer != null) {
|
try {
|
Map<String, Object> payload = new HashMap<>();
|
for (Integer index : brokenIndices) {
|
String stateField = "state" + (index + 1);
|
payload.put(stateField, 8);
|
}
|
plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
|
log.info("已标记玻璃为破损并写入PLC: deviceId={}, brokenIndices={}",
|
deviceId, brokenIndices);
|
} catch (Exception e) {
|
log.error("写入破损状态到PLC失败: deviceId={}, error={}", deviceId, e.getMessage());
|
}
|
}
|
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message(String.format("已标记玻璃为破损: indices=%s", brokenIndices))
|
.build();
|
}
|
|
/**
|
* 清空任务状态
|
*/
|
private void clearTaskStates(DeviceConfig deviceConfig, EnhancedS7Serializer serializer) {
|
try {
|
Map<String, Object> payload = new HashMap<>();
|
// 清空state1~6
|
for (int i = 1; i <= 6; i++) {
|
payload.put("state" + i, 0);
|
}
|
// 清空汇报字
|
payload.put("plcReport", 0);
|
plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
|
} catch (Exception e) {
|
log.error("清空任务状态异常: deviceId={}", deviceConfig.getDeviceId(), e);
|
}
|
}
|
|
/**
|
* 停止任务监控
|
*/
|
private DevicePlcVO.OperationResult handleStopTaskMonitor(DeviceConfig deviceConfig) {
|
String deviceId = deviceConfig.getDeviceId();
|
ScheduledFuture<?> future = taskMonitoringTasks.remove(deviceId);
|
if (future != null && !future.isCancelled()) {
|
future.cancel(false);
|
log.info("已停止任务监控: deviceId={}", deviceId);
|
}
|
return DevicePlcVO.OperationResult.builder()
|
.success(true)
|
.message("任务监控已停止")
|
.build();
|
}
|
|
/**
|
* 时间计算结果
|
*/
|
private static class TimeCalculation {
|
final long gotime; // 到起始位置的时间(毫秒)
|
final long cartime; // 从起始到目标位置的时间(毫秒)
|
|
TimeCalculation(long gotime, long cartime) {
|
this.gotime = gotime;
|
this.cartime = cartime;
|
}
|
}
|
|
/**
|
* 单个玻璃的任务信息
|
*/
|
private static class GlassTaskInfo {
|
String glassId;
|
Integer startSlot;
|
Integer targetSlot;
|
Integer startPosition;
|
Integer targetPosition;
|
Integer width;
|
Integer height;
|
Integer thickness;
|
}
|
|
/**
|
* MES任务信息(可能包含多个玻璃)
|
*/
|
private static class MesTaskInfo {
|
List<GlassTaskInfo> glasses = new ArrayList<>(); // 多个玻璃信息
|
Integer currentPosition;
|
long gotime;
|
long cartime;
|
long createdTime;
|
boolean isOutbound = false; // 是否为出片任务(false=进片,true=出片)
|
boolean transferPlcRequestCleared = false; // 是否已清空卧转立plcRequest(避免重复清空)
|
List<Integer> brokenGlassIndices = null; // 标记为破损的玻璃索引列表(0-based,用于测试场景)
|
Long mesConfirmStartTime = null; // MES确认开始等待的时间(用于超时检测)
|
String currentStepDesc = null; // 当前步骤描述(用于显示详细的执行状态)
|
}
|
|
/**
|
* 应用关闭时清理资源
|
*/
|
@PreDestroy
|
public void destroy() {
|
log.info("正在关闭大车监控线程池...");
|
|
// 停止所有监控任务
|
for (String deviceId : new ArrayList<>(monitoringTasks.keySet())) {
|
stopStateMonitoring(deviceId);
|
}
|
for (String deviceId : new ArrayList<>(idleMonitoringTasks.keySet())) {
|
ScheduledFuture<?> future = idleMonitoringTasks.remove(deviceId);
|
if (future != null) future.cancel(false);
|
}
|
for (String deviceId : new ArrayList<>(taskMonitoringTasks.keySet())) {
|
ScheduledFuture<?> future = taskMonitoringTasks.remove(deviceId);
|
if (future != null) future.cancel(false);
|
}
|
|
// 关闭线程池
|
shutdownExecutor(stateMonitorExecutor, "状态监控");
|
shutdownExecutor(idleMonitorExecutor, "空闲监控");
|
shutdownExecutor(taskMonitorExecutor, "任务监控");
|
|
log.info("大车监控线程池已关闭");
|
}
|
|
private void shutdownExecutor(ScheduledExecutorService executor, String name) {
|
executor.shutdown();
|
try {
|
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
executor.shutdownNow();
|
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
log.warn("{}线程池未能正常关闭", name);
|
}
|
}
|
} catch (InterruptedException e) {
|
executor.shutdownNow();
|
Thread.currentThread().interrupt();
|
}
|
}
|
|
/**
|
* 通知卧转立扫码设备暂停或继续
|
* @param params 参数,包含_taskContext引用
|
* @param pause true=暂停,false=继续
|
*/
|
private void notifyScannerPause(Map<String, Object> params, boolean pause) {
|
if (params == null) {
|
return;
|
}
|
|
Object contextObj = params.get("_taskContext");
|
if (contextObj instanceof TaskExecutionContext) {
|
TaskExecutionContext context = (TaskExecutionContext) contextObj;
|
HorizontalScannerLogicHandler.setPauseFlag(context, pause);
|
log.info("已通知卧转立扫码设备{}: pause={}", pause ? "暂停" : "继续", pause);
|
} else {
|
log.debug("未找到TaskExecutionContext,无法通知卧转立扫码设备暂停");
|
}
|
}
|
|
private void clearDynamicTaskStates(DeviceConfig deviceConfig) {
|
if (plcDynamicDataService == null || s7SerializerProvider == null) {
|
return;
|
}
|
try {
|
EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
|
if (serializer != null) {
|
clearTaskStates(deviceConfig, serializer);
|
}
|
} catch (Exception e) {
|
log.warn("清空大车state字段失败: deviceId={}, error={}", deviceConfig != null ? deviceConfig.getId() : "null", e.getMessage());
|
}
|
}
|
}
|