Explorar o código

1.添加OPC UA和OPC DA
2.出铁操作步骤功能添加

wangxiaofei hai 7 meses
pai
achega
a1430b34d0
Modificáronse 56 ficheiros con 3743 adicións e 464 borrados
  1. 0 0
      doc/bk_images.sh
  2. 12 0
      doc/export_all_images.sh
  3. 2 0
      doc/import_all_images.sh
  4. 4 0
      doc/kepserver模拟器数据
  5. BIN=BIN
      doc/采集数据集.jpg
  6. 9 3
      docker/Dockerfile
  7. 3 3
      docker/docker-compose.yml
  8. 82 0
      pom.xml
  9. 22 0
      taphole-admin/pom.xml
  10. 1 1
      taphole-admin/src/main/resources/loadFFmpeg.properties
  11. 59 0
      taphole-admin/src/test/java/com/sckj/taphole/opc/test/OPCTest.java
  12. 0 390
      taphole-camera/src/main/java/com.sckj.camera/hik/HCNetTools2.java
  13. 1 54
      taphole-camera/src/main/java/com.sckj.camera/service/CameraServiceImpl.java
  14. 51 0
      taphole-common/pom.xml
  15. 4 4
      taphole-common/src/main/java/com/sckj/common/config/ThreadPoolConfig.java
  16. 32 0
      taphole-common/src/main/java/com/sckj/common/eventbus/EventBusConfig.java
  17. 32 0
      taphole-common/src/main/java/com/sckj/common/eventbus/EventListener.java
  18. 20 7
      taphole-common/src/main/java/com/sckj/common/socketio/SocketHandler.java
  19. 4 1
      taphole-common/src/main/java/com/sckj/common/socketio/SocketUtil.java
  20. 24 1
      taphole-common/src/main/java/com/sckj/common/socketio/socktjs.html
  21. 4 0
      taphole-iron/pom.xml
  22. 64 0
      taphole-iron/src/main/java/com/sckj/iron/controller/IronVisualScreenController.java
  23. 41 0
      taphole-iron/src/main/java/com/sckj/iron/controller/TIronStepController.java
  24. 29 0
      taphole-iron/src/main/java/com/sckj/iron/dto/IronStepDTO.java
  25. 65 0
      taphole-iron/src/main/java/com/sckj/iron/entity/TIronStep.java
  26. 13 0
      taphole-iron/src/main/java/com/sckj/iron/mapper/TIronStepMapper.java
  27. 148 0
      taphole-iron/src/main/java/com/sckj/iron/service/impl/IronLoginServiceImpl.java
  28. 69 0
      taphole-iron/src/main/java/com/sckj/iron/service/impl/TIronStepServiceImpl.java
  29. 739 0
      taphole-iron/src/main/java/com/sckj/iron/socketio/DeviceEventListener.java
  30. 129 0
      taphole-iron/src/main/java/com/sckj/iron/socketio/PushData.java
  31. 30 0
      taphole-iron/src/main/java/com/sckj/iron/validate/IronLoginValidate.java
  32. 21 0
      taphole-iron/src/main/java/com/sckj/iron/vo/IronLoginVo.java
  33. 67 0
      taphole-iron/src/main/java/com/sckj/iron/vo/IronOperataion.java
  34. 45 0
      taphole-iron/src/main/java/com/sckj/iron/vo/IronStepVO.java
  35. 26 0
      taphole-opc/pom.xml
  36. 144 0
      taphole-opc/src/main/java/com/sckj/opc/controller/OPCDAController.java
  37. 71 0
      taphole-opc/src/main/java/com/sckj/opc/controller/OPCDataController.java
  38. 74 0
      taphole-opc/src/main/java/com/sckj/opc/controller/OPCPointController.java
  39. 71 0
      taphole-opc/src/main/java/com/sckj/opc/controller/OPCServerController.java
  40. 140 0
      taphole-opc/src/main/java/com/sckj/opc/controller/OPCUAController.java
  41. 46 0
      taphole-opc/src/main/java/com/sckj/opc/dto/OPCPointDTO.java
  42. 45 0
      taphole-opc/src/main/java/com/sckj/opc/entity/OPCData.java
  43. 55 0
      taphole-opc/src/main/java/com/sckj/opc/entity/OPCPoint.java
  44. 58 0
      taphole-opc/src/main/java/com/sckj/opc/entity/OPCServer.java
  45. 56 0
      taphole-opc/src/main/java/com/sckj/opc/listener/MySubscriptionListener.java
  46. 12 0
      taphole-opc/src/main/java/com/sckj/opc/mapper/OPCDataMapper.java
  47. 28 0
      taphole-opc/src/main/java/com/sckj/opc/mapper/OPCPointMapper.java
  48. 12 0
      taphole-opc/src/main/java/com/sckj/opc/mapper/OPCServerMapper.java
  49. 227 0
      taphole-opc/src/main/java/com/sckj/opc/opcua/OPCDAServiceImpl.java
  50. 537 0
      taphole-opc/src/main/java/com/sckj/opc/opcua/OPCUAServiceImpl.java
  51. 17 0
      taphole-opc/src/main/java/com/sckj/opc/service/OPCDataServiceImpl.java
  52. 27 0
      taphole-opc/src/main/java/com/sckj/opc/service/OPCPointServiceImpl.java
  53. 15 0
      taphole-opc/src/main/java/com/sckj/opc/service/OPCServerServiceImpl.java
  54. 80 0
      taphole-opc/src/main/java/com/sckj/opc/utils/CustomUtil.java
  55. 152 0
      taphole-opc/src/main/java/com/sckj/opc/utils/KeyStoreLoader.java
  56. 24 0
      taphole-opc/src/main/resources/mapper/OPCPointMapper.xml

+ 0 - 0
docker/bk_images.sh → doc/bk_images.sh


+ 12 - 0
doc/export_all_images.sh

@@ -0,0 +1,12 @@
+#!/bin/bash
+
+export_dir="/root/dockerimages"
+
+mkdir -p "$export_dir"
+
+docker images --format "{{.Repository}}:{{.Tag}}" | while read image; do
+    repo=$(echo "$image" | cut -d: -f1| tr '/' '_')
+    tag=$(echo "$image" | cut -d: -f2| tr '/' '_')
+
+    docker save -o "$export_dir/$repo-$tag.tar" "$image"
+done

+ 2 - 0
doc/import_all_images.sh

@@ -0,0 +1,2 @@
+#!/bin/bash
+ls /root/dockerimages/*.tar | xargs -I {} docker load -i {}

+ 4 - 0
doc/kepserver模拟器数据

@@ -0,0 +1,4 @@
+USER (1000,1,1,0,1,0,1,0,0,1,1,1,0,0,0)
+USER (1000,1,1000)
+
+

BIN=BIN
doc/采集数据集.jpg


+ 9 - 3
docker/Dockerfile

@@ -9,15 +9,21 @@ COPY target/taphole-admin-1.0.0.jar /app/springboot-app.jar
 
 
 # [复制 FFmpeg 静态二进制文件]  ffmpeg和ffprobe请务必要和loadFFmpeg.propertis中的path保持一致
-COPY ffmpeg/ffmpeg  /usr/local/ffmpeg/bin/ffmpeg
-COPY ffmpeg/ffprobe /usr/local/ffmpeg/bin/ffprobe
+#COPY ffmpeg/ffmpeg  /usr/local/ffmpeg/bin/ffmpeg
+#COPY ffmpeg/ffprobe /usr/local/ffmpeg/bin/ffprobe
 
 # 确保 FFmpeg 可执行
-RUN chmod +x /usr/local/ffmpeg/bin/ffmpeg /usr/local/ffmpeg/bin/ffprobe
+#RUN chmod +x /usr/local/ffmpeg/bin/ffmpeg /usr/local/ffmpeg/bin/ffprobe
 
 
 # 暴露应用端口
 EXPOSE 8080 33000
 
+# 设置环境变量
+#ENV SPRING_DATASOURCE_URL=jdbc:mysql://<MYSQL_HOST>:<MYSQL_PORT>/<DATABASE_NAME>
+#ENV SPRING_DATASOURCE_USERNAME=<MYSQL_USER>
+#ENV SPRING_DATASOURCE_PASSWORD=<MYSQL_PASSWORD>
+#ENV SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.alibaba.druid.pool.DruidDataSource
+
 # 启动 Spring Boot 应用
 ENTRYPOINT ["java", "-jar", "springboot-app.jar"]

+ 3 - 3
docker/docker-compose.yml

@@ -14,7 +14,7 @@ services:
     container_name: taphole-mysql
     image: mysql:5.7.29 #X86架构
     #image: amd64/mysql:5.7.29 #arm架构
-    # restart: always
+    restart: always
     environment:
       MYSQL_ROOT_PASSWORD: root
     #映射宿主机当前目录到容器
@@ -26,7 +26,7 @@ services:
     networks:
       - taphole
     ports:
-      - "53306:3306"
+      - "13306:3306"
     privileged: true
     #容器启动后执行的命令
     command: [
@@ -43,7 +43,7 @@ services:
     container_name: taphole-redis
     image: redis:7.0.4
     privileged: true
-    # restart: always
+    restart: always
     volumes:
       - ./redis/data:/data
     environment:

+ 82 - 0
pom.xml

@@ -18,6 +18,7 @@
         <module>taphole-device</module>
         <module>taphole-warn</module>
         <module>taphole-iron</module>
+        <module>taphole-opc</module>
     </modules>
 
     <!-- 特性信息 -->
@@ -61,6 +62,9 @@
         <knife4j>3.0.3</knife4j>
         <socketio>2.0.3</socketio>
         <druid.version>1.2.23</druid.version>
+
+        <milo.version>0.6.13</milo.version>
+
     </properties>
 
     <!-- 依赖声明 -->
@@ -278,6 +282,74 @@
             </dependency>
 
 
+            <!-- opcua 通信协议包 -->
+            <dependency>
+                <groupId>org.eclipse.milo</groupId>
+                <artifactId>sdk-client</artifactId>
+                <version>${milo.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.eclipse.milo</groupId>
+                <artifactId>sdk-server</artifactId>
+                <version>${milo.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.eclipse.milo</groupId>
+                <artifactId>stack-core</artifactId>
+                <version>${milo.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.bouncycastle</groupId>
+                <artifactId>bcpkix-jdk15on</artifactId>
+                <version>1.61</version>
+            </dependency>
+
+
+            <!--utgard  starting-->
+            <dependency>
+                <groupId>org.openscada.external</groupId>
+                <artifactId>org.openscada.external.jcifs</artifactId>
+                <version>1.2.25</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.bouncycastle</groupId>
+                        <artifactId>bcprov-jdk15on</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>org.openscada.jinterop</groupId>
+                <artifactId>org.openscada.jinterop.core</artifactId>
+                <version>2.1.8</version>
+            </dependency>
+            <dependency>
+                <groupId>org.openscada.jinterop</groupId>
+                <artifactId>org.openscada.jinterop.deps</artifactId>
+                <version>1.5.0</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.bouncycastle</groupId>
+                        <artifactId>bcprov-jdk15on</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>org.openscada.utgard</groupId>
+                <artifactId>org.openscada.opc.dcom</artifactId>
+                <version>1.5.0</version>
+            </dependency>
+            <dependency>
+                <groupId>org.openscada.utgard</groupId>
+                <artifactId>org.openscada.opc.lib</artifactId>
+                <version>1.5.0</version>
+            </dependency>
+            <!--   utgard   end   -->
+
+
+
             <!-- 全局工具 -->
             <dependency>
                 <groupId>com.sckj</groupId>
@@ -301,6 +373,16 @@
                 <artifactId>taphole-warn</artifactId>
                 <version>${taphole.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.sckj</groupId>
+                <artifactId>taphole-iron</artifactId>
+                <version>${taphole.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.sckj</groupId>
+                <artifactId>taphole-opc</artifactId>
+                <version>${taphole.version}</version>
+            </dependency>
 
         </dependencies>
     </dependencyManagement>

+ 22 - 0
taphole-admin/pom.xml

@@ -64,6 +64,28 @@
             <artifactId>taphole-warn</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.sckj</groupId>
+            <artifactId>taphole-iron</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.sckj</groupId>
+            <artifactId>taphole-opc</artifactId>
+        </dependency>
+
+        <!--<dependency>-->
+            <!--<groupId>org.springframework.boot</groupId>-->
+            <!--<artifactId>spring-boot-starter-test</artifactId>-->
+            <!--<scope>test</scope>-->
+            <!--<exclusions>-->
+                <!--<exclusion>-->
+                    <!--<groupId>org.junit.vintage</groupId>-->
+                    <!--<artifactId>junit-vintage-engine</artifactId>-->
+                <!--</exclusion>-->
+            <!--</exclusions>-->
+        <!--</dependency>-->
+
     </dependencies>
 
     <!-- 插件管理 -->

+ 1 - 1
taphole-admin/src/main/resources/loadFFmpeg.properties

@@ -1,6 +1,6 @@
 #ffmpeg执行路径,一般为ffmpeg的安装目录,该路径只能是目录,不能为具体文件路径,否则会报错
 # path 必须和Dockerfile中的[复制 FFmpeg 静态二进制文件]保持一致
-#path=/usr/local/ffmpeg/bin/ 该路径为docker部署路径
+#path=/usr/local/ffmpeg/bin/
 #path=/usr/bin/
 path=D:\\project\\xiha\\ffmpeg-2024-10-07-git-496b8d7a13-full_build\\bin\\
 #存放任务的默认Map的初始化大小

+ 59 - 0
taphole-admin/src/test/java/com/sckj/taphole/opc/test/OPCTest.java

@@ -0,0 +1,59 @@
+//package com.sckj.taphole.opc.test;
+//
+//import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+//import com.sckj.admin.TapholeAdminApplication;
+//import com.sckj.opc.entity.OPCPoint;
+//import com.sckj.opc.entity.OPCServer;
+//import com.sckj.opc.opcua.OPCUAServiceImpl;
+//import com.sckj.opc.service.OPCDataServiceImpl;
+//import com.sckj.opc.service.OPCPointServiceImpl;
+//import com.sckj.opc.service.OPCServerServiceImpl;
+//import org.apache.commons.lang3.ObjectUtils;
+//import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
+//import org.junit.jupiter.api.Test;
+//import org.springframework.boot.test.context.SpringBootTest;
+//
+//import javax.annotation.Resource;
+//import java.util.List;
+//
+///**
+// * @Author feng
+// * @Date 2024-11-19 下午 03:43
+// * @Description TODO
+// */
+//@SpringBootTest(classes = TapholeAdminApplication.class)
+//public class OPCTest {
+//    @Resource
+//    OPCServerServiceImpl opcServerService;
+//
+//    @Resource
+//    OPCPointServiceImpl opcPointService;
+//
+//    @Resource
+//    OPCDataServiceImpl opcDataService;
+//
+//    @Resource
+//    OPCUAServiceImpl opcuaService;
+//
+//    @Test
+//    public void testOPC() throws Exception {
+//
+////        List<OPCServer> opcServerList = opcServerService.list();
+////
+////        if(ObjectUtils.isEmpty(opcServerList)){
+////            return;
+////        }
+////
+////        for (OPCServer opcServer : opcServerList) {
+////            OpcUaClient opcUaClient = opcuaService.createServer(opcServer);
+////            if(ObjectUtils.isNotEmpty(opcUaClient)){
+////                QueryWrapper<OPCPoint> queryWrapper = new QueryWrapper<>();
+////                queryWrapper.lambda().eq(OPCPoint::getOpcServerId,opcServer.getId());
+////                List<OPCPoint> opcPointList = opcPointService.list(queryWrapper);
+////                opcuaService.createSubscription(opcUaClient,opcPointList);
+////            }
+////        }
+//
+//
+//    }
+//}

+ 0 - 390
taphole-camera/src/main/java/com.sckj.camera/hik/HCNetTools2.java

