add:新增光栅驱动以及科聪暂停和恢复车辆接口

This commit is contained in:
2025-12-22 08:50:07 +08:00
parent 6565ea8e51
commit e16fa8a789
22 changed files with 2023 additions and 53 deletions

View File

@@ -451,6 +451,16 @@
<artifactId>xercesImpl</artifactId>
<version>2.12.0</version>
</dependency>-->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.14.0</version>
</dependency>
<dependency>
<groupId>com.digitalpetri.modbus</groupId>
<artifactId>modbus-master-tcp</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>

View File

@@ -34,10 +34,17 @@ public class KeCongAgvController {
return new ResponseEntity<>(keCongAgvService.applyIn(requestParam), HttpStatus.OK);
}
@PostMapping("/requestResource")
@Log("请求资源")
@SaIgnore
public ResponseEntity<Object> requestResource(@RequestBody JSONObject requestParam) {
return new ResponseEntity<>(keCongAgvService.requestResource(requestParam), HttpStatus.OK);
}
@PostMapping("/releaseResource")
@Log("释放资源")
@SaIgnore
public ResponseEntity<AgvResponse> releaseResource(@RequestBody JSONObject requestParam) {
public ResponseEntity<Object> releaseResource(@RequestBody JSONObject requestParam) {
return new ResponseEntity<>(keCongAgvService.releaseResource(requestParam), HttpStatus.OK);
}

View File

@@ -39,10 +39,16 @@ public interface KeCongAgvService {
JSONObject applyIn(JSONObject requestParam);
AgvResponse releaseResource(JSONObject requestParam);
JSONObject releaseResource(JSONObject requestParam);
JSONObject requestAction(JSONObject requestParam);
HttpResponse pause(String device_code) throws Exception;
HttpResponse resume(String device_code) throws Exception;
JSONObject requestResource(JSONObject requestParam);
// HttpResponse getRobotInfo(String robotCode);
/**

View File

@@ -15,11 +15,14 @@ import org.nl.acs.agv.server.KeCongAgvService;
import org.nl.acs.agv.server.MagicAgvService;
import org.nl.acs.agv.server.dao.AgvResponse;
import org.nl.acs.common.base.CommonFinalParam;
import org.nl.acs.device.domain.Device;
import org.nl.acs.device.service.DeviceService;
import org.nl.acs.device.service.impl.DeviceServiceImpl;
import org.nl.acs.device_driver.raster.RasterDeviceDriver;
import org.nl.acs.ext.wms.service.AcsToWmsService;
import org.nl.acs.instruction.domain.Instruction;
import org.nl.acs.instruction.service.InstructionService;
import org.nl.acs.opc.DeviceAppService;
import org.nl.acs.storage_cell.domain.StorageCell;
import org.nl.acs.storage_cell.service.mapper.StorageCellMapper;
import org.nl.acs.task.enums.ActionTypeEnum;
@@ -53,6 +56,7 @@ public class KeCongAgvServiceImpl implements KeCongAgvService {
private TaskService taskserver;
@Autowired
private StorageCellMapper storageCellMapper;
private final DeviceAppService deviceAppService;
@Override
public HttpResponse deleteTask(String instCode) throws Exception {
@@ -386,11 +390,128 @@ public class KeCongAgvServiceImpl implements KeCongAgvService {
}
@Override
public AgvResponse releaseResource(JSONObject requestParam) {
AgvResponse agvResponse = new AgvResponse();
agvResponse.setResult("true");
agvResponse.setErrMsg("请求失败");
return agvResponse;
public JSONObject releaseResource(JSONObject requestParam) {
log.info("agv申请离开,请求参数:" + requestParam);
String resourceID = requestParam.getString("point_code");
String vehicleNo = requestParam.getString("VehicleNo");
LuceneLogDto logDto1 = LuceneLogDto.builder()
.device_code(vehicleNo)
.content("agv申请离开,请求参数:"+ requestParam)
.build();
logDto1.setLog_level(4);
luceneExecuteLogService.deviceExecuteLog(logDto1);
Device device = deviceAppService.findDeviceByCode(resourceID);
RasterDeviceDriver rasterDeviceDriver = (RasterDeviceDriver) device.getDeviceDriver();
rasterDeviceDriver.setOption(1);
JSONObject jo = new JSONObject();
jo.put("code", "200");
jo.put("message", "请求成功");
LuceneLogDto logDto2 = LuceneLogDto.builder()
.device_code(vehicleNo)
.content("agv申请离开成功返回agv参数:" + jo)
.build();
logDto2.setLog_level(4);
luceneExecuteLogService.deviceExecuteLog(logDto2);
return jo;
}
@Override
public HttpResponse pause(String device_code) throws Exception {
if (StrUtil.equals(paramService.findByCode(AcsConfig.FORKAGV).
getValue(), CommonFinalParam.ONE)) {
Device device = deviceAppService.findDeviceByCode(device_code);
RasterDeviceDriver rasterDeviceDriver = (RasterDeviceDriver) device.getDeviceDriver();
if (rasterDeviceDriver.getOption() == 1) {
String car_no = device.getCar_no();
String agvurl = paramService.findByCode(AcsConfig.AGVURL).getValue();
String agvport = paramService.findByCode(AcsConfig.AGVPORT).getValue();
agvurl = agvurl + ":" + agvport + "/api/fms/writeVariable";
log.info("暂停agv请求:{}", agvurl);
JSONObject ja = new JSONObject();
ja.put("VehicleNo", car_no);
ja.put("VariableValue", "1");
HttpResponse result = HttpRequest.post(agvurl)
//表单内容
.body(String.valueOf(ja))
//超时,毫秒
.timeout(20000)
.execute();
log.info("暂停agv请求反馈:{}", result);
return result;
}else {
return null;
}
} else {
return null;
}
}
@Override
public HttpResponse resume(String device_code) throws Exception {
if (StrUtil.equals(paramService.findByCode(AcsConfig.FORKAGV).
getValue(), CommonFinalParam.ONE)) {
Device device = deviceAppService.findDeviceByCode(device_code);
RasterDeviceDriver rasterDeviceDriver = (RasterDeviceDriver) device.getDeviceDriver();
if (rasterDeviceDriver.getOption() == 1) {
String car_no = device.getCar_no();
String agvurl = paramService.findByCode(AcsConfig.AGVURL).getValue();
String agvport = paramService.findByCode(AcsConfig.AGVPORT).getValue();
agvurl = agvurl + ":" + agvport + "/api/fms/writeVariable";
log.info("恢复所有agv请求:{}", agvurl);
JSONObject ja = new JSONObject();
ja.put("VehicleNo", car_no);
ja.put("VariableValue", "2");
HttpResponse result = HttpRequest.post(agvurl)
//表单内容
.body(String.valueOf(ja))
//超时,毫秒
.timeout(20000)
.execute();
log.info("恢复所有agv请求反馈:{}", result);
return result;
}else {
return null;
}
} else {
return null;
}
}
@Override
public JSONObject requestResource(JSONObject requestParam) {
log.info("agv申请进入,请求参数:" + requestParam);
String resourceID = requestParam.getString("point_code");
String vehicleNo = requestParam.getString("VehicleNo");
LuceneLogDto logDto1 = LuceneLogDto.builder()
.device_code(vehicleNo)
.content("agv申请进入,请求参数:"+ requestParam)
.build();
logDto1.setLog_level(4);
luceneExecuteLogService.deviceExecuteLog(logDto1);
Device device = deviceAppService.findDeviceByCode(resourceID);
RasterDeviceDriver rasterDeviceDriver = (RasterDeviceDriver) device.getDeviceDriver();
rasterDeviceDriver.setOption(1);
JSONObject jo = new JSONObject();
jo.put("code", "200");
jo.put("message", "请求成功");
LuceneLogDto logDto2 = LuceneLogDto.builder()
.device_code(vehicleNo)
.content("agv申请进入成功返回agv参数:" + jo)
.build();
logDto2.setLog_level(4);
luceneExecuteLogService.deviceExecuteLog(logDto2);
return jo;
}
@Override

View File

@@ -113,70 +113,76 @@ public class Device implements Serializable {
@TableField(exist = false)
private String barrels_status;
/**
* 进入当前区域的车号
*/
@TableField(exist = false)
private String car_no;
@TableId(type = IdType.ASSIGN_ID)
private String device_id;
@NotBlank
private String device_code;
@NotBlank
private String device_name;
private String device_type;
private String region;
private String manufacturer;
private String manufacturer_phone;
private String opc_server_id;
private String opc_plc_id;
@NotBlank
private String is_route;
private String driver_code;
private String remark;
@NotBlank
private String is_active;
@NotBlank
private String is_delete;
@NotBlank
@TableField(fill = FieldFill.INSERT)
private String create_by;
@NotBlank
@TableField(fill = FieldFill.INSERT)
private String create_time;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String update_by;
private String address;
@TableField(exist = false)

View File

@@ -60,6 +60,8 @@ public interface OpcDeviceDriver extends DeviceDriver {
dto.setItem_code(json.getString("extra_code"));
dto.setItem_value(json.getString("extra_value"));
dto.setItem_value_type(json.getString("value_type"));
dto.setItem_name(json.getString("extra_name"));
returns.add(dto);
}
return returns;
@@ -107,10 +109,15 @@ public interface OpcDeviceDriver extends DeviceDriver {
* @return
*/
default Integer getIntegeregerValue(String protocol) {
if (ObjectUtil.isEmpty(this.getValue(protocol)) && "heartbeat".equals(protocol)) {
return null;
Object value = this.getValue(protocol);
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof Short) {
return ((Short) value).intValue();
} else if (value instanceof Number) {
return ((Number) value).intValue();
}
return (Integer) (ObjectUtil.isEmpty(this.getValue(protocol)) ? 0 : this.getValue(protocol));
throw new ClassCastException("Cannot cast " + value.getClass().getName() + " to Integer");
}
/**

View File

@@ -0,0 +1,47 @@
package org.nl.acs.device_driver.raster;
import org.nl.acs.device.device_driver.standard_inspect.ItemDto;
import java.util.ArrayList;
import java.util.List;
public class ItemProtocol {
public static String item_raster = "raster";
private RasterDeviceDriver driver;
public ItemProtocol(RasterDeviceDriver driver){
this.driver=driver;
}
public int getItem_raster(){
return this.getOpcIntegerValue(item_raster);
}
Boolean isonline;
Boolean isError;
public int getOpcIntegerValue(String protocol) {
Integer value = this.driver.getIntegeregerValue(protocol);
if (value == null) {
return 0;
} else {
return value;
}
}
public static List<ItemDto> getReadableItemDtos() {
ArrayList list = new ArrayList();
list.add(new ItemDto(item_raster, "光栅信号", "31009"));
return list;
}
public static List<ItemDto> getWriteableItemDtos() {
ArrayList<ItemDto> list = new ArrayList<>();
return list;
}
@Override
public String toString() {
return "";
}
}

View File

@@ -0,0 +1,56 @@
package org.nl.acs.device_driver.raster;
import org.nl.acs.device.device_driver.standard_inspect.ItemDto;
import org.nl.acs.device.domain.Device;
import org.nl.acs.device.enums.DeviceType;
import org.nl.acs.device_driver.DeviceDriver;
import org.nl.acs.device_driver.defination.OpcDeviceDriverDefination;
import org.springframework.stereotype.Service;
import java.util.LinkedList;
import java.util.List;
@Service
public class RasterDefination implements OpcDeviceDriverDefination {
@Override
public String getDriverCode() {
return "raster";
}
@Override
public String getDriverName() {
return "光栅站点";
}
@Override
public String getDriverDescription() {
return "光栅站点";
}
@Override
public DeviceDriver getDriverInstance(Device device) {
return (new RasterDeviceDriver()).setDevice(device).setDriverDefination(this);
}
@Override
public Class<? extends DeviceDriver> getDeviceDriverType() {
return RasterDeviceDriver.class;
}
@Override
public List<DeviceType> getFitDeviceTypes() {
List<DeviceType> types = new LinkedList();
types.add(DeviceType.conveyor);
return types;
}
@Override
public List<ItemDto> getReadableItemDtos() {
return ItemProtocol.getReadableItemDtos();
}
@Override
public List<ItemDto> getWriteableItemDtos() {
return ItemProtocol.getWriteableItemDtos();
}
}

View File

@@ -0,0 +1,93 @@
package org.nl.acs.device_driver.raster;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.nl.acs.agv.server.KeCongAgvService;
import org.nl.acs.agv.server.impl.KeCongAgvServiceImpl;
import org.nl.acs.device.domain.Device;
import org.nl.acs.device_driver.DeviceDriver;
import org.nl.acs.device_driver.RouteableDeviceDriver;
import org.nl.acs.device_driver.driver.AbstractOpcDeviceDriver;
import org.nl.acs.device_driver.driver.ExecutableDeviceDriver;
import org.nl.acs.ext.wms.service.AcsToWmsService;
import org.nl.acs.ext.wms.service.impl.AcsToWmsServiceImpl;
import org.nl.acs.monitor.DeviceStageMonitor;
import org.nl.config.SpringContextHolder;
import org.nl.config.lucene.service.LuceneExecuteLogService;
import org.nl.config.lucene.service.dto.LuceneLogDto;
import org.springframework.beans.factory.annotation.Autowired;
@Slf4j
@Data
@RequiredArgsConstructor
public class RasterDeviceDriver extends AbstractOpcDeviceDriver implements DeviceDriver, ExecutableDeviceDriver, RouteableDeviceDriver, DeviceStageMonitor {
protected ItemProtocol itemProtocol = new ItemProtocol(this);
private final LuceneExecuteLogService logService = SpringContextHolder.getBean(LuceneExecuteLogService.class);
@Autowired
AcsToWmsService acsToWmsService = SpringContextHolder.getBean(AcsToWmsServiceImpl.class);
@Autowired
KeCongAgvService keCongAgvService = SpringContextHolder.getBean(KeCongAgvServiceImpl.class);
/**
* 当前设备号
*/
private String device_code;
// 1 上位系统允许进入 2 上位系统允许离开
int option = 0;
/**
* 光栅信号
*/
private int raster = 0;
private int last_raster = 0;
@Override
public Device getDevice() {
return this.device;
}
@Override
public void execute() throws Exception {
this.device_code = this.getDevice().getDevice_code();
this.raster = this.itemProtocol.getItem_raster();
if (this.raster != this.last_raster) {
logService.deviceExecuteLog(new LuceneLogDto(this.device_code, "自动线程读取信号: 光栅信号,由" + this.last_raster + "->" + this.raster));
}
if (this.raster == 1) {
keCongAgvService.pause(device_code);
}
if (this.raster == 0) {
keCongAgvService.resume(device_code);
}
last_raster = raster;
}
@Override
public JSONObject getDeviceStatusName() {
JSONObject jo = new JSONObject();
String action = "";
if (this.getRaster() == 0) {
action = "光栅关闭";
} else if (this.getRaster() == 1) {
action = "光栅开启";
}
jo.put("action", action);
return jo;
}
@Override
public void setDeviceStatus(JSONObject data) {
}
}

View File

@@ -0,0 +1,941 @@
package org.nl.acs.opc;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.digitalpetri.modbus.master.ModbusTcpMaster;
import com.digitalpetri.modbus.master.ModbusTcpMasterConfig;
import com.digitalpetri.modbus.requests.*;
import com.digitalpetri.modbus.responses.*;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;
import org.nl.acs.opc.service.dto.OpcServerManageDto;
import org.nl.acs.udw.UnifiedDataAccessor;
import org.nl.acs.udw.UnifiedDataAccessorFactory;
import org.nl.acs.udw.UnifiedDataAppService;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
@Slf4j
public class DeviceModbusTcpProtocolRunnable implements Runnable {
private List<OpcItemDto> protocols;
private OpcServerManageDto opcServer;
private ModbusTcpMaster modbusMaster;
private Map<String, OpcItemDto> itemSearchCache;
/**
* 失败地址跟踪:记录每个地址的连续失败次数和最后失败时间
* key: itemCode, value: FailureInfo
*/
private Map<String, FailureInfo> failureTracker;
/**
* 失败信息
*/
private static class FailureInfo {
int consecutiveFailures; // 连续失败次数
long lastFailureTime; // 最后失败时间
long skipUntilTime; // 跳过直到此时间
FailureInfo() {
this.consecutiveFailures = 0;
this.lastFailureTime = 0;
this.skipUntilTime = 0;
}
}
/**
* Modbus读取项配置
*/
private static class ModbusItemConfig {
String itemCode;
int address;
String dataType;
int registerType;
ModbusItemConfig(String itemCode, int address, String dataType, int registerType) {
this.itemCode = itemCode;
this.address = address;
this.dataType = dataType;
this.registerType = registerType;
}
}
public DeviceModbusTcpProtocolRunnable() {
this.itemSearchCache = new ConcurrentHashMap<>();
this.failureTracker = new ConcurrentHashMap<>();
this.modbusMaster = null;
}
public void setProtocols(List<OpcItemDto> protocols) {
this.protocols = protocols;
}
public void setOpcServer(OpcServerManageDto opcServer) {
this.opcServer = opcServer;
}
private OpcItemDto getItem(String item) {
OpcItemDto x = this.itemSearchCache.get(item);
if (x == null) {
for (OpcItemDto dto : this.protocols) {
if (StrUtil.equals(item, dto.getItem_code())) {
x = dto;
this.itemSearchCache.put(item, dto);
break;
}
}
}
return x;
}
@Override
public void run() {
this.runModbusTcp();
}
/**
* 断开Modbus连接
*/
private void disconnectModbus(String reason) {
if (this.modbusMaster != null) {
try {
modbusMaster.disconnect();
log.info("ModbusTCP连接已断开: {}", reason);
} catch (Exception e) {
log.error("断开ModbusTCP连接失败", e);
}
this.modbusMaster = null;
}
}
private void runModbusTcp() {
while (true) {
try {
// 断开之前的连接
disconnectModbus("准备重新连接");
String[] host_port = this.opcServer.getOpc_host().split(":");
ModbusTcpMasterConfig config = new ModbusTcpMasterConfig.Builder(host_port[0])
.setPort(Integer.parseInt(host_port[1]))
.setTimeout(Duration.ofSeconds(OpcConfig.modbus_tcp_timeout_seconds))
.build();
this.modbusMaster = new ModbusTcpMaster(config);
this.modbusMaster.connect();
log.info("ModbusTCP连接成功: {}", this.opcServer.getOpc_host());
List<String> itemsString = new ArrayList<>();
List<ModbusItemConfig> itemConfigs = new ArrayList<>();
for (OpcItemDto protocol : this.protocols) {
String itemCode = protocol.getItem_code();
itemsString.add(itemCode);
try {
ModbusItemConfig itemConfig = parseSimpleConfig(protocol);
if (itemConfig != null) {
itemConfigs.add(itemConfig);
log.trace("添加读取项: {} -> 地址:{}, 类型:{}", itemCode, itemConfig.address, itemConfig.dataType);
}
} catch (Exception e) {
log.error("解析配置失败: {}, 错误: {}", itemCode, e.getMessage());
}
}
if (!OpcStartTag.is_run) {
OpcStartTag.is_run = true;
}
UnifiedDataAccessor accessor_value = UnifiedDataAccessorFactory.getAccessor(OpcConfig.udw_opc_value_key);
long lastStatisticsTime = System.currentTimeMillis();
while (DeviceModbusTcpSynchronizeAutoRun.isRun) {
Map<String, Object> dataMap = batchReadModbus(itemConfigs);
for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
String itemId = entry.getKey();
Object value = entry.getValue();
Object his = accessor_value.getValue(itemId);
// 检查数值是否变化
if (!UnifiedDataAppService.isEquals(value, his)) {
OpcItemDto itemDto = this.getItem(itemId);
this.logItemChanged(itemId, accessor_value, value, itemDto);
if (!ObjectUtil.isEmpty(value) || "".equals(value)) {
accessor_value.setValue(itemId, value);
}
if (ObjectUtil.isEmpty(value) && !"".equals(value)) {
accessor_value.removeValue(itemId);
}
}
}
// 每5分钟打印一次失败地址统计
long currentTime = System.currentTimeMillis();
if (currentTime - lastStatisticsTime > 300000) { // 5分钟
logFailureStatistics();
lastStatisticsTime = currentTime;
}
// 等待读取间隔时间,避免频繁读取
ThreadUtl.sleep((long) OpcConfig.synchronized_millisecond);
}
// 正常退出时断开连接
disconnectModbus("正常退出");
return;
} catch (Exception e) {
log.error("ModbusTCP连接异常: {}", e.getMessage(), e);
disconnectModbus("异常处理");
if (!DeviceModbusTcpSynchronizeAutoRun.isRun) {
log.warn("ModbusTCP线程停止收到停止信号");
return;
}
log.info("{}秒后尝试重新连接...", OpcConfig.synchronized_exception_wait_second);
ThreadUtl.sleep((OpcConfig.synchronized_exception_wait_second * 1000));
}
}
}
private ModbusItemConfig parseSimpleConfig(OpcItemDto protocol) {
try {
String itemCode = protocol.getItem_code();
String itemName = protocol.getItem_name();
String itemValueType = protocol.getItem_value_type();
if (StrUtil.isEmpty(itemCode) || StrUtil.isEmpty(itemName)) {
log.error("配置错误: item_code或item_name为空");
return null;
}
// 解析Modbus地址确定寄存器类型转换为实际地址
int address = Integer.parseInt(itemName.trim());
int registerType = getRegisterTypeFromAddress(address);
int actualAddress = convertToActualAddress(address, registerType);
return new ModbusItemConfig(itemCode, actualAddress, itemValueType, registerType);
} catch (Exception e) {
log.error("解析配置失败: {}, 错误: {}", protocol.getItem_code(), e.getMessage());
return null;
}
}
/**
* Modbus标准地址约定
* 0xxxx (00001-09999): 线圈 (Coils)
* 1xxxx (10001-19999): 离散输入 (Discrete Inputs)
* 3xxxx (30001-39999): 输入寄存器 (Input Registers)
* 4xxxx (40001-49999): 保持寄存器 (Holding Registers)
* 1、线圈
* 2、离散输入
* 3、保持寄存器
* 4、输入寄存器
*/
private int getRegisterTypeFromAddress(int address) {
if (address >= 40001 && address <= 49999) {
return 3;
} else if (address >= 30001 && address <= 39999) {
return 4;
} else if (address >= 10001 && address <= 19999) {
return 2;
} else if (address >= 1 && address <= 9999) {
return 1;
} else {
return 3;
}
}
/**
* 转换为实际地址
* 如果是标准Modbus地址减去偏移量
* 1、线圈
* 2、离散输入
* 3、保持寄存器
* 4、输入寄存器
*/
private int convertToActualAddress(int address, int registerType) {
switch (registerType) {
case 3:
return address >= 40001 ? address - 40001 : address;
case 4:
return address >= 30001 ? address - 30001 : address;
case 2:
return address >= 10001 ? address - 10001 : address;
case 1:
return address >= 1 && address <= 9999 ? address - 1 : address;
default:
return address;
}
}
/**
* 将内部地址转换回Modbus标准地址格式
* 1、线圈: 0xxxx (1-9999)
* 2、离散输入: 1xxxx (10001-19999)
* 3、保持寄存器: 4xxxx (40001-49999)
* 4、输入寄存器: 3xxxx (30001-39999)
*/
private int convertToModbusAddress(int actualAddress, int registerType) {
switch (registerType) {
case 3:
return actualAddress + 40001;
case 4:
return actualAddress + 30001;
case 2:
return actualAddress + 10001;
case 1:
return actualAddress + 1;
default:
return actualAddress;
}
}
/**
* 获取寄存器类型名称
* 1、线圈 (Coils)
* 2、离散输入 (Discrete Inputs)
* 3、保持寄存器 (Holding Registers)
* 4、输入寄存器 (Input Registers)
*/
private String getRegisterTypeName(int registerType) {
switch (registerType) {
case 1:
return "线圈(Coils)";
case 2:
return "离散输入(Discrete Inputs)";
case 3:
return "保持寄存器(Holding Registers)";
case 4:
return "输入寄存器(Input Registers)";
default:
return "未知类型(" + registerType + ")";
}
}
/**
* 检查是否应该跳过该地址(因为持续失败)
*/
private boolean shouldSkipAddress(String itemCode) {
FailureInfo info = failureTracker.get(itemCode);
if (info == null) {
return false;
}
long currentTime = System.currentTimeMillis();
if (currentTime < info.skipUntilTime) {
// 还在跳过时间内
return true;
}
return false;
}
/**
* 记录读取成功,重置失败计数
*/
private void recordSuccess(String itemCode) {
FailureInfo info = failureTracker.get(itemCode);
if (info != null && info.consecutiveFailures > 0) {
log.info("地址恢复正常: {}, 之前连续失败{}次", itemCode, info.consecutiveFailures);
failureTracker.remove(itemCode);
}
}
/**
* 记录读取失败
*/
private void recordFailure(String itemCode, ModbusItemConfig config, Exception e) {
FailureInfo info = failureTracker.computeIfAbsent(itemCode, k -> new FailureInfo());
info.consecutiveFailures++;
info.lastFailureTime = System.currentTimeMillis();
// 判断是否是超时异常
boolean isTimeout = isTimeoutException(e);
// 如果连续失败次数达到阈值,设置跳过时间
if (info.consecutiveFailures >= OpcConfig.modbus_tcp_failure_threshold && isTimeout) {
// 根据失败次数计算跳过时间(递增策略)
int skipSeconds = OpcConfig.modbus_tcp_skip_time_seconds *
Math.min(info.consecutiveFailures - OpcConfig.modbus_tcp_failure_threshold + 1, 4);
info.skipUntilTime = info.lastFailureTime + skipSeconds * 1000L;
int modbusAddr = convertToModbusAddress(config.address, config.registerType);
log.warn("地址持续失败: {}, 地址:{} (Modbus:{}), 连续失败{}次, 将跳过{}秒",
itemCode, config.address, modbusAddr, info.consecutiveFailures, skipSeconds);
}
}
/**
* 判断是否是超时相关异常
*/
private boolean isTimeoutException(Exception e) {
if (e instanceof TimeoutException) {
return true;
}
if (e instanceof ExecutionException) {
Throwable cause = e.getCause();
if (cause != null) {
String causeClass = cause.getClass().getName();
return causeClass.contains("Timeout") || causeClass.contains("timeout");
}
}
String exMsg = e.getMessage();
return exMsg != null && (exMsg.contains("timeout") || exMsg.contains("timed out"));
}
/**
* 打印失败地址统计信息
*/
private void logFailureStatistics() {
if (failureTracker.isEmpty()) {
return;
}
int totalFailed = 0;
int skippedCount = 0;
StringBuilder sb = new StringBuilder();
sb.append("\n========== Modbus失败地址统计 ==========\n");
long currentTime = System.currentTimeMillis();
for (Map.Entry<String, FailureInfo> entry : failureTracker.entrySet()) {
String itemCode = entry.getKey();
FailureInfo info = entry.getValue();
totalFailed++;
boolean isSkipped = currentTime < info.skipUntilTime;
if (isSkipped) {
skippedCount++;
long remainSeconds = (info.skipUntilTime - currentTime) / 1000;
sb.append(String.format(" [跳过中] %s - 连续失败%d次, 剩余跳过时间:%d秒\n",
itemCode, info.consecutiveFailures, remainSeconds));
} else {
sb.append(String.format(" [监控中] %s - 连续失败%d次\n",
itemCode, info.consecutiveFailures));
}
}
sb.append(String.format("总计: %d个失败地址, 其中%d个正在跳过\n", totalFailed, skippedCount));
sb.append("========================================");
log.warn(sb.toString());
}
/**
* 批量读取Modbus数据
*/
private Map<String, Object> batchReadModbus(List<ModbusItemConfig> itemConfigs) {
Map<String, Object> dataMap = new HashMap<>();
// 按寄存器类型分组
Map<Integer, List<ModbusItemConfig>> groupedByType = new HashMap<>();
for (ModbusItemConfig config : itemConfigs) {
groupedByType.computeIfAbsent(config.registerType, k -> new ArrayList<>()).add(config);
}
// 对每种类型的寄存器进行批量读取
for (Map.Entry<Integer, List<ModbusItemConfig>> entry : groupedByType.entrySet()) {
int registerType = entry.getKey();
List<ModbusItemConfig> configs = entry.getValue();
// 读取这一组寄存器
Map<String, Object> typeData = readRegisterGroup(registerType, configs);
dataMap.putAll(typeData);
}
return dataMap;
}
/**
* 地址范围类:用于批量读取
*/
private static class AddressRange {
int startAddress;
int quantity;
List<ModbusItemConfig> configs;
AddressRange(int startAddress, int quantity) {
this.startAddress = startAddress;
this.quantity = quantity;
this.configs = new ArrayList<>();
}
}
/**
* 读取一组相同类型的寄存器
*/
private Map<String, Object> readRegisterGroup(int registerType, List<ModbusItemConfig> configs) {
Map<String, Object> result = new HashMap<>();
// 如果是线圈或离散输入,不进行合并(数据类型不同)
if (registerType == 1 || registerType == 2) {
for (ModbusItemConfig config : configs) {
// 检查是否应该跳过
if (shouldSkipAddress(config.itemCode)) {
result.put(config.itemCode, null);
continue;
}
try {
Object value = readSingleRegister(registerType, config);
result.put(config.itemCode, value);
recordSuccess(config.itemCode);
} catch (Exception e) {
int modbusAddr = convertToModbusAddress(config.address, config.registerType);
String errorMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
log.error("读取失败: {}, 地址:{} (Modbus:{}), 寄存器类型:{}, 异常类型:{}, 错误:{}",
config.itemCode, config.address, modbusAddr,
getRegisterTypeName(registerType), e.getClass().getName(), errorMsg, e);
recordFailure(config.itemCode, config, e);
result.put(config.itemCode, null);
}
}
return result;
}
List<ModbusItemConfig> sortedConfigs = configs.stream()
.sorted(Comparator.comparingInt(c -> c.address))
.collect(Collectors.toList());
List<AddressRange> ranges = mergeAddressRanges(sortedConfigs);
for (AddressRange range : ranges) {
try {
if (range.configs.size() == 1) {
// 单个读取
ModbusItemConfig config = range.configs.get(0);
// 检查是否应该跳过
if (shouldSkipAddress(config.itemCode)) {
result.put(config.itemCode, null);
continue;
}
Object value = readSingleRegister(registerType, config);
result.put(config.itemCode, value);
recordSuccess(config.itemCode);
} else {
// 批量读取
Map<String, Object> batchResult = readBatchRegisters(registerType, range);
result.putAll(batchResult);
// 批量读取成功,重置所有涉及的地址的失败计数
for (ModbusItemConfig config : range.configs) {
recordSuccess(config.itemCode);
}
}
} catch (Exception e) {
int modbusAddress = convertToModbusAddress(range.startAddress, registerType);
String errorMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
log.error("批量读取失败: 起始地址:{} (Modbus:{}), 数量:{}, 寄存器类型:{}, 异常类型:{}, 错误:{}",
range.startAddress, modbusAddress, range.quantity,
getRegisterTypeName(registerType), e.getClass().getName(), errorMsg, e);
// 失败时逐个读取
for (ModbusItemConfig config : range.configs) {
// 检查是否应该跳过
if (shouldSkipAddress(config.itemCode)) {
result.put(config.itemCode, null);
continue;
}
try {
Object value = readSingleRegister(registerType, config);
result.put(config.itemCode, value);
recordSuccess(config.itemCode);
} catch (Exception ex) {
int modbusAddr = convertToModbusAddress(config.address, config.registerType);
String errMsg = ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName();
log.error("单独读取失败: {}, 地址:{} (Modbus:{}), 异常类型:{}, 错误:{}",
config.itemCode, config.address, modbusAddr, ex.getClass().getName(), errMsg, ex);
recordFailure(config.itemCode, config, ex);
result.put(config.itemCode, null);
}
}
}
}
return result;
}
/**
* 合并连续地址范围
*/
private List<AddressRange> mergeAddressRanges(List<ModbusItemConfig> sortedConfigs) {
List<AddressRange> ranges = new ArrayList<>();
if (sortedConfigs.isEmpty()) {
return ranges;
}
AddressRange currentRange = null;
for (ModbusItemConfig config : sortedConfigs) {
int quantity = getQuantityByDataType(config.dataType);
if (currentRange == null) {
// 第一个范围
currentRange = new AddressRange(config.address, quantity);
currentRange.configs.add(config);
} else {
int expectedNextAddress = currentRange.startAddress + currentRange.quantity;
int gap = config.address - expectedNextAddress;
// 如果地址连续或间隔较小,合并到当前范围
if (gap >= 0 && gap <= OpcConfig.modbus_tcp_batch_address_gap) {
currentRange.quantity = config.address - currentRange.startAddress + quantity;
currentRange.configs.add(config);
} else {
// 地址不连续,开始新范围
ranges.add(currentRange);
currentRange = new AddressRange(config.address, quantity);
currentRange.configs.add(config);
}
}
}
if (currentRange != null) {
ranges.add(currentRange);
}
return ranges;
}
private Map<String, Object> readBatchRegisters(int registerType, AddressRange range)
throws ExecutionException, InterruptedException, TimeoutException {
Map<String, Object> result = new HashMap<>();
int unitId = OpcConfig.modbus_tcp_default_unit_id;
// 创建批量读取请求
ModbusRequest request;
switch (registerType) {
case 3:
request = new ReadHoldingRegistersRequest(range.startAddress, range.quantity);
break;
case 4:
request = new ReadInputRegistersRequest(range.startAddress, range.quantity);
break;
default:
throw new IllegalArgumentException("不支持批量读取的寄存器类型: " + registerType);
}
CompletableFuture<? extends ModbusResponse> future = modbusMaster.sendRequest(request, unitId);
ModbusResponse response = future.get(OpcConfig.modbus_tcp_timeout_seconds, TimeUnit.SECONDS);
// 解析响应
ByteBuf registers = null;
try {
if (response instanceof ReadHoldingRegistersResponse) {
registers = ((ReadHoldingRegistersResponse) response).getRegisters();
} else if (response instanceof ReadInputRegistersResponse) {
registers = ((ReadInputRegistersResponse) response).getRegisters();
}
if (registers != null) {
// 为每个配置项提取对应的数据
for (ModbusItemConfig config : range.configs) {
int offset = (config.address - range.startAddress) * 2;
int quantity = getQuantityByDataType(config.dataType);
// 创建一个新的ByteBuf用于解析当前项
ByteBuf itemBuf = registers.slice(offset, quantity * 2).retain();
try {
Object value = parseRegisterValue(itemBuf, config.dataType);
result.put(config.itemCode, value);
} finally {
ReferenceCountUtil.release(itemBuf);
}
}
}
} finally {
if (registers != null) {
ReferenceCountUtil.release(registers);
}
}
log.debug("批量读取成功: 起始地址:{}, 数量:{}, 读取项数:{}",
range.startAddress, range.quantity, range.configs.size());
return result;
}
private Object readSingleRegister(int registerType, ModbusItemConfig config)
throws ExecutionException, InterruptedException, TimeoutException {
// 从站ID使用配置
int unitId = OpcConfig.modbus_tcp_default_unit_id;
int quantity = getQuantityByDataType(config.dataType);
// 创建请求
ModbusRequest request;
switch (registerType) {
case 3:
request = new ReadHoldingRegistersRequest(config.address, quantity);
break;
case 4:
request = new ReadInputRegistersRequest(config.address, quantity);
break;
case 1:
request = new ReadCoilsRequest(config.address, quantity);
break;
case 2:
request = new ReadDiscreteInputsRequest(config.address, quantity);
break;
default:
throw new IllegalArgumentException("不支持的寄存器类型: " + registerType);
}
CompletableFuture<? extends ModbusResponse> future =
modbusMaster.sendRequest(request, unitId);
ModbusResponse response = future.get(OpcConfig.modbus_tcp_timeout_seconds, TimeUnit.SECONDS);
return parseResponse(response, config.dataType);
}
/**
* 解析Modbus响应
*/
private Object parseResponse(ModbusResponse response, String dataType) {
if (response instanceof ReadHoldingRegistersResponse) {
ReadHoldingRegistersResponse hrResponse = (ReadHoldingRegistersResponse) response;
ByteBuf registers = hrResponse.getRegisters();
try {
return parseRegisterValue(registers, dataType);
} finally {
ReferenceCountUtil.release(registers);
}
} else if (response instanceof ReadInputRegistersResponse) {
ReadInputRegistersResponse irResponse = (ReadInputRegistersResponse) response;
ByteBuf registers = irResponse.getRegisters();
try {
return parseRegisterValue(registers, dataType);
} finally {
ReferenceCountUtil.release(registers);
}
} else if (response instanceof ReadCoilsResponse) {
ReadCoilsResponse coilResponse = (ReadCoilsResponse) response;
ByteBuf coils = coilResponse.getCoilStatus();
try {
if (coils.readableBytes() > 0) {
byte status = coils.readByte();
return (status & 0x01) != 0;
}
} finally {
ReferenceCountUtil.release(coils);
}
} else if (response instanceof ReadDiscreteInputsResponse) {
ReadDiscreteInputsResponse diResponse = (ReadDiscreteInputsResponse) response;
ByteBuf inputs = diResponse.getInputStatus();
try {
if (inputs.readableBytes() > 0) {
byte status = inputs.readByte();
return (status & 0x01) != 0;
}
} finally {
ReferenceCountUtil.release(inputs);
}
}
return null;
}
/**
* 16位数据需要1个寄存器
* 32位数据需要2个寄存器
* 64位数据需要4个寄存器
* String类型STRING:长度,例如 STRING:10 表示10个字符需要5个寄存器
*/
private int getQuantityByDataType(String dataType) {
if (StrUtil.isEmpty(dataType)) {
return 1;
}
String type = dataType.toUpperCase().trim();
// 处理 STRING:长度 格式
if (type.startsWith("STRING:")) {
try {
String lengthStr = type.substring(7);
int charLength = Integer.parseInt(lengthStr);
// 每个寄存器存储2个字符向上取整
return (charLength + 1) / 2;
} catch (Exception e) {
log.error("解析String长度失败: {}", dataType);
return 1;
}
}
switch (type) {
case "FLOAT":
case "INT32":
case "UINT32":
case "DWORD":
return 2;
case "INT64":
case "UINT64":
case "DOUBLE":
return 4;
case "STRING":
// 默认String长度10个字符 = 5个寄存器
return 5;
case "INT16":
case "UINT16":
case "WORD":
case "BOOL":
case "BOOLEAN":
default:
return 1;
}
}
/**
* 根据数据类型解析寄存器值
*/
private Object parseRegisterValue(ByteBuf registers, String dataType) {
if (StrUtil.isEmpty(dataType)) {
if (registers.readableBytes() >= 2) {
return registers.readShort();
}
return null;
}
String type = dataType.toUpperCase().trim();
try {
switch (type) {
case "INT16":
if (registers.readableBytes() >= 2) {
return registers.readShort();
}
break;
case "UINT16":
case "WORD":
if (registers.readableBytes() >= 2) {
return registers.readUnsignedShort();
}
break;
case "INT32":
if (registers.readableBytes() >= 4) {
return registers.readInt();
}
break;
case "UINT32":
case "DWORD":
if (registers.readableBytes() >= 4) {
return registers.readUnsignedInt();
}
break;
case "FLOAT":
if (registers.readableBytes() >= 4) {
return registers.readFloat();
}
break;
case "INT64":
if (registers.readableBytes() >= 8) {
return registers.readLong();
}
break;
case "DOUBLE":
if (registers.readableBytes() >= 8) {
return registers.readDouble();
}
break;
case "BOOL":
case "BOOLEAN":
if (registers.readableBytes() >= 2) {
return registers.readShort() != 0;
}
break;
default:
// 处理 STRING 或 STRING:长度 格式
if (type.equals("STRING") || type.startsWith("STRING:")) {
return parseStringFromRegisters(registers, type);
}
// log.warn("未知的数据类型: {}, 使用INT16", dataType);
if (registers.readableBytes() >= 2) {
return registers.readShort();
}
}
} catch (Exception e) {
log.error("解析寄存器值失败,数据类型: {}, 错误: {}", dataType, e.getMessage());
}
return null;
}
/**
* 从寄存器中解析字符串
* Modbus 寄存器每个寄存器2字节可存储2个ASCII字符
* 字节序:大端序(高字节在前)
*/
private String parseStringFromRegisters(ByteBuf registers, String dataType) {
try {
int availableBytes = registers.readableBytes();
if (availableBytes == 0) {
return "";
}
// 确定字符串长度
int maxLength = availableBytes;
if (dataType.startsWith("STRING:")) {
try {
String lengthStr = dataType.substring(7);
maxLength = Math.min(Integer.parseInt(lengthStr), availableBytes);
} catch (Exception e) {
log.warn("解析String长度失败使用默认: {}", dataType);
}
}
// 读取字节并转换为字符串
byte[] bytes = new byte[availableBytes];
registers.readBytes(bytes);
// 转换为字符串去除末尾的null字符和空格
String result = new String(bytes, 0, Math.min(maxLength, bytes.length), "ASCII");
// 去除null字符
int nullIndex = result.indexOf('\0');
if (nullIndex >= 0) {
result = result.substring(0, nullIndex);
}
// 去除首尾空格
result = result.trim();
return result;
} catch (Exception e) {
log.error("解析String失败: {}", e.getMessage());
return "";
}
}
/**
* 记录数据变化日志
*/
private void logItemChanged(String itemId, UnifiedDataAccessor accessor_value, Object value, OpcItemDto itemDto) {
// 过滤心跳、时间等频繁变化的数据
if (itemDto.getItem_code().endsWith("heartbeat") ||
itemDto.getItem_code().endsWith("time") ||
itemDto.getItem_code().endsWith("consumption")) {
return;
}
Object his = accessor_value.getValue(itemId);
// 记录关联项的值
List<String> relate_items = itemDto.getRelate_items();
if (relate_items != null && !relate_items.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (String relate : relate_items) {
Object obj = accessor_value.getValue(relate);
sb.append(relate).append("=").append(obj).append(";");
}
if (log.isDebugEnabled()) {
log.debug("数据变化 - item:{}, 旧值:{}, 新值:{}, 关联:{}", itemId, his, value, sb.toString());
}
} else {
if (log.isDebugEnabled()) {
log.debug("数据变化 - item:{}, 旧值:{}, 新值:{}", itemId, his, value);
}
}
}
}

View File

@@ -0,0 +1,99 @@
package org.nl.acs.opc;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.dynamictp.core.support.ThreadPoolBuilder;
import org.nl.acs.auto.run.AbstractAutoRunnable;
import org.nl.acs.opc.service.dto.OpcServerManageDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import static org.dromara.dynamictp.common.em.QueueTypeEnum.MEMORY_SAFE_LINKED_BLOCKING_QUEUE;
/**
* OPC设备同步启动
*
* @author 20220102CG\noblelift
*/
@Component
public class DeviceModbusTcpSynchronizeAutoRun extends AbstractAutoRunnable {
public static volatile boolean isRun = false;
ExecutorService executorService = ThreadPoolBuilder.newBuilder()
.threadPoolName("deviceOpc_thread")
.threadFactory("deviceOpc_thread")
.corePoolSize(80)
.maximumPoolSize(100)
.keepAliveTime(40)
.timeUnit(TimeUnit.SECONDS)
.workQueue(MEMORY_SAFE_LINKED_BLOCKING_QUEUE.getName(), 2000)
.buildDynamic();
@Autowired
private DeviceAppService deviceAppService;
@Autowired
private OpcServerManageService opcServerManageService;
@Override
public String getCode() {
return DeviceModbusTcpSynchronizeAutoRun.class.getSimpleName();
}
@Override
public String getName() {
return "ModbusTCP/IP设备同步器";
}
@Override
public void autoRun() throws Exception {
{
isRun = true;
Map<String, OpcServerManageDto> servers = this.opcServerManageService.queryAllServerMap();
Map<String, List<List<OpcItemDto>>> pros;
do {
Thread.sleep(1000L);
pros = this.deviceAppService.findAllFormatProtocolFromDriver();
} while (ObjectUtil.isEmpty(pros));
Set<String> keys = pros.keySet();
Iterator var4 = keys.iterator();
//代码执行一次
while (var4.hasNext()) {
String key = (String) var4.next();
List<List<OpcItemDto>> list = pros.get(key);
OpcServerManageDto opcServer = servers.get(key);
Iterator var8 = list.iterator();
while (var8.hasNext()) {
List<OpcItemDto> groupProtols = (List) var8.next();
DeviceModbusTcpProtocolRunnable runnable = new DeviceModbusTcpProtocolRunnable();
runnable.setProtocols(groupProtols);
runnable.setOpcServer(opcServer);
this.executorService.submit(runnable);
}
}
while (true) {
Thread.sleep(3000L);
}
}
}
@Override
public void after() {
isRun = false;
this.executorService.shutdownNow();
this.executorService = ThreadPoolBuilder.newBuilder()
.threadPoolName("deviceOpc_thread")
.threadFactory("deviceOpc_thread")
.corePoolSize(80)
.maximumPoolSize(100)
.keepAliveTime(40)
.timeUnit(TimeUnit.SECONDS)
.workQueue(MEMORY_SAFE_LINKED_BLOCKING_QUEUE.getName(), 2000)
.buildDynamic();
}
}

View File

@@ -84,9 +84,9 @@ public class DeviceOpcProtocolRunable implements Runnable, DataCallback, ServerC
@Override
public void run() {
if (OpcConfig.opc_item_read_using_callback) {
this.runNew();
// this.runNew();
} else {
this.runOld();
// this.runOld();
}
}
@@ -440,26 +440,26 @@ public class DeviceOpcProtocolRunable implements Runnable, DataCallback, ServerC
&& !itemDto.getItem_code().endsWith("x") && !itemDto.getItem_code().endsWith("y")) {
// 存在上次点位值为null情况 则不记录日志
if(!(his instanceof Float) && !(value instanceof Float)){
LuceneLogDto luceneLogDto = new LuceneLogDto(itemDto.getOpc_server_code(), itemDto.getOpc_plc_code(),4, itemDto.getDevice_code(), itemDto.getItem_code().substring(itemDto.getItem_code().lastIndexOf(".") + 1),
String.valueOf(his), String.valueOf(value));
luceneLogDto.setLogType(LogTypeEnum.DEVICE_LOG.getDesc());
String logLevel = paramService.findByCode(AcsConfig.LOGLEVEL).getValue();
if(StrUtil.isNotEmpty(logLevel) && isNumeric(logLevel) && (luceneLogDto.getLog_level() >= Integer.parseInt(logLevel))){
log.info("{}", JSON.toJSONString(luceneLogDto));
}
// LuceneLogDto luceneLogDto = new LuceneLogDto(itemDto.getOpc_server_code(), itemDto.getOpc_plc_code(),4, itemDto.getDevice_code(), itemDto.getItem_code().substring(itemDto.getItem_code().lastIndexOf(".") + 1),
// String.valueOf(his), String.valueOf(value));
// luceneLogDto.setLogType(LogTypeEnum.DEVICE_LOG.getDesc());
// String logLevel = paramService.findByCode(AcsConfig.LOGLEVEL).getValue();
// if(StrUtil.isNotEmpty(logLevel) && isNumeric(logLevel) && (luceneLogDto.getLog_level() >= Integer.parseInt(logLevel))){
// log.info("{}", JSON.toJSONString(luceneLogDto));
// }
}
}
} else {
if (!itemDto.getItem_code().endsWith("heartbeat") && !itemDto.getItem_code().endsWith("time") && !itemDto.getItem_code().endsWith("consumption")
&& !itemDto.getItem_code().endsWith("x") && !itemDto.getItem_code().endsWith("y")) {
if(!(his instanceof Float) && !(value instanceof Float)){
LuceneLogDto luceneLogDto = new LuceneLogDto(itemDto.getOpc_server_code(), itemDto.getOpc_plc_code(),4, itemDto.getDevice_code(), itemDto.getItem_code().substring(itemDto.getItem_code().lastIndexOf(".") + 1),
String.valueOf(his), String.valueOf(value));
luceneLogDto.setLogType(LogTypeEnum.DEVICE_LOG.getDesc());
String logLevel = paramService.findByCode(AcsConfig.LOGLEVEL).getValue();
if(StrUtil.isNotEmpty(logLevel) && isNumeric(logLevel) && (luceneLogDto.getLog_level() >= Integer.parseInt(logLevel))){
log.info("{}", JSON.toJSONString(luceneLogDto));
}
// LuceneLogDto luceneLogDto = new LuceneLogDto(itemDto.getOpc_server_code(), itemDto.getOpc_plc_code(),4, itemDto.getDevice_code(), itemDto.getItem_code().substring(itemDto.getItem_code().lastIndexOf(".") + 1),
// String.valueOf(his), String.valueOf(value));
// luceneLogDto.setLogType(LogTypeEnum.DEVICE_LOG.getDesc());
// String logLevel = paramService.findByCode(AcsConfig.LOGLEVEL).getValue();
// if(StrUtil.isNotEmpty(logLevel) && isNumeric(logLevel) && (luceneLogDto.getLog_level() >= Integer.parseInt(logLevel))){
// log.info("{}", JSON.toJSONString(luceneLogDto));
// }
}
}
}

