From e16fa8a789333385a04b51b68ec717f79c7a9af0 Mon Sep 17 00:00:00 2001 From: tuqiang <437016993@qq.com> Date: Mon, 22 Dec 2025 08:50:07 +0800 Subject: [PATCH] =?UTF-8?q?add:=E6=96=B0=E5=A2=9E=E5=85=89=E6=A0=85?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=E4=BB=A5=E5=8F=8A=E7=A7=91=E8=81=AA=E6=9A=82?= =?UTF-8?q?=E5=81=9C=E5=92=8C=E6=81=A2=E5=A4=8D=E8=BD=A6=E8=BE=86=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- acs2/nladmin-system/nlsso-server/pom.xml | 10 + .../nl/acs/agv/rest/KeCongAgvController.java | 9 +- .../nl/acs/agv/server/KeCongAgvService.java | 8 +- .../agv/server/impl/KeCongAgvServiceImpl.java | 131 ++- .../java/org/nl/acs/device/domain/Device.java | 42 +- .../device_driver/driver/OpcDeviceDriver.java | 13 +- .../device_driver/raster/ItemProtocol.java | 47 + .../raster/RasterDefination.java | 56 ++ .../raster/RasterDeviceDriver.java | 93 ++ .../opc/DeviceModbusTcpProtocolRunnable.java | 941 ++++++++++++++++++ .../DeviceModbusTcpSynchronizeAutoRun.java | 99 ++ .../nl/acs/opc/DeviceOpcProtocolRunable.java | 32 +- .../main/java/org/nl/acs/opc/OpcConfig.java | 28 + .../main/java/org/nl/acs/opc/OpcItemDto.java | 2 + .../task/service/impl/TaskServiceImpl.java | 1 + .../main/resources/config/application-dev.yml | 2 +- .../src/main/resources/config/application.yml | 2 +- acs2/nladmin-ui/.env.development | 4 +- acs2/nladmin-ui/.env.production | 4 +- acs2/nladmin-ui/public/config.js | 4 +- .../src/views/acs/device/config.vue | 4 +- .../src/views/acs/device/driver/raster.vue | 544 ++++++++++ 22 files changed, 2023 insertions(+), 53 deletions(-) create mode 100644 acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/ItemProtocol.java create mode 100644 acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/RasterDefination.java create mode 100644 acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/RasterDeviceDriver.java create mode 100644 acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceModbusTcpProtocolRunnable.java create mode 100644 acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceModbusTcpSynchronizeAutoRun.java create mode 100644 acs2/nladmin-ui/src/views/acs/device/driver/raster.vue diff --git a/acs2/nladmin-system/nlsso-server/pom.xml b/acs2/nladmin-system/nlsso-server/pom.xml index ecfd722..f29a8b8 100644 --- a/acs2/nladmin-system/nlsso-server/pom.xml +++ b/acs2/nladmin-system/nlsso-server/pom.xml @@ -451,6 +451,16 @@ xercesImpl 2.12.0 --> + + net.java.dev.jna + jna + 5.14.0 + + + com.digitalpetri.modbus + modbus-master-tcp + 1.2.0 + diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/rest/KeCongAgvController.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/rest/KeCongAgvController.java index cb0ce53..3a0c0f3 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/rest/KeCongAgvController.java +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/rest/KeCongAgvController.java @@ -34,10 +34,17 @@ public class KeCongAgvController { return new ResponseEntity<>(keCongAgvService.applyIn(requestParam), HttpStatus.OK); } + @PostMapping("/requestResource") + @Log("请求资源") + @SaIgnore + public ResponseEntity requestResource(@RequestBody JSONObject requestParam) { + return new ResponseEntity<>(keCongAgvService.requestResource(requestParam), HttpStatus.OK); + } + @PostMapping("/releaseResource") @Log("释放资源") @SaIgnore - public ResponseEntity releaseResource(@RequestBody JSONObject requestParam) { + public ResponseEntity releaseResource(@RequestBody JSONObject requestParam) { return new ResponseEntity<>(keCongAgvService.releaseResource(requestParam), HttpStatus.OK); } diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/server/KeCongAgvService.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/server/KeCongAgvService.java index c53e80a..891c9c3 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/server/KeCongAgvService.java +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/server/KeCongAgvService.java @@ -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); /** diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/server/impl/KeCongAgvServiceImpl.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/server/impl/KeCongAgvServiceImpl.java index e2136c2..08328c0 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/server/impl/KeCongAgvServiceImpl.java +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/agv/server/impl/KeCongAgvServiceImpl.java @@ -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 diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device/domain/Device.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device/domain/Device.java index 143936d..b29a910 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device/domain/Device.java +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device/domain/Device.java @@ -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) diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/driver/OpcDeviceDriver.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/driver/OpcDeviceDriver.java index dec0681..5d79db4 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/driver/OpcDeviceDriver.java +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/driver/OpcDeviceDriver.java @@ -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"); } /** diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/ItemProtocol.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/ItemProtocol.java new file mode 100644 index 0000000..00d2e8c --- /dev/null +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/ItemProtocol.java @@ -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 getReadableItemDtos() { + ArrayList list = new ArrayList(); + list.add(new ItemDto(item_raster, "光栅信号", "31009")); + return list; + } + + public static List getWriteableItemDtos() { + ArrayList list = new ArrayList<>(); + return list; + } + + @Override + public String toString() { + return ""; + } +} diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/RasterDefination.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/RasterDefination.java new file mode 100644 index 0000000..f515f7d --- /dev/null +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/RasterDefination.java @@ -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 getDeviceDriverType() { + return RasterDeviceDriver.class; + } + + @Override + public List getFitDeviceTypes() { + List types = new LinkedList(); + types.add(DeviceType.conveyor); + return types; + } + + @Override + public List getReadableItemDtos() { + return ItemProtocol.getReadableItemDtos(); + } + + @Override + public List getWriteableItemDtos() { + return ItemProtocol.getWriteableItemDtos(); + } +} diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/RasterDeviceDriver.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/RasterDeviceDriver.java new file mode 100644 index 0000000..1433975 --- /dev/null +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/device_driver/raster/RasterDeviceDriver.java @@ -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) { + + } + +} diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceModbusTcpProtocolRunnable.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceModbusTcpProtocolRunnable.java new file mode 100644 index 0000000..7f97adb --- /dev/null +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceModbusTcpProtocolRunnable.java @@ -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 protocols; + private OpcServerManageDto opcServer; + private ModbusTcpMaster modbusMaster; + private Map itemSearchCache; + + /** + * 失败地址跟踪:记录每个地址的连续失败次数和最后失败时间 + * key: itemCode, value: FailureInfo + */ + private Map 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 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 itemsString = new ArrayList<>(); + List 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 dataMap = batchReadModbus(itemConfigs); + for (Map.Entry 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 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 batchReadModbus(List itemConfigs) { + Map dataMap = new HashMap<>(); + // 按寄存器类型分组 + Map> groupedByType = new HashMap<>(); + for (ModbusItemConfig config : itemConfigs) { + groupedByType.computeIfAbsent(config.registerType, k -> new ArrayList<>()).add(config); + } + // 对每种类型的寄存器进行批量读取 + for (Map.Entry> entry : groupedByType.entrySet()) { + int registerType = entry.getKey(); + List configs = entry.getValue(); + // 读取这一组寄存器 + Map typeData = readRegisterGroup(registerType, configs); + dataMap.putAll(typeData); + } + return dataMap; + } + + /** + * 地址范围类:用于批量读取 + */ + private static class AddressRange { + int startAddress; + int quantity; + List configs; + + AddressRange(int startAddress, int quantity) { + this.startAddress = startAddress; + this.quantity = quantity; + this.configs = new ArrayList<>(); + } + } + + /** + * 读取一组相同类型的寄存器 + */ + private Map readRegisterGroup(int registerType, List configs) { + Map 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 sortedConfigs = configs.stream() + .sorted(Comparator.comparingInt(c -> c.address)) + .collect(Collectors.toList()); + + List 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 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 mergeAddressRanges(List sortedConfigs) { + List 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 readBatchRegisters(int registerType, AddressRange range) + throws ExecutionException, InterruptedException, TimeoutException { + Map 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 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 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 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); + } + } + } + +} diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceModbusTcpSynchronizeAutoRun.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceModbusTcpSynchronizeAutoRun.java new file mode 100644 index 0000000..78666e8 --- /dev/null +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceModbusTcpSynchronizeAutoRun.java @@ -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 servers = this.opcServerManageService.queryAllServerMap(); + Map>> pros; + do { + Thread.sleep(1000L); + pros = this.deviceAppService.findAllFormatProtocolFromDriver(); + } while (ObjectUtil.isEmpty(pros)); + Set keys = pros.keySet(); + Iterator var4 = keys.iterator(); + //代码执行一次 + while (var4.hasNext()) { + String key = (String) var4.next(); + List> list = pros.get(key); + OpcServerManageDto opcServer = servers.get(key); + Iterator var8 = list.iterator(); + while (var8.hasNext()) { + List 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(); + } +} diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceOpcProtocolRunable.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceOpcProtocolRunable.java index ace2d51..eab600b 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceOpcProtocolRunable.java +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/DeviceOpcProtocolRunable.java @@ -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)); +// } } } } diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/OpcConfig.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/OpcConfig.java index 44eccaa..4d888e7 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/OpcConfig.java +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/OpcConfig.java @@ -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默认从站ID(Unit 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); } diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/OpcItemDto.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/OpcItemDto.java index e392778..ca81142 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/OpcItemDto.java +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/opc/OpcItemDto.java @@ -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; diff --git a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/task/service/impl/TaskServiceImpl.java b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/task/service/impl/TaskServiceImpl.java index 2123874..a24c89b 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/task/service/impl/TaskServiceImpl.java +++ b/acs2/nladmin-system/nlsso-server/src/main/java/org/nl/acs/task/service/impl/TaskServiceImpl.java @@ -947,6 +947,7 @@ public class TaskServiceImpl extends CommonServiceImpl 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); diff --git a/acs2/nladmin-system/nlsso-server/src/main/resources/config/application-dev.yml b/acs2/nladmin-system/nlsso-server/src/main/resources/config/application-dev.yml index 3dd82c1..95633b9 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/resources/config/application-dev.yml +++ b/acs2/nladmin-system/nlsso-server/src/main/resources/config/application-dev.yml @@ -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} diff --git a/acs2/nladmin-system/nlsso-server/src/main/resources/config/application.yml b/acs2/nladmin-system/nlsso-server/src/main/resources/config/application.yml index 8312337..c437422 100644 --- a/acs2/nladmin-system/nlsso-server/src/main/resources/config/application.yml +++ b/acs2/nladmin-system/nlsso-server/src/main/resources/config/application.yml @@ -6,7 +6,7 @@ spring: freemarker: check-template-location: false profiles: - active: prod + active: dev jackson: time-zone: GMT+8 data: diff --git a/acs2/nladmin-ui/.env.development b/acs2/nladmin-ui/.env.development index a93d815..e37a9a4 100644 --- a/acs2/nladmin-ui/.env.development +++ b/acs2/nladmin-ui/.env.development @@ -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 diff --git a/acs2/nladmin-ui/.env.production b/acs2/nladmin-ui/.env.production index 79f6ed0..4ce9728 100644 --- a/acs2/nladmin-ui/.env.production +++ b/acs2/nladmin-ui/.env.production @@ -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' diff --git a/acs2/nladmin-ui/public/config.js b/acs2/nladmin-ui/public/config.js index 2b49343..cb6284b 100644 --- a/acs2/nladmin-ui/public/config.js +++ b/acs2/nladmin-ui/public/config.js @@ -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' } } diff --git a/acs2/nladmin-ui/src/views/acs/device/config.vue b/acs2/nladmin-ui/src/views/acs/device/config.vue index 343ba7f..98aea5e 100644 --- a/acs2/nladmin-ui/src/views/acs/device/config.vue +++ b/acs2/nladmin-ui/src/views/acs/device/config.vue @@ -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], diff --git a/acs2/nladmin-ui/src/views/acs/device/driver/raster.vue b/acs2/nladmin-ui/src/views/acs/device/driver/raster.vue new file mode 100644 index 0000000..a8ee359 --- /dev/null +++ b/acs2/nladmin-ui/src/views/acs/device/driver/raster.vue @@ -0,0 +1,544 @@ + + + + +