@@ -1,390 +0,0 @@
-//package com.sckj.camera.hik;
-//
-//import com.sun.jna.Pointer;
-//import com.sun.jna.ptr.IntByReference;
-//import org.slf4j.Logger;
-//import org.slf4j.LoggerFactory;
-//
-//import java.time.LocalDateTime;
-//import java.time.format.DateTimeFormatter;
-//import java.util.ArrayList;
-//import java.util.HashMap;
-//import java.util.List;
-//
-//public class HCNetTools2 {
-//	private static final Logger logger = LoggerFactory.getLogger(HCNetTools2.class.getName());
-//
-//	static HCNetSDK hCNetSDK = HCNetSDK.INSTANCE;
-//
-//	//海康播放库(SDK里的PlayCtrl不是此处的PlayCtrl)
-//	//static PlayCtrl playControl = PlayCtrl.INSTANCE;
-//
-//	static HCNetSDK.NET_DVR_WORKSTATE_V30 m_strWorkState;
-//	static HCNetSDK.NET_DVR_DEVICEINFO_V30 m_strDeviceInfo;//设备信息
-//	static HCNetSDK.NET_DVR_IPPARACFG m_strIpparaCfg;//IP参数
-//	static HCNetSDK.NET_DVR_CLIENTINFO m_strClientInfo;//用户参数
-//
-//	static boolean bRealPlay;//是否在预览.
-//	static String m_sDeviceIP;//已登录设备的IP地址
-//
-//	static int lUserID;//用户句柄
-//	static int loadHandle;//下载句柄
-//	static int lPreviewHandle;//预览句柄
-//	static IntByReference m_lPort;//回调预览时播放库端口指针
-//	static IntByReference m_err;//错误号
-//
-//	public HCNetTools2() {
-//
-//	}
-//
-//	static {
-//		lUserID = -1;
-//		lPreviewHandle = -1;
-//		m_lPort = new IntByReference(-1);
-//		m_err = new IntByReference(-1);
-//
-//		boolean b = hCNetSDK.NET_DVR_Init();
-//		if (!b) {
-//			//初始化失败
-//			//throw new RuntimeException("初始化失败");
-//			logger.info("初始化失败");
-//		}
-//		FExceptionCallBack_Imp fExceptionCallBack = new FExceptionCallBack_Imp();
-//		Pointer pUser = null;
-//		if (!hCNetSDK.NET_DVR_SetExceptionCallBack_V30(0, 0, fExceptionCallBack, pUser)) {
-//			logger.info("设置异常消息回调失败");
-//		} else {
-//			logger.info("设置异常消息回调成功");
-//			//启动SDK写日志
-//			hCNetSDK.NET_DVR_SetLogToFile(3, "./logs", false);
-//		}
-//	}
-//
-//	/**
-//	 * 初始化资源配置
-//	 */
-////	public int initDevices() {
-////
-////		return 0;
-////	}
-//
-//	/**
-//	 * 设备注册
-//	 *
-//	 * @param name     设备用户名
-//	 * @param password 设备登录密码
-//	 * @param ip       IP地址
-//	 * @param port     端口
-//	 * @return 结果
-//	 */
-//	public static int deviceLogin(String ip, String port,String name, String password) {
-//
-//		if (bRealPlay) {//判断当前是否在预览
-//			return 2;//"注册新用户请先停止当前预览";
-//		}
-//		if (lUserID > -1) {//先注销,在登录
-//			hCNetSDK.NET_DVR_Logout_V30(lUserID);
-//			lUserID = -1;
-//		}
-//		//注册(既登录设备)开始
-//		m_sDeviceIP = ip;
-//		short iPort = Integer.valueOf(port).shortValue();
-//		m_strDeviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V30();//获取设备参数结构
-//		lUserID = hCNetSDK.NET_DVR_Login_V30(ip, iPort, name, password, m_strDeviceInfo);//登录设备
-//		long userID = lUserID;
-//		if (userID == -1) {
-//			int iErr = hCNetSDK.NET_DVR_GetLastError();
-//			logger.info(":注册失败,错误号:" + iErr);
-//			logger.info(hCNetSDK.NET_DVR_GetErrorMsg(m_err));
-//			m_sDeviceIP = "";//登录未成功,IP置为空
-//			return 3;//"注册失败";
-//		}
-//		return 0;
-//	}
-//
-//
-//
-//	/**
-//	 * 注销登陆
-//	 */
-//	public static void deviceLogout() {
-//
-//		//如果在预览,先停止预览, 释放句柄
-//		if (lPreviewHandle > -1)
-//		{
-//			hCNetSDK.NET_DVR_StopRealPlay(lPreviewHandle);
-//		}
-//
-//		//如果已经注册,注销
-//		if (lUserID> -1) {
-//			hCNetSDK.NET_DVR_Logout_V30(lUserID);
-//		}
-//		hCNetSDK.NET_DVR_Cleanup();
-//	}
-//
-//	/**
-//	 * 获取设备通道
-//	 */
-//	public static List<String> getChannelNumber() {
-//		List<String> channelList = new ArrayList<>();
-//
-//		IntByReference ibrBytesReturned = new IntByReference(0);//获取IP接入配置参数
-//		boolean bRet = false;
-//		m_strIpparaCfg = new HCNetSDK.NET_DVR_IPPARACFG();
-//		m_strIpparaCfg.write();
-//		Pointer lpIpParaConfig = m_strIpparaCfg.getPointer();
-//		bRet = (m_strDeviceInfo.byIPChanNum != 0);
-//
-//		String devices = "";
-//		if (!bRet) {
-//			//设备不支持,则表示没有IP通道
-//			for (int iChannum = 0; iChannum < m_strDeviceInfo.byChanNum; iChannum++) {
-//				devices = "Camera" + (iChannum + m_strDeviceInfo.byStartChan);
-//				channelList.add(devices);
-//			}
-//		} else {
-//			//设备支持IP通道
-//			boolean ok = hCNetSDK.NET_DVR_GetDVRConfig(lUserID, HCNetSDK.NET_DVR_GET_IPPARACFG, 0, lpIpParaConfig, m_strIpparaCfg.size(), ibrBytesReturned);
-//			if (!ok) {
-//				String err = hCNetSDK.NET_DVR_GetErrorMsg(m_err);
-//				logger.info("获取配置失败:" + m_err.getValue() + "," + err);
-//			}
-//			m_strIpparaCfg.read();
-//
-//			for (int iChannum = 0; iChannum < m_strDeviceInfo.byChanNum; iChannum++) {
-//				if(m_strIpparaCfg.byAnalogChanEnable[iChannum] == 1)
-//				{
-//					devices = "Camera" + (iChannum + m_strDeviceInfo.byStartChan);
-//					channelList.add(devices);
-//				}
-//			}
-//
-//			for (int iChannum = 0; iChannum < HCNetSDK.MAX_IP_CHANNEL; iChannum++) {
-//				if (m_strIpparaCfg.struIPChanInfo[iChannum].byEnable == 1) {
-//					devices = "IP" + (hCNetSDK.NET_DVR_SDKChannelToISAPI(lUserID,iChannum + m_strDeviceInfo.byStartDChan,true));
-//					String channelIP = (new String(m_strIpparaCfg.struIPDevInfo[iChannum].struIP.sIpV4));
-//					channelIP = channelIP.trim();
-//					devices = devices + "," + channelIP;
-//					channelList.add(devices);
-//				}
-//			}
-//		}
-//
-//		return channelList;
-//	}
-//
-//	/**
-//	 * 抓拍图片
-//	 *
-//	 * @param channel 通道号
-//	 */
-//	public boolean getDVRPic(int channel, String path) {
-//		HCNetSDK.NET_DVR_WORKSTATE_V30 devwork = new HCNetSDK.NET_DVR_WORKSTATE_V30();
-//		if (!hCNetSDK.NET_DVR_GetDVRWorkState_V30(lUserID, devwork)) {
-//			// 返回Boolean值,判断是否获取设备能力
-//			logger.info("hksdk(抓图)-返回设备状态失败");
-//		}
-//		//图片质量
-//		HCNetSDK.NET_DVR_JPEGPARA jpeg = new HCNetSDK.NET_DVR_JPEGPARA();
-//		//设置图片分辨率
-//		jpeg.wPicSize = 4;
-//		//设置图片质量
-//		jpeg.wPicQuality = 0;
-//		IntByReference a = new IntByReference();
-//		//需要加入通道
-//		//byte[] bytes = path.getBytes(StandardCharsets.UTF_8);
-//		boolean ok = hCNetSDK.NET_DVR_CaptureJPEGPicture(lUserID,channel,jpeg,path);
-//		if (ok) {
-//			logger.info("hksdk(抓图)-结果状态值(0表示成功):" + hCNetSDK.NET_DVR_GetLastError());
-//		} else {
-//			String err = hCNetSDK.NET_DVR_GetErrorMsg(m_err);
-//			logger.info("hksdk(抓图)-抓取失败,错误码:" + m_err.getValue() + "," + err);
-//		}
-//		return ok;
-//	}
-//
-//	/**
-//	 * 下载录像
-//	 * @return boolean
-//	 */
-//	public boolean downloadBack(LocalDateTime startTime, LocalDateTime endTime, String filePath, int channel) throws InterruptedException {
-//		loadHandle = -1;
-//		loadHandle = hCNetSDK.NET_DVR_GetFileByTime(lUserID, channel, getHkTime(startTime), getHkTime(endTime), filePath);
-//		if (loadHandle >= 0) {
-//			//开始下载
-//			boolean downloadFlag = hCNetSDK.NET_DVR_PlayBackControl(loadHandle, HCNetSDK.NET_DVR_PLAYSTART, 0, null);
-//			int tmp = -1;
-//			IntByReference pos = new IntByReference(0);
-//			while (loadHandle >= 0) {
-//				boolean backFlag = hCNetSDK.NET_DVR_PlayBackControl(loadHandle, HCNetSDK.NET_DVR_PLAYGETPOS, 0, pos);
-//				if (!backFlag) {//防止单个线程死循环
-//					return downloadFlag;
-//				}
-//				int produce = pos.getValue();
-//				if ((produce % 10) == 0 && tmp != produce) {//输出进度
-//					tmp = produce;
-//					logger.info("视频下载进度:" + "==" + produce + "%");
-//				}
-//				if (produce == 100) {//下载成功
-//					hCNetSDK.NET_DVR_StopGetFile(loadHandle);
-//					loadHandle = -1;
-//					String err = hCNetSDK.NET_DVR_GetErrorMsg(m_err);
-//					logger.info("下载结束,last error: " + m_err.getValue() + "," + err);
-//					return true;
-//				}
-//				if (produce > 100) {//下载失败
-//					hCNetSDK.NET_DVR_StopGetFile(loadHandle);
-//					loadHandle = -1;
-//					String err = hCNetSDK.NET_DVR_GetErrorMsg(m_err);
-//					logger.warn("下载异常终止!错误原因:" + m_err.getValue() + "," + err);
-//					return false;
-//				}
-//				Thread.sleep(500);
-//			}
-//			String err = hCNetSDK.NET_DVR_GetErrorMsg(m_err);
-//			logger.warn("下载异常终止!错误原因:" + m_err.getValue() + "," + err);
-//			return false;
-//		} else {
-//			String err = hCNetSDK.NET_DVR_GetErrorMsg(m_err);
-//			logger.warn("获取录像文件错误!错误原因:" + m_err.getValue() + "," + err);
-//			return false;
-//		}
-//	}
-//
-//	/**
-//	 * 获取录像文件信息
-//	 * @Return:
-//	 */
-//	public List<HashMap<String, String>> getVideoFileList(LocalDateTime startTime, LocalDateTime endTime, int channel) {
-//		List<HashMap<String, String>> fileList = new ArrayList<>();
-//
-//		// 搜索条件
-//		HCNetSDK.NET_DVR_FILECOND m_strFilecond = new HCNetSDK.NET_DVR_FILECOND();
-//
-//		m_strFilecond.struStartTime = getHkTime(startTime);
-//		m_strFilecond.struStopTime = getHkTime(endTime);
-//		m_strFilecond.lChannel = channel;//通道号
-//
-//		int lFindFile = hCNetSDK.NET_DVR_FindFile_V30(lUserID, m_strFilecond);
-//		HCNetSDK.NET_DVR_FINDDATA_V30 strFile;
-//		if (lFindFile < 0) {
-//			String err = hCNetSDK.NET_DVR_GetErrorMsg(m_err);
-//			logger.warn("获取录像文件错误!错误原因:" + m_err.getValue() + "," + err);
-//			return null;
-//		}
-//		int lnext;
-//		strFile = new HCNetSDK.NET_DVR_FINDDATA_V30();
-//		HashMap<String, String> map = new HashMap<>();
-//		boolean flag=true;
-//		while (flag) {
-//			lnext = hCNetSDK.NET_DVR_FindNextFile_V30(lFindFile, strFile);
-//			if (lnext == HCNetSDK.NET_DVR_FILE_SUCCESS) {
-//				//搜索成功
-//				//添加文件名信息
-//				String[] s = new String[2];
-//				s = new String(strFile.sFileName).split("\0", 2);
-//				map.put("fileName:", new String(s[0]));
-//
-//				int iTemp;
-//				String MyString;
-//				if (strFile.dwFileSize < 1024 * 1024) {
-//					iTemp = (strFile.dwFileSize) / (1024);
-//					MyString = iTemp + "K";
-//				} else {
-//					iTemp = (strFile.dwFileSize) / (1024 * 1024);
-//					MyString = iTemp + "M   ";
-//					iTemp = ((strFile.dwFileSize) % (1024 * 1024)) / (1204);
-//					MyString = MyString + iTemp + "K";
-//				}
-//				map.put("fileSize", MyString);                      //添加文件大小信息
-//				map.put("struStartTime", strFile.struStartTime.toStringTime());                      //添加开始时间信息
-//				map.put("struStopTime", strFile.struStopTime.toStringTime());                      //添加结束时间信息
-//				fileList.add(map);
-//			} else {
-//				if (lnext == HCNetSDK.NET_DVR_ISFINDING) {//搜索中
-//					//System.out.println("搜索中");
-//					continue;
-//				} else {
-//					flag=false;
-//					if (lnext == HCNetSDK.NET_DVR_FILE_NOFIND) {
-//						//flag=false;
-//					} else {
-//						//flag=false;
-//						logger.info("搜索文件结束");
-//						boolean flag2 = hCNetSDK.NET_DVR_FindClose_V30(lFindFile);
-//						if (flag2 == false) {
-//							logger.info("结束搜索失败");
-//						}
-//					}
-//				}
-//			}
-//		}
-//		return fileList;
-//	}
-//
-//
-//	/**
-//	 * 获取海康录像机格式的时间
-//	 *
-//	 * @param time
-//	 * @return
-//	 */
-//	public HCNetSDK.NET_DVR_TIME getHkTime(LocalDateTime time) {
-//		HCNetSDK.NET_DVR_TIME structTime = new HCNetSDK.NET_DVR_TIME();
-//
-//		String str = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss").format(time);
-//		String[] times = str.split("-");
-//		structTime.dwYear = Integer.parseInt(times[0]);
-//		structTime.dwMonth = Integer.parseInt(times[1]);
-//		structTime.dwDay = Integer.parseInt(times[2]);
-//		structTime.dwHour = Integer.parseInt(times[3]);
-//		structTime.dwMinute = Integer.parseInt(times[4]);
-//		structTime.dwSecond = Integer.parseInt(times[5]);
-//		return structTime;
-//	}
-//
-//
-//	/***
-//     * 获取设备状态
-//	 * @return 设备的状态,0-正常,1-CPU占用率太高,超过85%,2-硬件错误,例如串口死掉
-//	 */
-//	public int getState(){
-//		HCNetSDK.NET_DVR_WORKSTATE_V30 devwork = new HCNetSDK.NET_DVR_WORKSTATE_V30();
-//		if (!hCNetSDK.NET_DVR_GetDVRWorkState_V30(lUserID, devwork)) {
-//			// 返回Boolean值,判断是否获取设备能力
-//			logger.info("hksdk(抓图)-返回设备状态失败");
-//			return -1;
-//		}
-//		return devwork.dwDeviceStatic;
-//	}
-//
-//	static class FExceptionCallBack_Imp implements HCNetSDK.FExceptionCallBack {
-//
-//		@Override
-//		public void invoke(int dwType, int lUserID, int lHandle, Pointer pUser) {
-//			logger.info(String.format("dwType:%s,lUserID:%s,lHandle:%s",dwType,lUserID,lHandle));
-//			logger.info("异常事件类型:" + dwType + ",十六进制:0x" + Integer.toHexString(dwType));
-//			switch(dwType)
-//			{
-//				case HCNetSDK.EXCEPTION_PREVIEW:			//网络预览时异常
-//					logger.info("网络预览时网络异常!!!");
-//					//TODO: 关闭网络预览
-//					break;
-//				case HCNetSDK.EXCEPTION_SERIAL:			//透明通道传输时异常
-//					logger.info("透明通道传输时网络异常!!!");
-//					//TODO: 关闭透明通道
-//					break;
-//				case HCNetSDK.EXCEPTION_RECONNECT:			//预览时重连
-//					logger.info("预览时重连!!!");
-//					break;
-//				case HCNetSDK.EXCEPTION_EXCHANGE:			//用户交互时异常
-//					logger.info("用户交互时异常!!!");
-//					break;
-//				default:
-//					break;
-//			}
-//			return;
-//		}
-//	}
-//
-//}

+ 1 - 54
taphole-camera/src/main/java/com.sckj.camera/service/CameraServiceImpl.java

@@ -174,59 +174,6 @@ public class CameraServiceImpl extends ServiceImpl<CameraMapper, Camera> {
             flowService.removeById(cameraFlow.getId());
         }
 
-//        // 初始化Socket.IO服务器
-//        Configuration config = new Configuration();
-//        config.setHostname("0.0.0.0");
-//        config.setPort(3000);
-//        server = new SocketIOServer(config);
-//        server.addListeners(new DataListener<byte[]>() {
-//            @Override
-//            public void onData(SocketIOClient client, byte[] data, AckRequest ackSender) throws Exception {
-//                // 处理接收到的数据
-//            }
-//
-//        });
-//
-//        server.addEventListener("videoFrame", byte[].class, (client, data, ackRequest) -> {
-//            // 发送视频帧数据
-//            client.sendEvent("videoFrame", data);
-//        });
-//
-//        server.start();
-//        System.out.println("Socket.IO server started on port 3000");
-//
-//        HCNetTools hcTool = cameraDTO.getHcTool();
-//        hcTool.getRealStreamData(new HCNetSDK.FRealDataCallBack_V30() {
-//            @Override
-//            public void invoke(int lRealHandle, int dwDataType, ByteByReference pBuffer, int dwBufSize, Pointer pUser) {
-//                try {
-//                    if (dwDataType == HCNetSDK.NET_DVR_STREAMDATA) {
-//                        if(dwBufSize > 0){
-//                            // 获取所有连接的客户端
-//                            if(ObjectUtils.isEmpty(server.getAllClients())){
-//                                return;
-//                            }
-//
-//                            System.out.println("client size:" + server.getAllClients().size());
-//                            for (SocketIOClient client : server.getAllClients()) {
-//                                System.out.println("client...");
-//                                client.sendEvent("videoFrame",pBuffer.getPointer().getByteArray(0,dwBufSize),0,dwBufSize);
-//                            }
-//                        }
-//                    }
-//                } catch (Exception e) {
-//                    e.printStackTrace();
-//                }
-//            }
-//        });
-//
-//        try {
-//            Thread.sleep(Long.MAX_VALUE);
-//        } catch (InterruptedException e) {
-//            e.printStackTrace();
-//        }
-
-
 
         //开始推流
         String taskName = ffmpegService.startTranscoding(appName, rtspName, rtmpUrl);
@@ -311,8 +258,8 @@ public class CameraServiceImpl extends ServiceImpl<CameraMapper, Camera> {
         } else {
             rtspName += cameraDTO.getVcr().getAccount() + ":" + cameraDTO.getVcr().getPassword() + "@" + cameraDTO.getVcr().getIp() + ":554/Streaming/tracks/" + channelNumberStr + "?starttime=" + startTime;
         }
-        //resultDTO = pushStream(cameraDTO, appName, rtspName);
         resultDTO.setRtspUrl(rtspName);
+        resultDTO.setWebrtcUrl("http://"+cameraProperties.getRtmp().getWebrtchost());
         return resultDTO;
     }
 

+ 51 - 0
taphole-common/pom.xml

@@ -250,6 +250,57 @@
             <artifactId>sa-token-dao-redis-jackson</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.eclipse.milo</groupId>
+            <artifactId>sdk-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.milo</groupId>
+            <artifactId>sdk-server</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.milo</groupId>
+            <artifactId>stack-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcpkix-jdk15on</artifactId>
+        </dependency>
+
+        <!--utgard  starting-->
+        <dependency>
+            <groupId>org.openscada.external</groupId>
+            <artifactId>org.openscada.external.jcifs</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.bouncycastle</groupId>
+                    <artifactId>bcprov-jdk15on</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.openscada.jinterop</groupId>
+            <artifactId>org.openscada.jinterop.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.openscada.jinterop</groupId>
+            <artifactId>org.openscada.jinterop.deps</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.bouncycastle</groupId>
+                    <artifactId>bcprov-jdk15on</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.openscada.utgard</groupId>
+            <artifactId>org.openscada.opc.dcom</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.openscada.utgard</groupId>
+            <artifactId>org.openscada.opc.lib</artifactId>
+        </dependency>
+        <!--   utgard   end   -->
 
     </dependencies>
 

+ 4 - 4
taphole-common/src/main/java/com/sckj/common/config/ThreadPoolConfig.java