View File

@@ -18,4 +18,32 @@ public class OpcConfig {
* OPC 数据同步是否采用回调机制实现。之前是线程定期全部读,效率低。
*/
public static Boolean opc_item_read_using_callback = false;
/**
* ModbusTCP连接超时时间
*/
public static Integer modbus_tcp_timeout_seconds = Integer.valueOf(3);
/**
* ModbusTCP默认从站IDUnit ID
*/
public static Integer modbus_tcp_default_unit_id = Integer.valueOf(1);
/**
* ModbusTCP批量读取连续地址的最大间隔超过此间隔则分开读取
*/
public static Integer modbus_tcp_batch_address_gap = Integer.valueOf(100);
/**
* ModbusTCP连续失败多少次后开始跳过该地址
*/
public static Integer modbus_tcp_failure_threshold = Integer.valueOf(3);
/**
* ModbusTCP失败后跳过的时间
* 第1次失败跳过 30 秒
* 第2次失败跳过 60 秒
* 第3次及以上跳过 120 秒
*/
public static Integer modbus_tcp_skip_time_seconds = Integer.valueOf(30);
}

View File

@@ -15,8 +15,10 @@ public class OpcItemDto {
private String driver_code;
private String opc_server_code;
private String opc_plc_code;
private String item_name;
private String item_code;
private Object item_value;
private String item_value_type;
private Object his_item_value;

View File

@@ -947,6 +947,7 @@ public class TaskServiceImpl extends CommonServiceImpl<TaskMapper, Task> impleme
this.removeByCodeFromCache(entity.getTask_code());
//反馈上位系统任务状态
this.feedWmsTaskStatus(entity);
//关闭仙工运单序列
if (StrUtil.equals(task.getTask_type(), TaskTypeEnum.Standard_AGV_Task.getCode()) && (StrUtil.equals(task.getAgv_system_type(), AgvSystemTypeEnum.One_NDC_System_Type.getCode()) || StrUtil.equals(task.getAgv_system_type(), AgvSystemTypeEnum.XG_System_Type.getCode()))) {
this.markComplete(entity);

View File

@@ -10,7 +10,7 @@ spring:
driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
# url: jdbc:log4jdbc:mysql://${DB_HOST:192.168.81.252}:${DB_PORT:3306}/${DB_NAME:stand_acs}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true
# url: jdbc:log4jdbc:mysql://${DB_HOST:47.111.78.178}:${DB_PORT:3306}/${DB_NAME:lzhl_two_acs}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true
url: jdbc:log4jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:fengtiandl_acs}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true
url: jdbc:log4jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:dty_acs}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true
username: ${DB_USER:root}
# password: ${DB_PWD:Root.123456}