@@ -15,14 +15,14 @@ import java.util.concurrent.Executor;
 public class ThreadPoolConfig {
 
     @Bean("taskExecutor")
-    public Executor asyncServiceExecutor() {
+    public ThreadPoolTaskExecutor asyncServiceExecutor() {
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
         // 设置核心线程数
-        executor.setCorePoolSize(5);
+        executor.setCorePoolSize(50);
         // 设置最大线程数
-        executor.setMaxPoolSize(20);
+        executor.setMaxPoolSize(200);
         // 配置队列大小
-        executor.setQueueCapacity(Integer.MAX_VALUE);
+        executor.setQueueCapacity(1000);
         // 设置线程活跃时间(秒)
         executor.setKeepAliveSeconds(60);
         // 设置默认线程名称

+ 32 - 0
taphole-common/src/main/java/com/sckj/common/eventbus/EventBusConfig.java

@@ -0,0 +1,32 @@
+package com.sckj.common.eventbus;
+
+import com.google.common.eventbus.AsyncEventBus;
+import com.google.common.eventbus.EventBus;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+import java.util.concurrent.Executor;
+
+
+/**
+ * @Author feng
+ * @Date 2024-11-19 上午 11:24
+ * @Description TODO
+ */
+@Configuration
+@Slf4j
+public class EventBusConfig {
+    @Bean
+    public EventBus eventBus() {
+        return new EventBus();
+    }
+
+    @Bean("myAsyncEventBus")
+    @Lazy(value = true)
+    public AsyncEventBus createAsyncEventBus(Executor taskExecutor) {
+        log.info("myAsyncEventBus=============");
+        return new AsyncEventBus(taskExecutor);
+    }
+}

+ 32 - 0
taphole-common/src/main/java/com/sckj/common/eventbus/EventListener.java

@@ -0,0 +1,32 @@
+package com.sckj.common.eventbus;
+
+import com.google.common.eventbus.AsyncEventBus;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @Author feng
+ * @Date 2024-11-19 上午 11:08
+ * @Description TODO
+ */
+public abstract class EventListener implements InitializingBean, DisposableBean {
+
+    @Autowired
+    protected AsyncEventBus asyncEventBus;
+
+    /**
+     * 注册异步任务
+     */
+    @Override
+    public void afterPropertiesSet() {
+        System.out.println("AsyncEventBus start listener");
+        asyncEventBus.register(this);
+    }
+
+    @Override
+    public void destroy(){
+        System.out.println("AsyncEventBus stop listener");
+        asyncEventBus.unregister(this);
+    }
+}

+ 20 - 7
taphole-common/src/main/java/com/sckj/common/socketio/SocketHandler.java

@@ -2,6 +2,7 @@ package com.sckj.common.socketio;
 
 import com.corundumstudio.socketio.SocketIOServer;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -19,6 +20,8 @@ public class SocketHandler {
     @Autowired
     private SocketIOServer socketIoServer;
 
+    private static final String USERID = "userId";
+
     /**
      *  容器销毁前,自动调用此方法,关闭 socketIo 服务端
      *
@@ -29,7 +32,9 @@ public class SocketHandler {
     private void destroy(){
         try {
             log.debug("关闭 socket 服务端");
-            socketIoServer.stop();
+            if(null != socketIoServer){
+                socketIoServer.stop();
+            }
         }catch (Exception e){
             e.printStackTrace();
         }
@@ -41,17 +46,25 @@ public class SocketHandler {
 
         //添加监听,客户端自动连接到 socket 服务端
         socketIoServer.addConnectListener(client -> {
-            String userId = client.getHandshakeData().getSingleUrlParam("userId");
-            SocketUtil.connectMap.put(userId, client);
-            log.debug("客户端userId: "+ userId+ "已连接,客户端ID为:" + client.getSessionId());
+            String userId = client.getHandshakeData().getSingleUrlParam(USERID);
+            log.info("客户端userId: {},已连接,客户端ID为:{}",userId,client.getSessionId());
+
+            if(ObjectUtils.isNotEmpty(userId)){
+                SocketUtil.connectMap.put(userId, client);
+                SocketUtil.clientUserIds.put(client,userId);
+            }
         });
 
         //添加监听,客户端跟 socket 服务端自动断开
         socketIoServer.addDisconnectListener(client -> {
-            String userId = client.getHandshakeData().getSingleUrlParam("userId");
-            SocketUtil.connectMap.remove(userId, client);
-            log.debug("客户端userId:" + userId + "断开连接,客户端ID为:" + client.getSessionId());
+            String userId = client.getHandshakeData().getSingleUrlParam(USERID);
+            log.info("客户端userId: {},断开连接,客户端ID为:{}",userId,client.getSessionId());
+            if(ObjectUtils.isNotEmpty(userId)){
+                SocketUtil.connectMap.remove(userId, client);
+                SocketUtil.clientUserIds.remove(client,userId);
+            }
         });
+
     }
 
 

+ 4 - 1
taphole-common/src/main/java/com/sckj/common/socketio/SocketUtil.java

@@ -21,7 +21,10 @@ import java.util.concurrent.ConcurrentMap;
 public class SocketUtil {
 
     //暂且把用户&客户端信息存在缓存
-    public static ConcurrentMap<String, SocketIOClient> connectMap = new ConcurrentHashMap<>();
+    public static final ConcurrentMap<String, SocketIOClient> connectMap = new ConcurrentHashMap<>();
+
+    public static final Map<SocketIOClient, String> clientUserIds = new ConcurrentHashMap<>();
+
 
     /**
      *  单发消息(以 userId 为标识符,给用户发送消息)

+ 24 - 1
taphole-common/src/main/java/com/sckj/common/socketio/socktjs.html

@@ -47,7 +47,30 @@
             let msg= JSON.stringify(data)
             output('收到 channel_user 频道消息了:' + msg );
             console.log(data);
- 
+        });
+
+        socket.on('channel_system', function (data) {
+            let msg= JSON.stringify(data)
+            output('收到 channel_system 频道消息了:' + msg );
+            console.log(data);
+        });
+
+        socket.on('IRON_OPERATE', function (data) {
+            let msg= JSON.stringify(data)
+            output('收到 IRON_OPERATE 频道消息了:' + msg );
+            console.log(data);
+        });
+
+        socket.on('IRON_TREND', function (data) {
+            let msg= JSON.stringify(data)
+            output('收到 IRON_TREND 频道消息了:' + msg );
+            console.log(data);
+        });
+
+        socket.on('IRON_REALTIME', function (data) {
+            let msg= JSON.stringify(data)
+            output('收到 IRON_REALTIME 频道消息了:' + msg );
+            console.log(data);
         });
  
     }

+ 4 - 0
taphole-iron/pom.xml

@@ -26,6 +26,10 @@
             <groupId>com.sckj</groupId>
             <artifactId>taphole-common</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.sckj</groupId>
+            <artifactId>taphole-opc</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 64 - 0
taphole-iron/src/main/java/com/sckj/iron/controller/IronVisualScreenController.java

@@ -0,0 +1,64 @@
+package com.sckj.iron.controller;
+
+import com.sckj.common.aop.NotLogin;
+import com.sckj.common.aop.NotPower;
+import com.sckj.common.core.AjaxResult;
+import com.sckj.iron.service.impl.IronLoginServiceImpl;
+import com.sckj.iron.validate.IronLoginValidate;
+import com.sckj.iron.vo.IronLoginVo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @Author feng
+ * @Date 2024-11-18 上午 09:44
+ * @Description 可视化大屏登录
+ */
+@RestController
+@RequestMapping("api/visual/screen")
+@Api(tags = "可视化大屏")
+public class IronVisualScreenController {
+
+    @Resource
+    IronLoginServiceImpl ironLoginService;
+
+
+
+    @NotLogin
+    @PostMapping("/login")
+    @ApiOperation(value="登录系统")
+    public AjaxResult<IronLoginVo> login(@Validated() @RequestBody IronLoginValidate loginsValidate) {
+        IronLoginVo vo = ironLoginService.login(loginsValidate);
+        return AjaxResult.success(vo);
+    }
+
+    @NotPower
+    @PostMapping("/logout")
+    @ApiOperation(value="退出登录")
+    public AjaxResult<Object> logout(HttpServletRequest request) {
+        ironLoginService.logout(request.getHeader("token"));
+        return AjaxResult.success();
+    }
+
+
+
+    @NotLogin
+    @PostMapping("/getDeviceInfo")
+    @ApiOperation(value="获取设备信息")
+    public AjaxResult<Object> getDeviceInfo(String loginsValidate) {
+        String vo = ironLoginService.getDeviceInfo(loginsValidate);
+        return AjaxResult.success(vo);
+    }
+
+
+
+
+}

+ 41 - 0
taphole-iron/src/main/java/com/sckj/iron/controller/TIronStepController.java

@@ -0,0 +1,41 @@
+package com.sckj.iron.controller;
+
+
+import com.sckj.common.aop.NotLogin;
+import com.sckj.common.core.AjaxResult;
+import com.sckj.iron.service.impl.TIronStepServiceImpl;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+
+@RestController
+@RequestMapping("api/step")
+@Api(tags = "出铁步骤配置管理")
+public class TIronStepController {
+
+    @Resource
+    TIronStepServiceImpl iTIronStepService;
+
+    @GetMapping("/getSteps")
+    @ApiOperation(value="获取步骤")
+    @NotLogin
+    public AjaxResult getSteps() {
+        return AjaxResult.success(iTIronStepService.getSteps());
+    }
+
+
+    @GetMapping("/refreshSteps")
+    @ApiOperation(value="刷新步骤")
+    @NotLogin
+    public AjaxResult refreshSteps() {
+        iTIronStepService.refreshSteps();
+        return AjaxResult.success(LocalDateTime.now());
+    }
+
+
+}

+ 29 - 0
taphole-iron/src/main/java/com/sckj/iron/dto/IronStepDTO.java

@@ -0,0 +1,29 @@
+package com.sckj.iron.dto;
+
+import com.sckj.iron.entity.TIronStep;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @Author feng
+ * @Date 2024-11-21 上午 11:32
+ * @Description 步骤
+ */
+@Data
+public class IronStepDTO extends TIronStep {
+
+//    private String stepId;
+//
+//    private String stepName;
+
+    @ApiModelProperty(value = "是否通过(1是 0否)")
+    private boolean pass;
+
+    @ApiModelProperty(value = "数据")
+    private Object data;
+
+    private List<IronStepDTO> childs;
+
+}

+ 65 - 0
taphole-iron/src/main/java/com/sckj/iron/entity/TIronStep.java

@@ -0,0 +1,65 @@
+package com.sckj.iron.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@ApiModel("出铁步骤配置实体")
+@TableName("t_iron_step")
+public class TIronStep implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "创建人")
+    private String createBy;
+
+    @ApiModelProperty(value = "创建时间")
+    private String createTime;
+
+    @ApiModelProperty(value = "更新人")
+    private String updateBy;
+
+    @ApiModelProperty(value = "更新时间")
+    private String updateTime;
+
+    @TableId(value="step_id", type= IdType.AUTO)
+    @ApiModelProperty(value = "stepId")
+    private String stepId;
+
+    @ApiModelProperty(value = "是否流程必须项(0否 1是)")
+    private String required;
+
+    @ApiModelProperty(value = "步骤名称")
+    private String stepName;
+
+    @ApiModelProperty(value = "通过条件")
+    private String stepTj;
+
+    @ApiModelProperty(value = "下一步")
+    private String nextStep;
+
+    @ApiModelProperty(value = "节点类型(start、end、node、child)")
+    private String nodeType;
+
+    @ApiModelProperty(value = "状态(1正常 0停用)")
+    private String status;
+
+    @ApiModelProperty(value = "确认方式(1自动 2手动)")
+    private String confirmMode;
+
+    @ApiModelProperty(value = "唯一名称")
+    private String identifier;
+
+    @ApiModelProperty(value = "排序")
+    private Integer sort;
+
+    @ApiModelProperty(value = "订阅点名称(通道.设备.标识)")
+    private String pointName;
+
+}

+ 13 - 0
taphole-iron/src/main/java/com/sckj/iron/mapper/TIronStepMapper.java

@@ -0,0 +1,13 @@
+package com.sckj.iron.mapper;
+
+import com.sckj.common.core.basics.IBaseMapper;
+import com.sckj.iron.entity.TIronStep;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 出铁步骤配置Mapper
+ * @author LikeAdmin
+ */
+@Mapper
+public interface TIronStepMapper extends IBaseMapper<TIronStep> {
+}

+ 148 - 0
taphole-iron/src/main/java/com/sckj/iron/service/impl/IronLoginServiceImpl.java

@@ -0,0 +1,148 @@
+package com.sckj.iron.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.sckj.common.entity.system.SystemAuthAdmin;
+import com.sckj.common.entity.system.SystemLogLogin;
+import com.sckj.common.enums.ErrorEnum;
+import com.sckj.common.exception.LoginException;
+import com.sckj.common.exception.OperateException;
+import com.sckj.common.mapper.system.SystemAuthAdminMapper;
+import com.sckj.common.mapper.system.SystemLogLoginMapper;
+import com.sckj.common.util.IpUtils;
+import com.sckj.common.util.RequestUtils;
+import com.sckj.common.util.StringUtils;
+import com.sckj.common.util.ToolUtils;
+import com.sckj.iron.validate.IronLoginValidate;
+import com.sckj.iron.vo.IronLoginVo;
+import lombok.extern.slf4j.Slf4j;
+import nl.bitwalker.useragentutils.UserAgent;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * @Author feng
+ * @Date 2024-11-18 上午 09:44
+ * @Description TODO
+ */
+@Service
+@Slf4j
+public class IronLoginServiceImpl {
+
+    @Resource
+    SystemLogLoginMapper systemLogLoginMapper;
+
+    @Resource
+    SystemAuthAdminMapper systemAuthAdminMapper;
+
+
+    @Resource(name = "taskExecutor")
+    Executor taskExecutor;
+
+
+    /**
+     * 登录
+     *
+     * @param loginsValidate 登录参数
+     * @return SystemLoginVo
+     * @author fzr
+     */
+    public IronLoginVo login(IronLoginValidate loginsValidate) {
+        String username = loginsValidate.getUsername();
+        String password = loginsValidate.getPassword();
+
+        SystemAuthAdmin sysAdmin = systemAuthAdminMapper.selectOne(new QueryWrapper<SystemAuthAdmin>()
+                .eq("username", username)
+                .last("limit 1"));
+
+        if (StringUtils.isNull(sysAdmin) || sysAdmin.getIsDelete().equals(1)) {
+            this.recordLoginLog(0, loginsValidate.getUsername(), ErrorEnum.LOGIN_ACCOUNT_ERROR.getMsg());
+            throw new LoginException(ErrorEnum.LOGIN_ACCOUNT_ERROR.getCode(), ErrorEnum.LOGIN_ACCOUNT_ERROR.getMsg());
+        }
+
+        if (sysAdmin.getIsDisable().equals(1)) {
+            this.recordLoginLog(sysAdmin.getId(), loginsValidate.getUsername(), ErrorEnum.LOGIN_DISABLE_ERROR.getMsg());
+            throw new LoginException(ErrorEnum.LOGIN_DISABLE_ERROR.getCode(), ErrorEnum.LOGIN_DISABLE_ERROR.getMsg());
+        }
+
+        String newPWd = password + sysAdmin.getSalt();
+        String md5Pwd = ToolUtils.makeMd5(newPWd);
+        if (!md5Pwd.equals(sysAdmin.getPassword())) {
+            this.recordLoginLog(sysAdmin.getId(), loginsValidate.getUsername(), ErrorEnum.LOGIN_ACCOUNT_ERROR.getMsg());
+            throw new LoginException(ErrorEnum.LOGIN_ACCOUNT_ERROR.getCode(), ErrorEnum.LOGIN_ACCOUNT_ERROR.getMsg());
+        }
+
+        try {
+            // 禁止多处登录
+            if (sysAdmin.getIsMultipoint().equals(0)) {
+                StpUtil.logout(sysAdmin.getId());
+            }
+
+            // 实现账号登录
+            StpUtil.login(sysAdmin.getId());
+
+            // 更新登录信息
+            sysAdmin.setLastLoginIp(IpUtils.getIpAddress());
+            sysAdmin.setLastLoginTime(System.currentTimeMillis() / 1000);
+            systemAuthAdminMapper.updateById(sysAdmin);
+
+            // 记录登录日志
+            this.recordLoginLog(sysAdmin.getId(), loginsValidate.getUsername(), "");
+
+            // 响应登录信息
+            IronLoginVo vo = new IronLoginVo();
+            vo.setId(sysAdmin.getId());
+            vo.setToken(StpUtil.getTokenValue());
+
+            return vo;
+        } catch (Exception e) {
+            Integer adminId = StringUtils.isNotNull(sysAdmin.getId()) ? sysAdmin.getId() : 0;
+            String error = StringUtils.isEmpty(e.getMessage()) ? "未知错误" : e.getMessage();
+            this.recordLoginLog(adminId, loginsValidate.getUsername(), error);
+            throw new OperateException(e.getMessage());
+        }
+    }
+
+    /**
+     * 退出
+     *
+     * @param token 令牌
+     * @author fzr
+     */
+    public void logout(String token) {
+        //RedisUtil.del(AdminConfig.backstageTokenKey + token);
+    }
+
+    public String getDeviceInfo(String token){
+
+        return  "";
+    }
+
+    /**
+     * 记录登录日志
+     */
+    private void recordLoginLog(Integer adminId, String username, String error) {
+        taskExecutor.execute(() -> {
+            try {
+                HttpServletRequest request = Objects.requireNonNull(RequestUtils.handler());
+                final UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
+                SystemLogLogin model = new SystemLogLogin();
+                model.setAdminId(adminId);
+                model.setUsername(username);
+                model.setIp(IpUtils.getIpAddress());
+                model.setOs(userAgent.getOperatingSystem().getName());
+                model.setBrowser(userAgent.getBrowser().getName());
+                model.setStatus(StringUtils.isEmpty(error) ? 1 : 0);
+                model.setCreateTime(System.currentTimeMillis() / 1000);
+                systemLogLoginMapper.insert(model);
+            } catch (Exception e) {
+                log.error("记录登录日志异常 {}" + e.getMessage());
+            }
+        });
+    }
+
+}

+ 69 - 0
taphole-iron/src/main/java/com/sckj/iron/service/impl/TIronStepServiceImpl.java

@@ -0,0 +1,69 @@
+package com.sckj.iron.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.google.common.eventbus.AsyncEventBus;
+import com.sckj.iron.dto.IronStepDTO;
+import com.sckj.iron.entity.TIronStep;
+import com.sckj.iron.mapper.TIronStepMapper;
+import com.sckj.iron.vo.IronStepVO;
+import com.sckj.opc.entity.OPCData;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 出铁步骤配置实现类
+ *
+ * @author LikeAdmin
+ */
+@Service
+public class TIronStepServiceImpl extends ServiceImpl<TIronStepMapper, TIronStep> {
+
+    @Resource
+    TIronStepMapper tIronStepMapper;
+
+    @Resource
+    private AsyncEventBus asyncEventBus;
+
+    /***
+     * 获取步骤及其子步骤
+     * @return
+     */
+    public List<IronStepVO> getSteps() {
+        QueryWrapper<TIronStep> queryWrapper = new QueryWrapper<>();
+        queryWrapper.lambda().eq(TIronStep::getStatus, "1").orderByAsc(TIronStep::getSort);
+        List<TIronStep> stepList = list(queryWrapper);
+        List<IronStepVO> stepDTOList = new ArrayList<>();
+        IronStepVO stepDTO = null;
+        for (TIronStep step : stepList) {
+            if (step.getStepId().length() == 3) {
+                stepDTO = new IronStepVO();
+                stepDTO.setChilds(new ArrayList<>());
+                BeanUtils.copyProperties(step, stepDTO);
+                stepDTOList.add(stepDTO);
+            } else {
+                if (ObjectUtils.isNotEmpty(stepDTO) && StringUtils.startsWith(step.getStepId(), stepDTO.getStepId())) {
+                    IronStepVO stepChildDTO = new IronStepVO();
+                    BeanUtils.copyProperties(step, stepChildDTO);
+                    stepDTO.getChilds().add(stepChildDTO);
+                }
+            }
+        }
+        return stepDTOList;
+    }
+
+
+    public void refreshSteps() {
+        List<IronStepVO> steps = this.getSteps();
+        asyncEventBus.post(steps);
+    }
+
+
+}
+

+ 739 - 0
taphole-iron/src/main/java/com/sckj/iron/socketio/DeviceEventListener.java

@@ -0,0 +1,739 @@
+package com.sckj.iron.socketio;
+
+import com.alibaba.fastjson2.JSON;
+import com.corundumstudio.socketio.SocketIOClient;
+import com.corundumstudio.socketio.annotation.OnEvent;
+import com.google.common.eventbus.Subscribe;
+import com.sckj.common.eventbus.EventListener;
+import com.sckj.common.socketio.SocketUtil;
+import com.sckj.iron.dto.IronStepDTO;
+import com.sckj.iron.service.impl.TIronStepServiceImpl;
+import com.sckj.iron.vo.IronOperataion;
+import com.sckj.iron.vo.IronStepVO;
+import com.sckj.opc.entity.OPCData;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
+
+/**
+ * @Author feng
+ * @Date 2024-11-19 上午 11:05
+ * @Description TODO
+ */
+@Component
+@Slf4j
+public class DeviceEventListener extends EventListener {
+
+    private static final String TAG1 = "TAG1";
+    private static final String TAG2 = "TAG2";
+    private static final String TAG3 = "TAG3";
+    private static final String TAG4 = "TAG4";
+    private static final String TAG5 = "TAG5";
+    private static final String TAG6 = "TAG6";
+    private static final String TAG7 = "TAG7";
+    private static final String TAG8 = "TAG8";
+    private static final String TAG9 = "TAG9";
+    private static final String TAG10 = "TAG10";
+    private static final String TAG11 = "TAG11";
+    private static final String TAG12 = "TAG12";
+    private static final String TAG13 = "TAG13";
+    private static final String TAG14 = "TAG14";
+    private static final String TAG15 = "TAG15";
+
+    @Resource
+    TIronStepServiceImpl ironStepService;
+
+    @Deprecated
+    private Map<String, IronOperataion> operateMap = new ConcurrentHashMap<>();
+
+    private Map<SocketIOClient, IronOperataion> operateMap2 = new ConcurrentHashMap<>();
+
+    private Map<SocketIOClient, List<IronStepDTO>> operateMap3 = new ConcurrentHashMap<>();
+
+    // 1.创建表达式解析器
+    private SpelExpressionParser parser = new SpelExpressionParser();
+
+    // 2.创建变量上下文,设置变量
+    private StandardEvaluationContext ctx = new StandardEvaluationContext();
+
+    private List<IronStepVO> mSteps;
+
+    @PostConstruct
+    public void init() {
+        mSteps = ironStepService.getSteps();
+    }
+
+    @Subscribe
+    public void onMessageEvent(OPCData opcData) {
+        if (SocketUtil.connectMap.isEmpty()) {
+            return;
+        }
+
+        operate3(opcData);
+
+//        for (Map.Entry<SocketIOClient, String> entry : SocketUtil.clientUserIds.entrySet()) {
+//            //根据在线用户初始化炼铁操作数据
+//            if (!operateMap2.containsKey(entry.getKey())) {
+//                operateMap2.put(entry.getKey(), new IronOperataion());
+//            }
+//        }
+//
+//        for (Map.Entry<SocketIOClient, String> entry : SocketUtil.clientUserIds.entrySet()) {
+//            //根据在线用户初始化炼铁操作数据
+//            if (!operateMap3.containsKey(entry.getKey())) {
+//                operateMap3.put(entry.getKey(), ironStepService.getSteps());
+//            }
+//        }
+//
+//
+//        for (Map.Entry<SocketIOClient, List<IronStepDTO>> entry : operateMap3.entrySet()) {
+//            //根据公共数据和私有数据进行填充
+//            List<IronStepDTO> ironOperate = entry.getValue();
+////            if (!operateMap3.containsKey(entry.getKey())) {
+////                operateMap3.put()
+////            }
+//
+//            operate3(ironOperate, opcData, entry.getKey());
+//        }
+
+
+//        for (Map.Entry<SocketIOClient, IronOperataion> entry : operateMap2.entrySet()) {
+//            //根据公共数据和私有数据进行填充
+//            IronOperataion ironOperate = entry.getValue();
+//            operate2(ironOperate, opcData, entry.getKey());
+//        }
+//        for (Map.Entry<String, SocketIOClient> entry : SocketUtil.connectMap.entrySet()) {
+//            //根据在线用户初始化炼铁操作数据
+//            if (!operateMap.containsKey(entry.getKey())) {
+//                operateMap.put(entry.getKey(), new IronOperataion());
+//            }
+//        }
+//        for (Map.Entry<String, IronOperataion> entry : operateMap.entrySet()) {
+//            //根据公共数据和私有数据进行填充
+//            IronOperataion ironOperate = entry.getValue();
+//            operate(ironOperate, opcData, entry.getKey());
+//        }
+    }
+
+    @Deprecated
+    private void operate(IronOperataion ironOperate, OPCData opcData, String userId) {
+        String identifier = opcData.getPointName();
+        try {
+            identifier = identifier.split("\\.")[2];
+        } catch (Exception e) {
+            e.printStackTrace();
+            return;
+        }
+        Object data = opcData.getData();
+
+        //出铁
+        if (TAG1.equals(identifier)
+                || TAG2.equals(identifier)
+                || TAG3.equals(identifier)
+                || TAG4.equals(identifier)
+                || TAG5.equals(identifier)
+                || TAG6.equals(identifier)
+                || TAG7.equals(identifier)
+                || TAG8.equals(identifier)
+                || TAG9.equals(identifier)
+                || TAG10.equals(identifier)
+        ) {
+            //ylgcdw 鱼雷罐车到位
+            if (identifier.equals(TAG1)) {
+                if ("0".equals(data)) {
+                    ironOperate.setYlgcdw("0");
+                } else {
+                    ironOperate.setYlgcdw("1");
+                }
+            }
+
+            //tbxtzc 铁摆系统正常
+            if (identifier.equals(TAG2)) {
+                if ("0".equals(data)) {
+                    ironOperate.setTbxtzc("0");
+                } else {
+                    ironOperate.setTbxtzc("1");
+                }
+            }
+
+            //npkkjzbwb 泥炮、开口机准备完毕
+            if (identifier.equals(TAG3)) {
+                if ("0".equals(data)) {
+                    ironOperate.setNpkkjzbwb("0");
+                } else {
+                    ironOperate.setNpkkjzbwb("1");
+                }
+            }
+
+
+            //炉前准备
+            if ("1".equals(ironOperate.getYlgcdw())
+                    && "1".equals(ironOperate.getTbxtzc())
+                    && "1".equals(ironOperate.getNpkkjzbwb())
+                    && "1".equals(ironOperate.getRydw())
+            ) {
+                ironOperate.setLqzb("1");
+            } else {
+                log.info("lqzb failed => ylgcdw:{},tbxtzc:{},npkkjzbwb:{},rydw:{}", ironOperate.getYlgcdw(), ironOperate.getTbxtzc(), ironOperate.getNpkkjzbwb(), ironOperate.getRydw());
+
+                ironOperate.setRydw("0");
+                ironOperate.setLqzb("0");
+
+                ironOperate.setSzbykq("0");
+                ironOperate.setCtmskq("0");
+                ironOperate.setZggbdj("0");
+                ironOperate.setWwtj("0");
+
+                ironOperate.setLqsqct("0");
+
+                ironOperate.setGlyxqk("0");
+                ironOperate.setTlcjs("0");
+                ironOperate.setCtfs("0");
+                ironOperate.setLncttj("0");
+
+                ironOperate.setYpqrct("0");
+
+                ironOperate.setLqctcz("0");
+
+                PushData.send2Operation(ironOperate, userId);
+                return;
+            }
+
+//            if (identifier.contains(TAG4)) {
+//                ironOperate.setRydw(data);
+//            }
+
+            //水闸泵已开启,水闸槽水量正常
+            if (identifier.equals(TAG4)) {
+                if ("0".equals(data)) {
+                    ironOperate.setSzbykq("0");
+                } else {
+                    ironOperate.setSzbykq("1");
+                }
+            }
+
+            //除尘开启出铁模式
+            if (identifier.equals(TAG5)) {
+                if ("0".equals(data)) {
+                    ironOperate.setCtmskq("0");
+                } else {
+                    ironOperate.setCtmskq("1");
+                }
+            }
+
+            //主沟盖板吊装机处于待机位
+            if (identifier.equals(TAG6)) {
+                if ("0".equals(data)) {
+                    ironOperate.setZggbdj("0");
+                } else {
+                    ironOperate.setZggbdj("1");
+                }
+            }
+
+            //外围条件
+            if ("1".equals(ironOperate.getSzbykq())
+                    && "1".equals(ironOperate.getCtmskq())
+                    && "1".equals(ironOperate.getZggbdj())
+            ) {
+                ironOperate.setWwtj("1");
+            } else {
+                log.info("wwtj failed => szbykq:{},ctmskq:{},zggbdj:{}", ironOperate.getSzbykq(), ironOperate.getCtmskq(), ironOperate.getZggbdj());
+
+                ironOperate.setWwtj("0");
+
+                ironOperate.setLqsqct("0");
+
+                ironOperate.setGlyxqk("0");
+                ironOperate.setTlcjs("0");
+                ironOperate.setCtfs("0");
+                ironOperate.setLncttj("0");
+
+                ironOperate.setYpqrct("0");
+
+                ironOperate.setLqctcz("0");
+
+                PushData.send2Operation(ironOperate, userId);
+
+                return;
+
+            }
+
+            if ("0".equals(ironOperate.getLqsqct())) {
+                ironOperate.setGlyxqk("0");
+                ironOperate.setTlcjs("0");
+                ironOperate.setCtfs("0");
+                ironOperate.setLncttj("0");
+
+                ironOperate.setYpqrct("0");
+
+                ironOperate.setLqctcz("0");
+
+                PushData.send2Operation(ironOperate, userId);
+                return;
+            }
+
+
+            //高炉运行情况
+            if (identifier.equals(TAG7)) {
+                if ("0".equals(data)) {
+                    ironOperate.setGlyxqk("0");
+                } else {
+                    ironOperate.setGlyxqk("1");
+                }
+            }
+
+            //理论铁量、铁铁差计算
+            if (identifier.equals(TAG8)) {
+                if ("0".equals(data)) {
+                    ironOperate.setTlcjs("0");
+                } else {
+                    ironOperate.setTlcjs("1");
+                }
+            }
+
+            //出铁口模式
+            if (identifier.equals(TAG9)) {
+                if ("0".equals(data)) {
+                    ironOperate.setCtfs("0");
+                } else {
+                    ironOperate.setCtfs("1");
+                }
+            }
+
+            //炉内出铁条件
+            if ("1".equals(ironOperate.getLqsqct())
+                    && "1".equals(ironOperate.getGlyxqk())
+                    && "1".equals(ironOperate.getTlcjs())
+                    && "1".equals(ironOperate.getCtfs())
+            ) {
+                ironOperate.setLncttj("1");
+            } else {
+                ironOperate.setLncttj("0");
+
+                log.info("lncttj failed => glyxqk:{},tlcjs:{},ctkms:{}", ironOperate.getGlyxqk(), ironOperate.getTlcjs(), ironOperate.getCtfs());
+
+                ironOperate.setYpqrct("0");
+
+                ironOperate.setLqctcz("0");
+
+                PushData.send2Operation(ironOperate, userId);
+                return;
+            }
+
+            if ("1".equals(ironOperate.getYpqrct())) {
+                ironOperate.setLqctcz("1");
+            } else {
+                ironOperate.setLqctcz("0");
+            }
+
+            PushData.send2Operation(ironOperate, userId);
+        }
+
+        //glyxqk 高炉运行情况
+        //tlcjs 理论铁量、铁铁差计算
+        //ctkms 出铁口模式
+
+        //趋势
+        if (Stream.of("TAG5", "TAG6", "TAG7", "TAG8").anyMatch(identifier::contains)) {
+            // PushData.sendToTrend(JSON.toJSONString(opcData));
+            //铁水成分
+            //铁水流速
+            //铁水流量
+            //铁水温度
+        }
+
+        //实时数据
+        if (Stream.of("TAG9", "TAG10").anyMatch(identifier::contains)) {
+//            PushData.sendToRealtime(JSON.toJSONString(opcData));
+            //除铁中
+            //
+            //
+            //
+            //
+        }
+    }
+
+    private void operate2(IronOperataion ironOperate, OPCData opcData, SocketIOClient socketIOClient) {
+        String identifier = opcData.getPointName();
+        try {
+            identifier = identifier.split("\\.")[2];
+        } catch (Exception e) {
+            e.printStackTrace();
+            return;
+        }
+        Object data = opcData.getData();
+
+        //出铁
+        if (TAG1.equals(identifier)
+                || TAG2.equals(identifier)
+                || TAG3.equals(identifier)
+                || TAG4.equals(identifier)
+                || TAG5.equals(identifier)
+                || TAG6.equals(identifier)
+                || TAG7.equals(identifier)
+                || TAG8.equals(identifier)
+                || TAG9.equals(identifier)
+                || TAG10.equals(identifier)
+        ) {
+            //ylgcdw 鱼雷罐车到位
+            if (identifier.equals(TAG1)) {
+                if ("0".equals(data)) {
+                    ironOperate.setYlgcdw("0");
+                } else {
+                    ironOperate.setYlgcdw("1");
+                }
+            }
+
+            //tbxtzc 铁摆系统正常
+            if (identifier.equals(TAG2)) {
+                if ("0".equals(data)) {
+                    ironOperate.setTbxtzc("0");
+                } else {
+                    ironOperate.setTbxtzc("1");
+                }
+            }
+
+            //npkkjzbwb 泥炮、开口机准备完毕
+            if (identifier.equals(TAG3)) {
+                if ("0".equals(data)) {
+                    ironOperate.setNpkkjzbwb("0");
+                } else {
+                    ironOperate.setNpkkjzbwb("1");
+                }
+            }
+
+
+            //炉前准备
+            if ("1".equals(ironOperate.getYlgcdw())
+                    && "1".equals(ironOperate.getTbxtzc())
+                    && "1".equals(ironOperate.getNpkkjzbwb())
+                    && "1".equals(ironOperate.getRydw())
+            ) {
+                ironOperate.setLqzb("1");
+            } else {
+                log.info("lqzb failed => ylgcdw:{},tbxtzc:{},npkkjzbwb:{},rydw:{}", ironOperate.getYlgcdw(), ironOperate.getTbxtzc(), ironOperate.getNpkkjzbwb(), ironOperate.getRydw());
+
+                ironOperate.setRydw("0");
+                ironOperate.setLqzb("0");
+
+                ironOperate.setSzbykq("0");
+                ironOperate.setCtmskq("0");
+                ironOperate.setZggbdj("0");
+                ironOperate.setWwtj("0");
+
+                ironOperate.setLqsqct("0");
+
+                ironOperate.setGlyxqk("0");
+                ironOperate.setTlcjs("0");
+                ironOperate.setCtfs("0");
+                ironOperate.setLncttj("0");
+
+                ironOperate.setYpqrct("0");
+
+                ironOperate.setLqctcz("0");
+
+
+                PushData.send2Operation(ironOperate, socketIOClient);
+                return;
+            }
+
+//            if (identifier.contains(TAG4)) {
+//                ironOperate.setRydw(data);
+//            }
+
+            //水闸泵已开启,水闸槽水量正常
+            if (identifier.equals(TAG4)) {
+                if ("0".equals(data)) {
+                    ironOperate.setSzbykq("0");
+                } else {
+                    ironOperate.setSzbykq("1");
+                }
+            }
+
+            //除尘开启出铁模式
+            if (identifier.equals(TAG5)) {
+                if ("0".equals(data)) {
+                    ironOperate.setCtmskq("0");
+                } else {
+                    ironOperate.setCtmskq("1");
+                }
+            }
+
+            //主沟盖板吊装机处于待机位
+            if (identifier.equals(TAG6)) {
+                if ("0".equals(data)) {
+                    ironOperate.setZggbdj("0");
+                } else {
+                    ironOperate.setZggbdj("1");
+                }
+            }
+
+            //外围条件
+            if ("1".equals(ironOperate.getSzbykq())
+                    && "1".equals(ironOperate.getCtmskq())
+                    && "1".equals(ironOperate.getZggbdj())
+            ) {
+                ironOperate.setWwtj("1");
+            } else {
+                log.info("wwtj failed => szbykq:{},ctmskq:{},zggbdj:{}", ironOperate.getSzbykq(), ironOperate.getCtmskq(), ironOperate.getZggbdj());
+
+                ironOperate.setWwtj("0");
+
+                ironOperate.setLqsqct("0");
+
+                ironOperate.setGlyxqk("0");
+                ironOperate.setTlcjs("0");
+                ironOperate.setCtfs("0");
+                ironOperate.setLncttj("0");
+
+                ironOperate.setYpqrct("0");
+
+                ironOperate.setLqctcz("0");
+
+                PushData.send2Operation(ironOperate, socketIOClient);
+
+                return;
+
+            }
+
+            if ("0".equals(ironOperate.getLqsqct())) {
+                ironOperate.setGlyxqk("0");
+                ironOperate.setTlcjs("0");
+                ironOperate.setCtfs("0");
+                ironOperate.setLncttj("0");
+
+                ironOperate.setYpqrct("0");
+
+                ironOperate.setLqctcz("0");
+
+                PushData.send2Operation(ironOperate, socketIOClient);
+                return;
+            }
+
+
+            //高炉运行情况
+            if (identifier.equals(TAG7)) {
+                if ("0".equals(data)) {
+                    ironOperate.setGlyxqk("0");
+                } else {
+                    ironOperate.setGlyxqk("1");
+                }
+            }
+
+            //理论铁量、铁铁差计算
+            if (identifier.equals(TAG8)) {
+                if ("0".equals(data)) {
+                    ironOperate.setTlcjs("0");
+                } else {
+                    ironOperate.setTlcjs("1");
+                }
+            }
+
+            //出铁口模式
+            if (identifier.equals(TAG9)) {
+                if ("0".equals(data)) {
+                    ironOperate.setCtfs("0");
+                } else {
+                    ironOperate.setCtfs("1");
+                }
+            }
+
+            //炉内出铁条件
+            if ("1".equals(ironOperate.getLqsqct())
+                    && "1".equals(ironOperate.getGlyxqk())
+                    && "1".equals(ironOperate.getTlcjs())
+                    && "1".equals(ironOperate.getCtfs())
+            ) {
+                ironOperate.setLqsqct(ironOperate.getLqsqct());
+                ironOperate.setLncttj("1");
+            } else {
+                ironOperate.setLncttj("0");
+
+                log.info("lncttj failed => glyxqk:{},tlcjs:{},ctkms:{}", ironOperate.getGlyxqk(), ironOperate.getTlcjs(), ironOperate.getCtfs());
+
+                ironOperate.setYpqrct("0");
+
+                ironOperate.setLqctcz("0");
+
+                PushData.send2Operation(ironOperate, socketIOClient);
+                return;
+            }
+
+            if ("1".equals(ironOperate.getYpqrct())) {
+                ironOperate.setLqctcz("1");
+            } else {
+                ironOperate.setLqctcz("0");
+            }
+
+            PushData.send2Operation(ironOperate, socketIOClient);
+        }
+
+        //glyxqk 高炉运行情况
+        //tlcjs 理论铁量、铁铁差计算
+        //ctkms 出铁口模式
+
+        //趋势
+        if (Stream.of("TAG5", "TAG6", "TAG7", "TAG8").anyMatch(identifier::contains)) {
+            // PushData.sendToTrend(JSON.toJSONString(opcData));
+            //铁水成分
+            //铁水流速
+            //铁水流量
+            //铁水温度
+        }
+
+        //实时数据
+        if (Stream.of("TAG9", "TAG10").anyMatch(identifier::contains)) {
+//            PushData.sendToRealtime(JSON.toJSONString(opcData));
+            //除铁中
+            //
+            //
+            //
+            //
+        }
+    }
+
+    private void operate3(OPCData opcData) {
+        String pointName = opcData.getPointName();
+        Object data = opcData.getData();
+
+        for (IronStepVO stepDTO : mSteps) {
+            //log.info("nodetype:{},pointname:{},opc pointname:{}",stepDTO.getNodeType(),stepDTO.getPointName(),pointName);
+            if ("node".equalsIgnoreCase(stepDTO.getNodeType())) {
+                //处理子项
+                for (IronStepVO child : stepDTO.getChilds()) {
+                    if (Objects.equals(child.getPointName(), pointName)) {
+                        //3.创建变量上下文,设置变量
+                        ctx.setVariable(child.getIdentifier(), data);
+                        child.setData(data);
+
+                        boolean result = false;
+
+                        if (ObjectUtils.isNotEmpty(child.getStepTj())) {
+                            try {
+                                result = parser.parseExpression(child.getStepTj()).getValue(ctx, Boolean.class);
+                            } catch (Exception e) {
+
+                            }
+                        }
+
+                        child.setPass(result);
+
+                        //log.info("pointName:{},data:{},tj:{},result:{}", pointName, data, child.getStepTj(), result);
+                    }
+                }
+                try {
+                    boolean result = false;
+                    if (ObjectUtils.isNotEmpty(stepDTO.getStepTj())) {
+                        try {
+                            result = parser.parseExpression(stepDTO.getStepTj()).getValue(ctx, Boolean.class);
+                        } catch (Exception e) {
+
+                        }
+                    }
+                    stepDTO.setPass(result);
+                } catch (Exception e) {
+                    stepDTO.setPass(false);
+                }
+
+            }
+        }
+
+        log.info("==========================================");
+        //log.info("steps before:{}", JSON.toJSONString(mSteps));
+        log.info("==========================================");
+
+        boolean foundFalsePass = false;
+
+        //发现第一个pass是false,后续的手动操作都为false,包括父项和子项
+        for (IronStepVO stepDTO : mSteps) {
+            for (IronStepVO child : stepDTO.getChilds()) {
+                if (!foundFalsePass && !child.isPass()) {
+                    foundFalsePass = true;
+                }
+                if (foundFalsePass) {
+                    child.setPass(false);
+                }
+            }
+            if (foundFalsePass) {
+                stepDTO.setPass(false);
+            }
+        }
+
+        log.info("==========================================");
+        log.info("steps after:{}", JSON.toJSONString(mSteps));
+        log.info("==========================================");
+
+
+        PushData.send2Operation(mSteps);
+    }
+
+    @OnEvent(value = PushData.IRON_CONFIRM)
+    public void confirmIron(SocketIOClient client, IronStepDTO message) {
+        if (ObjectUtils.isEmpty(message) || ObjectUtils.isEmpty(client)) {
+            return;
+        }
+        log.info("---> {}", SocketUtil.clientUserIds.get(client));
+        for (IronStepVO stepDTO : mSteps) {
+            for (IronStepVO child : stepDTO.getChilds()) {
+                if (child.getStepId().equals(message.getStepId())) {
+                    ctx.setVariable(child.getIdentifier(), message.getData());
+                    child.setPass(message.isPass());
+                    PushData.send2Operation(mSteps);
+                    return;
+                }
+            }
+        }
+    }
+
+
+    /***
+     * 数据库更改后调用接口刷新步骤
+     * @param steps
+     */
+    @Subscribe
+    public void onIronStep(List<IronStepVO> steps) {
+        if (ObjectUtils.isNotEmpty(steps)) {
+            this.mSteps = steps;
+        }
+    }
+
+
+//    @OnEvent(value = PushData.IRON_CONFIRM)
+//    public void confirmIron(SocketIOClient client, IronOperataion message) {
+//        if (ObjectUtils.isEmpty(message) || ObjectUtils.isEmpty(client)) {
+//            return;
+//        }
+//        log.info("---> {}", SocketUtil.clientUserIds.get(client));
+//        log.info("人员到位(rydw):{},{}", message.getUserId(), message.getRydw());
+//        log.info("炉前申请出铁(rydw):{},{}", message.getUserId(), message.getLqsqct());
+//        log.info("预判和确认出铁(rydw):{},{}", message.getUserId(), message.getYpqrct());
+//        IronOperataion ironOperataion = operateMap2.get(client);
+//        if (ObjectUtils.isNotEmpty(ironOperataion)) {
+//            if (ObjectUtils.isNotEmpty(message.getRydw())) {
+//                ironOperataion.setRydw(message.getRydw());
+//            }
+//            if (ObjectUtils.isNotEmpty(message.getLqsqct())) {
+//                ironOperataion.setLqsqct(message.getLqsqct());
+//            }
+//            if (ObjectUtils.isNotEmpty(message.getYpqrct())) {
+//                ironOperataion.setYpqrct(message.getYpqrct());
+//            }
+//            PushData.send2Operation(ironOperataion, client);
+//        } else {
+//            log.info("失效的会话");
+//        }
+//
+//    }
+
+
+}

+ 129 - 0
taphole-iron/src/main/java/com/sckj/iron/socketio/PushData.java

@@ -0,0 +1,129 @@
+package com.sckj.iron.socketio;
+
+import com.corundumstudio.socketio.SocketIOClient;
+import com.sckj.common.socketio.SocketUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @Author feng
+ * @Date 2024-11-19 下午 12:12
+ * @Description TODO
+ */
+@Slf4j
+public class PushData {
+
+    /**
+     * 出铁操作
+     **/
+    public static final String IRON_OPERATION = "IRON_OPERATION";
+
+    /**
+     * 趋势数据
+     **/
+    public static final String IRON_TREND = "IRON_TREND";
+
+    /**
+     * 实时数据
+     **/
+    public static final String IRON_REALTIME = "IRON_REALTIME";
+
+    /***
+     * 人员到位、炉前确认出铁、预判和确认出铁
+     */
+    public static final String IRON_CONFIRM = "IRON_CONFIRM";
+
+
+    /**
+     * 出铁操作
+     *
+     * @return
+     * @Param
+     **/
+    public static void send2Operation(Object message, SocketIOClient socketClient) {
+        if (SocketUtil.clientUserIds.isEmpty()) {
+            return;
+        }
+        //单独发消息
+        socketClient.sendEvent(PushData.IRON_OPERATION, message);
+    }
+    /**
+     * 出铁操作
+     *
+     * @return
+     * @Param
+     **/
+    public static void send2Operation(Object message, String  userId) {
+        if (SocketUtil.connectMap.isEmpty()) {
+            return;
+        }
+        if (ObjectUtils.isEmpty(userId)) {
+            for (Map.Entry<String, SocketIOClient> entry : SocketUtil.connectMap.entrySet()) {
+                entry.getValue().sendEvent(PushData.IRON_OPERATION, message);
+            }
+        } else {
+            //某个客户端信息
+            SocketIOClient socketClient = SocketUtil.getSocketClient(userId);
+            if (Objects.nonNull(socketClient)) {
+                //单独给他发消息
+                socketClient.sendEvent(PushData.IRON_OPERATION, message);
+            } else {
+                log.info(userId + "已下线,暂不发送消息。");
+            }
+        }
+    }
+
+    /**
+     * 趋势
+     *
+     * @return
+     * @Param
+     **/
+    public static void send2Operation(Object message) {
+        if (SocketUtil.connectMap.isEmpty()) {
+            return;
+        }
+        //
+        for (Map.Entry<String, SocketIOClient> entry : SocketUtil.connectMap.entrySet()) {
+            entry.getValue().sendEvent(PushData.IRON_OPERATION, message);
+        }
+    }
+
+
+    /**
+     * 趋势
+     *
+     * @return
+     * @Param
+     **/
+    public static void send2Trend(Object message) {
+        if (SocketUtil.connectMap.isEmpty()) {
+            return;
+        }
+        //
+        for (Map.Entry<String, SocketIOClient> entry : SocketUtil.connectMap.entrySet()) {
+            entry.getValue().sendEvent(PushData.IRON_TREND, message);
+        }
+    }
+
+    /**
+     * 实时数据
+     *
+     * @return
+     * @Param
+     **/
+    public static void send2Realtime(Object message) {
+        if (SocketUtil.connectMap.isEmpty()) {
+            return;
+        }
+        //
+        for (Map.Entry<String, SocketIOClient> entry : SocketUtil.connectMap.entrySet()) {
+            entry.getValue().sendEvent(PushData.IRON_REALTIME, message);
+        }
+    }
+
+
+}

+ 30 - 0
taphole-iron/src/main/java/com/sckj/iron/validate/IronLoginValidate.java

@@ -0,0 +1,30 @@
+package com.sckj.iron.validate;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotEmpty;
+import java.io.Serializable;
+
+@Data
+@ApiModel("可视化大屏登录参数")
+public class IronLoginValidate implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @NotEmpty(message = "账号不能为空")
+    @Length(min = 2, max = 20, message = "账号或密码错误")
+    @ApiModelProperty(value = "登录账号", required = true)
+    private String username;
+
+    @NotEmpty(message = "密码不能为空")
+    @Length(min = 6, max = 18, message = "账号或密码错误")
+    @ApiModelProperty(value = "登录密码", required = true)
+    private String password;
+
+    @ApiModelProperty(value = "标识码")
+    private String uuid;
+
+}

+ 21 - 0
taphole-iron/src/main/java/com/sckj/iron/vo/IronLoginVo.java

@@ -0,0 +1,21 @@
+package com.sckj.iron.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@ApiModel("可视化大屏登录Vo")
+public class IronLoginVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "管理员ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "登录令牌")
+    private String token;
+
+}

+ 67 - 0
taphole-iron/src/main/java/com/sckj/iron/vo/IronOperataion.java

@@ -0,0 +1,67 @@
+package com.sckj.iron.vo;
+
+import lombok.Data;
+
+/**
+ * @Author feng
+ * @Date 2024-11-22 下午 01:32
+ * @Description 出铁操作
+ */
+@Data
+public class IronOperataion {
+    private String userId;
+
+    //鱼雷罐车到位
+    private String ylgcdw;
+
+    //铁摆系统正常
+    private String tbxtzc;
+
+    // 泥炮、开口机准备完毕
+    private String npkkjzbwb;
+
+    //人员到位
+    private String rydw;
+
+    //炉前准备
+    private String lqzb;
+
+
+    //水闸泵已开启,水闸槽水量正常
+    private String szbykq;
+
+    //除尘开启出铁模式
+    private String ctmskq;
+
+    //主沟盖板吊装机处于待机位
+    private String zggbdj;
+
+    //外围条件
+    private String wwtj;
+
+
+    //炉前申请出铁
+    private String lqsqct;
+
+
+    //高炉运行情况
+    private String glyxqk;
+
+    //理论铁量、铁铁差计算
+    private String tlcjs;
+
+    //出铁方式(单铁口 双铁口)
+    private String ctfs;
+
+    //炉内出铁条件
+    private String lncttj;
+
+
+
+    //预判和确认出铁
+    private String ypqrct;
+
+    //炉前出铁操作
+    private String lqctcz;
+
+}