View File

@@ -6,7 +6,7 @@ spring:
freemarker:
check-template-location: false
profiles:
active: prod
active: dev
jackson:
time-zone: GMT+8
data:

View File

@@ -1,8 +1,8 @@
ENV = 'development'
# 接口地址
VUE_APP_BASE_API = 'http://192.168.217.2:8011'
VUE_APP_WS_API = 'ws://192.168.217.2:8011'
VUE_APP_BASE_API = 'http://localhost:8011'
VUE_APP_WS_API = 'ws://localhost:8011'
# 是否启用 babel-plugin-dynamic-import-node插件
VUE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@@ -2,6 +2,6 @@ ENV = 'production'
# 如果使用 Nginx 代理后端接口,那么此处需要改为 '/',文件查看 Docker 部署篇Nginx 配置
# 接口地址,注意协议,如果你没有配置 ssl需要将 https 改为 http
VUE_APP_BASE_API = 'http://192.168.217.2:8011'
VUE_APP_BASE_API = 'http://localhost:8011'
# 如果接口是 http 形式, wss 需要改为 ws
VUE_APP_WS_API = 'ws://192.168.217.2:8011'
VUE_APP_WS_API = 'ws://localhost:8011'

View File

@@ -1,9 +1,9 @@
window.g = {
dev: {
VUE_APP_BASE_API: 'http://192.168.217.2:8011'
VUE_APP_BASE_API: 'http://localhost:8011'
},
prod: {
VUE_APP_BASE_API: 'http://192.168.217.2:8011'
VUE_APP_BASE_API: 'http://localhost:8011'
}
}