+ 45 - 0
taphole-iron/src/main/java/com/sckj/iron/vo/IronStepVO.java

@@ -0,0 +1,45 @@
+package com.sckj.iron.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @Author feng
+ * @Date 2024-11-21 上午 11:32
+ * @Description 步骤
+ */
+@Data
+public class IronStepVO {
+
+    @ApiModelProperty(value = "stepId")
+    private String stepId;
+
+    @ApiModelProperty(value = "步骤名称")
+    private String stepName;
+
+    @ApiModelProperty(value = "唯一名称")
+    private String identifier;
+
+    @ApiModelProperty(value = "节点类型(start、end、node、child)")
+    private String nodeType;
+
+    @ApiModelProperty(value = "订阅点名称(通道.设备.标识)")
+    private String pointName;
+
+    @ApiModelProperty(value = "通过条件")
+    private String stepTj;
+
+    @ApiModelProperty(value = "是否通过(1是 0否)")
+    private boolean pass;
+
+    @ApiModelProperty(value = "确认方式(1自动 2手动)")
+    private String confirmMode;
+
+    @ApiModelProperty(value = "数据")
+    private Object data;
+
+    private List<IronStepVO> childs;
+
+}

+ 26 - 0
taphole-opc/pom.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>taphole-java</artifactId>
+        <groupId>com.sckj</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>taphole-opc</artifactId>
+
+    <name>taphole-opc</name>
+
+
+    <dependencies>
+        <!-- 公共依赖 -->
+        <dependency>
+            <groupId>com.sckj</groupId>
+            <artifactId>taphole-common</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 144 - 0
taphole-opc/src/main/java/com/sckj/opc/controller/OPCDAController.java

@@ -0,0 +1,144 @@
+package com.sckj.opc.controller;
+
+import com.sckj.common.aop.NotLogin;
+import com.sckj.common.core.AjaxResult;
+import com.sckj.common.enums.ErrorEnum;
+import com.sckj.opc.entity.OPCPoint;
+import com.sckj.opc.entity.OPCServer;
+import com.sckj.opc.opcua.OPCDAServiceImpl;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * @Author feng
+ * @Date 2024-11-21 上午 10:00
+ * @Description TODO
+ */
+@RestController
+@RequestMapping("api/opcda")
+@Api(tags = "OPC DA接口")
+public class OPCDAController {
+    @Resource
+    OPCDAServiceImpl opcuaService;
+
+    /**
+     * @return
+     * @MethodName: connect
+     * @Description: opcua连接
+     */
+    @PostMapping("/connect")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "连接")
+    public AjaxResult connect(OPCServer opcServer) {
+        try {
+            opcuaService.createServer(opcServer);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success("连接成功");
+    }
+
+
+
+    /**
+     * @return
+     * @MethodName: subscribe
+     * @Description: 订阅所有可用的订阅点
+     */
+    @PostMapping("/subscribeAvailable")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "订阅可用订阅点")
+    public AjaxResult subscribeAvailable() {
+        try {
+            opcuaService.subscribeAvailable();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * @return
+     * @MethodName: subscribe
+     * @Description: 订阅所有可用的订阅点
+     */
+    @PostMapping("/unsubscribeAvailable")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "取消订阅可用订阅点")
+    public AjaxResult unsubscribeAvailable() {
+        try {
+            opcuaService.unsubscribeAvailable();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * @return
+     * @MethodName: subscribe
+     * @Description: 订阅
+     */
+    @PostMapping("/subscribe")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "订阅")
+    public AjaxResult subscribe(OPCPoint opcPoint) {
+        try {
+            return  AjaxResult.success(opcuaService.subscribe(opcPoint));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * @return
+     * @MethodName: unsubscribe
+     * @Description: 取消订阅
+     */
+    @PostMapping("/unsubscribe")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "取消订阅")
+    public AjaxResult unsubscribe(OPCPoint opcPoint) {
+        try {
+            opcuaService.unsubscribe(opcPoint);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * @return
+     * @MethodName: subscribe
+     * @Description: 读取节点数据
+     */
+    @PostMapping("/readNodeValue")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "读取节点数据")
+    public AjaxResult readNodeValue(OPCPoint opcPoint) {
+        try {
+            return AjaxResult.success(ErrorEnum.SUCCESS.getCode(), ErrorEnum.SUCCESS.getMsg(), opcuaService.readNodeValue(opcPoint));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+
+
+}

+ 71 - 0
taphole-opc/src/main/java/com/sckj/opc/controller/OPCDataController.java

@@ -0,0 +1,71 @@
+package com.sckj.opc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.sckj.common.aop.Log;
+import com.sckj.common.aop.NotLogin;
+import com.sckj.common.core.AjaxResult;
+import com.sckj.common.validate.commons.IdValidate;
+import com.sckj.common.validate.commons.PageValidate;
+import com.sckj.common.validator.annotation.IDMust;
+import com.sckj.opc.entity.OPCData;
+import com.sckj.opc.service.OPCDataServiceImpl;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("api/opcdata")
+@Api(tags = "OPC数据")
+public class OPCDataController {
+
+    @Resource
+    OPCDataServiceImpl opcDataService;
+
+    @NotLogin
+    @GetMapping("/list")
+    @ApiOperation(value = "列表")
+    public AjaxResult<Object> list(@Validated PageValidate pageValidate, @Validated OPCData opcPoint) {
+        QueryWrapper<OPCData> queryWrapper = new QueryWrapper<>(opcPoint);
+        IPage<OPCData> page = new Page<>(pageValidate.getPageNo(),pageValidate.getPageSize());
+        IPage<OPCData> result = opcDataService.page(page, queryWrapper);
+        return AjaxResult.success(result);
+    }
+
+    @GetMapping("/detail")
+    @ApiOperation(value = "详情")
+    public AjaxResult<OPCData> detail(@Validated @IDMust() @RequestParam("id") Integer id) {
+        OPCData detail = opcDataService.getById(id);
+        return AjaxResult.success(detail);
+    }
+
+    @Log(title = "新增")
+    @PostMapping("/add")
+    @ApiOperation(value = "新增")
+    public AjaxResult<Object> add(@Validated @RequestBody OPCData opcPoint) {
+        opcDataService.save(opcPoint);
+        return AjaxResult.success();
+    }
+
+    @Log(title = "编辑")
+    @PostMapping("/edit")
+    @ApiOperation(value = "编辑")
+    public AjaxResult<Object> edit(@Validated @RequestBody OPCData opcPoint) {
+        opcDataService.saveOrUpdate(opcPoint);
+        return AjaxResult.success();
+    }
+
+    @Log(title = "删除")
+    @PostMapping("/del")
+    @ApiOperation(value = "删除")
+    public AjaxResult<Object> del(@Validated @RequestBody IdValidate idValidate) {
+        opcDataService.removeById(idValidate.getId());
+        return AjaxResult.success();
+    }
+
+
+}

+ 74 - 0
taphole-opc/src/main/java/com/sckj/opc/controller/OPCPointController.java

@@ -0,0 +1,74 @@
+package com.sckj.opc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.sckj.common.aop.Log;
+import com.sckj.common.aop.NotLogin;
+import com.sckj.common.core.AjaxResult;
+import com.sckj.common.validate.commons.IdValidate;
+import com.sckj.common.validate.commons.PageValidate;
+import com.sckj.common.validator.annotation.IDMust;
+import com.sckj.opc.entity.OPCPoint;
+import com.sckj.opc.service.OPCPointServiceImpl;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("api/opcpoint")
+@Api(tags = "OPC订阅点")
+public class OPCPointController {
+
+    @Resource
+    OPCPointServiceImpl opcPointService;
+
+    @NotLogin
+    @GetMapping("/list")
+    @ApiOperation(value = "列表")
+    public AjaxResult<Object> list(@Validated PageValidate pageValidate, @Validated OPCPoint opcPoint) {
+        QueryWrapper<OPCPoint> queryWrapper = new QueryWrapper<>(opcPoint);
+        IPage<OPCPoint> page = new Page<>(pageValidate.getPageNo(),pageValidate.getPageSize());
+        IPage<OPCPoint> result = opcPointService.page(page, queryWrapper);
+        return AjaxResult.success(result);
+    }
+
+    @GetMapping("/detail")
+    @ApiOperation(value = "详情")
+    public AjaxResult<OPCPoint> detail(@Validated @IDMust() @RequestParam("id") Integer id) {
+        OPCPoint detail = opcPointService.getById(id);
+        return AjaxResult.success(detail);
+    }
+
+    @Log(title = "新增")
+    @PostMapping("/add")
+    @ApiOperation(value = "新增")
+    public AjaxResult<Object> add(@Validated @RequestBody OPCPoint opcPoint) {
+        opcPointService.save(opcPoint);
+        return AjaxResult.success();
+    }
+
+    @Log(title = "编辑")
+    @PostMapping("/edit")
+    @ApiOperation(value = "编辑")
+    public AjaxResult<Object> edit(@Validated @RequestBody OPCPoint opcPoint) {
+        opcPointService.saveOrUpdate(opcPoint);
+        return AjaxResult.success();
+    }
+
+    @Log(title = "删除")
+    @PostMapping("/del")
+    @ApiOperation(value = "删除")
+    public AjaxResult<Object> del(@Validated @RequestBody IdValidate idValidate) {
+        opcPointService.removeById(idValidate.getId());
+        return AjaxResult.success();
+    }
+
+
+
+
+
+}

+ 71 - 0
taphole-opc/src/main/java/com/sckj/opc/controller/OPCServerController.java

@@ -0,0 +1,71 @@
+package com.sckj.opc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.sckj.common.aop.Log;
+import com.sckj.common.aop.NotLogin;
+import com.sckj.common.core.AjaxResult;
+import com.sckj.common.validate.commons.IdValidate;
+import com.sckj.common.validate.commons.PageValidate;
+import com.sckj.common.validator.annotation.IDMust;
+import com.sckj.opc.entity.OPCServer;
+import com.sckj.opc.service.OPCServerServiceImpl;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("api/opcserver")
+@Api(tags = "OPC服务器")
+public class OPCServerController {
+
+    @Resource
+    OPCServerServiceImpl opcServerService;
+
+    @NotLogin
+    @GetMapping("/list")
+    @ApiOperation(value = "列表")
+    public AjaxResult<Object> list(@Validated PageValidate pageValidate, @Validated OPCServer opcPoint) {
+        QueryWrapper<OPCServer> queryWrapper = new QueryWrapper<>(opcPoint);
+        IPage<OPCServer> page = new Page<>(pageValidate.getPageNo(),pageValidate.getPageSize());
+        IPage<OPCServer> result = opcServerService.page(page, queryWrapper);
+        return AjaxResult.success(result);
+    }
+
+    @GetMapping("/detail")
+    @ApiOperation(value = "详情")
+    public AjaxResult<OPCServer> detail(@Validated @IDMust() @RequestParam("id") Integer id) {
+        OPCServer detail = opcServerService.getById(id);
+        return AjaxResult.success(detail);
+    }
+
+    @Log(title = "新增")
+    @PostMapping("/add")
+    @ApiOperation(value = "新增")
+    public AjaxResult<Object> add(@Validated @RequestBody OPCServer opcPoint) {
+        opcServerService.save(opcPoint);
+        return AjaxResult.success();
+    }
+
+    @Log(title = "编辑")
+    @PostMapping("/edit")
+    @ApiOperation(value = "编辑")
+    public AjaxResult<Object> edit(@Validated @RequestBody OPCServer opcPoint) {
+        opcServerService.saveOrUpdate(opcPoint);
+        return AjaxResult.success();
+    }
+
+    @Log(title = "删除")
+    @PostMapping("/del")
+    @ApiOperation(value = "删除")
+    public AjaxResult<Object> del(@Validated @RequestBody IdValidate idValidate) {
+        opcServerService.removeById(idValidate.getId());
+        return AjaxResult.success();
+    }
+
+
+}

+ 140 - 0
taphole-opc/src/main/java/com/sckj/opc/controller/OPCUAController.java

@@ -0,0 +1,140 @@
+package com.sckj.opc.controller;
+
+import com.sckj.common.aop.NotLogin;
+import com.sckj.common.core.AjaxResult;
+import com.sckj.opc.entity.OPCPoint;
+import com.sckj.opc.entity.OPCServer;
+import com.sckj.opc.opcua.OPCUAServiceImpl;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * @Author feng
+ * @Date 2024-11-21 上午 10:00
+ * @Description TODO
+ */
+@RestController
+@RequestMapping("api/opcua")
+@Api(tags = "OPC UA接口")
+public class OPCUAController {
+    @Resource
+    OPCUAServiceImpl opcuaService;
+
+    /**
+     * @return
+     * @MethodName: connect
+     * @Description: opcua连接
+     */
+    @PostMapping("/connect")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "连接")
+    public AjaxResult connect(OPCServer opcServer) {
+        try {
+            opcuaService.createClient(opcServer);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * @return
+     * @MethodName: subscribe
+     * @Description: 订阅所有可用的订阅点
+     */
+    @PostMapping("/subscribeAvailable")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "订阅可用的订阅点")
+    public AjaxResult subscribeAvailable() {
+        try {
+            opcuaService.subscribeAvailable();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * @return
+     * @MethodName: subscribe
+     * @Description: 订阅所有可用的订阅点
+     */
+    @PostMapping("/unsubscribeAvailable")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "取消订阅可用订阅点")
+    public AjaxResult unsubscribeAvailable() {
+        try {
+            opcuaService.unsubscribeAvailable();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * @return
+     * @MethodName: subscribe
+     * @Description: 订阅
+     */
+    @PostMapping("/subscribe")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "订阅")
+    public AjaxResult subscribe(OPCPoint opcPoint) {
+        try {
+            opcuaService.subscribe(opcPoint);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * @return
+     * @MethodName: unsubscribe
+     * @Description: 取消订阅
+     */
+    @PostMapping("/unsubscribe")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "取消订阅")
+    public AjaxResult unsubscribe(OPCPoint opcPoint) {
+        try {
+            opcuaService.unsubscribe(opcPoint);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * @return
+     * @MethodName: subscribe
+     * @Description: 读取节点数据
+     */
+    @PostMapping("/readNodeValue")
+    @ResponseBody
+    @NotLogin
+    @ApiOperation(value = "读取节点数据")
+    public AjaxResult readNodeValue(OPCPoint opcPoint) {
+        try {
+            return AjaxResult.success(opcuaService.readNodeValue(opcPoint));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return AjaxResult.success();
+    }
+
+
+}

+ 46 - 0
taphole-opc/src/main/java/com/sckj/opc/dto/OPCPointDTO.java

@@ -0,0 +1,46 @@
+package com.sckj.opc.dto;
+
+import com.sckj.opc.entity.OPCPoint;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @Author feng
+ * @Date 2024-11-21 上午 11:32
+ * @Description TODO
+ */
+@Data
+@Builder
+public class OPCPointDTO extends OPCPoint {
+    @ApiModelProperty(value = "OPC UA地址")
+    private String endpointUrl;
+
+    @ApiModelProperty(value = "用户名")
+    private String username;
+
+    @ApiModelProperty(value = "密码")
+    private String password;
+
+    @ApiModelProperty(value = "创建人")
+    private String applicationName;
+
+    @ApiModelProperty(value = "PLC连接信息")
+    private String applicationUri;
+
+    @ApiModelProperty(value = "安全策略")
+    private String securityPolicy;
+
+    @ApiModelProperty(value = "认证证书路径")
+    private String certPath;
+
+    @ApiModelProperty(value = "IP地址")
+    private String ip;
+
+    @ApiModelProperty(value = "注册表ID")
+    private String clsid;
+
+
+}

+ 45 - 0
taphole-opc/src/main/java/com/sckj/opc/entity/OPCData.java

@@ -0,0 +1,45 @@
+package com.sckj.opc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Builder;
+import lombok.Data;
+import lombok.ToString;
+
+import java.util.Date;
+
+/**
+ * @Author feng
+ * @Date 2024-11-15 上午 10:28
+ * @Description OPC数据
+ */
+@Data
+@Builder
+@TableName(value = "t_opc_data")
+@ApiModel("OPC数据")
+@ToString
+public class OPCData {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(type= IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value = "数据")
+    private Object data;
+
+    @ApiModelProperty(value = "PLC时间")
+    private Date sourceTime;
+
+    @ApiModelProperty(value = "OPCServer时间")
+    private Date serverTime;
+
+    @ApiModelProperty(value = "PLC状态码")
+    private Long statusCode;
+
+    @ApiModelProperty(value = "订阅点名称")
+    private String pointName;
+
+}

+ 55 - 0
taphole-opc/src/main/java/com/sckj/opc/entity/OPCPoint.java

@@ -0,0 +1,55 @@
+package com.sckj.opc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * @Author feng
+ * @Date 2024-11-15 上午 10:28
+ * @Description OPC订阅点
+ */
+@Data
+@TableName(value = "t_opc_point")
+@ApiModel("OPC订阅点")
+public class OPCPoint {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(type= IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value = "所属opcserver")
+    private Long opcServerId;
+
+//    @ApiModelProperty(value = "订阅点名称")
+//    private String identifier;
+//
+//    @ApiModelProperty(value = "订阅点说明")
+//    private String identifierDesc;
+
+
+    @ApiModelProperty(value = "所属锅炉")
+    private String boilerId;
+
+
+    @ApiModelProperty(value = "流程步骤")
+    private String stepId;
+
+    @ApiModelProperty(value = "命名空间,默认为2")
+    private Integer namespaceIndex;
+
+
+    @ApiModelProperty(value = "状态")
+    private String status;
+
+    @ApiModelProperty(value = "订阅点名称")
+    private String pointName;
+
+    @ApiModelProperty(value = "订阅点说明")
+    private String pointDesc;
+
+}

+ 58 - 0
taphole-opc/src/main/java/com/sckj/opc/entity/OPCServer.java

@@ -0,0 +1,58 @@
+package com.sckj.opc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * @Author feng
+ * @Date 2024-11-15 上午 10:18
+ * @Description opc服务器
+ */
+@Data
+@TableName(value = "t_opc_server")
+@ApiModel("OPC服务器")
+public class OPCServer {
+    @ApiModelProperty(value = "主键ID")
+    @TableId(type= IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value = "OPC UA地址")
+    private String endpointUrl;
+
+    @ApiModelProperty(value = "用户名")
+    private String username;
+
+    @ApiModelProperty(value = "密码")
+    private String password;
+
+    @ApiModelProperty(value = "创建人")
+    private String applicationName;
+
+    @ApiModelProperty(value = "PLC连接信息")
+    private String applicationUri;
+
+    @ApiModelProperty(value = "安全策略")
+    private String securityPolicy;
+
+    @ApiModelProperty(value = "认证证书路径")
+    private String certPath;
+
+    @ApiModelProperty(value = "IP地址")
+    private String ip;
+
+
+
+
+    @ApiModelProperty(value = "状态")
+    private String status;
+
+
+    @ApiModelProperty(value = "KEPServer的注册表ID")
+    private String clsid;
+
+}

+ 56 - 0
taphole-opc/src/main/java/com/sckj/opc/listener/MySubscriptionListener.java

@@ -0,0 +1,56 @@
+package com.sckj.opc.listener;
+
+import com.alibaba.fastjson2.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.milo.opcua.sdk.client.api.UaClient;
+import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription;
+import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscriptionManager;
+import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscriptionManager;
+import org.eclipse.milo.opcua.stack.core.UaException;
+import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
+import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
+
+/**
+ * @Author feng
+ * @Date 2024-11-19 下午 05:44
+ * @Description TODO
+ */
+@Slf4j
+public class MySubscriptionListener implements UaSubscriptionManager.SubscriptionListener {
+
+    public MySubscriptionListener( ) {
+    }
+
+    @Override
+    public void onKeepAlive(UaSubscription subscription, DateTime publishTime) {
+        log.info("onKeepAlive");
+    }
+
+    @Override
+    public void onStatusChanged(UaSubscription subscription, StatusCode status) {
+        log.info("onStatusChanged");
+    }
+
+    @Override
+    public void onPublishFailure(UaException exception) {
+        exception.printStackTrace();
+        log.info(">>> onPublishFailure:{}",exception.toString());
+    }
+
+    @Override
+    public void onNotificationDataLost(UaSubscription subscription) {
+        log.info("onNotificationDataLost");
+    }
+
+    /**
+     * 重连时 尝试恢复之前的订阅失败时 会调用此方法
+     *
+     * @param subscription 订阅
+     * @param statusCode   状态
+     */
+    @Override
+    public void onSubscriptionTransferFailed(UaSubscription subscription, StatusCode statusCode) {
+        log.info("onSubscriptionTransferFailed,statusCode:{}", JSON.toJSONString(statusCode));
+
+    }
+}

+ 12 - 0
taphole-opc/src/main/java/com/sckj/opc/mapper/OPCDataMapper.java

@@ -0,0 +1,12 @@
+package com.sckj.opc.mapper;
+
+
+import com.sckj.common.core.basics.IBaseMapper;
+import com.sckj.opc.entity.OPCData;
+import com.sckj.opc.entity.OPCPoint;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface OPCDataMapper extends IBaseMapper<OPCData> {
+
+}

+ 28 - 0
taphole-opc/src/main/java/com/sckj/opc/mapper/OPCPointMapper.java

@@ -0,0 +1,28 @@
+package com.sckj.opc.mapper;
+
+
+import com.sckj.common.core.basics.IBaseMapper;
+import com.sckj.opc.dto.OPCPointDTO;
+import com.sckj.opc.entity.OPCPoint;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface OPCPointMapper extends IBaseMapper<OPCPoint> {
+    /***
+     * 获取带有server信息的订阅点
+     * @param opcPoint
+     * @return
+     */
+    OPCPointDTO selectInfoWithServer(OPCPoint opcPoint);
+
+
+    /***
+     * 获取可用的订阅点
+     * @return
+     */
+    List<OPCPointDTO> getAvailablePoints();
+
+}

+ 12 - 0
taphole-opc/src/main/java/com/sckj/opc/mapper/OPCServerMapper.java

@@ -0,0 +1,12 @@
+package com.sckj.opc.mapper;
+
+
+import com.sckj.common.core.basics.IBaseMapper;
+import com.sckj.opc.entity.OPCPoint;
+import com.sckj.opc.entity.OPCServer;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface OPCServerMapper extends IBaseMapper<OPCServer> {
+
+}

+ 227 - 0
taphole-opc/src/main/java/com/sckj/opc/opcua/OPCDAServiceImpl.java

@@ -0,0 +1,227 @@
+package com.sckj.opc.opcua;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.google.common.eventbus.AsyncEventBus;
+import com.sckj.opc.dto.OPCPointDTO;
+import com.sckj.opc.entity.OPCData;
+import com.sckj.opc.entity.OPCPoint;
+import com.sckj.opc.entity.OPCServer;
+import com.sckj.opc.service.OPCDataServiceImpl;
+import com.sckj.opc.service.OPCPointServiceImpl;
+import com.sckj.opc.service.OPCServerServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+import org.jinterop.dcom.common.JIException;
+import org.openscada.opc.lib.common.ConnectionInformation;
+import org.openscada.opc.lib.common.NotConnectedException;
+import org.openscada.opc.lib.da.*;
+import org.springframework.beans.BeanUtils;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PreDestroy;
+import javax.annotation.Resource;
+import java.net.UnknownHostException;
+import java.util.*;
+import java.util.concurrent.*;
+
+import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
+
+@Service
+@Slf4j
+public class OPCDAServiceImpl {
+
+    @Resource
+    OPCServerServiceImpl opcServerService;
+
+    @Resource
+    OPCPointServiceImpl opcPointService;
+
+    @Resource
+    OPCDataServiceImpl opcDataService;
+
+    @Resource
+    private AsyncEventBus asyncEventBus;
+
+    @Resource(name = "taskExecutor")
+    ThreadPoolTaskExecutor taskExecutor;
+
+    private ConcurrentHashMap<Long, Server> mOPCDaClientMap = new ConcurrentHashMap<>();
+    private ConcurrentHashMap<Long, Object> mOPCDaPointsMap = new ConcurrentHashMap<>();
+
+
+    /**
+     * 获得基础的连接信息
+     */
+    public Server createServer(OPCServer opcServer) {
+        if (mOPCDaClientMap.containsKey(opcServer.getId())) {
+            return mOPCDaClientMap.get(opcServer.getId());
+        }
+        final ConnectionInformation ci = new ConnectionInformation();
+        ci.setHost(opcServer.getIp());          // OPCDA IP
+        ci.setUser(opcServer.getUsername());              // 用户名,配置DCOM时配置的
+        ci.setPassword(opcServer.getPassword());           // 密码
+        ci.setClsid(opcServer.getClsid()); //KEPServer的注册表ID
+
+        log.info("availableProcessors:{}",Runtime.getRuntime().availableProcessors());
+        // 启动服务
+        final Server server = new Server(ci, Executors.newSingleThreadScheduledExecutor());
+        AutoReconnectController controller = new AutoReconnectController(server);
+        // 连接到服务
+        controller.connect();
+        mOPCDaClientMap.put(opcServer.getId(), server);
+        return server;
+    }
+
+
+    public Object createSubscription(OPCServer opcServer, OPCPoint opcPoint) {
+        final int PERIOD = 100 * 10;
+        StringBuilder sb = new StringBuilder();
+        if (ObjectUtils.isNotEmpty(opcPoint) && !mOPCDaPointsMap.containsKey(opcPoint.getId())) {
+            Map<String, Object> pointChangedMap = new LinkedHashMap<>();
+            Future<String> future = taskExecutor.submit(new Callable<String>() {
+                @Override
+                public String call() {
+                    try {
+                        Server server = createServer(opcServer);
+                        AccessBase access = new SyncAccess(server, PERIOD);
+                        log.info("{} start subscribe",opcPoint.getPointName());
+                        access.addItem(opcPoint.getPointName(), (item, itemstate) -> {
+                            int errorCode = itemstate.getErrorCode();
+                            String pointName = item.getId();
+                            Calendar calendar = itemstate.getTimestamp();
+                            Object object = null;
+                            try {
+                                object = itemstate.getValue().getObject();
+                            } catch (JIException e) {
+                                e.printStackTrace();
+                            }
+                            //DA中订阅是按照定时计算的,存在重复的数据项,进行过滤
+                            if (!Objects.equals(pointChangedMap.get(item.getId()), object)) {
+                                pointChangedMap.put(item.getId(), object);
+                                OPCData opcData = OPCData.builder()
+                                        .data(object)
+                                        .serverTime(calendar.getTime())
+                                        .sourceTime(calendar.getTime())
+                                        .statusCode((long) errorCode)
+                                        .pointName(pointName)
+                                        .build();
+                                //post给其他模块使用
+                                asyncEventBus.post(opcData);
+                                //opcDataService.save(opcData);
+                                log.info("DA,{},{}",item.getId(), itemstate.toString());
+                            }
+                        });
+
+                        access.bind();
+                        mOPCDaPointsMap.put(opcPoint.getId(), opcPoint);
+                        while (mOPCDaPointsMap.containsKey(opcPoint.getId())) {
+                            //阻塞,直到关闭订阅
+                        }
+                        access.unbind();
+                        log.info("{} stop subscribe",opcPoint.getPointName());
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                        return e.getMessage();
+                    }
+                    return "";
+                }
+            });
+
+            try {
+                String result = future.get(1,TimeUnit.SECONDS); //
+                log.error(">>> error:{}", result);
+                if (ObjectUtils.isNotEmpty(result)) {
+                    sb.append(opcPoint.getPointName() + "订阅异常:" + result);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return sb;
+    }
+
+    public Object subscribe(OPCPoint opcPoint) {
+        try {
+            OPCPointDTO opcPointDTO = opcPointService.selectInfoWithServer(opcPoint);
+            QueryWrapper<OPCPoint> pointQueryWrapper = new QueryWrapper<>();
+            pointQueryWrapper.lambda().eq(OPCPoint::getStatus, "1");
+            OPCServer opcServer = new OPCServer();
+            BeanUtils.copyProperties(opcPointDTO, opcServer);
+            return createSubscription(opcServer, opcPointDTO);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+
+    /***
+     * 取消订阅
+     * @param opcPoint
+     */
+    public void unsubscribe(OPCPoint opcPoint) {
+        mOPCDaPointsMap.remove(opcPoint.getId());
+    }
+
+    /***
+     * 取消可用订阅
+     */
+    public void unsubscribeAvailable() {
+        mOPCDaPointsMap.clear();
+    }
+
+    /**
+     * 项目启动时自动创建 PLC连接并订阅节点
+     */
+    public void subscribeAvailable() {
+        QueryWrapper<OPCServer> serverQueryWrapper = new QueryWrapper<>();
+        serverQueryWrapper.lambda().eq(OPCServer::getStatus, "1");
+        List<OPCServer> opcServerList = opcServerService.list(serverQueryWrapper);
+        if (ObjectUtils.isEmpty(opcServerList)) {
+            return;
+        }
+        for (OPCServer opcServer : opcServerList) {
+            QueryWrapper<OPCPoint> pointQueryWrapper = new QueryWrapper<>();
+            pointQueryWrapper.lambda().eq(OPCPoint::getOpcServerId, opcServer.getId()).eq(OPCPoint::getStatus, "1");
+            List<OPCPoint> opcPointList = opcPointService.list(pointQueryWrapper);
+            if (ObjectUtils.isNotEmpty(opcPointList)) {
+                for (OPCPoint opcPoint : opcPointList) {
+                    createSubscription(opcServer, opcPoint);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * 读取节点数据
+     * <p>
+     * identifier也可以通过UaExpert客户端去查询,这个值=通道名称.设备名称.标记名称
+     *
+     * @param opcPoint
+     * @throws Exception
+     */
+    public String readNodeValue(OPCPoint opcPoint) throws Exception {
+        OPCPointDTO opcPointDTO = opcPointService.selectInfoWithServer(opcPoint);
+        if (ObjectUtils.isEmpty(opcPointDTO)) {
+            return null;
+        }
+        OPCServer opcServer = new OPCServer();
+        BeanUtils.copyProperties(opcPointDTO, opcServer);
+        Server server = createServer(opcServer);
+        Group group = server.addGroup();
+        Item item = group.addItem(opcPointDTO.getPointName());
+        return String.valueOf(item.read(true).getValue().getObject());
+    }
+
+    @PreDestroy
+    public void releaseServer() {
+        if (ObjectUtils.isNotEmpty(mOPCDaClientMap)) {
+            for (Server server : mOPCDaClientMap.values()) {
+                server.dispose();
+            }
+        }
+    }
+
+}

+ 537 - 0
taphole-opc/src/main/java/com/sckj/opc/opcua/OPCUAServiceImpl.java

@@ -0,0 +1,537 @@
+package com.sckj.opc.opcua;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.eventbus.AsyncEventBus;
+import com.sckj.common.exception.OperateException;
+import com.sckj.opc.dto.OPCPointDTO;
+import com.sckj.opc.entity.OPCData;
+import com.sckj.opc.entity.OPCPoint;
+import com.sckj.opc.entity.OPCServer;
+import com.sckj.opc.listener.MySubscriptionListener;
+import com.sckj.opc.service.OPCDataServiceImpl;
+import com.sckj.opc.service.OPCPointServiceImpl;
+import com.sckj.opc.service.OPCServerServiceImpl;
+import com.sckj.opc.utils.KeyStoreLoader;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
+import org.eclipse.milo.opcua.sdk.client.SessionActivityListener;
+import org.eclipse.milo.opcua.sdk.client.api.ServiceFaultListener;
+import org.eclipse.milo.opcua.sdk.client.api.UaSession;
+import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder;
+import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;
+import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider;
+import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
+import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem;
+import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription;
+import org.eclipse.milo.opcua.sdk.client.subscriptions.ManagedDataItem;
+import org.eclipse.milo.opcua.sdk.client.subscriptions.ManagedSubscription;
+import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscriptionManager;
+import org.eclipse.milo.opcua.stack.core.AttributeId;
+import org.eclipse.milo.opcua.stack.core.types.builtin.*;
+import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
+import org.eclipse.milo.opcua.stack.core.types.structured.*;
+import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.annotation.Resource;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+
+import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
+
+@Service
+@Slf4j
+public class OPCUAServiceImpl {
+
+    @Resource
+    OPCServerServiceImpl opcServerService;
+
+    @Resource
+    OPCPointServiceImpl opcPointService;
+
+    @Resource
+    OPCDataServiceImpl opcDataService;
+
+    @Autowired
+    private AsyncEventBus asyncEventBus;
+
+    private ConcurrentHashMap<Long, OpcUaClient> mOPCUaClientMap = new ConcurrentHashMap<>();
+    private ConcurrentHashMap<Long, Set<NodeId>> mOPCUaPointsMap = new ConcurrentHashMap<>();
+
+
+    public OpcUaClient createClient(OPCServer opcServer) throws Exception {
+        if (mOPCUaClientMap.containsKey(opcServer.getId())) {
+            return mOPCUaClientMap.get(opcServer.getId());
+        }
+        OpcUaClient opcUaClient;
+
+//        Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security2");
+//        try {
+//            Files.createDirectories(securityTempDir);
+//        } catch (IOException e) {
+//            throw new RuntimeException(e);
+//        }
+//        if (!Files.exists(securityTempDir)) {
+//            try {
+//                throw new Exception("不能够创建安全路径: " + securityTempDir);
+//            } catch (Exception e) {
+//                throw new RuntimeException(e);
+//            }
+//        }
+//        KeyStoreLoader loader = new KeyStoreLoader().load(securityTempDir);
+        // 获取OPC UA的服务器端节点,OPCUA有多种加密方法(可在PLC中设置),当前只列出了不使用任何加密的方式进行连接,其他方式的连接会在以后进行补充
+        //applicationUri 可在PLC中查询到,applicationName为uri冒号后面的内容;
+//        EndpointDescription[] endpoints =
+//                new EndpointDescription[0];
+//        try {
+//            endpoints = UaStackClient.getEndpoints(opcServer.getEndpointUrl()).get();
+//        } catch (InterruptedException e) {
+//            throw new RuntimeException(e);
+//        } catch (ExecutionException e) {
+//            throw new RuntimeException(e);
+//        }
+//        EndpointDescription endpoint = null;
+//        try {
+//            endpoint = Arrays.stream(endpoints)
+//                    .filter(e -> e.getEndpointUrl().equals(opcServer.getEndpointUrl()))
+//                    .findFirst().orElseThrow(() -> new Exception("没有节点返回"));
+//        } catch (Exception e) {
+//            throw new RuntimeException(e);
+//        }
+
+//        EndpointDescription build = EndpointDescription.builder().endpointUrl(opcServer.getEndpointUrl()).build();
+//
+//        // 设置OPC UA的配置信息
+//        OpcUaClientConfig config =
+//                OpcUaClientConfig.builder()
+////                        .setApplicationName(LocalizedText.english("PLC_NAME"))
+//                        .setApplicationName(LocalizedText.english(opcServer.getApplicationName())) //此处填Uri中冒号后面的字符串
+////                        .setApplicationUri("urn:SIMATIC.S7-1500.OPC-UA.Application:PLC_APPNAME")
+//                        .setApplicationUri(opcServer.getApplicationUri()) //PLC连接信息
+////                        .setCertificate(loader.getClientCertificate())
+////                        .setKeyPair(loader.getClientKeyPair())
+//                        .setEndpoint(build)
+////                        .setIdentityProvider(new AnonymousProvider())
+//                        .setIdentityProvider(new UsernameProvider(opcServer.getUsername(), opcServer.getPassword()))
+//                        .setRequestTimeout(uint(5000))
+//                        .build();
+
+        opcUaClient = OpcUaClient.create(opcServer.getEndpointUrl(), endpoints -> {
+                    EndpointDescription description = endpoints.stream()
+//                                .filter(e -> securityPolicy(key).getUri().equals(e.getSecurityPolicyUri()))
+                            .findFirst().orElseThrow(() -> new OperateException("no desired endpoints returned"));
+                    if (!description.getEndpointUrl().equals(opcServer.getEndpointUrl())) {
+                        description = EndpointUtil.updateUrl(description, getUri(opcServer.getEndpointUrl()).getHost(), getUri(opcServer.getEndpointUrl()).getPort());
+                    }
+                    return Optional.of(description);
+                }, configBuilder ->
+                {
+                    OpcUaClientConfigBuilder builder = configBuilder
+                            .setApplicationName(LocalizedText.english(opcServer.getApplicationName()))
+                            .setApplicationUri(opcServer.getApplicationUri())
+                            .setIdentityProvider(this.identityProvider(opcServer))
+                            .setRequestTimeout(uint(5000));
+                    //看看是否需要客户端认证证书
+                    if (ObjectUtils.isNotEmpty(opcServer.getCertPath())) {
+                        KeyStoreLoader loader = null;
+                        try {
+                            loader = new KeyStoreLoader().load(opcServer.getCertPath());
+                            builder.setKeyPair(loader.getClientKeyPair())
+                                    .setCertificate(loader.getClientCertificate());
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
+                    }
+                    return builder.build();
+                }
+        );
+        //创建连接
+        //Thread.sleep(5*1000); // 线程休眠一下再返回对象,给创建过程一个时间。
+        opcUaClient.addFaultListener(new ServiceFaultListener() {
+            @Override
+            public void onServiceFault(ServiceFault serviceFault) {
+                log.info("Added ServiceFaultListener >> : {}", serviceFault.toString());
+//                if (serviceFault.getResponseHeader().getServiceResult().isBad()) {
+//                    opcUaClient.disconnect();
+//                }
+            }
+        });
+
+        opcUaClient.addSessionActivityListener(new SessionActivityListener() {
+            @Override
+            public void onSessionActive(UaSession session) {
+                log.info("onSessionActive: {}", JSON.toJSONString(session));
+            }
+
+            @Override
+            public void onSessionInactive(UaSession session) {
+                log.info("onSessionInactive: {}", JSON.toJSONString(session));
+            }
+        });
+
+        opcUaClient.connect().get();
+
+        mOPCUaClientMap.put(opcServer.getId(), opcUaClient);
+
+        return opcUaClient;
+    }
+
+    public void createSubscription(OPCServer opcServer, List<OPCPoint> opcPointList) throws Exception {
+        OpcUaClient client = createClient(opcServer);
+        OpcUaSubscriptionManager subscriptionManager = client.getSubscriptionManager();
+
+
+        subscriptionManager.addSubscriptionListener(new MySubscriptionListener() {
+            @Override
+            public void onSubscriptionTransferFailed(UaSubscription subscription, StatusCode statusCode) {
+                super.onSubscriptionTransferFailed(subscription, statusCode);
+                try {
+                    handleClientSubscription(subscriptionManager, opcPointList);
+                } catch (ExecutionException e) {
+                    e.printStackTrace();
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        });
+        handleClientSubscription(subscriptionManager, opcPointList);
+    }
+
+    private void handleClientSubscription(OpcUaSubscriptionManager subscriptionManager, List<OPCPoint> opcPointList) throws ExecutionException, InterruptedException {
+        UaSubscription subscription = subscriptionManager.createSubscription(100).get();
+        final List<MonitoredItemCreateRequest> requests = new ArrayList<>();
+
+        for (OPCPoint opcPoint : opcPointList) {
+            //创建监控的参数
+            MonitoringParameters parameters = new MonitoringParameters(
+                    subscription.nextClientHandle(),
+                    1000.0,     // sampling interval
+                    null,       // filter, null means use default
+                    uint(10),   // queue size
+                    true        // discard oldest
+            );
+            //创建订阅
+            NodeId nodeId = new NodeId(opcPoint.getNamespaceIndex(), opcPoint.getPointName());
+            ReadValueId readValueId = new ReadValueId(nodeId, AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE);
+            //该请求最后用于创建订阅。
+            MonitoredItemCreateRequest monitoredItemCreateRequest = new MonitoredItemCreateRequest(readValueId, MonitoringMode.Reporting, parameters);
+            requests.add(monitoredItemCreateRequest);
+        }
+
+        //创建发布间隔1000ms的订阅对象
+        //创建监控项,并且注册变量值改变时候的回调函数。
+        final UaSubscription.ItemCreationCallback onItemCreated = (item, id) -> item.setValueConsumer(this::onValueArrived);
+
+        subscription.createMonitoredItems(
+                TimestampsToReturn.Both,
+                requests, onItemCreated
+//                new UaSubscription.ItemCreationCallback() {
+//                    @Override
+//                    public void onItemCreated(UaMonitoredItem item, int index) {
+//                        uaMonitoredItemList.add(item);
+//
+//                        log.info("item count:{},size:{}", atomicInteger.incrementAndGet(), uaMonitoredItemList.size());
+//
+//                        item.setValueConsumer(new UaMonitoredItem.ValueConsumer() {
+//                            @Override
+//                            public void onValueArrived(UaMonitoredItem item, DataValue value) {
+////                                log.info("id :" + index);
+//                                log.info("identifier:{},value:{}", item.getReadValueId().getNodeId().getIdentifier(), value.getValue());
+//                                //todo 此处监听回调的地址以及数据
+//
+//                            }
+//                        });
+//                    }
+//                }
+        );
+    }
+
+    private void onValueArrived(UaMonitoredItem item, DataValue dataValue) {
+        log.info(
+                "subscription value received: item={}, value={}",
+                item.getReadValueId().getNodeId(), dataValue.getValue());
+
+    }
+
+
+    public void createSubscription2(OPCServer opcServer, OPCPoint opcPoint) throws Exception {
+        OpcUaClient opcUaClient = createClient(opcServer);
+        if (null == opcUaClient) {
+            return;
+        }
+        mOPCUaPointsMap.computeIfAbsent(opcServer.getId(), k -> new HashSet<>());
+        Set<NodeId> nodeIdList = new HashSet<>();
+//        for (OPCPoint opcPoint : opcPointList) {
+//            NodeId nodeId = new NodeId(opcPoint.getNamespaceIndex(), opcPoint.getIdentifier());
+//            if (!mOPCUaPointsMap.get(opcServer.getEndpointUrl()).contains(nodeId)) {
+//                //只允许订阅一次,防止重复的数据
+//                nodeIdList.add(nodeId);
+//                mOPCUaPointsMap.get(opcServer.getEndpointUrl()).add(nodeId);
+//            }
+//        }
+        NodeId nodeId = new NodeId(opcPoint.getNamespaceIndex(), opcPoint.getPointName());
+        if (!mOPCUaPointsMap.getOrDefault(opcServer.getId(), new HashSet<>()).contains(nodeId)) {
+            //只允许订阅一次,防止重复的数据
+            nodeIdList.add(nodeId);
+            mOPCUaPointsMap.get(opcServer.getId()).add(nodeId);
+        }
+        if (ObjectUtils.isEmpty(nodeIdList)) {
+            return;
+        }
+        //添加订阅监听器,用于处理断线重连后的订阅问题
+        opcUaClient.getSubscriptionManager().addSubscriptionListener(new MySubscriptionListener() {
+            @Override
+            public void onSubscriptionTransferFailed(UaSubscription subscription, StatusCode statusCode) {
+                super.onSubscriptionTransferFailed(subscription, statusCode);
+                handleClientSubscription(opcUaClient, new ArrayList<>(nodeIdList));
+            }
+        });
+        handleClientSubscription(opcUaClient, new ArrayList<>(nodeIdList));
+    }
+
+    private void handleClientSubscription(OpcUaClient opcUaClient, List<NodeId> nodeIdList) {
+        try {
+            //创建订阅
+            ManagedSubscription managedSubscription = ManagedSubscription.create(opcUaClient, 100);
+            managedSubscription.setDefaultSamplingInterval(1000);
+            managedSubscription.setDefaultQueueSize(UInteger.valueOf(10));
+            List<ManagedDataItem> dataItemList = managedSubscription.createDataItems(nodeIdList);
+            for (ManagedDataItem dataItem : dataItemList) {
+                dataItem.addDataValueListener(this::onDataValueReceived);
+            }
+            log.info("创建订阅点成功:{}", JSON.toJSONString(nodeIdList));
+        } catch (Exception e) {
+            log.error("订阅出现异常:{}", e.getMessage(), e);
+        }
+    }
+
+    /***
+     * for createsubstrition2
+     * @param item
+     * @param dataValue
+     */
+    private void onDataValueReceived(ManagedDataItem item, DataValue dataValue) {
+        log.info("UA subscription value received: item={}, value={}", item.getReadValueId().getNodeId(), dataValue.getValue());
+        String identifier = String.valueOf(item.getReadValueId().getNodeId().getIdentifier());
+        OPCData opcData = OPCData.builder()
+                .data(dataValue.getValue().getValue())
+                .serverTime(dataValue.getServerTime() != null ? dataValue.getServerTime().getJavaDate() : null)
+                .sourceTime(dataValue.getSourceTime() != null ? dataValue.getSourceTime().getJavaDate() : null)
+                .statusCode(dataValue.getStatusCode() != null ? dataValue.getStatusCode().getValue() : StatusCode.BAD.getValue())
+                .pointName(identifier)
+                .build();
+        try {
+            // opcDataService.save(opcData);
+//            log.info("UA 订阅点数据");
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.error("保存数据失败,未保存的数据为:{}", JSON.toJSONString(opcData));
+        }
+
+
+        //出铁操作
+        asyncEventBus.post(opcData);
+    }
+
+    /**
+     * 读取节点数据
+     * <p>
+     * namespaceIndex可以通过UaExpert客户端去查询,一般来说这个值是2。
+     * identifier也可以通过UaExpert客户端去查询,这个值=通道名称.设备名称.标记名称
+     *
+     * @param opcPoint
+     * @throws Exception
+     */
+    public Object readNodeValue(OPCPoint opcPoint) throws Exception {
+        OPCPointDTO opcPointDTO = opcPointService.selectInfoWithServer(opcPoint);
+        if (ObjectUtils.isEmpty(opcPointDTO)) {
+            return null;
+        }
+        OPCServer opcServer = new OPCServer();
+        BeanUtils.copyProperties(opcPointDTO, opcServer);
+        OpcUaClient client = createClient(opcServer);
+
+        //节点
+        NodeId nodeId = new NodeId(opcPointDTO.getNamespaceIndex(), opcPointDTO.getPointName());
+
+        //读取节点数据
+        DataValue value = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();
+
+        // 状态
+        log.info("Status: " + value.getStatusCode());
+
+        //标识符
+        String id = String.valueOf(nodeId.getIdentifier());
+        log.info(id + ": " + value.getValue().getValue());
+        return value.getValue().getValue();
+    }
+
+    /**
+     * 读取节点数据
+     * <p>
+     * namespaceIndex可以通过UaExpert客户端去查询,一般来说这个值是2。
+     * identifier也可以通过UaExpert客户端去查询,这个值=通道名称.设备名称.标记名称
+     *
+     * @param client
+     * @param namespaceIndex
+     * @param identifier
+     * @throws Exception
+     */
+    public void readNodeValue(OpcUaClient client, int namespaceIndex, String identifier) throws Exception {
+        //节点
+        NodeId nodeId = new NodeId(namespaceIndex, identifier);
+
+        //读取节点数据
+        DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
+
+        // 状态
+        log.info("Status: " + value.getStatusCode());
+
+        //标识符
+        String id = String.valueOf(nodeId.getIdentifier());
+        log.info(id + ": " + value.getValue().getValue());
+    }
+
+
+    private URI getUri(String endpointUrl) {
+        try {
+            return new URI(endpointUrl);
+        } catch (URISyntaxException e) {
+            throw new OperateException("endpoint 配置异常");
+        }
+    }
+
+    private IdentityProvider identityProvider(OPCServer opcServer) {
+        log.info("SecurityPolicy:{}", opcServer.getSecurityPolicy());
+        if (ObjectUtils.isEmpty(opcServer.getSecurityPolicy()) || "none".equals(opcServer.getSecurityPolicy())) {
+            return new AnonymousProvider();
+        }
+        return new UsernameProvider(opcServer.getUsername(), opcServer.getPassword());
+    }
+
+
+    /**
+     * 项目启动时自动创建 PLC连接并订阅节点
+     */
+    //@PostConstruct
+    public void subscribeAvailable() {
+        try {
+            QueryWrapper<OPCServer> serverQueryWrapper = new QueryWrapper<>();
+            serverQueryWrapper.lambda().eq(OPCServer::getStatus, "1");
+            List<OPCServer> opcServerList = opcServerService.list(serverQueryWrapper);
+            if (ObjectUtils.isEmpty(opcServerList)) {
+                return;
+            }
+            for (OPCServer opcServer : opcServerList) {
+                QueryWrapper<OPCPoint> pointQueryWrapper = new QueryWrapper<>();
+                pointQueryWrapper.lambda().eq(OPCPoint::getOpcServerId, opcServer.getId()).eq(OPCPoint::getStatus, "1");
+                List<OPCPoint> opcPointList = opcPointService.list(pointQueryWrapper);
+                if (ObjectUtils.isNotEmpty(opcPointList)) {
+                    for (OPCPoint opcPoint : opcPointList) {
+                        createSubscription2(opcServer, opcPoint);
+                    }
+                }
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /***
+     * 取消可用订阅
+     */
+    public void unsubscribeAvailable() {
+        if (ObjectUtils.isNotEmpty(mOPCUaClientMap)) {
+            for (OpcUaClient opcUaClient : mOPCUaClientMap.values()) {
+                ImmutableList<UaSubscription> subscriptions = opcUaClient.getSubscriptionManager().getSubscriptions();
+                for (UaSubscription subscription : subscriptions) {
+                    opcUaClient.getSubscriptionManager().deleteSubscription(subscription.getSubscriptionId());
+                }
+            }
+            mOPCUaPointsMap.clear();
+        }
+    }
+
+
+    public void subscribe(OPCPoint opcPoint) {
+        try {
+            OPCPointDTO opcPointDTO = opcPointService.selectInfoWithServer(opcPoint);
+          //  QueryWrapper<OPCPoint> pointQueryWrapper = new QueryWrapper<>();
+           // pointQueryWrapper.lambda().eq(OPCPoint::getStatus, "1");
+            OPCServer opcServer = new OPCServer();
+            BeanUtils.copyProperties(opcPointDTO, opcServer);
+            createSubscription2(opcServer, opcPointDTO);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    public void unsubscribe(OPCPoint opcPoint) {
+        try {
+            OPCPointDTO opcPointDTO = opcPointService.selectInfoWithServer(opcPoint);
+           // QueryWrapper<OPCPoint> pointQueryWrapper = new QueryWrapper<>();
+           // pointQueryWrapper.lambda().eq(OPCPoint::getStatus, "1");
+            OPCServer opcServer = new OPCServer();
+            BeanUtils.copyProperties(opcPointDTO, opcServer);
+            unsubscribe(opcServer, Arrays.asList(opcPointDTO));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /***
+     * 取消订阅
+     * @param opcServer
+     * @param opcPointList
+     * @throws Exception
+     */
+    public void unsubscribe(OPCServer opcServer, List<OPCPoint> opcPointList) throws Exception {
+        OpcUaClient opcUaClient = createClient(opcServer);
+        if (null == opcUaClient) {
+            return;
+        }
+        Set<NodeId> nodeIdList = new HashSet<>();
+        for (OPCPoint opcPoint : opcPointList) {
+            NodeId nodeId = new NodeId(opcPoint.getNamespaceIndex(), opcPoint.getPointName());
+            nodeIdList.add(nodeId);
+        }
+        if (ObjectUtils.isEmpty(nodeIdList)) {
+            return;
+        }
+        try {
+            ImmutableList<UaSubscription> subscriptions = opcUaClient.getSubscriptionManager().getSubscriptions();
+            for (UaSubscription subscription : subscriptions) {
+                ImmutableList<UaMonitoredItem> monitoredItems = subscription.getMonitoredItems();
+                for (UaMonitoredItem monitoredItem : monitoredItems) {
+                    if (nodeIdList.contains(monitoredItem.getReadValueId().getNodeId())) {
+                        mOPCUaPointsMap.get(opcServer.getId()).remove(monitoredItem.getReadValueId().getNodeId());
+                        opcUaClient.getSubscriptionManager().deleteSubscription(subscription.getSubscriptionId());
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("取消订阅出现异常:{}", e.getMessage(), e);
+        }
+    }
+
+
+    @PreDestroy
+    public void releaseServer() {
+
+    }
+
+}

+ 17 - 0
taphole-opc/src/main/java/com/sckj/opc/service/OPCDataServiceImpl.java

@@ -0,0 +1,17 @@
+package com.sckj.opc.service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckj.opc.entity.OPCData;
+import com.sckj.opc.entity.OPCPoint;
+import com.sckj.opc.mapper.OPCDataMapper;
+import com.sckj.opc.mapper.OPCPointMapper;
+import org.springframework.stereotype.Service;
+
+/**
+ * @Author feng
+ * @Date 2024-11-15 下午 04:39
+ * @Description TODO
+ */
+@Service
+public class OPCDataServiceImpl extends ServiceImpl<OPCDataMapper, OPCData> {
+}

+ 27 - 0
taphole-opc/src/main/java/com/sckj/opc/service/OPCPointServiceImpl.java

@@ -0,0 +1,27 @@
+package com.sckj.opc.service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckj.opc.dto.OPCPointDTO;
+import com.sckj.opc.entity.OPCPoint;
+import com.sckj.opc.mapper.OPCPointMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Author feng
+ * @Date 2024-11-15 下午 04:39
+ * @Description TODO
+ */
+@Service
+public class OPCPointServiceImpl  extends ServiceImpl<OPCPointMapper, OPCPoint> {
+
+    @Resource
+    OPCPointMapper opcPointMapper;
+
+    public OPCPointDTO selectInfoWithServer(OPCPoint opcPoint){
+        return opcPointMapper.selectInfoWithServer(opcPoint);
+    }
+}

+ 15 - 0
taphole-opc/src/main/java/com/sckj/opc/service/OPCServerServiceImpl.java

@@ -0,0 +1,15 @@
+package com.sckj.opc.service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckj.opc.entity.OPCServer;
+import com.sckj.opc.mapper.OPCServerMapper;
+import org.springframework.stereotype.Service;
+
+/**
+ * @Author feng
+ * @Date 2024-11-15 下午 04:39
+ * @Description TODO
+ */
+@Service
+public class OPCServerServiceImpl extends ServiceImpl<OPCServerMapper, OPCServer> {
+}

+ 80 - 0
taphole-opc/src/main/java/com/sckj/opc/utils/CustomUtil.java

@@ -0,0 +1,80 @@
+package com.sckj.opc.utils;
+
+import com.google.common.collect.Sets;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.springframework.util.StringUtils;
+
+import java.net.*;
+import java.util.*;
+
+/**
+ * @author kangaroo hy
+ * @version 0.0.1
+ * @desc
+ * @since 2020/4/13
+ */
+@Slf4j
+public class CustomUtil {
+
+    private static final String OPC_UA_NOT_CONFIG = "请配置OPC UA地址信息";
+
+    private CustomUtil() {
+    }
+
+    public static String getHostname() {
+        try {
+            return InetAddress.getLocalHost().getHostName();
+        } catch (UnknownHostException var1) {
+            return "localhost";
+        }
+    }
+
+    public static Set<String> getHostnames(String address) {
+        return getHostnames(address, true);
+    }
+
+    public static Set<String> getHostnames(String address, boolean includeLoopback) {
+        HashSet<String> hostnames = Sets.newHashSet();
+
+        try {
+            InetAddress inetAddress = InetAddress.getByName(address);
+            if (inetAddress.isAnyLocalAddress()) {
+                try {
+                    Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces();
+
+                    for (NetworkInterface ni : Collections.list(nis)) {
+                        Collections.list(ni.getInetAddresses()).forEach((ia) -> {
+                            if (ia instanceof Inet4Address) {
+                                boolean loopback = ia.isLoopbackAddress();
+                                if (!loopback || includeLoopback) {
+                                    hostnames.add(ia.getHostName());
+                                    hostnames.add(ia.getHostAddress());
+                                    hostnames.add(ia.getCanonicalHostName());
+                                }
+                            }
+
+                        });
+                    }
+                } catch (SocketException var7) {
+                    log.warn("Failed to NetworkInterfaces for bind address: {}", address, var7);
+                }
+            } else {
+                boolean loopback = inetAddress.isLoopbackAddress();
+                if (!loopback || includeLoopback) {
+                    hostnames.add(inetAddress.getHostName());
+                    hostnames.add(inetAddress.getHostAddress());
+                    hostnames.add(inetAddress.getCanonicalHostName());
+                }
+            }
+        } catch (UnknownHostException var8) {
+            log.warn("Failed to get InetAddress for bind address: {}", address, var8);
+        }
+
+        return hostnames;
+    }
+
+
+
+
+}

+ 152 - 0
taphole-opc/src/main/java/com/sckj/opc/utils/KeyStoreLoader.java

@@ -0,0 +1,152 @@
+/**
+ * Created by Jellyleo on 2019年12月19日
+ * Copyright © 2019 jellyleo.com 
+ * All rights reserved. 
+ */
+package com.sckj.opc.utils;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil;
+import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder;
+import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.cert.X509Certificate;
+import java.util.regex.Pattern;
+
+/**
+ * @ClassName: KeyStoreLoader
+ * @Description: KeyStoreLoader
+ * @author Jellyleo
+ * @date 2019年12月11日
+ */
+public class KeyStoreLoader {
+
+	private static final Pattern IP_ADDR_PATTERN = Pattern
+			.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
+
+	// 证书别名
+	private static final String CLIENT_ALIAS = "jlclient-ai";
+	// 获取私钥的密码
+	private static final char[] PASSWORD = "123456".toCharArray();
+
+	private final Logger logger = LoggerFactory.getLogger(getClass());
+
+	// 证书对象
+	private X509Certificate clientCertificate;
+	// 密钥对对象
+	private KeyPair clientKeyPair;
+
+	public static void main(String[] args) throws Exception {
+		new KeyStoreLoader().load("D:\\project\\xiha\\opcserver\\security");
+	}
+
+	/**
+	 * @MethodName: load
+	 * @Description: load
+	 * @param certPath
+	 * @return
+	 * @throws Exception
+	 * @CreateTime 2019年12月11日 下午4:02:43
+	 */
+	public KeyStoreLoader load(String certPath) throws Exception {
+
+		if(ObjectUtils.isEmpty(certPath)){
+			certPath = System.getProperty("java.io.tmpdir");
+		}
+
+		Path securityTempDir = Paths.get(certPath, "security");
+
+		Files.createDirectories(securityTempDir);
+		if (!Files.exists(securityTempDir)) {
+			System.err.println("unable to create security dir: " + securityTempDir);
+			return null;
+		}
+
+		// 创建一个使用`PKCS12`加密标准的KeyStore。KeyStore在后面将作为读取和生成证书的对象。
+		KeyStore keyStore = KeyStore.getInstance("PKCS12");
+
+		// PKCS12的加密标准的文件后缀是.pfx,其中包含了公钥和私钥。
+		// 而其他如.der等的格式只包含公钥,私钥在另外的文件中。
+		Path serverKeyStore = securityTempDir.resolve("jellyleo-client.pfx");
+
+		logger.info("Loading KeyStore at {}", serverKeyStore);
+
+		// 如果文件不存在则创建.pfx证书文件。
+		if (!Files.exists(serverKeyStore)) {
+			keyStore.load(null, PASSWORD);
+
+			// 用2048位的RAS算法。`SelfSignedCertificateGenerator`为Milo库的对象。
+			KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048);
+
+			// `SelfSignedCertificateBuilder`也是Milo库的对象,用来生成证书。
+			// 中间所设置的证书属性可以自行修改。
+			SelfSignedCertificateBuilder builder = new SelfSignedCertificateBuilder(keyPair)
+					.setCommonName("UaClient@Jellyleo")
+					.setOrganization("JL")
+					.setOrganizationalUnit("per")
+					.setLocalityName("jl")
+					.setStateName("CN")
+					.setCountryCode("CN")
+					.setApplicationUri("urn:Jellyleo:UnifiedAutomation:UaExpert@Jellyleo")
+					.addDnsName("localhost")
+					.addIpAddress("127.0.0.1");
+
+			// Get as many hostnames and IP addresses as we can listed in the certificate.
+			for (String hostname : HostnameUtil.getHostnames("0.0.0.0")) {
+				System.out.println("print hostname");
+				System.out.println(hostname);
+				if (IP_ADDR_PATTERN.matcher(hostname).matches()) {
+					builder.addIpAddress(hostname);
+				} else {
+					builder.addDnsName(hostname);
+				}
+			}
+			// 创建证书
+			X509Certificate certificate = builder.build();
+
+			// 设置对应私钥的别名,密码,证书链
+			keyStore.setKeyEntry(CLIENT_ALIAS, keyPair.getPrivate(), PASSWORD, new X509Certificate[] { certificate });
+			try (OutputStream out = Files.newOutputStream(serverKeyStore)) {
+				// 保存证书到输出流
+				keyStore.store(out, PASSWORD);
+			}
+		} else {
+			try (InputStream in = Files.newInputStream(serverKeyStore)) {
+				// 如果文件存在则读取
+				keyStore.load(in, PASSWORD);
+			}
+		}
+
+		// 用密码获取对应别名的私钥。
+		Key serverPrivateKey = keyStore.getKey(CLIENT_ALIAS, PASSWORD);
+		if (serverPrivateKey instanceof PrivateKey) {
+			// 获取对应别名的证书对象。
+			clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS);
+			// 获取公钥
+			PublicKey serverPublicKey = clientCertificate.getPublicKey();
+			// 创建Keypair对象。
+			clientKeyPair = new KeyPair(serverPublicKey, (PrivateKey) serverPrivateKey);
+		}
+
+		return this;
+	}
+
+	// 返回证书
+	public X509Certificate getClientCertificate() {
+		return clientCertificate;
+	}
+
+	// 返回密钥对
+	public KeyPair getClientKeyPair() {
+		return clientKeyPair;
+	}
+}

+ 24 - 0
taphole-opc/src/main/resources/mapper/OPCPointMapper.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckj.opc.mapper.OPCPointMapper">
+
+
+    <select id="selectInfoWithServer" resultType="com.sckj.opc.dto.OPCPointDTO">
+        select a.*,b.application_name,b.application_uri,b.endpoint_url,b.ip,b.clsid,b.username,b.password from
+        t_opc_point a left join t_opc_server b on a.opc_server_id=b.id
+        <where>
+            <if test="id != null">
+                a.id = #{id}
+            </if>
+        </where>
+        limit 1
+    </select>
+
+    <select id="getAvailablePoints" resultType="com.sckj.opc.dto.OPCPointDTO">
+        select a.*,b.application_name,b.application_uri,b.endpoint_url,b.ip,b.clsid,b.username,b.password from t_opc_point a
+          inner join (select * from t_opc_server where status='1') b on a.opc_server_id=b.id
+        where a.status = '1'
+    </select>
+
+
+</mapper>