View File

@@ -77,6 +77,7 @@ import standard_autodoor from '@/views/acs/device/driver/standard_autodoor'
import standard_inspect_site from './driver/standard_inspect_site'
import standard_ordinary_site from './driver/standard_ordinary_site'
import belt_conveyor from '@/views/acs/device/driver/belt_conveyor'
import raster from '@/views/acs/device/driver/raster'
import agv_ndc_one from '@/views/acs/device/driver/agv/agv_ndc_one'
import agv_ndc_two from '@/views/acs/device/driver/agv/agv_ndc_two'
import xg_agv from '@/views/acs/device/driver/agv/xg_agv'
@@ -97,7 +98,8 @@ export default {
standard_manipulator,
xg_agv_car,
standard_storage,
kc_agv
kc_agv,
raster
},
dicts: ['device_type'],
mixins: [crud],

View File

@@ -0,0 +1,544 @@
<template>
<!--标准版-输送机-控制点-->
<div>
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span class="role-span">设备协议</span>
</div>
<el-row>
<el-col :span="12">
OpcServer:
<el-select
v-model="opc_id"
placeholder="无"
clearable
filterable
@change="changeOpc"
>
<el-option
v-for="item in dataOpcservers"
:key="item.opc_id"
:label="item.opc_name"
:value="item.opc_id"
/>
</el-select>
</el-col>
<el-col :span="12">
PLC:
<el-select
v-model="plc_id"
placeholder="无"
clearable
@change="changePlc"
>
<el-option
v-for="item in dataOpcPlcs"
:key="item.plc_id"
:label="item.plc_name"
:value="item.plc_id"
/>
</el-select>
</el-col>
</el-row>
</el-card>
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span class="role-span">输送系统</span>
</div>
<el-form ref="form" :inline="true" :model="form" :rules="rules" size="small" label-width="78px">
<el-row>
<el-col :span="8">
<el-form-item label="电气调度号" label-width="150px">
<el-input v-model="form.address" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span class="role-span">指令相关</span>
</div>
<el-form ref="form" :inline="true" :model="form" :rules="rules" size="small" label-width="78px">
<el-row>
<el-col :span="8">
<el-form-item label="检验有货">
<el-switch v-model="form.inspect_in_stocck" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="取货校验" label-width="150px">
<el-switch v-model="form.ignore_pickup_check" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="放货校验" label-width="150px">
<el-switch v-model="form.ignore_release_check" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="取货完成反馈" label-width="78px">
<el-switch v-model="form.feedbackToLms" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="呼叫">
<el-switch v-model="form.apply_task" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="响应" label-width="150px">
<el-switch v-model="form.manual_create_task" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="关联设备" prop="device_code">
<el-select
v-model="form.link_device_code"
filterable
multiple
placeholder="请选择"
>
<el-option
v-for="item in deviceList"
:key="item.device_code"
:label="item.device_name"
:value="item.device_code"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="关联三色灯" prop="device_code" label-width="100px">
<el-select
v-model="form.link_three_lamp"
filterable
clearable
placeholder="请选择"
>
<el-option
v-for="item in deviceList"
:key="item.device_code"
:label="item.device_name"
:value="item.device_code"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="是否输入物料" label-width="150px">
<el-switch v-model="form.input_material" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="是否需要反馈光电" label-width="150px">
<el-switch v-model="form.ship_device_update" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span class="role-span">AGV相关</span>
</div>
<el-form ref="form" :inline="true" :model="form" :rules="rules" size="small" label-width="78px">
<el-row>
<el-col :span="8">
<el-form-item label="取货">
<el-switch v-model="form.is_pickup" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="放货">
<el-switch v-model="form.is_release" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span class="role-span">PLC读取字段</span>
</div>
<el-form ref="form" :inline="true" :model="form" :rules="rules" size="small" label-width="78px">
<el-table
v-loading="false"
:data="data1"
:max-height="550"
size="small"
style="width: 100%;margin-bottom: 15px"
>
<el-table-column prop="name" label="用途" />
<el-table-column prop="code" label="别名要求" />
<el-table-column prop="db" label="DB块">
<template slot-scope="scope">
<el-input
v-model="data1[scope.$index].db"
size="mini"
class="edit-input"
@input="finishReadEdit(data1[scope.$index])"
/>
</template>
</el-table-column>
<el-table-column prop="dbr_value">
<template slot="header">
<el-link type="primary" :underline="false" @click.native="test_read1()">测试读</el-link>
</template>
<template slot-scope="scope">
<el-input v-model="data1[scope.$index].dbr_value" size="mini" class="edit-input" />
</template>
</el-table-column>
</el-table>
</el-form>
</el-card>
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span class="role-span">PLC写入字段</span>
</div>
<el-form ref="form" :inline="true" :model="form" :rules="rules" size="small" label-width="78px">
<el-table
v-loading="false"
:data="data2"
:max-height="550"
size="small"
style="width: 100%;margin-bottom: 15px"
>
<el-table-column prop="name" label="用途" />
<el-table-column prop="code" label="别名要求" />
<el-table-column prop="db" label="DB块">
<template slot-scope="scope">
<el-input
v-model="data2[scope.$index].db"
size="mini"
class="edit-input"
@input="finishWriteEdit(data2[scope.$index])"
/>
</template>
</el-table-column>
<el-table-column prop="dbr_value2">
<template slot="header">
<el-link type="primary" :underline="false" @click.native="test_read2()">测试读</el-link>
</template>
<template slot-scope="scope">
<el-input v-model="data2[scope.$index].dbr_value" size="mini" class="edit-input" />
</template>
</el-table-column>
<el-table-column prop="dbw_value">
<template slot="header">
<el-link type="primary" :underline="false" @click.native="test_write1()">测试写</el-link>
</template>
<template slot-scope="scope">
<el-input v-model="data2[scope.$index].dbw_value" size="mini" class="edit-input" />
</template>
</el-table-column>
</el-table>
</el-form>
</el-card>
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span class="role-span" />
<el-button
:loading="false"
icon="el-icon-check"
size="mini"
style="float: right; padding: 6px 9px"
type="primary"
@click="doSubmit"
>保存
</el-button>
</div>
</el-card>
</div>
</template>
<script>
import {
queryDriverConfig,
updateConfig,
testRead,
testwrite
} from '@/api/acs/device/driverConfig'
import { selectOpcList } from '@/api/acs/device/opc'
import { selectPlcList } from '@/api/acs/device/opcPlc'
import { selectListByOpcID } from '@/api/acs/device/opcPlc'
import crud from '@/mixins/crud'
import deviceCrud from '@/api/acs/device/device'
export default {
name: 'Raster',
mixins: [crud],
props: {
parentForm: {
type: Object,
required: true
}
},
data() {
return {
device_code: '',
device_id: '',
plc_id: '',
plc_code: '',
address: '',
opc_id: '',
opc_code: '',
configLoading: false,
dataOpcservers: [],
dataOpcPlcs: [],
deviceList: [],
data1: [],
data2: [],
form: {
inspect_in_stocck: true,
ignore_pickup_check: true,
ignore_release_check: true,
apply_task: true,
link_three_lamp: '',
manual_create_task: true,
is_pickup: true,
is_release: true,
link_device_code: [],
ship_device_update: true
},
rules: {}
}
},
created() {
this.$nextTick(() => {
// 从父表单获取设备编码
this.device_id = this.$props.parentForm.device_id
this.device_code = this.$props.parentForm.device_code
queryDriverConfig(this.device_id, this.$props.parentForm.driver_code).then(data => {
// 给表单赋值,并且属性不能为空
if (data.form) {
const arr = Object.keys(data.form)
// 不为空
if (arr.length > 0) {
this.form = data.form
}
}
// 给表单赋值,并且属性不能为空
if (data.parentForm) {
const arr = Object.keys(data.parentForm)
// 不为空
if (arr.length > 0) {
this.opc_code = data.parentForm.opc_code
this.plc_code = data.parentForm.plc_code
}
}
this.data1 = data.rs
this.data2 = data.ws
this.sliceItem()
})
selectPlcList().then(data => {
this.dataOpcPlcs = data
this.plc_id = this.$props.parentForm.opc_plc_id
})
selectOpcList().then(data => {
this.dataOpcservers = data
this.opc_id = this.$props.parentForm.opc_server_id
})
deviceCrud.selectDeviceList().then(data => {
this.deviceList = data
})
})
},
methods: {
finishReadEdit(data) {
// 编辑的是code列,并且值包含mode
if (data.code.indexOf('mode') !== -1) {
const dbValue = data.db
// .之前的字符串
const beforeStr = dbValue.match(/(\S*)\./)[1]
// .之后的字符串
const afterStr = dbValue.match(/\.(\S*)/)[1]
// 取最后数字
const endNumber = afterStr.substring(1)
// 最后为非数字
if (isNaN(parseInt(endNumber))) {
return
}
for (const val in this.data1) {
if (this.data1[val].code.indexOf('mode') !== -1) {
this.data1[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 0)
}
if (this.data1[val].code.indexOf('move') !== -1) {
this.data1[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 1)
}
if (this.data1[val].code.indexOf('carrier_direction') !== -1) {
this.data1[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 2)
}
if (this.data1[val].code.indexOf('error') !== -1) {
this.data1[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 4)
}
if (this.data1[val].code.indexOf('task') !== -1) {
this.data1[val].db = beforeStr + '.' + 'D' + (parseInt(endNumber) + 8)
}
}
}
},
finishWriteEdit(data) {
// 编辑的是code列,并且值包含mode
if (data.code.indexOf('to_command') !== -1) {
const dbValue = data.db
// .之前的字符串
const beforeStr = dbValue.match(/(\S*)\./)[1]
// .之后的字符串
const afterStr = dbValue.match(/\.(\S*)/)[1]
// 取最后数字
const endNumber = afterStr.substring(1)
// 最后为非数字
if (isNaN(parseInt(endNumber))) {
return
}
console.log(endNumber)
for (const val in this.data2) {
if (this.data2[val].code.indexOf('to_command') !== -1) {
this.data2[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 0)
}
if (this.data2[val].code.indexOf('to_target') !== -1) {
this.data2[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 2)
}
if (this.data2[val].code.indexOf('to_container_type') !== -1) {
this.data2[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 4)
}
if (this.data2[val].code.indexOf('to_task') !== -1) {
this.data2[val].db = beforeStr + '.' + 'D' + (parseInt(endNumber) + 6)
}
if (this.data2[val].code.indexOf('to_strap_times') !== -1) {
this.data2[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 10)
}
if (this.data2[val].code.indexOf('to_length') !== -1) {
this.data2[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 12)
}
if (this.data2[val].code.indexOf('to_weight') !== -1) {
this.data2[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 14)
}
if (this.data2[val].code.indexOf('to_height') !== -1) {
this.data2[val].db = beforeStr + '.' + afterStr.substring(0, 1) + (parseInt(endNumber) + 16)
}
}
}
},
changeOpc(val) {
this.dataOpcservers.forEach(item => {
if (item.opc_id === val) {
this.opc_code = item.opc_code
}
})
selectListByOpcID(val).then(data => {
this.dataOpcPlcs = data
this.plc_id = ''
this.plc_code = ''
if (this.dataOpcPlcs && this.dataOpcPlcs.length > 0) {
this.plc_id = this.dataOpcPlcs[0].plc_id
this.plc_code = this.dataOpcPlcs[0].plc_code
}
this.sliceItem()
})
},
changePlc(val) {
this.dataOpcPlcs.forEach(item => {
if (item.plc_id === val) {
this.plc_code = item.plc_code
this.sliceItem()
return
}
})
},
test_read1() {
testRead(this.data1, this.opc_id).then(data => {
this.data1 = data
console.log(this.data1)
this.notify('操作成功!', 'success')
}).catch(err => {
console.log(err.response.data.message)
})
},
test_write1() {
testwrite(this.data2, this.opc_id).then(data => {
this.notify('操作成功!', 'success')
}).catch(err => {
console.log(err.response.data.message)
})
},
test_read2() {
testRead(this.data2, this.opc_id).then(data => {
this.data2 = data
console.log(this.data2)
this.notify('操作成功!', 'success')
}).catch(err => {
console.log(err.response.data.message)
})
},
doSubmit() {
this.$refs['form'].validate((valid) => {
if (valid) {
this.configLoading = true
// 根据驱动类型判断是否为路由设备
const parentForm = this.parentForm
parentForm.is_route = true
parentForm.plc_id = this.plc_id
parentForm.opc_id = this.opc_id
updateConfig(parentForm, this.form, this.data1, this.data2).then(res => {
this.notify('保存成功', 'success')
this.configLoading = false
}).catch(err => {
this.configLoading = false
console.log(err.response.data.message)
})
}
})
},
sliceItem() { // 拼接DB的Item值
this.data1.forEach(item => {
const str = item.code
// 是否包含.
if (str.search('.') !== -1) {
// 截取最后一位
item.code = this.opc_code + '.' + this.plc_code + '.' + this.device_code + '.' + str.slice(str.lastIndexOf('.') + 1)
} else {
item.code = this.opc_code + '.' + this.plc_code + '.' + this.device_code + '.' + item.code
}
})
this.data2.forEach(item => {
const str = item.code
// 是否包含.
if (str.search('.') !== -1) {
// 截取最后一位
item.code = this.opc_code + '.' + this.plc_code + '.' + this.device_code + '.' + str.slice(str.lastIndexOf('.') + 1)
} else {
item.code = this.opc_code + '.' + this.plc_code + '.' + this.device_code + '.' + item.code
}
})
}
}
}
</script>
<style scoped>
</style>