From ab6d1810751918bc45dedfc1802fea2b4da61eee Mon Sep 17 00:00:00 2001 From: yinhuaiwei Date: Tue, 22 Sep 2026 18:44:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B4=E5=90=88=E6=B5=B7=E5=BA=B7?= =?UTF-8?q?=E3=80=81=E5=A4=A7=E5=8D=8E=E3=80=81=E6=9C=97=E9=A9=B0=E3=80=81?= =?UTF-8?q?=E6=9C=97=E9=A9=B0=E6=B5=B7=E5=BA=B7=E7=9B=B8=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdkLog/SdkLog_1_W.log | 4 - .../com/inspect/nvr/config/SdkCleanup.java | 6 +- .../nvr/controller/CameraController.java | 6 +- .../nvr/controller/IvsCameraController.java | 10 - .../inspect/nvr/domain/Infrared/Camera.java | 53 ++++- .../nvr/domain/Infrared/InfraredInfo.java | 6 +- .../com/inspect/nvr/enums/CameraEnum.java | 15 +- .../nvr/jna/lincseek/IRNetSDKConst.java | 9 + .../nvr/service/DahuaCameraService.java | 152 ------------- .../nvr/service/DahuaLoginService.java | 126 ----------- .../inspect/nvr/service/HikLoginService.java | 125 ----------- .../inspect/nvr/service/IvsCameraService.java | 2 - .../nvr/service/LincseekCameraService.java | 186 ---------------- .../nvr/service/LincseekLoginService.java | 123 ----------- .../service/camera/AbstractCameraService.java | 186 ++++++++++++++++ .../AbstractCameraSupport.java} | 178 ++++++++++----- .../service/camera/BrandCameraService.java | 47 ++++ .../service/camera/CameraServiceFactory.java | 54 +++++ .../service/camera/DahuaCameraService.java | 204 ++++++++++++++++++ .../{ => camera}/HikCameraService.java | 195 ++++++++++------- .../camera/LincHikCompositeCameraService.java | 78 +++++++ .../service/camera/LincseekCameraService.java | 193 +++++++++++++++++ .../camera/login/AbstractLoginService.java | 114 ++++++++++ .../camera/login/DahuaLoginService.java | 54 +++++ .../service/camera/login/HikLoginService.java | 47 ++++ .../camera/login/LincseekLoginService.java | 53 +++++ .../nvr/service/impl/DahuaServiceImpl.java | 16 +- .../service/impl/HikVisionServiceImpl.java | 20 +- .../service/impl/IvsCameraServiceImpl.java | 156 ++++---------- .../inspect/nvr/task/ImageCleanupTask.java | 2 +- .../com/inspect/nvr/utils/DLT664Service.java | 193 +++++++++++++++++ src/main/resources/application.yml | 2 +- 32 files changed, 1590 insertions(+), 1025 deletions(-) delete mode 100644 sdkLog/SdkLog_1_W.log delete mode 100644 src/main/java/com/inspect/nvr/service/DahuaCameraService.java delete mode 100644 src/main/java/com/inspect/nvr/service/DahuaLoginService.java delete mode 100644 src/main/java/com/inspect/nvr/service/HikLoginService.java delete mode 100644 src/main/java/com/inspect/nvr/service/LincseekCameraService.java delete mode 100644 src/main/java/com/inspect/nvr/service/LincseekLoginService.java create mode 100644 src/main/java/com/inspect/nvr/service/camera/AbstractCameraService.java rename src/main/java/com/inspect/nvr/service/{CommonCameraService.java => camera/AbstractCameraSupport.java} (67%) create mode 100644 src/main/java/com/inspect/nvr/service/camera/BrandCameraService.java create mode 100644 src/main/java/com/inspect/nvr/service/camera/CameraServiceFactory.java create mode 100644 src/main/java/com/inspect/nvr/service/camera/DahuaCameraService.java rename src/main/java/com/inspect/nvr/service/{ => camera}/HikCameraService.java (53%) create mode 100644 src/main/java/com/inspect/nvr/service/camera/LincHikCompositeCameraService.java create mode 100644 src/main/java/com/inspect/nvr/service/camera/LincseekCameraService.java create mode 100644 src/main/java/com/inspect/nvr/service/camera/login/AbstractLoginService.java create mode 100644 src/main/java/com/inspect/nvr/service/camera/login/DahuaLoginService.java create mode 100644 src/main/java/com/inspect/nvr/service/camera/login/HikLoginService.java create mode 100644 src/main/java/com/inspect/nvr/service/camera/login/LincseekLoginService.java create mode 100644 src/main/java/com/inspect/nvr/utils/DLT664Service.java diff --git a/sdkLog/SdkLog_1_W.log b/sdkLog/SdkLog_1_W.log deleted file mode 100644 index 3d4ca9e7..00000000 --- a/sdkLog/SdkLog_1_W.log +++ /dev/null @@ -1,4 +0,0 @@ -[2025-09-16 11:20:34.679][DBG] CCoreGlobalCtrlBase::LoadDSo, HPR_LoadDSo Succ, Path[D:/workspace/inspect-nvr/lib/zlib1.dll], hHandleRet[1841233920] -[2025-09-16 11:20:34.679][INF] The COM:HCCoreBase ver is 6.1.4.15, 2020_03_05. Async:1. -[2025-09-16 11:20:34.679][INF] The COM:Core ver is 6.1.9.47, 2022_11_11. Async:1. -[2025-09-16 11:20:34.679][INF] This HCNetSDK ver is 6.1.9.47 Ver 2022_11_11. diff --git a/src/main/java/com/inspect/nvr/config/SdkCleanup.java b/src/main/java/com/inspect/nvr/config/SdkCleanup.java index d1370cdc..db822a24 100644 --- a/src/main/java/com/inspect/nvr/config/SdkCleanup.java +++ b/src/main/java/com/inspect/nvr/config/SdkCleanup.java @@ -3,9 +3,9 @@ package com.inspect.nvr.config; import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; import com.inspect.nvr.jna.lincseek.IRNetSDK; -import com.inspect.nvr.service.DahuaLoginService; -import com.inspect.nvr.service.HikLoginService; -import com.inspect.nvr.service.LincseekLoginService; +import com.inspect.nvr.service.camera.login.DahuaLoginService; +import com.inspect.nvr.service.camera.login.HikLoginService; +import com.inspect.nvr.service.camera.login.LincseekLoginService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/inspect/nvr/controller/CameraController.java b/src/main/java/com/inspect/nvr/controller/CameraController.java index ef9f1255..00dbb9c3 100644 --- a/src/main/java/com/inspect/nvr/controller/CameraController.java +++ b/src/main/java/com/inspect/nvr/controller/CameraController.java @@ -7,7 +7,7 @@ import com.inspect.nvr.hikVision.utils.AjaxResult; import com.inspect.nvr.service.DahuaService; import com.inspect.nvr.service.HikVisionService; import com.inspect.nvr.service.IvsCameraService; -import com.inspect.nvr.service.LincseekCameraService; +import com.inspect.nvr.service.camera.LincseekCameraService; import com.inspect.nvr.utils.redis.RedisService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -52,7 +52,9 @@ public class CameraController { if (CameraEnum.DAHUA.getCode() == camera.getCameraType()) { temperatureData = dahuaService.StartRemote(camera); } else if (CameraEnum.LINCSEEK.getCode() == camera.getCameraType()) { - temperatureData = lincseekCameraService.getTemp(camera); + temperatureData = lincseekCameraService.measureTemperature(camera); + } else if (CameraEnum.LINC_HIK.getCode() == camera.getCameraType()) { + temperatureData = lincseekCameraService.measureTemperature(camera); } else { temperatureData = executeWithTimeout(() -> cameraService.StartRemote(camera), 7, TimeUnit.SECONDS); // temperatureData = cameraService.StartRemote(camera); diff --git a/src/main/java/com/inspect/nvr/controller/IvsCameraController.java b/src/main/java/com/inspect/nvr/controller/IvsCameraController.java index 3e153ba5..df570a17 100644 --- a/src/main/java/com/inspect/nvr/controller/IvsCameraController.java +++ b/src/main/java/com/inspect/nvr/controller/IvsCameraController.java @@ -99,14 +99,4 @@ public class IvsCameraController { ByteArrayInputStream byteArrayInputStream = ivsCameraService.capture(fileSessionId); return ResponseEntity.ok().body(new InputStreamResource(byteArrayInputStream)); } - - @TimeTrace - @GetMapping("/digest") - public ResponseEntity digest(@RequestParam("filesessionid") String fileSessionId) { - byte[] bytes = ivsCameraService.captureDigest(fileSessionId); - if (bytes == null || bytes.length == 0) { - return ResponseEntity.ok().body("Not OK"); - } - return ResponseEntity.ok().body("OK"); - } } diff --git a/src/main/java/com/inspect/nvr/domain/Infrared/Camera.java b/src/main/java/com/inspect/nvr/domain/Infrared/Camera.java index 601b86cc..f868a624 100644 --- a/src/main/java/com/inspect/nvr/domain/Infrared/Camera.java +++ b/src/main/java/com/inspect/nvr/domain/Infrared/Camera.java @@ -50,4 +50,55 @@ public class Camera { private int presetId; private String address; -} + + // 测温规则 + private int ruleId = 1; + + /** + * 摄像头直连解析:连接信息取摄像头本身字段(7:ip 8:port 9:channel 10:用户名 11:密码)。 + */ + public static Camera fromAddressString(String rawString) { + return parse(rawString, false); + } + + /** + * NVR 接入解析:连接信息取 NVR 字段(0:ip 1:port 2:NVR通道 5:用户名 6:密码)。 + */ + public static Camera fromNvrAddressString(String rawString) { + return parse(rawString, true); + } + + /** + * 解析相机地址串(冒号分隔,共 12 段): + * 0:NVRip 1:NVR端口 2:NVR通道 3:预置点位 4:摄像头品牌 5:NVR用户名 6:NVR密码 + * 7:摄像头ip 8:摄像头端口 9:摄像头通道 10:摄像头用户名 11:摄像头密码 + * viaNvr=false 取摄像头本身信息(7/8/9/10/11);viaNvr=true 取 NVR 接入信息(0/1/2/5/6)。 + * pointNum/cameraType 两种模式共用(3/4);NVR 冗余字段不填充,ruleId/lUserID 保留默认值。 + */ + private static Camera parse(String rawString, boolean viaNvr) { + if (rawString == null || rawString.trim().isEmpty()) { + throw new IllegalArgumentException("相机地址串不能为空"); + } + String[] p = rawString.trim().split(":"); + if (p.length != 12) { + throw new IllegalArgumentException("相机地址串段数应为12,实际" + p.length + ":" + rawString); + } + Camera camera = new Camera(); + if (viaNvr) { + camera.setIp(p[0]); + camera.setPort(Integer.parseInt(p[1])); + camera.setChannel(Integer.parseInt(p[2])); + camera.setUserName(p[5]); + camera.setPassword(p[6]); + } else { + camera.setIp(p[7]); + camera.setPort(Integer.parseInt(p[8])); + camera.setChannel(Integer.parseInt(p[9])); + camera.setUserName(p[10]); + camera.setPassword(p[11]); + } + camera.setPointNum(Integer.parseInt(p[3])); + camera.setCameraType(Integer.parseInt(p[4])); + return camera; + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/domain/Infrared/InfraredInfo.java b/src/main/java/com/inspect/nvr/domain/Infrared/InfraredInfo.java index fe12c1b1..4efc8132 100644 --- a/src/main/java/com/inspect/nvr/domain/Infrared/InfraredInfo.java +++ b/src/main/java/com/inspect/nvr/domain/Infrared/InfraredInfo.java @@ -11,7 +11,7 @@ import lombok.NoArgsConstructor; public class InfraredInfo { //文件版本 - private String fileVersion; + private short fileVersion; //矩阵宽度 private short matrixWidth; //矩阵高度 @@ -25,7 +25,7 @@ public class InfraredInfo { //镜头度数 private byte lensDegree; //拍摄距离 - private long distance; + private long distance; //相对湿度 private byte humidity; //反射温度 @@ -65,6 +65,4 @@ public class InfraredInfo { //标注后图片路径 private String outPath; - - } diff --git a/src/main/java/com/inspect/nvr/enums/CameraEnum.java b/src/main/java/com/inspect/nvr/enums/CameraEnum.java index 4d1bbfbf..fe28de7d 100644 --- a/src/main/java/com/inspect/nvr/enums/CameraEnum.java +++ b/src/main/java/com/inspect/nvr/enums/CameraEnum.java @@ -8,18 +8,21 @@ import lombok.Getter; @Getter public enum CameraEnum { // 海康威视 - HIKVISION(0), + HIKVISION(0, "hik", "海康"), // 大华 - DAHUA(1), + DAHUA(1, "dahua", "大华"), // 朗驰欣创 - LINCSEEK(2), + LINCSEEK(2, "linc", "朗驰"), // 海康朗驰(海康部分:可见光、云台;朗驰部分:热成像,测温) - // todo 待实现 - HIKLINC(3); + LINC_HIK(3,"linc&hik", "朗驰海康"); final private int code; + final private String value; + final private String name; - CameraEnum(int code) { + CameraEnum(int code, String value, String name) { this.code = code; + this.value = value; + this.name = name; } } diff --git a/src/main/java/com/inspect/nvr/jna/lincseek/IRNetSDKConst.java b/src/main/java/com/inspect/nvr/jna/lincseek/IRNetSDKConst.java index 99f037d9..1b6ca88b 100644 --- a/src/main/java/com/inspect/nvr/jna/lincseek/IRNetSDKConst.java +++ b/src/main/java/com/inspect/nvr/jna/lincseek/IRNetSDKConst.java @@ -270,6 +270,15 @@ public interface IRNetSDKConst { int EN_FT_SDK_CHANNEL_JPG = 1; // 通道JPG int EN_FT_SDK_CHANNEL_BMP = 2; // 通道BMP + // ───────────────────────────────────────────────────────────────────── + // 抓图文件类型 + // ───────────────────────────────────────────────────────────────────── + int VSNET_CAPTURE_TYPE_FFF = 0; + int VSNET_CAPTURE_TYPE_TIFF = 1; + int VSNET_CAPTURE_TYPE_RAW = 2; + int VSNET_CAPTURE_TYPE_JPG = 3; + int VSNET_CAPTURE_TYPE_LCR = 4; + // ───────────────────────────────────────────────────────────────────── // 调色板模式 // ───────────────────────────────────────────────────────────────────── diff --git a/src/main/java/com/inspect/nvr/service/DahuaCameraService.java b/src/main/java/com/inspect/nvr/service/DahuaCameraService.java deleted file mode 100644 index ead86b99..00000000 --- a/src/main/java/com/inspect/nvr/service/DahuaCameraService.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.inspect.nvr.service; - -import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; -import com.inspect.nvr.daHuaCarme.jna.dahua.ToolKits; -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.sun.jna.Pointer; -import com.sun.jna.ptr.IntByReference; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * 大华设备SDK服务 - * 同一个IP,同一个通道,串行 - * 同一个IP,不同通道,最多4个线程并发 - * 不同IP,并发抓图,不限制 - */ -@Slf4j -@Service -public class DahuaCameraService extends CommonCameraService { - /** - * Digest认证抓图URL: - * http:///cgi-bin/snapshot.cgi?channel=&subtype= - * subtype: 0-主码流, 1-子码流 - */ - private static final String DIGEST_URL_TEMPLATE = "http://%s/cgi-bin/snapshot.cgi?channel=%d&subtype=1"; - // Key: loginId+CmdSerial (登录句柄+流水号), Value: CompletableFuture (用于通知调用线程) - private static final ConcurrentHashMap> PENDING_REQUESTS = new ConcurrentHashMap<>(); - private static final fCaptureReceiveCB CAPTURE_RECEIVE_CB = new fCaptureReceiveCB(); - // 1. [新增]全局流水号生成器 - private static final AtomicInteger SERIAL_COUNTER = new AtomicInteger(1); - @Resource - private NetSDKLib dhNetSDK; - @Resource - private DahuaLoginService dahuaLoginService; - - // CmdSerial请求序列号,有效值范围 0~65535,超过范围会被截断 - public static int nextSerial() { - return SERIAL_COUNTER.updateAndGet(current -> (current + 1) & 0xFFFF); - } - - public byte[] capture(NvrInfo nvrInfo, int channel) { - return withConcurrencyControl(nvrInfo, channel, () -> captureWithRetry(nvrInfo, channel)); - } - - private byte[] captureWithRetry(NvrInfo nvrInfo, int channel) { - Path fullPath = getFullPath("dh", nvrInfo.getNvrIp(), channel); - int retryCount = 0; - int maxRetries = DEFAULT_MAX_RETRIES; - while (retryCount < maxRetries) { - try { - byte[] imageBytes = snapPictureEx(nvrInfo, channel, fullPath); - log.info("[大华]抓图成功:第{}次,文件地址:{}", retryCount + 1, fullPath); - return imageBytes; - } catch (Exception e) { - log.error("[大华]抓图异常:第{}次,{}", retryCount + 1, e.getMessage()); - } finally { - retryCount++; - } - } - // 当SDK抓图失败时,尝试使用Digest认证抓图 - byte[] imageBytes = captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, fullPath); - if (imageBytes == null) { - // 当所有抓图方式均失败时,记录失败图片 - writeCaptureFailedImage(fullPath); - log.error("[大华]所有抓图方式均失败,图片地址:{}", fullPath); - return new byte[0]; - } - log.info("[大华]digest抓图成功,图片地址:{}", fullPath); - return imageBytes; - } - - /** - * 大华SDK抓图具体实现(异步) - * 通过CompletableFuture实现异步回调通知 - */ - private byte[] snapPictureEx(NvrInfo nvrInfo, int channel, Path fullPath) { - NetSDKLib.LLong loginID = dahuaLoginService.login(nvrInfo); - - NetSDKLib.SNAP_PARAMS snapParams = new NetSDKLib.SNAP_PARAMS(); - snapParams.Channel = channel - 1; // 通道号从0开始 - snapParams.mode = 0; // 抓图模式:0-单次抓 - snapParams.Quality = 3; - snapParams.InterSnap = 0; - int mySerialId = nextSerial(); - snapParams.CmdSerial = mySerialId; - IntByReference reference = new IntByReference(0); - // 设置异步抓图回调函数 - dhNetSDK.CLIENT_SetSnapRevCallBack(CAPTURE_RECEIVE_CB, null); - - String requestKey = loginID.longValue() + ":" + mySerialId; - CompletableFuture future = new CompletableFuture<>(); - PENDING_REQUESTS.put(requestKey, future); - final int TIMEOUT_SEC = 5; - try { - log.info("[大华]开始抓图,LoginID={},IP={},Channel={},Serial={}", loginID, nvrInfo.getNvrIp(), channel, mySerialId); - boolean isCaptured = dhNetSDK.CLIENT_SnapPictureEx(loginID, snapParams, reference); - if (!isCaptured) { - String errorMsg = ToolKits.getErrorCodePrint(dhNetSDK.CLIENT_GetLastError()); - throw new RuntimeException("SDK抓图失败:" + errorMsg); - } - byte[] byteArray = future.get(TIMEOUT_SEC, TimeUnit.SECONDS); - Files.write(fullPath, byteArray); - return byteArray; - } catch (TimeoutException e) { - log.error("[大华]抓图超时:在 {} 秒内未收到设备回调,requestKey={}", TIMEOUT_SEC, requestKey); - } catch (Exception e) { - log.error("[大华]抓图异常:", e); - } finally { - PENDING_REQUESTS.remove(requestKey); - } - return null; - } - - /** - * Digest认证抓图 - */ - public byte[] captureDigest(NvrInfo nvrInfo, int channel) { - return captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, getFullPath("dh_digest", nvrInfo.getNvrIp(), channel)); - } - - /** - * CLIENT_SnapPictureEx异步抓图回调函数重写 - */ - public static class fCaptureReceiveCB implements NetSDKLib.fSnapRev { - @Override - public void invoke(NetSDKLib.LLong lLoginID, Pointer pBuf, int RevLen, int EncodeType, int CmdSerial, Pointer dwUser) { - // 1. 检查是否有等待该流水号的请求 - String requestKey = lLoginID.longValue() + ":" + CmdSerial; - CompletableFuture future = PENDING_REQUESTS.remove(requestKey); - if (future != null) { - log.info("[大华]匹配到抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial); - if (pBuf != null && RevLen > 0) { - // 2. 读取图片数据 - byte[] data = pBuf.getByteArray(0, RevLen); - // 3. 完成Future,通知主线程 - future.complete(data); - } else { - future.completeExceptionally(new RuntimeException("Empty image data")); - } - } else { - // 可能是由于超时已经被移除了,或者是其他类型的抓图 - log.error("[大华]收到未匹配的抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial); - } - } - } -} diff --git a/src/main/java/com/inspect/nvr/service/DahuaLoginService.java b/src/main/java/com/inspect/nvr/service/DahuaLoginService.java deleted file mode 100644 index 502ab8a4..00000000 --- a/src/main/java/com/inspect/nvr/service/DahuaLoginService.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.inspect.nvr.service; - -import com.alibaba.fastjson.JSONObject; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.RemovalCause; -import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; -import com.inspect.nvr.daHuaCarme.jna.NetSDKLib.LLong; -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.utils.redis.RedisService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; -import java.util.concurrent.TimeUnit; - -/** - * 大华登录服务 - */ -@Slf4j -@Service -public class DahuaLoginService { - private static final String ERROR_LOGOUT_KEY = "dahua:error"; - - @Autowired - private NetSDKLib dhNetSDK; - @Resource - private RedisService redisService; - - // 使用 Caffeine 缓存,10分钟未访问自动移除并登出 - private final Cache sessionCache = Caffeine.newBuilder() - // 10分钟未被get()就过期 - .expireAfterAccess(10, TimeUnit.MINUTES) - .removalListener((String ip, LLong loginID, RemovalCause cause) -> { - if (loginID != null && (cause == RemovalCause.EXPIRED || cause == RemovalCause.SIZE)) { - log.info("[大华]会话超时自动登出,ip: {},loginID: {}", ip, loginID); - doLogout(ip, loginID); - } - }).build(); - - public synchronized LLong login(NvrInfo nvrInfo) { - String ip = nvrInfo.getNvrIp(); - LLong existLoginID = sessionCache.getIfPresent(ip); - if (existLoginID != null) { - log.info("[大华]登录命中缓存,ip: {},loginID: {}", ip, existLoginID); - return existLoginID; - } - // 执行登录 - NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam = new NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY(); - pstInParam.szIP = nvrInfo.getNvrIp().getBytes(); - pstInParam.nPort = nvrInfo.getServerPort(); - pstInParam.szUserName = nvrInfo.getAccount().getBytes(); - pstInParam.szPassword = nvrInfo.getPassword().getBytes(); - NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam = new NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY(); - LLong loginID = dhNetSDK.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam); - if (loginID.intValue() == 0) { - int errorCode = dhNetSDK.CLIENT_GetLastError(); - throw new RuntimeException("登录失败,错误码:" + errorCode); - } - // 放入缓存,自动开始计时10分钟 - sessionCache.put(nvrInfo.getNvrIp(), loginID); - log.info("[大华]登录成功,ip:{},loginID:{}", ip, loginID); - return loginID; - } - - /** - * 记录注销失败信息到 Redis - */ - private void recordLogoutError(String ip, LLong loginID, int errorCode) { - JSONObject json = new JSONObject(); - json.put("ip", ip); - json.put("userId", loginID); - json.put("errorCode", errorCode); - json.put("time", System.currentTimeMillis()); - redisService.redisTemplate.opsForZSet().add(ERROR_LOGOUT_KEY, json.toJSONString(), System.currentTimeMillis()); - } - - /** - * 登出具体实现 - */ - public void doLogout(String ip, LLong loginID) { - if (loginID != null) { - // 执行登出操作 - boolean isLogout = dhNetSDK.CLIENT_Logout(loginID); - if (isLogout) { - log.info("[大华]自动注销成功,ip: {},loginID: {}", ip, loginID.longValue()); - } else { - int errorCode = dhNetSDK.CLIENT_GetLastError(); - log.error("[大华]自动注销失败,ip: {},loginID: {},错误码: {}", ip, loginID.longValue(), errorCode); - // 记录失败日志到 Redis - recordLogoutError(ip, loginID, errorCode); - } - } - } - - /** - * 登出 - */ - public synchronized void logout(String ip) { - LLong loginID = sessionCache.getIfPresent(ip); - if (loginID != null) { - sessionCache.invalidate(ip); - } - } - - /** - * 登出所有用户 - */ - public void logoutAll() { - // 获取所有缓存的IP和userID - sessionCache.asMap().forEach((ip, loginID) -> { - doLogout(ip, loginID); - }); - // 清空整个缓存 - sessionCache.invalidateAll(); - log.info("[大华]所有用户已登出"); - } - - /** - * 检查是否已登录(同时刷新过期时间) - */ - public boolean isLoggedIn(String ip) { - return sessionCache.getIfPresent(ip) != null; - } -} diff --git a/src/main/java/com/inspect/nvr/service/HikLoginService.java b/src/main/java/com/inspect/nvr/service/HikLoginService.java deleted file mode 100644 index 813c92c3..00000000 --- a/src/main/java/com/inspect/nvr/service/HikLoginService.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.inspect.nvr.service; - -import com.alibaba.fastjson.JSONObject; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.RemovalCause; -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; -import com.inspect.nvr.hikVision.utils.jna.HikVisionUtils; -import com.inspect.nvr.utils.redis.RedisService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; -import java.util.concurrent.TimeUnit; - -/** - * 海康登录服务统一管理 - */ -@Slf4j -@Service -public class HikLoginService { - private static final String ERROR_LOGOUT_KEY = "hik:error"; - @Resource - private RedisService redisService; - - @Autowired - private HCNetSDK hcNetSDK; - - // 使用 Caffeine 缓存,10分钟未访问自动移除并登出 - private final Cache sessionCache = Caffeine.newBuilder() - // 10分钟未被get()就过期 - .expireAfterAccess(10, TimeUnit.MINUTES) - .removalListener((String ip, Integer userID, RemovalCause cause) -> { - if (userID != null && (cause == RemovalCause.EXPIRED || cause == RemovalCause.SIZE)) { - log.info("[海康]会话超时自动登出,ip: {},userID: {}", ip, userID); - doLogout(ip, userID); - } - }).build(); - - /** - * 登录 - */ - public synchronized int login(NvrInfo nvrInfo) { - String ip = nvrInfo.getNvrIp(); - Integer existUserId = sessionCache.getIfPresent(ip); - if (existUserId != null) { - log.info("[海康]登录命中缓存,ip: {},userID: {}", ip, existUserId); - return existUserId; - } - // 执行登录 - HCNetSDK.NET_DVR_USER_LOGIN_INFO m_strLoginInfo = HikVisionUtils.login_V40(nvrInfo.getNvrIp(), nvrInfo.getServerPort().shortValue(), nvrInfo.getAccount(), nvrInfo.getPassword()); - HCNetSDK.NET_DVR_DEVICEINFO_V40 m_strDeviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40(); - int userID = hcNetSDK.NET_DVR_Login_V40(m_strLoginInfo, m_strDeviceInfo); - if (userID < 0) { - int errorCode = hcNetSDK.NET_DVR_GetLastError(); - throw new RuntimeException("登录失败,错误码:" + errorCode); - } - // 放入缓存,自动开始计时10分钟 - sessionCache.put(nvrInfo.getNvrIp(), userID); - log.info("[海康]登录成功,ip:{},userID:{}", ip, userID); - return userID; - } - - /** - * 记录注销失败信息到 Redis - */ - private void recordLogoutError(String ip, Integer userID, int errorCode) { - JSONObject json = new JSONObject(); - json.put("ip", ip); - json.put("userID", userID); - json.put("errorCode", errorCode); - json.put("time", System.currentTimeMillis()); - redisService.redisTemplate.opsForZSet().add(ERROR_LOGOUT_KEY, json.toJSONString(), System.currentTimeMillis()); - } - - /** - * 登出具体实现 - */ - public void doLogout(String ip, Integer userID) { - if (userID != null) { - // 调用登出SDK - boolean isLogout = hcNetSDK.NET_DVR_Logout(userID); - if (isLogout) { - log.info("[海康]登出成功,ip: {},userID: {}", ip, userID); - } else { - int errorCode = hcNetSDK.NET_DVR_GetLastError(); - log.error("[海康]登出失败,ip: {},userID: {},错误码: {}", ip, userID, errorCode); - // 登出失败日志记录到Redis中 - recordLogoutError(ip, userID, errorCode); - } - } - } - - /** - * 登出 - */ - public synchronized void logout(String ip) { - Integer userID = sessionCache.getIfPresent(ip); - if (userID != null) { - sessionCache.invalidate(ip); - } - } - - /** - * 登出所有用户 - */ - public void logoutAll() { - // 获取所有缓存的IP和userID - sessionCache.asMap().forEach((ip, userID) -> { - doLogout(ip, userID); - }); - // 清空整个缓存 - sessionCache.invalidateAll(); - log.info("[海康]所有用户已登出"); - } - - /** - * 检查是否已登录(同时刷新过期时间) - */ - public boolean isLoggedIn(String ip) { - return sessionCache.getIfPresent(ip) != null; - } -} diff --git a/src/main/java/com/inspect/nvr/service/IvsCameraService.java b/src/main/java/com/inspect/nvr/service/IvsCameraService.java index 9a82060b..20fef01b 100644 --- a/src/main/java/com/inspect/nvr/service/IvsCameraService.java +++ b/src/main/java/com/inspect/nvr/service/IvsCameraService.java @@ -20,6 +20,4 @@ public interface IvsCameraService { ByteArrayInputStream downloadFile(String fileSessionId); ByteArrayInputStream capture(String fileSessionId); - - byte[] captureDigest(String fileSessionId); } diff --git a/src/main/java/com/inspect/nvr/service/LincseekCameraService.java b/src/main/java/com/inspect/nvr/service/LincseekCameraService.java deleted file mode 100644 index db000b7b..00000000 --- a/src/main/java/com/inspect/nvr/service/LincseekCameraService.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.inspect.nvr.service; - -import com.inspect.nvr.domain.Infrared.Camera; -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.domain.Infrared.TemperatureData; -import com.inspect.nvr.jna.lincseek.IRNetSDK; -import com.inspect.nvr.jna.lincseek.IRNetSDKCallback; -import com.inspect.nvr.jna.lincseek.IRNetSDKConst; -import com.inspect.nvr.jna.lincseek.IRNetSDKStruct; -import com.inspect.nvr.jna.lincseek.IRNetSDKStruct.IRNETHANDLE; -import com.sun.jna.Memory; -import com.sun.jna.Pointer; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -@Slf4j -@Service -public class LincseekCameraService extends CommonCameraService { - // [新增]全局流水号生成器 - private static final AtomicInteger SERIAL_COUNTER = new AtomicInteger(1); - // Key: CmdSerial (流水号), Value: CompletableFuture (用于通知调用线程) - private static final ConcurrentHashMap> PENDING_REQUESTS = new ConcurrentHashMap<>(); - - @Resource - private IRNetSDK irNetSDK; - @Resource - private LincseekLoginService lincseekLoginService; - - public static int nextSerial() { - return SERIAL_COUNTER.updateAndGet(current -> (current + 1) & 0xFFFF); - } - - public static Pointer stringToPointer(String str) { - if (str == null) return null; - byte[] bytes = str.getBytes(); // 1. 转换为字节数组 - Pointer pointer = new Memory(bytes.length + 1); // 2. 分配内存(+1 用于存放 \0) - pointer.write(0, bytes, 0, bytes.length); // 3. 写入字节 - pointer.setByte(bytes.length, (byte) 0); // 4. 追加空字节(Null-termination) - return pointer; - } - - /** - * 受并发控制的朗驰抓图 - */ - public byte[] capture(NvrInfo nvrInfo, int channel) { - return withConcurrencyControl(nvrInfo, channel, () -> captureWithRetry(nvrInfo, channel)); - } - - private byte[] captureWithRetry(NvrInfo nvrInfo, int channel) { - Path fullPath = getFullPath("lc", nvrInfo.getNvrIp(), channel); - ensureDirectoryExists(fullPath.getParent()); - int retryCount = 0; - int maxRetries = DEFAULT_MAX_RETRIES; - while (retryCount < maxRetries) { - try { - byte[] imageBytes = jpegCapSingle(nvrInfo, channel, fullPath); - log.info("[朗驰]抓图成功:第{}次,文件地址:{}", retryCount + 1, fullPath); - return imageBytes; - } catch (Exception e) { - log.error("[朗驰]抓图异常(第{}次):{}", retryCount + 1, e.getMessage()); - } finally { - retryCount++; - } - } - // 当所有抓图方式均失败时,记录失败图片 - writeCaptureFailedImage(fullPath); - log.error("[朗驰]抓图失败,图片地址:{}", fullPath); - return new byte[0]; - } - - private byte[] jpegCapSingle(NvrInfo nvrInfo, int channel, Path fullPath) { - IRNetSDKCallback.JpegDataCallback jpegCb = (hHandle, mCh, pBuffer, size, extraData, userdata) -> { - String requestKey = userdata.getString(0); - CompletableFuture future = PENDING_REQUESTS.remove(requestKey); - if (future != null) { - log.info("[朗驰]匹配到抓图回调 (requestKey={}, 通道={}, 大小={}B)", requestKey, mCh, size); - byte[] jpeg = pBuffer.getByteArray(0, size); - future.complete(jpeg); - } else { - // 可能是由于超时已经被移除了,或者是其他类型的抓图 - log.error("[朗驰]未匹配的抓图回调,requestKey={}", requestKey); - } - }; - - int mySerialId = nextSerial(); - String requestKey = ":" + mySerialId; - Pointer userData = stringToPointer(requestKey); - IRNETHANDLE capHandle = irNetSDK.IRNET_ClientJpegCapStart("video server", nvrInfo.getNvrIp(), nvrInfo.getAccount(), nvrInfo.getPassword(), (short) nvrInfo.getServerPort().intValue(), jpegCb, userData); - if (!IRNETHANDLE.INVALID_HANDLE_VALUE.equals(capHandle)) { - CompletableFuture future = new CompletableFuture<>(); - log.info("capHandle: {}, {}", capHandle.toString(), capHandle.hashCode()); - PENDING_REQUESTS.put(requestKey, future); - final int TIMEOUT_SEC = 10; - try { - boolean isCaptured = irNetSDK.IRNET_ClientJpegCapSingle(capHandle, channel, 100); - if (!isCaptured) { - throw new RuntimeException("SDK抓图失败"); - } - byte[] byteArray = future.get(TIMEOUT_SEC, TimeUnit.SECONDS); - Files.write(fullPath, byteArray); - return byteArray; - } catch (Exception e) { - throw new RuntimeException("[朗驰]抓图异常:", e); - } finally { - PENDING_REQUESTS.remove(requestKey); - irNetSDK.IRNET_ClientJpegCapStop(capHandle); - } - } - return null; - } - - public TemperatureData getTemp(Camera camera) { - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp(camera.getIp()); - nvrInfo.setServerPort(camera.getPort()); - nvrInfo.setAccount(camera.getUserName()); - nvrInfo.setPassword(camera.getPassword()); - - int channel = camera.getChannel(); - return withConcurrencyControl(nvrInfo, channel, () -> tempWithRetry(nvrInfo, channel)); - } - - /** - * 获取最高温、最低温、平均温 - */ - private TemperatureData tempWithRetry(NvrInfo nvrInfo, int channel) { - IRNETHANDLE msgHandle = irNetSDK.IRNET_ClientMessageOpen("video server", nvrInfo.getNvrIp(), nvrInfo.getAccount(), nvrInfo.getPassword(), (short) nvrInfo.getServerPort().intValue()); - try { - if (!IRNETHANDLE.INVALID_HANDLE_VALUE.equals(msgHandle)) { - IRNetSDKStruct.VSNET_TEMP_VALUE_EX temps = new IRNetSDKStruct.VSNET_TEMP_VALUE_EX(); - temps.write(); - int r = irNetSDK.IRNET_ClientMessageOpt(msgHandle, IRNetSDKConst.MessageOpt.MESSAGE_CMD_GET_TEMPVALUE_EX, channel, temps.getPointer(), null, null); - if (r == 1) { - temps.read(); - float maxTemp = temps.m_maxtempinfo.m_temp_value; - int maxX = temps.m_maxtempinfo.m_temp_x; - int maxY = temps.m_maxtempinfo.m_temp_y; - float minTemp = temps.m_mintempinfo.m_temp_value; - int minX = temps.m_mintempinfo.m_temp_x; - int minY = temps.m_mintempinfo.m_temp_y; - float avgTemp = temps.m_avgtempinfo; - log.info("[朗驰]测温结果: 最高={}℃({},{}), 最低={}℃({},{}), 平均={}℃", - maxTemp, maxX, maxY, minTemp, minX, minY, avgTemp); - return new TemperatureData( - String.valueOf(maxTemp), - String.valueOf(minTemp), - avgTemp, - 0, - channel, - -1 - ); - } - } - } catch (Exception e) { - log.error("[朗驰]测温异常:", e); - } finally { - irNetSDK.IRNET_ClientMessageClose(msgHandle); - } - return null; - } - - /** - * 跳转预置位 - */ - public boolean ptzPreset(Camera camera) { - //预置位跳转 - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp(camera.getIp()); - nvrInfo.setServerPort(camera.getPort()); - nvrInfo.setAccount(camera.getUserName()); - nvrInfo.setPassword(camera.getPassword()); - nvrInfo.setChannelNumber(camera.getChannel()); - IRNETHANDLE hHandle = lincseekLoginService.login(nvrInfo); - boolean ret = irNetSDK.IRNET_ClientPTZCtrl(hHandle, IRNetSDKConst.PTZ_GOTOPOINT, camera.getPointNum(), 0, null, 0); - log.info("[朗驰]跳转预置位{}, 结果: {}", camera.getPointNum(), ret); - return ret; - } -} diff --git a/src/main/java/com/inspect/nvr/service/LincseekLoginService.java b/src/main/java/com/inspect/nvr/service/LincseekLoginService.java deleted file mode 100644 index fcd38ed4..00000000 --- a/src/main/java/com/inspect/nvr/service/LincseekLoginService.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.inspect.nvr.service; - -import com.alibaba.fastjson.JSONObject; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.RemovalCause; -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.jna.lincseek.IRNetSDK; -import com.inspect.nvr.jna.lincseek.IRNetSDKStruct; -import com.inspect.nvr.jna.lincseek.IRNetSDKStruct.IRNETHANDLE; -import com.inspect.nvr.utils.redis.RedisService; -import com.sun.jna.Pointer; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; -import java.util.concurrent.TimeUnit; - -@Slf4j -@Service -public class LincseekLoginService { - private static final String ERROR_LOGOUT_KEY = "lincseek:error"; - - @Resource - private RedisService redisService; - @Resource - private IRNetSDK irNetSDK; - - // 使用 Caffeine 缓存,10分钟未访问自动移除并登出 - private final Cache sessionCache = Caffeine.newBuilder() - // 10分钟未被get()就过期 - .expireAfterAccess(10, TimeUnit.MINUTES) - .removalListener((String ip, IRNETHANDLE handle, RemovalCause cause) -> { - if (handle != null && (cause == RemovalCause.EXPIRED || cause == RemovalCause.SIZE)) { - log.info("[朗驰]会话超时自动登出,ip: {},handler: {}", ip, handle); - doLogout(ip, handle); - } - }).build(); - - /** - * 登录(Caffeine.get 保证同一 IP 的并发请求只执行一次登录) - */ - public IRNETHANDLE login(NvrInfo nvrInfo) { - String ip = nvrInfo.getNvrIp(); - Integer port = nvrInfo.getServerPort(); - if (port == null) { - throw new IllegalArgumentException("serverPort 不能为空"); - } - - return sessionCache.get(ip, key -> { - IRNetSDKStruct.CHANNEL_CLIENTINFO info = IRNetSDKStruct.CHANNEL_CLIENTINFO.createWithUrl( - "video server", nvrInfo.getAccount(), nvrInfo.getPassword(), - (byte) nvrInfo.getChannelNumber().intValue(), ip, null, null, 0, null, null); - IRNETHANDLE handle = irNetSDK.IRNET_ClientStart(ip, info, port.shortValue(), 0); - - if (IRNETHANDLE.INVALID_HANDLE_VALUE.equals(handle)) { - throw new RuntimeException("[朗驰]登录失败, ip: " + ip); - } - - log.info("[朗驰]登录成功,ip:{},handler:{}", ip, handle); - return handle; - }); - } - - /** - * 记录注销失败信息到 Redis - */ - private void recordLogoutError(String ip, Integer userID, int errorCode) { - JSONObject json = new JSONObject(); - json.put("ip", ip); - json.put("userID", userID); - json.put("errorCode", errorCode); - json.put("time", System.currentTimeMillis()); - redisService.redisTemplate.opsForZSet().add(ERROR_LOGOUT_KEY, json.toJSONString(), System.currentTimeMillis()); - } - - /** - * 登出具体实现 - */ - public void doLogout(String ip, IRNETHANDLE handle) { - if (handle != null) { - // 调用登出SDK - boolean isLogout = irNetSDK.IRNET_ClientStop(handle); - if (isLogout) { - log.info("[朗驰]登出成功,ip: {},handle: {}", ip, handle); - } else { - log.error("[朗驰]登出失败,ip: {},handle: {}", ip, handle); - // 登出失败日志记录到Redis中 - recordLogoutError(ip, (int) Pointer.nativeValue(handle), -1); - } - } - } - - /** - * 登出 - */ - public synchronized void logout(String ip) { - IRNETHANDLE handle = sessionCache.getIfPresent(ip); - if (handle != null) { - sessionCache.invalidate(ip); - } - } - - /** - * 登出所有用户 - */ - public void logoutAll() { - // 获取所有缓存的IP和userID - sessionCache.asMap().forEach((ip, handle) -> { - doLogout(ip, handle); - }); - // 清空整个缓存 - sessionCache.invalidateAll(); - log.info("[朗驰]所有用户已登出"); - } - - /** - * 检查是否已登录(同时刷新过期时间) - */ - public boolean isLoggedIn(String ip) { - return sessionCache.getIfPresent(ip) != null; - } -} diff --git a/src/main/java/com/inspect/nvr/service/camera/AbstractCameraService.java b/src/main/java/com/inspect/nvr/service/camera/AbstractCameraService.java new file mode 100644 index 00000000..f85517d4 --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/AbstractCameraService.java @@ -0,0 +1,186 @@ +package com.inspect.nvr.service.camera; + +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.domain.Infrared.TemperatureData; +import com.inspect.nvr.enums.CameraEnum; +import lombok.extern.slf4j.Slf4j; + +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * 品牌相机服务抽象基类。 + * 统一抓图、云台预置位、测温的并发控制与重试模板,子类只实现品牌 SDK 差异钩子。 + * 并发控制、统一重试等基础设施见 AbstractCameraSupport。 + */ +@Slf4j +public abstract class AbstractCameraService extends AbstractCameraSupport + implements BrandCameraService { + + // ------------------------------------------------------------------ + // 品牌差异钩子 + // ------------------------------------------------------------------ + + /** + * 品牌对应的摄像头枚举,brandName/fileFlag 均由它派生。 + */ + @Override + public abstract CameraEnum cameraType(); + + /** + * 日志品牌名:取枚举 name 字段(中文名),如 大华/海康/朗驰。 + */ + @Override + protected String brandName() { + return cameraType().getName(); + } + + /** + * 抓图文件名前缀:取枚举 value,如 dahua/hik/linc。 + */ + protected String fileFlag() { + return cameraType().getValue(); + } + + /** + * 单次 SDK 抓图,失败返回 null 或抛异常,由模板负责重试与落盘。 + */ + protected abstract byte[] doCapture(Camera camera) throws Exception; + + /** + * 单次 SDK 预置位跳转,成功 true / 失败 false。 + */ + protected abstract boolean doGotoPreset(Camera camera) throws Exception; + + /** + * 单次 SDK 测温,失败返回 null 或抛异常,由模板负责重试。各品牌必须实现。 + */ + protected abstract TemperatureData doMeasureTemperature(Camera camera) throws Exception; + + /** + * Digest 认证抓图 URL 模板;不支持 Digest 兜底的品牌返回 null。 + */ + protected String digestUrlTemplate() { + return null; + } + + /** + * 是否支持 DLT664 抓图,默认不支持,支持的品牌重写为 true。 + */ + protected boolean supportsDlt664() { + return false; + } + + // ------------------------------------------------------------------ + // 抓图模板 + // ------------------------------------------------------------------ + @Override + public byte[] capture(Camera camera) { + return withConcurrencyControl(camera, () -> captureWithRetry(camera)); + } + + private byte[] captureWithRetry(Camera camera) { + Path fullPath = getFullPath(fileFlag(), camera.getIp(), camera.getChannel()); + // 抓图与预置位/测温共用统一重试模板:取图+落盘在一次尝试内,返回 null 触发重试 + byte[] imageBytes = withRetry(camera, "抓图", () -> { + byte[] data = doCapture(camera); + if (data == null) { + return null; + } + Files.write(fullPath, data); + log.info("[{}]抓图成功:{}", brandName(), fullPath); + return data; + }); + if (imageBytes != null) { + return imageBytes; + } + // SDK 抓图全部失败后,尝试 Digest 认证抓图兜底(3参静态方法,避免重复进入并发控制) + String digestUrlTemplate = digestUrlTemplate(); + if (digestUrlTemplate != null) { + byte[] digestBytes = captureDigest(camera, digestUrlTemplate, fullPath); + if (digestBytes != null) { + log.info("[{}]digest抓图成功,图片地址:{}", brandName(), fullPath); + return digestBytes; + } + } + writeCaptureFailedImage(fullPath); + log.error("[{}]抓图失败,图片地址:{}", brandName(), fullPath); + return null; + } + + /** + * Digest 认证抓图入口;不支持 Digest 的品牌返回 null。 + */ + public byte[] captureDigest(Camera camera) { + String digestUrlTemplate = digestUrlTemplate(); + if (digestUrlTemplate == null) { + log.warn("[{}]不支持Digest认证抓图", brandName()); + return null; + } + Path fullPath = getFullPath(fileFlag() + "_digest", camera.getIp(), camera.getChannel()); + return withConcurrencyControl(camera, + () -> captureDigest(camera, digestUrlTemplate, fullPath)); + } + + /** + * DLT664 抓图:不支持时返回 null;支持的品牌其 doCapture 已内嵌温度数据,直接复用抓图模板。 + */ + @Override + public byte[] captureDlt664(Camera camera) { + if (!supportsDlt664()) { + log.warn("[{}]不支持DLT664抓图", brandName()); + return null; + } + return capture(camera); + } + + // ------------------------------------------------------------------ + // 云台模板 + // ------------------------------------------------------------------ + + @Override + public boolean gotoPreset(Camera camera) { + return withConcurrencyControl(camera, () -> { + Boolean result = withRetry(camera, "预置位跳转", () -> doGotoPreset(camera)); + return Boolean.TRUE.equals(result); + }); + } + + /** + * 组合操作模板:一次预置位跳转 + 一次测温视为一次尝试,任一步失败则整对重试。 + */ + @Override + public TemperatureData gotoPresetAndMeasure(Camera ptzCamera, Camera thermalCamera) { + Camera lockCamera = ptzCamera != null ? ptzCamera : thermalCamera; + return withConcurrencyControl(lockCamera, + () -> withRetry(lockCamera, "预置位跳转+测温", + () -> doGotoPresetAndMeasure(ptzCamera, thermalCamera))); + } + + /** + * 单次“预置位跳转 + 测温”,普通品牌无需重写;双模在复合实现中分别委托两个品牌。 + */ + protected TemperatureData doGotoPresetAndMeasure(Camera ptzCamera, Camera thermalCamera) throws Exception { + if (!doGotoPreset(ptzCamera)) { + throw new RuntimeException("预置位跳转失败"); + } + if (presetSettleMillis > 0) { + Thread.sleep(presetSettleMillis); + } + TemperatureData data = doMeasureTemperature(thermalCamera); + if (data == null) { + throw new RuntimeException("测温返回空"); + } + return data; + } + + // ------------------------------------------------------------------ + // 测温模板 + // ------------------------------------------------------------------ + + @Override + public TemperatureData measureTemperature(Camera camera) { + return withConcurrencyControl(camera, + () -> withRetry(camera, "测温", () -> doMeasureTemperature(camera))); + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/CommonCameraService.java b/src/main/java/com/inspect/nvr/service/camera/AbstractCameraSupport.java similarity index 67% rename from src/main/java/com/inspect/nvr/service/CommonCameraService.java rename to src/main/java/com/inspect/nvr/service/camera/AbstractCameraSupport.java index 440da000..85e8d456 100644 --- a/src/main/java/com/inspect/nvr/service/CommonCameraService.java +++ b/src/main/java/com/inspect/nvr/service/camera/AbstractCameraSupport.java @@ -1,7 +1,9 @@ -package com.inspect.nvr.service; +package com.inspect.nvr.service.camera; +import com.inspect.nvr.domain.Infrared.Camera; import com.inspect.nvr.domain.Infrared.NvrInfo; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.classic.methods.HttpGet; @@ -27,50 +29,39 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.locks.ReentrantLock; /** - * 通用摄像头服务类 + * 品牌相机设施基类(不独立成 Bean):并发控制、统一重试、抓图落盘与 Digest 抓图。 */ @Slf4j -public class CommonCameraService { - // 兜底图路径 - static final String IMAGE_CAPTURE_FAILED = "images\\imageCaptureFailed.jpg"; +public abstract class AbstractCameraSupport { // 抓图保存目录 public static final String FILE_DIR = determineCaptureDirectory(); - // 抓图失败重试次数 - static final int DEFAULT_MAX_RETRIES = 10; + // 兜底图路径 + static final String IMAGE_CAPTURE_FAILED = "images\\imageCaptureFailed.jpg"; // 每个NVR最多4个并发抓图任务 static final int MAX_CONCURRENT_PER_NVR = 4; + + /** + * 统一重试次数,可通过 camera.retry.max-retries 覆盖。 + */ + @Value("${camera.retry.max-retries:5}") + protected int maxRetries; + + /** + * 统一重试间隔(毫秒),可通过 camera.retry.interval-millis 覆盖;<=0 表示不等待。 + */ + @Value("${camera.retry.interval-millis:2000}") + protected long retryIntervalMillis; + + /** + * 跳转预置位后等待云台到位的时间(毫秒),camera.ptz.settle-millis 可覆盖,<=0 不等待。 + */ + @Value("${camera.ptz.settle-millis:5000}") + protected long presetSettleMillis; // 每个NVR对应一个信号量,控制并发数 final ConcurrentHashMap nvrSemaphoreMap = new ConcurrentHashMap<>(); // 每个(ip_chanel)对应一个锁,确保同通道串行 final ConcurrentHashMap channelLockMap = new ConcurrentHashMap<>(); - // 控制信号量和并发数 - T withConcurrencyControl(NvrInfo nvrInfo, int channel, Callable task) { - String ip = nvrInfo.getNvrIp(); - Semaphore nvrSemaphore = getOrCreateSemaphore(ip); - ReentrantLock channelLock = getOrCreateLock(ip, channel); - // 1.先获取该NVR的全局并发许可 - nvrSemaphore.acquireUninterruptibly(); - try { - // 2.再获取该通道的独占锁(保证同一通道串行) - channelLock.lock(); - try { - return task.call(); - } catch (Exception e) { - log.error("任务执行异常:", e); - return null; - } finally { - // 必须先unlock通道锁,再释放NVR全局许可 - if (channelLock.isHeldByCurrentThread()) { - channelLock.unlock(); - } - } - } finally { - // 3.释放NVR全局许可 - nvrSemaphore.release(); - } - } - /** * 抓图失败时,写入兜底图 * 可考虑返回byte[] @@ -89,15 +80,15 @@ public class CommonCameraService { /** * Digest认证抓图 * - * @param nvrInfo 设备相关信息 + * @param camera 设备相关信息 * @param urlTemplate 抓图URL模板 * @param fullPath 图片保存全路径 */ - public static byte[] captureDigest(NvrInfo nvrInfo, int channel, String urlTemplate, Path fullPath) { - String ip = nvrInfo.getNvrIp(); - String username = nvrInfo.getAccount(); - String password = nvrInfo.getPassword(); - String url = String.format(urlTemplate, ip, channel); + public static byte[] captureDigest(Camera camera, String urlTemplate, Path fullPath) { + String ip = camera.getIp(); + String username = camera.getUserName(); + String password = camera.getPassword(); + String url = String.format(urlTemplate, ip, camera.getChannel()); BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(new AuthScope(null, -1), @@ -132,6 +123,93 @@ public class CommonCameraService { return null; } + /** + * 抓图保存目录 - 跨平台兼容 + * 注意路径避免包含中文,否则可能会导致文件保存失败或乱码 + */ + private static String determineCaptureDirectory() { + return System.getProperty("user.dir") + "/captures/"; + } + + // 控制信号量和并发数 + T withConcurrencyControl(Camera camera, Callable task) { + String ip = camera.getIp(); + int channel = camera.getChannel(); + Semaphore nvrSemaphore = getOrCreateSemaphore(ip); + ReentrantLock channelLock = getOrCreateLock(ip, channel); + // 1.先获取该NVR的全局并发许可 + nvrSemaphore.acquireUninterruptibly(); + try { + // 2.再获取该通道的独占锁(保证同一通道串行) + channelLock.lock(); + try { + return task.call(); + } catch (Exception e) { + log.error("任务执行异常:", e); + return null; + } finally { + // 必须先unlock通道锁,再释放NVR全局许可 + if (channelLock.isHeldByCurrentThread()) { + channelLock.unlock(); + } + } + } finally { + // 3.释放NVR全局许可 + nvrSemaphore.release(); + } + } + /** + * 日志品牌名,由子类按 CameraEnum#getName 提供(海康/大华/朗驰/朗驰海康)。 + */ + protected abstract String brandName(); + + /** + * 统一重试执行:返回 null 或 Boolean.FALSE 均视为失败并重试;全部失败返回 null。 + */ + protected T withRetry(Camera camera, String action, Callable task) { + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + T result = task.call(); + if (!isFailedResult(result)) { + return result; + } + log.warn("[{}]{}{}失败(第{}/{}次)", + brandName(), action, cameraDesc(camera), attempt, maxRetries); + } catch (Exception e) { + log.error("[{}]{}{}异常(第{}/{}次):{}", + brandName(), action, cameraDesc(camera), attempt, maxRetries, e.getMessage()); + } + if (attempt < maxRetries && !sleepBeforeRetry()) { + break; + } + } + return null; + } + + private static String cameraDesc(Camera camera) { + return "[" + camera.getIp() + "/" + camera.getChannel() + "]"; + } + + private static boolean isFailedResult(Object result) { + return result == null || Boolean.FALSE.equals(result); + } + + /** + * 重试间隔等待;被中断时恢复中断标志并返回 false(结束重试)。 + */ + private boolean sleepBeforeRetry() { + if (retryIntervalMillis <= 0) { + return true; + } + try { + Thread.sleep(retryIntervalMillis); + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + /** * 创建目录(如果不存在) */ @@ -173,29 +251,11 @@ public class CommonCameraService { * 获取文件Path,并创建目录 */ Path getFullPath(String flag, String ip, int channel) { - String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss_SSS")); + String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss_SSS")); String fileName = flag + "_" + ip + "_" + channel + "_" + timeStr + ".jpg"; String saveDir = FILE_DIR + LocalDate.now(); Path fullPath = Paths.get(saveDir, fileName).toAbsolutePath(); ensureDirectoryExists(fullPath.getParent()); return fullPath; } - - /** - * 抓图保存目录 - 跨平台兼容 - * 注意路径避免包含中文,否则可能会导致文件保存失败或乱码 - */ - private static String determineCaptureDirectory() { - String os = System.getProperty("os.name").toLowerCase(); - log.info("当前操作系统: {}", os); - String userDir = System.getProperty("user.dir"); - log.info("userDir: {}", userDir); - - if (os.contains("win")) { - return "D:/captures/"; - } else { - // Linux/Unix/Mac 系统使用项目同级目录 - return userDir + "/captures/"; - } - } } diff --git a/src/main/java/com/inspect/nvr/service/camera/BrandCameraService.java b/src/main/java/com/inspect/nvr/service/camera/BrandCameraService.java new file mode 100644 index 00000000..408b98fa --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/BrandCameraService.java @@ -0,0 +1,47 @@ +package com.inspect.nvr.service.camera; + +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.domain.Infrared.TemperatureData; +import com.inspect.nvr.enums.CameraEnum; + +/** + * 品牌相机统一能力接口:抓图、预置位跳转、测温。 + * 品牌公共流程见 AbstractCameraService;双模 LINC_HIK 见 LincHikCompositeCameraService。 + */ +public interface BrandCameraService { + + /** + * 该实现对应的品牌枚举,工厂注册表以此为依据。 + */ + CameraEnum cameraType(); + + /** + * 抓图。SDK 重试与 Digest 兜底全部失败后返回 null(磁盘上仍写入兜底占位图)。 + */ + byte[] capture(Camera camera); + + /** + * 跳转预置位。全部重试失败返回 false。 + */ + boolean gotoPreset(Camera camera); + + /** + * 组合操作:跳转预置位一次,等待到位后测温一次,两步视为一个整体重试。 + * 单目品牌两个参数传同一个 Camera;双模时 ptzCamera 传可见光相机、thermalCamera 传热成像相机。 + * 全部重试失败返回 null。 + */ + TemperatureData gotoPresetAndMeasure(Camera ptzCamera, Camera thermalCamera); + + /** + * DLT664 红外热成像图抓图(可选能力,默认不支持)。 + * 支持的品牌重写;不支持或抓图失败时返回 null。 + */ + default byte[] captureDlt664(Camera camera) { + return null; + } + + /** + * 测温。品牌暂未实现或全部重试失败时返回 null。 + */ + TemperatureData measureTemperature(Camera camera); +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/camera/CameraServiceFactory.java b/src/main/java/com/inspect/nvr/service/camera/CameraServiceFactory.java new file mode 100644 index 00000000..46bb8dfa --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/CameraServiceFactory.java @@ -0,0 +1,54 @@ +package com.inspect.nvr.service.camera; + +import com.inspect.nvr.enums.CameraEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 品牌相机服务工厂:按 {@link CameraEnum#getCode()} 选择实现。 + * Spring 自动收集所有 {@link BrandCameraService} 建立注册表; + * 未知类型回退海康实现,保持历史默认行为。 + */ +@Slf4j +@Component +public class CameraServiceFactory { + + private final Map registry = new HashMap<>(); + + private final HikCameraService defaultService; + + public CameraServiceFactory(List services, HikCameraService defaultService) { + for (BrandCameraService service : services) { + Integer code = service.cameraType().getCode(); + BrandCameraService previous = registry.put(code, service); + if (previous != null) { + throw new IllegalStateException("重复注册的相机品牌服务,cameraType=" + service.cameraType()); + } + log.info("注册品牌相机服务: {} -> {}", code, service.getClass().getSimpleName()); + } + this.defaultService = defaultService; + } + + /** + * 按枚举获取品牌服务。 + */ + public BrandCameraService get(CameraEnum cameraType) { + return get(cameraType.getCode()); + } + + /** + * 按 cameraType 编码获取品牌服务,未知类型回退海康。 + */ + public BrandCameraService get(int cameraType) { + BrandCameraService service = registry.get(cameraType); + if (service == null) { + log.warn("未知摄像头类型: {},回退到海康实现", cameraType); + return defaultService; + } + return service; + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/camera/DahuaCameraService.java b/src/main/java/com/inspect/nvr/service/camera/DahuaCameraService.java new file mode 100644 index 00000000..bb9da398 --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/DahuaCameraService.java @@ -0,0 +1,204 @@ +package com.inspect.nvr.service.camera; + +import com.inspect.nvr.service.camera.login.DahuaLoginService; +import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; +import com.inspect.nvr.daHuaCarme.jna.dahua.ToolKits; +import com.inspect.nvr.daHuaCarme.utils.jna.DahuaUtils; +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.domain.Infrared.TemperatureData; +import com.inspect.nvr.enums.CameraEnum; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 大华设备SDK服务 + * 同一个IP,同一个通道,串行 + * 同一个IP,不同通道,最大4个线程并发 + * 不同IP,并发抓图,不限制 + */ +@Slf4j +@Service +public class DahuaCameraService extends AbstractCameraService { + /** + * Digest认证抓图URL: + * http:///cgi-bin/snapshot.cgi?channel=&subtype= + * subtype: 0-主码流 1-子码流 + */ + private static final String DIGEST_URL_TEMPLATE = "http://%s/cgi-bin/snapshot.cgi?channel=%d&subtype=1"; + // Key: loginId+CmdSerial(登录句柄+流水号),Value: CompletableFuture(用于通知调用线程) + private static final ConcurrentHashMap> PENDING_REQUESTS = new ConcurrentHashMap<>(); + private static final fCaptureReceiveCB CAPTURE_RECEIVE_CB = new fCaptureReceiveCB(); + // 全局流水号生成器 + private static final AtomicInteger SERIAL_COUNTER = new AtomicInteger(1); + @Resource + private NetSDKLib dhNetSDK; + @Resource + private DahuaLoginService dahuaLoginService; + + // CmdSerial请求序列号,有效值范围 0~65535,超出范围会被截断 + public static int nextSerial() { + return SERIAL_COUNTER.updateAndGet(current -> (current + 1) & 0xFFFF); + } + + @Override + public CameraEnum cameraType() { + return CameraEnum.DAHUA; + } + + @Override + protected String digestUrlTemplate() { + return DIGEST_URL_TEMPLATE; + } + + @Override + protected byte[] doCapture(Camera camera) { + return snapPictureEx(camera); + } + + /** + * 预置位跳转 + */ + @Override + protected boolean doGotoPreset(Camera camera) { + NetSDKLib.LLong loginHandle = dahuaLoginService.login(camera); + log.info("[" + brandName() + "]预置位跳转,IP={},LoginHandle={},pointNum={}", camera.getIp(), loginHandle, camera.getPointNum()); + boolean result = dhNetSDK.CLIENT_DHPTZControlEx2(loginHandle, camera.getChannel() - 1, + DahuaUtils.PTZCommand("GOTO_PRESET"), 0, camera.getPointNum(), 0, 0, null); + if (result) { + log.info("[" + brandName() + "]CLIENT_DHPTZControlEx success,pointNum={}", camera.getPointNum()); + } else { + log.error("[" + brandName() + "]CLIENT_DHPTZControlEx Failed!!{}", ToolKits.getErrorCodePrint(dhNetSDK.CLIENT_GetLastError())); + } + return result; + } + + /** + * 大华实时测温(蓝本:DahuaServiceImpl#StartRemote)。 + * 通过 CLIENT_QueryDevInfo 查询测温规则区域温度。 + */ + @Override + protected TemperatureData doMeasureTemperature(Camera camera) { + log.info("[" + brandName() + "]实时测温开始,ip={}, channel={}, presetId={}", camera.getIp(), camera.getChannel(), camera.getPresetId()); + NetSDKLib.LLong loginHandle = dahuaLoginService.login(camera); + if (loginHandle == null) { + log.error("[" + brandName() + "]测温失败:登录句柄为空,ip={}", camera.getIp()); + return null; + } + + // 初始化输入结构体,设置条件参数 + NetSDKLib.NET_IN_RADIOMETRY_GETTEMPER netIn = new NetSDKLib.NET_IN_RADIOMETRY_GETTEMPER(); + netIn.stCondition.nPresetId = camera.getPresetId(); + netIn.stCondition.nRuleId = camera.getRuleId(); + netIn.stCondition.nMeterType = NetSDKLib.NET_RADIOMETRY_METERTYPE.NET_RADIOMETRY_METERTYPE_AREA; + netIn.stCondition.nChannel = camera.getChannel() - 1; + + // 输出结构体 + NetSDKLib.NET_OUT_RADIOMETRY_GETTEMPER netOut = new NetSDKLib.NET_OUT_RADIOMETRY_GETTEMPER(); + netOut.stTempInfo = new NetSDKLib.NET_RADIOMETRYINFO(); + netIn.write(); + netOut.write(); + + boolean success = dhNetSDK.CLIENT_QueryDevInfo( + loginHandle, + NetSDKLib.NET_QUERY_DEV_RADIOMETRY_TEMPER, + netIn.getPointer(), + netOut.getPointer(), + null, + 5000 + ); + + if (!success) { + log.error("[" + brandName() + "]获取设备参数失败,错误码:{}", ToolKits.getErrorCodePrint(dhNetSDK.CLIENT_GetLastError())); + return null; + } + + // 将本地内存同步到 Java 字段 + netOut.read(); + NetSDKLib.NET_RADIOMETRYINFO stTempInfo = netOut.stTempInfo; + log.info("[" + brandName() + "]测温结果:最高={}, 最低={}, 平均={}, 温差={}", + stTempInfo.fTemperMax, stTempInfo.fTemperMin, stTempInfo.fTemperAver, stTempInfo.fTemperStd); + return new TemperatureData( + String.valueOf(stTempInfo.fTemperMax), + String.valueOf(stTempInfo.fTemperMin), + stTempInfo.fTemperAver, + stTempInfo.fTemperStd, + netIn.stCondition.nPresetId, + netIn.stCondition.nRuleId + ); + } + + /** + * 大华SDK抓图具体实现(异步) + * 通过CompletableFuture实现异步回调通知 + */ + private byte[] snapPictureEx(Camera camera) { + NetSDKLib.LLong loginID = dahuaLoginService.login(camera); + + NetSDKLib.SNAP_PARAMS snapParams = new NetSDKLib.SNAP_PARAMS(); + snapParams.Channel = camera.getChannel() - 1; // 通道号从0开始 + snapParams.mode = 0; // 抓图模式:0-单次抓 + snapParams.Quality = 3; + snapParams.InterSnap = 0; + int mySerialId = nextSerial(); + snapParams.CmdSerial = mySerialId; + IntByReference reference = new IntByReference(0); + // 设置异步抓图回调函数 + dhNetSDK.CLIENT_SetSnapRevCallBack(CAPTURE_RECEIVE_CB, null); + + String requestKey = loginID.longValue() + ":" + mySerialId; + CompletableFuture future = new CompletableFuture<>(); + PENDING_REQUESTS.put(requestKey, future); + final int TIMEOUT_SEC = 5; + try { + log.info("[" + brandName() + "]开始抓图,LoginID={},IP={},Channel={},Serial={}", loginID, camera.getIp(), camera.getChannel(), mySerialId); + boolean isCaptured = dhNetSDK.CLIENT_SnapPictureEx(loginID, snapParams, reference); + if (!isCaptured) { + String errorMsg = ToolKits.getErrorCodePrint(dhNetSDK.CLIENT_GetLastError()); + throw new RuntimeException("SDK抓图失败:" + errorMsg); + } + return future.get(TIMEOUT_SEC, TimeUnit.SECONDS); + } catch (TimeoutException e) { + log.error("[" + brandName() + "]抓图超时:在 {} 秒内未收到设备回调,requestKey={}", TIMEOUT_SEC, requestKey); + } catch (Exception e) { + log.error("[" + brandName() + "]抓图异常:", e); + } finally { + PENDING_REQUESTS.remove(requestKey); + } + return null; + } + + /** + * CLIENT_SnapPictureEx异步抓图回调函数重写 + */ + public static class fCaptureReceiveCB implements NetSDKLib.fSnapRev { + @Override + public void invoke(NetSDKLib.LLong lLoginID, Pointer pBuf, int RevLen, int EncodeType, int CmdSerial, Pointer dwUser) { + // 检查是否有等待该流水号的请求 + String requestKey = lLoginID.longValue() + ":" + CmdSerial; + CompletableFuture future = PENDING_REQUESTS.remove(requestKey); + if (future != null) { + log.info("[" + CameraEnum.DAHUA.getName() + "]匹配到抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial); + if (pBuf != null && RevLen > 0) { + // 读取图片数据 + byte[] data = pBuf.getByteArray(0, RevLen); + // 完成Future,通知主线程 + future.complete(data); + } else { + future.completeExceptionally(new RuntimeException("Empty image data")); + } + } else { + // 可能是由于超时已经被移除了,或者是其他类型的抓图 + log.error("[" + CameraEnum.DAHUA.getName() + "]收到未匹配的抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/HikCameraService.java b/src/main/java/com/inspect/nvr/service/camera/HikCameraService.java similarity index 53% rename from src/main/java/com/inspect/nvr/service/HikCameraService.java rename to src/main/java/com/inspect/nvr/service/camera/HikCameraService.java index 0b1c90e3..b233ee44 100644 --- a/src/main/java/com/inspect/nvr/service/HikCameraService.java +++ b/src/main/java/com/inspect/nvr/service/camera/HikCameraService.java @@ -1,8 +1,11 @@ -package com.inspect.nvr.service; +package com.inspect.nvr.service.camera; -import com.inspect.nvr.domain.Infrared.NvrInfo; +import com.inspect.nvr.service.camera.login.HikLoginService; +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.enums.CameraEnum; import com.inspect.nvr.domain.Infrared.TemperatureData; import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; +import com.inspect.nvr.utils.DLT664Service; import com.sun.jna.Pointer; import com.sun.jna.ptr.IntByReference; import lombok.extern.slf4j.Slf4j; @@ -11,14 +14,15 @@ import org.springframework.util.StreamUtils; import javax.annotation.Resource; import java.io.File; -import java.io.FileOutputStream; import java.io.InputStream; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.concurrent.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; /** @@ -29,7 +33,7 @@ import java.util.concurrent.atomic.AtomicLong; */ @Slf4j @Service -public class HikCameraService extends CommonCameraService { +public class HikCameraService extends AbstractCameraService { /** * Digest认证抓图URL: * http:///ISAPI/Streaming/channels//picture @@ -45,83 +49,64 @@ public class HikCameraService extends CommonCameraService { @Resource private HCNetSDK hcNetSDK; - /** - * 受并发控制的海康抓图 - */ - public byte[] capture(NvrInfo nvrInfo, int channel) { - return withConcurrencyControl(nvrInfo, channel, () -> captureWithRetry(nvrInfo, channel)); + @Override + public CameraEnum cameraType() { + return CameraEnum.HIKVISION; } - /** - * Digest认证抓图 - */ - public byte[] captureDigest(NvrInfo nvrInfo, int channel) { - return withConcurrencyControl(nvrInfo, channel, () -> captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, getFullPath("hk_digest", nvrInfo.getNvrIp(), channel))); + @Override + protected String digestUrlTemplate() { + return DIGEST_URL_TEMPLATE; + } + + @Override + protected boolean supportsDlt664() { + return true; + } + + @Override + protected byte[] doCapture(Camera camera) throws Exception { +// return captureJPEGPictureNew(camera); + return captureJPEGPictureWithAppendData(camera); } /** - * 受并发控制的海康实时测温 + * 预置位跳转 */ - public TemperatureData realTimeThermometry(NvrInfo nvrInfo, int channel) { - return withConcurrencyControl(nvrInfo, channel, () -> getRealTimeThermometry(nvrInfo, channel)); + @Override + protected boolean doGotoPreset(Camera camera) { + int userId = hikLoginService.login(camera); + boolean gotoPreset = hcNetSDK.NET_DVR_PTZPreset_Other(userId, camera.getChannel(), HCNetSDK.GOTO_PRESET, camera.getPointNum()); + if (!gotoPreset) { + log.error("[" + brandName() + "]预置位跳转设备参数失败,错误码:" + hcNetSDK.NET_DVR_GetLastError()); + } else { + log.info("[" + brandName() + "]成功跳转到预置位,pointNum={}", camera.getPointNum()); + } + return gotoPreset; } - private byte[] captureWithRetry(NvrInfo nvrInfo, int channel) { - Path fullPath = getFullPath("hk", nvrInfo.getNvrIp(), channel); - ensureDirectoryExists(fullPath.getParent()); - int retryCount = 0; - int maxRetries = DEFAULT_MAX_RETRIES; - while (retryCount < maxRetries) { - try { -// byte[] imageBytes = captureJPEGPicture(nvrInfo, channel, fullPath); - byte[] imageBytes = captureJPEGPictureNew(nvrInfo, channel, fullPath); - if (imageBytes == null) { - int errorCode = hcNetSDK.NET_DVR_GetLastError(); - throw new RuntimeException("SDK抓图失败,错误码" + errorCode); - } - log.info("[海康]抓图成功(第{}次):{}", retryCount + 1, fullPath); - return imageBytes; - } catch (Exception e) { - log.error("[海康]抓图异常(第{}次):{}", retryCount + 1, e.getMessage()); - try { - Thread.sleep(2000); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - break; - } - } finally { - retryCount++; - } - } - // 当SDK抓图失败时,尝试使用Digest认证抓图 - byte[] imageBytes = captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, fullPath); - if (imageBytes == null) { - // 当所有抓图方式均失败时,记录失败图片 - writeCaptureFailedImage(fullPath); - log.info("[海康]所有抓图方式均失败,图片地址:{}", fullPath); - return new byte[0]; - } - log.info("[海康]digest抓图成功,图片地址:{}", fullPath); - return imageBytes; + @Override + protected TemperatureData doMeasureTemperature(Camera camera) { + return getRealTimeThermometry(camera); } /** - * 大华SDK抓图具体实现(旧版) + * \u6d77\u5eb7SDK\u6293\u56fe\u5177\u4f53\u5b9e\u73b0\uff08\u65e7\u7248\uff09 */ - private byte[] captureJPEGPicture(NvrInfo nvrInfo, int channel, Path fullPath) throws Exception { - int userId = hikLoginService.login(nvrInfo); + private byte[] captureJPEGPicture(Camera camera, Path fullPath) throws Exception { + int userId = hikLoginService.login(camera); // 兼容 C/C++ 编写的本地库, C 语言用 \0 标记字符串结束 HCNetSDK.NET_DVR_JPEGPARA jpegpara = new HCNetSDK.NET_DVR_JPEGPARA(); jpegpara.wPicSize = 0xff; jpegpara.wPicQuality = 1; jpegpara.write(); byte[] filePathBytes = (fullPath.toString() + "\0").getBytes("GBK"); - boolean isCaptured = hcNetSDK.NET_DVR_CaptureJPEGPicture(userId, channel, jpegpara, filePathBytes); + boolean isCaptured = hcNetSDK.NET_DVR_CaptureJPEGPicture(userId, camera.getChannel(), jpegpara, filePathBytes); if (isCaptured) { // 验证文件是否有效 if (!isValidJpeg(fullPath)) { // 文件乱码,修改文件名 - log.info("[海康]文件无效,图片地址:{}", fullPath); + log.info("[" + brandName() + "]文件无效,图片地址:{}", fullPath); String dirName = fullPath.getParent().toString(); String fileName = fullPath.getFileName().toString(); File dir = new File(dirName); @@ -143,37 +128,33 @@ public class HikCameraService extends CommonCameraService { /** * 大华SDK抓图具体实现(新版) */ - private byte[] captureJPEGPictureNew(NvrInfo nvrInfo, int channel, Path fullPath) throws Exception { - int userId = hikLoginService.login(nvrInfo); + private byte[] captureJPEGPictureNew(Camera camera) throws Exception { + int userId = hikLoginService.login(camera); HCNetSDK.NET_DVR_JPEGPARA jpegpara = new HCNetSDK.NET_DVR_JPEGPARA(); jpegpara.wPicSize = 0xff; jpegpara.wPicQuality = 0; jpegpara.write(); HCNetSDK.BYTE_ARRAY byteArray = new HCNetSDK.BYTE_ARRAY(10 * 1024 * 1024); IntByReference ret = new IntByReference(0); - log.info("[海康]开始抓图,UserID={},IP={},Channel={}", userId, nvrInfo.getNvrIp(), channel); - boolean isCaptured = hcNetSDK.NET_DVR_CaptureJPEGPicture_NEW(userId, channel, jpegpara, byteArray.getPointer(), byteArray.size(), ret); + log.info("[" + brandName() + "]开始抓图,UserID={},IP={},Channel={}", userId, camera.getIp(), camera.getChannel()); + boolean isCaptured = hcNetSDK.NET_DVR_CaptureJPEGPicture_NEW(userId, camera.getChannel(), jpegpara, byteArray.getPointer(), byteArray.size(), ret); if (isCaptured) { byteArray.read(); byte[] imageBytes = byteArray.byValue; - // 图片写入本地 - try (FileOutputStream fos = new FileOutputStream(fullPath.toString())) { - fos.write(imageBytes, 0, ret.getValue()); - } return imageBytes; } return null; } /** - * 海康SDK实施测温具体实现(异步) + * 海康SDK测温具体实现(异步) * 通过CompletableFuture实现异步回调通知 */ - private TemperatureData getRealTimeThermometry(NvrInfo nvrInfo, int channel) { - int userId = hikLoginService.login(nvrInfo); + private TemperatureData getRealTimeThermometry(Camera camera) { + int userId = hikLoginService.login(camera); HCNetSDK.NET_DVR_REALTIME_THERMOMETRY_COND cond = new HCNetSDK.NET_DVR_REALTIME_THERMOMETRY_COND(); cond.dwSize = cond.size(); - cond.dwChan = channel; + cond.dwChan = camera.getChannel(); cond.byRuleID = 1;//规则ID,0代表获取全部规则,具体规则ID从1开始 cond.byMode = 1;//长连接模式:0-保留;1-定时模式;2-温差模式 cond.wInterval = 5;//上传间隔(仅温差模式支持),取值范围:1-3600 秒,填0则默认3600S上传一次 @@ -192,7 +173,7 @@ public class HikCameraService extends CommonCameraService { // 测温返回的句柄 int lHandle = -1; try { - log.info("[海康]开始实时测温,UserID={},IP={},Channel={},requestId={}", userId, nvrInfo.getNvrIp(), channel, requestId); + log.info("[" + brandName() + "]开始实时测温,UserID={},IP={},Channel={},requestId={}", userId, camera.getIp(), camera.getChannel(), requestId); lHandle = hcNetSDK.NET_DVR_StartRemoteConfig(userId, HCNetSDK.NET_DVR_GET_REALTIME_THERMOMETRY, lpInBuffer, dwInBufferSize, new fRemoteConfigCB(), pUserData); if (lHandle < 0) { int errorCode = hcNetSDK.NET_DVR_GetLastError(); @@ -201,9 +182,9 @@ public class HikCameraService extends CommonCameraService { return future.get(TIMEOUT_SEC, TimeUnit.SECONDS); } catch (TimeoutException e) { - log.error("[海康]测温超时:在 {} 秒内未收到设备回调,requestId={}", TIMEOUT_SEC, requestId); + log.error("[" + brandName() + "]测温超时:在 {} 秒内未收到设备回调,requestId={}", TIMEOUT_SEC, requestId); } catch (Exception e) { - log.error("[海康]测温异常:{}", e.getMessage()); + log.error("[" + brandName() + "]测温异常:{}", e.getMessage()); } finally { THERMOMETRY_REQUESTS.remove(requestId); hcNetSDK.NET_DVR_StopRemoteConfig(lHandle); @@ -211,6 +192,60 @@ public class HikCameraService extends CommonCameraService { return null; } + public byte[] captureJPEGPictureWithAppendData(Camera camera) { + try { + int lUserId = hikLoginService.login(camera); + HCNetSDK.NET_DVR_JPEGPICTURE_WITH_APPENDDATA jpegPictureWithAppendData = new HCNetSDK.NET_DVR_JPEGPICTURE_WITH_APPENDDATA(); + + jpegPictureWithAppendData.dwSize = jpegPictureWithAppendData.size(); + jpegPictureWithAppendData.dwChannel = camera.getChannel(); + HCNetSDK.BYTE_ARRAY ptrJpegByte = new HCNetSDK.BYTE_ARRAY(3 * 1024 * 1024); + HCNetSDK.BYTE_ARRAY ptrP2PDataByte = new HCNetSDK.BYTE_ARRAY(3 * 1024 * 1024); + jpegPictureWithAppendData.pJpegPicBuff = ptrJpegByte.getPointer(); + jpegPictureWithAppendData.pP2PDataBuff = ptrP2PDataByte.getPointer(); + + boolean bRet = hcNetSDK.NET_DVR_CaptureJPEGPicture_WithAppendData(lUserId, camera.getChannel(), jpegPictureWithAppendData); + if (bRet) { + // 保存热成像图片 + if (jpegPictureWithAppendData.dwJpegPicLen > 0 && jpegPictureWithAppendData.pJpegPicBuff != null) { + ByteBuffer buffer = jpegPictureWithAppendData.pJpegPicBuff.getByteBuffer(0, jpegPictureWithAppendData.dwJpegPicLen); + byte[] bytes = new byte[jpegPictureWithAppendData.dwJpegPicLen]; + buffer.rewind(); + buffer.get(bytes); + + if (jpegPictureWithAppendData.dwP2PDataLen > 0 && jpegPictureWithAppendData.pP2PDataBuff != null) { + try { + ByteBuffer p2pBuffer = jpegPictureWithAppendData.pP2PDataBuff.getByteBuffer(0, jpegPictureWithAppendData.dwP2PDataLen); + byte[] p2pBytes = new byte[jpegPictureWithAppendData.dwP2PDataLen]; + p2pBuffer.rewind(); + p2pBuffer.get(p2pBytes); + int width = jpegPictureWithAppendData.dwJpegPicWidth; + int height = jpegPictureWithAppendData.dwJpegPicHeight; + float[][] matrix = new float[height][width]; + + ByteBuffer buf = ByteBuffer.wrap(p2pBytes).order(ByteOrder.LITTLE_ENDIAN); + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + matrix[row][col] = buf.getFloat(); + } + } + bytes = DLT664Service.generate(bytes, matrix, cameraType().name()); + log.info("[" + brandName() + "]DLT664文件格式生成成功!"); + } catch (Exception e) { + log.error("[" + brandName() + "]DLT664文件格式生成失败!"); + } + } + + return bytes; + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return null; + } + + /** * NET_DVR_StartRemoteConfig实时测温回调函数重写 */ @@ -220,7 +255,7 @@ public class HikCameraService extends CommonCameraService { long requestId = Pointer.nativeValue(pUserData); CompletableFuture future = THERMOMETRY_REQUESTS.remove(requestId); if (future != null) { - log.info("[海康]匹配到测温回调,类型: {}, 长度:{}", dwType, dwBufLen); + log.info("[" + CameraEnum.HIKVISION.getName() + "]匹配到测温回调,类型: {}, 长度:{}", dwType, dwBufLen); if (dwType == 2) { HCNetSDK.NET_DVR_THERMOMETRY_UPLOAD thermometryUpload = new HCNetSDK.NET_DVR_THERMOMETRY_UPLOAD(); thermometryUpload.write(); @@ -229,7 +264,7 @@ public class HikCameraService extends CommonCameraService { thermometryUpload.read(); String strTemp = "规则ID:" + thermometryUpload.byRuleID + "规则名称:" + thermometryUpload.szRuleName + "规则类型:" + thermometryUpload.byRuleCalibType + "预置点号:" + thermometryUpload.wPresetNo + "点,温度:" + thermometryUpload.struPointThermCfg.fTemperature + "点坐标:" + thermometryUpload.struPointThermCfg.struPoint.fX + "," + thermometryUpload.struPointThermCfg.struPoint.fY + "区域最高温度:" + thermometryUpload.struLinePolygonThermCfg.fMaxTemperature + "区域最低温度:" + thermometryUpload.struLinePolygonThermCfg.fMinTemperature + "区域平均温度:" + thermometryUpload.struLinePolygonThermCfg.fAverageTemperature + "区域温差:" + thermometryUpload.struLinePolygonThermCfg.fTemperatureDiff + "\n"; - log.info("[海康]实时测温成功,requestId={}, data={}", requestId, strTemp); + log.info("[" + CameraEnum.HIKVISION.getName() + "]实时测温成功,requestId={}, data={}", requestId, strTemp); HCNetSDK.NET_DVR_LINEPOLYGON_THERM_CFG struLinePolygonThermCfg = thermometryUpload.struLinePolygonThermCfg; // 封装所有温度数据 @@ -237,7 +272,7 @@ public class HikCameraService extends CommonCameraService { future.complete(data); } } else { - log.error("[海康]收到未匹配的测温回调, requestId={}", requestId); + log.error("[" + CameraEnum.HIKVISION.getName() + "]收到未匹配的测温回调, requestId={}", requestId); } } } diff --git a/src/main/java/com/inspect/nvr/service/camera/LincHikCompositeCameraService.java b/src/main/java/com/inspect/nvr/service/camera/LincHikCompositeCameraService.java new file mode 100644 index 00000000..0a5258ee --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/LincHikCompositeCameraService.java @@ -0,0 +1,78 @@ +package com.inspect.nvr.service.camera; + +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.domain.Infrared.TemperatureData; +import com.inspect.nvr.enums.CameraEnum; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * LINC_HIK(3) 双模相机复合实现。 + * 海康部分:可见光、云台;朗驰部分:热成像抓图、测温。 + * 复用 AbstractCameraSupport 的统一重试;通道锁借用海康服务的锁表, + * 防止同设备同通道的其他预置位调用插队转动云台。 + */ +@Component +public class LincHikCompositeCameraService extends AbstractCameraSupport implements BrandCameraService { + + @Resource + private HikCameraService hikCameraService; + @Resource + private LincseekCameraService lincseekCameraService; + + @Override + public CameraEnum cameraType() { + return CameraEnum.LINC_HIK; + } + + @Override + protected String brandName() { + return CameraEnum.LINC_HIK.getName(); + } + + @Override + public byte[] capture(Camera camera) { + return lincseekCameraService.capture(camera); + } + + @Override + public boolean gotoPreset(Camera camera) { + return hikCameraService.gotoPreset(camera); + } + + @Override + public TemperatureData measureTemperature(Camera camera) { + return lincseekCameraService.measureTemperature(camera); + } + + @Override + public byte[] captureDlt664(Camera camera) { + return lincseekCameraService.captureDlt664(camera); + } + + /** + * 双模组合操作:云台走海康、测温走朗驰;直接调单次钩子,避免两个服务各自内部重试叠加。 + * 复用海康服务的通道锁(锁可见光相机的 ip+channel),5s 到位等待与整对重试都在锁内。 + */ + @Override + public TemperatureData gotoPresetAndMeasure(Camera ptzCamera, Camera thermalCamera) { + return hikCameraService.withConcurrencyControl(ptzCamera, + () -> withRetry(ptzCamera, "预置位跳转+测温", + () -> doPresetAndMeasure(ptzCamera, thermalCamera))); + } + + private TemperatureData doPresetAndMeasure(Camera ptzCamera, Camera thermalCamera) throws Exception { + if (!hikCameraService.doGotoPreset(ptzCamera)) { + throw new RuntimeException("预置位跳转失败"); + } + if (presetSettleMillis > 0) { + Thread.sleep(presetSettleMillis); + } + TemperatureData data = lincseekCameraService.doMeasureTemperature(thermalCamera); + if (data == null) { + throw new RuntimeException("测温返回空"); + } + return data; + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/camera/LincseekCameraService.java b/src/main/java/com/inspect/nvr/service/camera/LincseekCameraService.java new file mode 100644 index 00000000..23d9fd4d --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/LincseekCameraService.java @@ -0,0 +1,193 @@ +package com.inspect.nvr.service.camera; + +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.domain.Infrared.TemperatureData; +import com.inspect.nvr.enums.CameraEnum; +import com.inspect.nvr.jna.lincseek.IRNetSDK; +import com.inspect.nvr.jna.lincseek.IRNetSDKCallback; +import com.inspect.nvr.jna.lincseek.IRNetSDKConst; +import com.inspect.nvr.jna.lincseek.IRNetSDKStruct; +import com.inspect.nvr.jna.lincseek.IRNetSDKStruct.IRNETHANDLE; +import com.inspect.nvr.service.camera.login.LincseekLoginService; +import com.inspect.nvr.utils.DLT664Service; +import com.sun.jna.Memory; +import com.sun.jna.Pointer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.inspect.nvr.jna.lincseek.IRNetSDKConst.MessageOpt.MESSAGE_CMD_SET_CAPTURETYPE; +import static com.inspect.nvr.jna.lincseek.IRNetSDKConst.VSNET_CAPTURE_TYPE_LCR; + +@Slf4j +@Service +public class LincseekCameraService extends AbstractCameraService { + // [新增]全局流水号生成器 + private static final AtomicInteger SERIAL_COUNTER = new AtomicInteger(1); + // Key: CmdSerial (流水号), Value: CompletableFuture (用于通知调用线程) + private static final ConcurrentHashMap> PENDING_REQUESTS = new ConcurrentHashMap<>(); + + @Resource + private IRNetSDK irNetSDK; + @Resource + private LincseekLoginService lincseekLoginService; + + public static int nextSerial() { + return SERIAL_COUNTER.updateAndGet(current -> (current + 1) & 0xFFFF); + } + + public static Pointer stringToPointer(String str) { + if (str == null) return null; + byte[] bytes = str.getBytes(); // 1. 转换为字节数组 + Pointer pointer = new Memory(bytes.length + 1); // 2. 分配内存(+1 用于存放 \0) + pointer.write(0, bytes, 0, bytes.length); // 3. 写入字节 + pointer.setByte(bytes.length, (byte) 0); // 4. 追加空字节(Null-termination) + return pointer; + } + + @Override + public CameraEnum cameraType() { + return CameraEnum.LINCSEEK; + } + + @Override + protected boolean supportsDlt664() { + return true; + } + + @Override + protected byte[] doCapture(Camera camera) { + return jpegCapSingle(camera); + } + + /** + * 预置位跳转 + */ + @Override + protected boolean doGotoPreset(Camera camera) { + IRNETHANDLE handle = lincseekLoginService.login(camera); + boolean result = irNetSDK.IRNET_ClientPTZCtrl(handle, IRNetSDKConst.PTZ_GOTOPOINT, camera.getPointNum(), 0, null, 0); + log.info("[" + brandName() + "]跳转预置位{},结果: {}", camera.getPointNum(), result); + return result; + } + + private byte[] jpegCapSingle(Camera camera) { + IRNetSDKCallback.JpegDataCallback jpegCb = (hHandle, mCh, pBuffer, size, extraData, userdata) -> { + String requestKey = userdata.getString(0); + CompletableFuture future = PENDING_REQUESTS.remove(requestKey); + if (pBuffer != null && future != null) { + log.info("[" + brandName() + "]匹配到抓图回调 (requestKey={}, 通道={}, 大小={}B)", requestKey, mCh, size); + byte[] jpgBytes = pBuffer.getByteArray(0, size); + if (extraData != null) { + IRNetSDKStruct.FFF_TEMPERATURE_DATA tempData = new IRNetSDKStruct.FFF_TEMPERATURE_DATA(extraData); + short w = tempData.width; + short h = tempData.height; + int pixelCount = w * h; + if (tempData.temperatueData != null && pixelCount > 0) { + log.info("[" + brandName() + "]获取到测温矩阵,w:{}, h:{}", w, h); + float[] temps = tempData.temperatueData.getFloatArray(0, pixelCount); + float[][] irData = new float[h][w]; + for (int row = 0; row < h; row++) { + System.arraycopy(temps, row * w, irData[row], 0, w); + } + try { + jpgBytes = DLT664Service.generate(jpgBytes, irData, cameraType().name()); + log.info("[" + brandName() + "]DLT664文件格式生成成功!"); + } catch (Exception e) { + log.error("[" + brandName() + "]DLT664文件格式生成失败!"); + } + } + } + future.complete(jpgBytes); + } else { + // 可能是由于超时已经被移除了,或者是其他类型的抓图 + log.error("[{}]未匹配的抓图回调,requestKey={}", brandName(), requestKey); + } + }; + + int mySerialId = nextSerial(); + String requestKey = ":" + mySerialId; + Pointer userData = stringToPointer(requestKey); + IRNETHANDLE capHandle = irNetSDK.IRNET_ClientJpegCapStart("video server", camera.getIp(), camera.getUserName(), camera.getPassword(), (short) camera.getPort(), jpegCb, userData); + if (!IRNETHANDLE.INVALID_HANDLE_VALUE.equals(capHandle)) { + CompletableFuture future = new CompletableFuture<>(); + log.info("capHandle: {}, {}, channel: {}", capHandle.toString(), capHandle.hashCode(), camera.getChannel()); + PENDING_REQUESTS.put(requestKey, future); + final int TIMEOUT_SEC = 10; + + try { + Memory cpty = new Memory(2); + cpty.setShort(0, (short) VSNET_CAPTURE_TYPE_LCR); + IRNETHANDLE msgHandle = irNetSDK.IRNET_ClientMessageOpen("video server", camera.getIp(), camera.getUserName(), camera.getPassword(), (short) camera.getPort()); + if (!IRNETHANDLE.INVALID_HANDLE_VALUE.equals(msgHandle)) { + int ret = irNetSDK.IRNET_ClientMessageOpt(msgHandle, MESSAGE_CMD_SET_CAPTURETYPE, 0, cpty, null, null); + if (ret == 0) { + log.error("CaptureType set failed!"); + } + irNetSDK.IRNET_ClientMessageClose(msgHandle); + } + + boolean isCaptured = irNetSDK.IRNET_ClientJpegCapSingle(capHandle, camera.getChannel(), 100); + if (!isCaptured) { + throw new RuntimeException("SDK抓图失败"); + } + return future.get(TIMEOUT_SEC, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException("[" + brandName() + "]抓图异常:", e); + } finally { + PENDING_REQUESTS.remove(requestKey); + irNetSDK.IRNET_ClientJpegCapStop(capHandle); + } + } + return null; + } + + @Override + protected TemperatureData doMeasureTemperature(Camera camera) { + return getTempValueEx(camera); + } + + /** + * 获取最高温、最低温、平均温 + */ + private TemperatureData getTempValueEx(Camera camera) { + IRNETHANDLE msgHandle = irNetSDK.IRNET_ClientMessageOpen("video server", camera.getIp(), camera.getUserName(), camera.getPassword(), (short) camera.getPort()); + try { + if (!IRNETHANDLE.INVALID_HANDLE_VALUE.equals(msgHandle)) { + IRNetSDKStruct.VSNET_TEMP_VALUE_EX temps = new IRNetSDKStruct.VSNET_TEMP_VALUE_EX(); + temps.write(); + int r = irNetSDK.IRNET_ClientMessageOpt(msgHandle, IRNetSDKConst.MessageOpt.MESSAGE_CMD_GET_TEMPVALUE_EX, camera.getChannel(), temps.getPointer(), null, null); + if (r == 1) { + temps.read(); + float maxTemp = temps.m_maxtempinfo.m_temp_value; + int maxX = temps.m_maxtempinfo.m_temp_x; + int maxY = temps.m_maxtempinfo.m_temp_y; + float minTemp = temps.m_mintempinfo.m_temp_value; + int minX = temps.m_mintempinfo.m_temp_x; + int minY = temps.m_mintempinfo.m_temp_y; + float avgTemp = temps.m_avgtempinfo; + log.info("[" + brandName() + "]测温结果: 最高={}℃({},{}), 最低={}℃({},{}), 平均={}℃", + maxTemp, maxX, maxY, minTemp, minX, minY, avgTemp); + return new TemperatureData( + String.valueOf(maxTemp), + String.valueOf(minTemp), + avgTemp, + 0, + camera.getChannel(), + -1 + ); + } + } + } catch (Exception e) { + log.error("[" + brandName() + "]测温异常:", e); + } finally { + irNetSDK.IRNET_ClientMessageClose(msgHandle); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/camera/login/AbstractLoginService.java b/src/main/java/com/inspect/nvr/service/camera/login/AbstractLoginService.java new file mode 100644 index 00000000..2c900de1 --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/login/AbstractLoginService.java @@ -0,0 +1,114 @@ +package com.inspect.nvr.service.camera.login; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.enums.CameraEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * 品牌相机登录会话服务抽象基类。 + * 统一管理会话句柄缓存(10分钟未访问自动过期并登出)、登出失败记录, + * 子类只需实现品牌 SDK 的登录/登出差异钩子。 + */ +@Slf4j +public abstract class AbstractLoginService { + + /** + * 会话缓存:10分钟未被访问即过期;仅过期/容量淘汰时回调 SDK 登出。 + */ + private final Cache sessionCache = Caffeine.newBuilder() + .expireAfterAccess(10, TimeUnit.MINUTES) + .removalListener((String ip, H handle, RemovalCause cause) -> { + if (handle != null && (cause == RemovalCause.EXPIRED || cause == RemovalCause.SIZE)) { + log.info("[{}]会话超时自动登出,ip: {},句柄: {}", brandName(), ip, handle); + doLogout(ip, handle); + } + }).build(); + + /** + * 品牌对应的摄像头枚举,与 AbstractCameraService 同一口径。 + */ + protected abstract CameraEnum cameraType(); + + /** + * 日志品牌名:取枚举 name 字段(中文名),如海康/大华/朗驰。 + */ + protected String brandName() { + return cameraType().getName(); + } + + /** + * 品牌 SDK 登录,失败抛 RuntimeException,不允许返回 null。 + */ + protected abstract H doSdkLogin(Camera camera); + + /** + * 品牌 SDK 登出,成功返回 true。 + */ + protected abstract boolean doSdkLogout(H handle); + + /** + * 最近一次 SDK 错误码;无错误码能力的品牌返回 -1。 + */ + protected abstract int lastErrorCode(); + + /** + * 登录:命中缓存直接返回并刷新过期时间;未命中时 Caffeine.get 保证同一 IP 并发只执行一次 SDK 登录。 + */ + public H login(Camera camera) { + String ip = camera.getIp(); + H existHandle = sessionCache.getIfPresent(ip); + if (existHandle != null) { + log.info("[{}]登录命中缓存,ip: {},句柄: {}", brandName(), ip, existHandle); + return existHandle; + } + return sessionCache.get(ip, key -> { + H handle = doSdkLogin(camera); + log.info("[{}]登录成功,ip:{},句柄:{}", brandName(), ip, handle); + return handle; + }); + } + + /** + * 登出具体实现;SDK 登出失败时记录到 Redis。 + */ + public void doLogout(String ip, H handle) { + if (handle == null) { + return; + } + if (doSdkLogout(handle)) { + log.info("[{}]登出成功,ip: {},句柄: {}", brandName(), ip, handle); + return; + } + int errorCode = lastErrorCode(); + log.error("[{}]登出失败,ip: {},句柄: {},错误码: {}", brandName(), ip, handle, errorCode); +// recordLogoutError(ip, handle, errorCode); + } + + /** + * 主动登出指定 IP:仅移除缓存。移除原因为 EXPLICIT,不会触发自动登出回调(不额外调用 SDK 登出)。 + */ + public void logout(String ip) { + sessionCache.invalidate(ip); + } + + /** + * 登出所有会话并清空缓存。 + */ + public void logoutAll() { + sessionCache.asMap().forEach(this::doLogout); + sessionCache.invalidateAll(); + log.info("[{}]所有用户已登出", brandName()); + } + + /** + * 是否已登录(getIfPresent 同时刷新访问过期时间)。 + */ + public boolean isLoggedIn(String ip) { + return sessionCache.getIfPresent(ip) != null; + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/camera/login/DahuaLoginService.java b/src/main/java/com/inspect/nvr/service/camera/login/DahuaLoginService.java new file mode 100644 index 00000000..70fbe71f --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/login/DahuaLoginService.java @@ -0,0 +1,54 @@ +package com.inspect.nvr.service.camera.login; + +import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; +import com.inspect.nvr.daHuaCarme.jna.NetSDKLib.LLong; +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.enums.CameraEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 大华登录服务:会话句柄为 CLIENT_LoginWithHighLevelSecurity 返回的 LLong。 + */ +@Slf4j +@Service +public class DahuaLoginService extends AbstractLoginService { + + @Resource + private NetSDKLib dhNetSDK; + + @Override + protected CameraEnum cameraType() { + return CameraEnum.DAHUA; + } + + @Override + protected LLong doSdkLogin(Camera camera) { + NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY inParam = + new NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY(); + inParam.szIP = camera.getIp().getBytes(); + inParam.nPort = camera.getPort(); + inParam.szUserName = camera.getUserName().getBytes(); + inParam.szPassword = camera.getPassword().getBytes(); + NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY outParam = + new NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY(); + LLong loginID = dhNetSDK.CLIENT_LoginWithHighLevelSecurity(inParam, outParam); + if (loginID == null || loginID.intValue() == 0) { + throw new RuntimeException("登录失败,错误码:" + lastErrorCode()); + } + return loginID; + } + + @Override + protected boolean doSdkLogout(LLong loginID) { + return dhNetSDK.CLIENT_Logout(loginID); + } + + @Override + protected int lastErrorCode() { + return dhNetSDK.CLIENT_GetLastError(); + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/camera/login/HikLoginService.java b/src/main/java/com/inspect/nvr/service/camera/login/HikLoginService.java new file mode 100644 index 00000000..7bf2fd23 --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/login/HikLoginService.java @@ -0,0 +1,47 @@ +package com.inspect.nvr.service.camera.login; + +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.enums.CameraEnum; +import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; +import com.inspect.nvr.hikVision.utils.jna.HikVisionUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 海康登录服务:会话句柄为 NET_DVR_Login_V40 返回的 userID(int)。 + */ +@Slf4j +@Service +public class HikLoginService extends AbstractLoginService { + + @Autowired + private HCNetSDK hcNetSDK; + + @Override + protected CameraEnum cameraType() { + return CameraEnum.HIKVISION; + } + + @Override + protected Integer doSdkLogin(Camera camera) { + HCNetSDK.NET_DVR_USER_LOGIN_INFO loginInfo = HikVisionUtils.login_V40( + camera.getIp(), (short) camera.getPort(), camera.getUserName(), camera.getPassword()); + HCNetSDK.NET_DVR_DEVICEINFO_V40 deviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40(); + int userID = hcNetSDK.NET_DVR_Login_V40(loginInfo, deviceInfo); + if (userID < 0) { + throw new RuntimeException("登录失败,错误码:" + lastErrorCode()); + } + return userID; + } + + @Override + protected boolean doSdkLogout(Integer userID) { + return hcNetSDK.NET_DVR_Logout(userID); + } + + @Override + protected int lastErrorCode() { + return hcNetSDK.NET_DVR_GetLastError(); + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/camera/login/LincseekLoginService.java b/src/main/java/com/inspect/nvr/service/camera/login/LincseekLoginService.java new file mode 100644 index 00000000..919ff1e6 --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/camera/login/LincseekLoginService.java @@ -0,0 +1,53 @@ +package com.inspect.nvr.service.camera.login; + +import com.inspect.nvr.domain.Infrared.Camera; +import com.inspect.nvr.enums.CameraEnum; +import com.inspect.nvr.jna.lincseek.IRNetSDK; +import com.inspect.nvr.jna.lincseek.IRNetSDKStruct; +import com.inspect.nvr.jna.lincseek.IRNetSDKStruct.IRNETHANDLE; +import com.sun.jna.Pointer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 朗驰登录服务:会话句柄为 IRNET_ClientStart 返回的 IRNETHANDLE。 + * 朗驰 SDK 无错误码接口,登出失败时错误码记 -1。 + */ +@Slf4j +@Service +public class LincseekLoginService extends AbstractLoginService { + + @Resource + private IRNetSDK irNetSDK; + + @Override + protected CameraEnum cameraType() { + return CameraEnum.LINCSEEK; + } + + @Override + protected IRNETHANDLE doSdkLogin(Camera camera) { + String ip = camera.getIp(); + IRNetSDKStruct.CHANNEL_CLIENTINFO info = IRNetSDKStruct.CHANNEL_CLIENTINFO.createWithUrl( + "video server", camera.getUserName(), camera.getPassword(), + (byte) camera.getChannel(), ip, null, null, 0, null, null); + IRNETHANDLE handle = irNetSDK.IRNET_ClientStart(ip, info, (short) camera.getPort(), 0); + if (IRNETHANDLE.INVALID_HANDLE_VALUE.equals(handle)) { + throw new RuntimeException("[" + brandName() + "]登录失败, ip: " + ip); + } + return handle; + } + + @Override + protected boolean doSdkLogout(IRNETHANDLE handle) { + return irNetSDK.IRNET_ClientStop(handle); + } + + @Override + protected int lastErrorCode() { + // 朗驰 SDK 未提供错误码接口 + return -1; + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/impl/DahuaServiceImpl.java b/src/main/java/com/inspect/nvr/service/impl/DahuaServiceImpl.java index 640d0ca5..0cbec997 100644 --- a/src/main/java/com/inspect/nvr/service/impl/DahuaServiceImpl.java +++ b/src/main/java/com/inspect/nvr/service/impl/DahuaServiceImpl.java @@ -8,7 +8,7 @@ import com.inspect.nvr.daHuaCarme.utils.jna.DahuaUtils; import com.inspect.nvr.domain.Infrared.Camera; import com.inspect.nvr.domain.Infrared.NvrInfo; import com.inspect.nvr.domain.Infrared.TemperatureData; -import com.inspect.nvr.service.DahuaLoginService; +import com.inspect.nvr.service.camera.login.DahuaLoginService; import com.inspect.nvr.service.DahuaService; import com.inspect.nvr.utils.StringUtils; import com.inspect.nvr.utils.redis.RedisService; @@ -106,12 +106,7 @@ public class DahuaServiceImpl implements DahuaService { // // 3. 安全转换并创建LLong对象 // if (ObjectUtil.isEmpty(m_hLoginHandle)) { log.info("大华摄像机登录 ip:{}", camera.getIp()); - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp(camera.getIp()); - nvrInfo.setServerPort(camera.getPort()); - nvrInfo.setAccount(camera.getUserName()); - nvrInfo.setPassword(camera.getPassword()); - m_hLoginHandle = dahuaLoginService.login(nvrInfo); + m_hLoginHandle = dahuaLoginService.login(camera); // m_hLoginHandle = new NetSDKLib.LLong((Long) redisService.redisTemplate.opsForValue().get(camera.getIp() + "_m_hLoginHandle")); if (StringUtils.isNull(m_hLoginHandle)) { return null; @@ -200,12 +195,7 @@ public class DahuaServiceImpl implements DahuaService { LLong m_hLoginHandle = null; // } // if (ObjectUtil.isEmpty(m_hLoginHandle)) { - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp(camera.getIp()); - nvrInfo.setServerPort(camera.getPort()); - nvrInfo.setAccount(camera.getUserName()); - nvrInfo.setPassword(camera.getPassword()); - m_hLoginHandle = dahuaLoginService.login(nvrInfo); + m_hLoginHandle = dahuaLoginService.login(camera); log.info("相机:m_hLoginHandle" + camera.getIp() + "m_hLoginHandle" + m_hLoginHandle); // } String ctr = "GOTO_PRESET"; diff --git a/src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java b/src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java index 7d8db5fc..9785682b 100644 --- a/src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java +++ b/src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java @@ -17,9 +17,9 @@ import com.inspect.nvr.hikVision.utils.AjaxResult; import com.inspect.nvr.hikVision.utils.StringUtils; import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; import com.inspect.nvr.hikVision.utils.jna.HikVisionUtils; -import com.inspect.nvr.service.HikCameraService; +import com.inspect.nvr.service.camera.HikCameraService; import com.inspect.nvr.service.HikFRemoteConfigCallBack_imp; -import com.inspect.nvr.service.HikLoginService; +import com.inspect.nvr.service.camera.login.HikLoginService; import com.inspect.nvr.service.HikVisionService; import com.inspect.nvr.tempCount.TempCount; import com.inspect.nvr.utils.DateUtils; @@ -226,13 +226,9 @@ public class HikVisionServiceImpl implements HikVisionService { * 海康实时测温接口 * 优化并发场景 */ + @Override public TemperatureData StartRemote(Camera camera) { - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp(camera.getIp()); - nvrInfo.setServerPort(camera.getPort()); - nvrInfo.setAccount(camera.getUserName()); - nvrInfo.setPassword(camera.getPassword()); - TemperatureData data = hikCameraService.realTimeThermometry(nvrInfo, camera.getChannel()); + TemperatureData data = hikCameraService.measureTemperature(camera); return data; } @@ -249,12 +245,7 @@ public class HikVisionServiceImpl implements HikVisionService { log.info("实时测温入口================================================="); if (camera.getLUserID() != 0) { - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp(camera.getIp()); - nvrInfo.setServerPort(camera.getPort()); - nvrInfo.setAccount(camera.getUserName()); - nvrInfo.setPassword(camera.getPassword()); - int userId = hikLoginService.login(nvrInfo); + int userId = hikLoginService.login(camera); camera.setLUserID(userId); } @@ -451,6 +442,7 @@ public class HikVisionServiceImpl implements HikVisionService { } //红外摄像头任务流程 + @Override public InfraredInfo cameraCalculatePicture(Camera camera) { log.info("进入摄像头红外图片接口!"); picPath = "/2/"; diff --git a/src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java b/src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java index 6131e065..ef934299 100644 --- a/src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java +++ b/src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java @@ -12,6 +12,8 @@ import com.inspect.nvr.hikVision.utils.StringUtils; import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; import com.inspect.nvr.hikVision.utils.jna.HikVisionUtils; import com.inspect.nvr.service.*; +import com.inspect.nvr.service.camera.*; +import com.inspect.nvr.service.camera.login.*; import com.inspect.nvr.utils.StringHexConverter; import com.inspect.nvr.utils.redis.RedisService; import com.sun.jna.ptr.IntByReference; @@ -63,6 +65,8 @@ public class IvsCameraServiceImpl implements IvsCameraService { private HikLoginService hikLoginService; @Resource private LincseekCameraService lincseekCameraService; + @Resource + private CameraServiceFactory cameraServiceFactory; @Override public IvsPresetListView ptzPresetList(String cameraCode, String domainCode) { @@ -86,58 +90,37 @@ public class IvsCameraServiceImpl implements IvsCameraService { //跳转预置位 @Override public PtzControlResult ptzControl(PtzControlParam param) { - PtzControlResult ptzControlResult = null; - Camera camera = new Camera(); - String[] splitArray = param.getAddress().split(":"); - //给camera赋值 - //cameraType=0海康 1大华 2朗驰 - String ip = splitArray[0]; - int port = Integer.parseInt(splitArray[1]); - int channel = Integer.parseInt(splitArray[2]); - int pointNum = Integer.parseInt(splitArray[3]); - String username = splitArray[5]; - String password = splitArray[6]; - int cameraType = Integer.parseInt(splitArray[4]); - camera.setIp(ip); - camera.setPort(port); - camera.setChannel(channel); - camera.setPointNum(pointNum); - camera.setUserName(username); - camera.setPassword(password); - log.info("预置位NVR:" + ip + " channel:" + channel + " pointNum:" + pointNum + " cameraType:" + cameraType); - //给camera赋值结束 - ptzDetailControl(camera, cameraType); + // PTZ 走 NVR 接入信息(地址串 0/1/2/5/6 段);测温走摄像头直连信息(7/8/9/10/11 段) + // 地址串非法(空/段数不对/数字非法)时直接抛出,快速失败,不静默吞掉 + Camera ptzCamera = Camera.fromNvrAddressString(param.getAddress()); + Camera thermalCamera = Camera.fromAddressString(param.getAddress()); + thermalCamera.setAddress(param.getAddress()); + thermalCamera.setPresetId(thermalCamera.getPointNum()); + log.info("预置位NVR:{} channel:{} pointNum:{} cameraType:{}", + ptzCamera.getIp(), ptzCamera.getChannel(), ptzCamera.getPointNum(), ptzCamera.getCameraType()); + + // 组合操作:转预置位 -> 到位等待 -> 测温,失败整对重试;模板内部已捕获 SDK 异常,失败返回 null + TemperatureData temperatureData = cameraServiceFactory.get(ptzCamera.getCameraType()) + .gotoPresetAndMeasure(ptzCamera, thermalCamera); + if (temperatureData == null) { + log.error("预置位跳转/测温失败,cameraType={}, ip={}, channel={}", + thermalCamera.getCameraType(), thermalCamera.getIp(), thermalCamera.getChannel()); + return PtzControlResult.builder() + .resultCode("-1") + .build(); + } + // 云台+测温已成功;缓存写入失败不影响业务结果,仅记录日志 + String redisKey = thermalCamera.getIp() + "_" + thermalCamera.getPresetId(); try { - //等待5s开始调用红外 - Thread.sleep(5000); - Camera cameraHw = new Camera(); - cameraHw.setAddress(param.getAddress()); - cameraHw.setIp(splitArray[7]); - cameraHw.setPort(Integer.parseInt(splitArray[8])); - cameraHw.setChannel(Integer.parseInt(splitArray[9])); - cameraHw.setPresetId(Integer.parseInt(splitArray[3])); - cameraHw.setCameraType(Integer.parseInt(splitArray[4])); - cameraHw.setUserName(splitArray[10]); - cameraHw.setPassword(splitArray[11]); - //获取红外温度 - TemperatureData temper = retry(cameraHw, camera, cameraType); - if (retryMode == 1 && StringUtils.isNull(temper)) { - ptzControlResult = PtzControlResult.builder() - .resultCode("-1") - .build(); - } else { - ptzControlResult = PtzControlResult.builder() - .resultCode("0") - .build(); - } + redisService.setCacheObject(redisKey, temperatureData, 14400L, TimeUnit.SECONDS); } catch (Exception e) { -// throw new RuntimeException(e); + log.error("测温结果写入Redis失败,redisKey={}", redisKey, e); } - return ptzControlResult; - + return PtzControlResult.builder() + .resultCode("0") + .build(); } - public PtzControlResult ptzDetailControl(Camera camera, int cameraType) { //大华预置位跳转 PtzControlResult ptzControlResult = null; @@ -146,15 +129,10 @@ public class IvsCameraServiceImpl implements IvsCameraService { log.info("开始登录大华NVR 进行跳转预置位"); dahuaService.cameraControl(camera, 0, 0, 0); } else if (CameraEnum.LINCSEEK.getCode() == cameraType) { - lincseekCameraService.ptzPreset(camera); + lincseekCameraService.gotoPreset(camera); } else { //海康预置位跳转 - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp(camera.getIp()); - nvrInfo.setServerPort(camera.getPort()); - nvrInfo.setAccount(camera.getUserName()); - nvrInfo.setPassword(camera.getPassword()); - int userId = hikLoginService.login(nvrInfo); + int userId = hikLoginService.login(camera); //参数:登录令牌,通道号,预置位跳转,跳转的预置位码 boolean gotoPreset = hcNetSDK.NET_DVR_PTZPreset_Other(userId, camera.getChannel(), HCNetSDK.GOTO_PRESET, camera.getPointNum()); if (!gotoPreset) { @@ -385,67 +363,19 @@ public class IvsCameraServiceImpl implements IvsCameraService { public ByteArrayInputStream capture(String fileSessionId) { final String rawString = StringHexConverter.fromHex(fileSessionId); log.info("downloadImage hexString: {}, rawString: {}", fileSessionId, rawString); - String[] cameraAddressInfos = rawString.split((":")); - log.info("doCapture ip: {}, port: {}, channel: {}, pointName: {},cameraType: {}, username: {}, password: {}", cameraAddressInfos[0], cameraAddressInfos[1], cameraAddressInfos[2], cameraAddressInfos[3], cameraAddressInfos[4], cameraAddressInfos[5], cameraAddressInfos[6]); - Camera camera = new Camera(); - camera.setIp(cameraAddressInfos[0]); - camera.setPort(Integer.parseInt(cameraAddressInfos[1])); - camera.setChannel(Integer.parseInt(cameraAddressInfos[2])); - camera.setPointNum(Integer.parseInt(cameraAddressInfos[3])); - camera.setCameraType(Integer.parseInt(cameraAddressInfos[4])); - camera.setUserName(cameraAddressInfos[5]); - camera.setPassword(cameraAddressInfos[6]); + // 抓图走摄像头本身信息(地址串 7/8/9/10/11 段) + Camera camera = Camera.fromAddressString(rawString); + log.info("doCapture ip: {}, port: {}, channel: {}, pointName: {}, cameraType: {}, username: {}", + camera.getIp(), camera.getPort(), camera.getChannel(), camera.getPointNum(), camera.getCameraType(), camera.getUserName()); - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp(camera.getIp()); - nvrInfo.setServerPort(camera.getPort()); - nvrInfo.setAccount(camera.getUserName()); - nvrInfo.setPassword(camera.getPassword()); - - if (CameraEnum.DAHUA.getCode() == camera.getCameraType()) { - log.info("大华相机抓图"); - byte[] bytes = dahuaCameraService.capture(nvrInfo, camera.getChannel()); - return new ByteArrayInputStream(bytes); - } else if (CameraEnum.LINCSEEK.getCode() == camera.getCameraType()) { - log.info("朗驰相机抓图"); - byte[] bytes = lincseekCameraService.capture(nvrInfo, camera.getChannel()); - return new ByteArrayInputStream(bytes); - } else { - log.info("海康相机抓图"); - byte[] bytes = hikCameraService.capture(nvrInfo, camera.getChannel()); - return new ByteArrayInputStream(bytes); + // ===== 改用工厂统一分发 ===== + BrandCameraService cameraService = cameraServiceFactory.get(camera.getCameraType()); + byte[] bytes = cameraService.capture(camera); + if (bytes == null) { + log.error("相机抓图失败,cameraType={}, ip={}, channel={}", camera.getCameraType(), camera.getIp(), camera.getChannel()); + return new ByteArrayInputStream(new byte[0]); } - } - - @Override - public byte[] captureDigest(String fileSessionId) { - final String rawString = StringHexConverter.fromHex(fileSessionId); - log.info("downloadImage hexString: {}, rawString: {}", fileSessionId, rawString); - String[] cameraAddressInfos = rawString.split((":")); - log.info("captureDigest ip: {}, port: {}, channel: {}, pointName: {},cameraType: {}, username: {}, password: {}", cameraAddressInfos[0], cameraAddressInfos[1], cameraAddressInfos[2], cameraAddressInfos[3], cameraAddressInfos[4], cameraAddressInfos[5], cameraAddressInfos[6]); - Camera camera = new Camera(); - camera.setIp(cameraAddressInfos[0]); - camera.setPort(Integer.parseInt(cameraAddressInfos[1])); - camera.setChannel(Integer.parseInt(cameraAddressInfos[2])); - camera.setPointNum(Integer.parseInt(cameraAddressInfos[3])); - camera.setCameraType(Integer.parseInt(cameraAddressInfos[4])); - camera.setUserName(cameraAddressInfos[5]); - camera.setPassword(cameraAddressInfos[6]); - - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp(camera.getIp()); - nvrInfo.setServerPort(camera.getPort()); - nvrInfo.setAccount(camera.getUserName()); - nvrInfo.setPassword(camera.getPassword()); - if (CameraEnum.DAHUA.getCode() == camera.getCameraType()) { - log.info("大华相机Digest抓图"); - byte[] bytes = dahuaCameraService.captureDigest(nvrInfo, camera.getChannel()); - return bytes; - } else { - log.info("海康相机Digest抓图"); - byte[] bytes = hikCameraService.captureDigest(nvrInfo, camera.getChannel()); - return bytes; - } + return new ByteArrayInputStream(bytes); } } diff --git a/src/main/java/com/inspect/nvr/task/ImageCleanupTask.java b/src/main/java/com/inspect/nvr/task/ImageCleanupTask.java index 9ba83f4e..d2d62215 100644 --- a/src/main/java/com/inspect/nvr/task/ImageCleanupTask.java +++ b/src/main/java/com/inspect/nvr/task/ImageCleanupTask.java @@ -14,7 +14,7 @@ import java.time.format.DateTimeParseException; import java.util.Comparator; import java.util.stream.Stream; -import static com.inspect.nvr.service.CommonCameraService.FILE_DIR; +import static com.inspect.nvr.service.camera.AbstractCameraSupport.FILE_DIR; /** * 定期清理抓拍图片 diff --git a/src/main/java/com/inspect/nvr/utils/DLT664Service.java b/src/main/java/com/inspect/nvr/utils/DLT664Service.java new file mode 100644 index 00000000..8f2e65ad --- /dev/null +++ b/src/main/java/com/inspect/nvr/utils/DLT664Service.java @@ -0,0 +1,193 @@ +package com.inspect.nvr.utils; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; + +/** + * 规范参考 DL/T 664——2016 《带电设备红外诊断应用规范》 + */ +@Slf4j +public class DLT664Service { + + // 各字段字节长度:数值字段直接取对应 Java 类型的字节数,定长字节数组用命名常量 + private static final int SIZE_FILE_VERSION = Short.BYTES; + private static final int SIZE_WIDTH = Short.BYTES; + private static final int SIZE_HEIGHT = Short.BYTES; + private static final int SIZE_DATE_TIME = 14; + private static final int SIZE_EMISS = Float.BYTES; + private static final int SIZE_AMBIENT_TEMPERATURE = Float.BYTES; + private static final int SIZE_LEN = Byte.BYTES; + private static final int SIZE_DISTANCE = Integer.BYTES; + private static final int SIZE_RELATIVE_HUMIDITY = Byte.BYTES; + private static final int SIZE_REFLECTED_TEMPERATURE = Float.BYTES; + private static final int SIZE_STRING_FIELD = 32; + private static final int SIZE_LONGITUDE = Double.BYTES; + private static final int SIZE_LATITUDE = Double.BYTES; + private static final int SIZE_ALTITUDE = Integer.BYTES; + private static final int SIZE_DESCRIPTION_LENGTH = Integer.BYTES; + private static final int SIZE_OFFSET = Integer.BYTES; + private static final int SIZE_FILE_END_TYPE = 16; + + // 定长 ASCII 字符串字段的补齐字节 + private static final byte STRING_PAD = (byte) ' '; + + public static byte[] generate(byte[] imageBytes, float[][] temperatureMatrix, String productor) { + Info info = new Info(); + info.setWidth((short) temperatureMatrix[0].length); + info.setHeight((short) temperatureMatrix.length); + info.setIrData(temperatureMatrix); + String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + info.setDateTime(toAsciiBytes(dateTime)); + info.setProductor(toAsciiBytes(productor)); + return generate(imageBytes, info); + } + + public static byte[] generate(byte[] imageBytes, Info info) { + float[][] irData = info.getIrData(); + int width = info.getWidth(); + int height = info.getHeight(); + int descriptionLength = info.getDescriptionLength(); + + // 红外数据块大小,表达式顺序与下方写入顺序一一对应 + int dataSize = SIZE_FILE_VERSION + SIZE_WIDTH + SIZE_HEIGHT + + SIZE_DATE_TIME + + width * height * Float.BYTES + + SIZE_EMISS + SIZE_AMBIENT_TEMPERATURE + SIZE_LEN + SIZE_DISTANCE + + SIZE_RELATIVE_HUMIDITY + SIZE_REFLECTED_TEMPERATURE + + SIZE_STRING_FIELD * 3 + + SIZE_LONGITUDE + SIZE_LATITUDE + SIZE_ALTITUDE + + SIZE_DESCRIPTION_LENGTH + descriptionLength; + + ByteBuffer dataBuf = ByteBuffer.allocate(dataSize).order(ByteOrder.LITTLE_ENDIAN); + + // 文件版本、矩阵宽高 + dataBuf.putShort(info.getFileVersion()); + dataBuf.putShort((short) width); + dataBuf.putShort((short) height); + + // 拍摄时间 + putFixedBytes(dataBuf, info.getDateTime(), SIZE_DATE_TIME, STRING_PAD); + + // 温度矩阵 + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + dataBuf.putFloat(irData[row][col]); + } + } + + // 环境参数 + dataBuf.putFloat(info.getEmiss()); + dataBuf.putFloat(info.getAmbientTemperature()); + dataBuf.put(info.getLen()); + dataBuf.putInt(info.getDistance()); + dataBuf.put(info.getRelativeHumidity()); + dataBuf.putFloat(info.getReflectedTemperature()); + + // 定长字符串 + putFixedBytes(dataBuf, info.getProductor(), SIZE_STRING_FIELD, STRING_PAD); + putFixedBytes(dataBuf, info.getType(), SIZE_STRING_FIELD, STRING_PAD); + putFixedBytes(dataBuf, info.getSerialNo(), SIZE_STRING_FIELD, STRING_PAD); + + // 经纬度、海拔、备注 + dataBuf.putDouble(info.getLongitude()); + dataBuf.putDouble(info.getLatitude()); + dataBuf.putInt(info.getAltitude()); + dataBuf.putInt(descriptionLength); + if (descriptionLength > 0) { + putFixedBytes(dataBuf, info.getDescriptionData(), descriptionLength, (byte) 0); + } + + if (dataBuf.hasRemaining()) { + throw new IllegalStateException("dataSize 与实际写入字节数不一致,剩余 " + dataBuf.remaining() + " 字节"); + } + + byte[] dataBlock = dataBuf.array(); + + // 拼接: 图片 + 红外数据块 + 偏移量(4) + 文件末尾标识(16) + int offset = imageBytes.length; + byte[] result = new byte[offset + dataBlock.length + SIZE_OFFSET + SIZE_FILE_END_TYPE]; + System.arraycopy(imageBytes, 0, result, 0, offset); + System.arraycopy(dataBlock, 0, result, offset, dataBlock.length); + ByteBuffer.wrap(result, offset + dataBlock.length, SIZE_OFFSET) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(offset); + System.arraycopy(info.getFileEndType(), 0, result, offset + dataBlock.length + SIZE_OFFSET, SIZE_FILE_END_TYPE); + + log.info("DLT664文件组装成功"); + return result; + } + + /** + * 写入定长字节:不足补 fill,超出截断。 + */ + private static void putFixedBytes(ByteBuffer buf, byte[] src, int len, byte fill) { + byte[] bytes = new byte[len]; + Arrays.fill(bytes, fill); + if (src != null) { + System.arraycopy(src, 0, bytes, 0, Math.min(src.length, len)); + } + buf.put(bytes); + } + + private static byte[] toAsciiBytes(String s) { + return s != null ? s.getBytes(StandardCharsets.US_ASCII) : null; + } + + @Data + public static class Info { + // 文件版本 + private short fileVersion = (short) 0x0100; + // 矩阵宽度 + private short width; + // 矩阵高度 + private short height; + // 拍摄时间 (14 bytes ASCII) + private byte[] dateTime; + // 整个温度矩阵 + private float[][] irData; + // 辐射率 + private float emiss; + // 环境温度 + private float ambientTemperature; + // 镜头度数 + private byte len; + // 拍摄距离 (4 bytes) + private int distance; + // 相对湿度 + private byte relativeHumidity; + // 反射温度 + private float reflectedTemperature; + // 生产厂家 (32 bytes) + private byte[] productor; + // 产品型号 (32 bytes) + private byte[] type; + // 产品序列号 (32 bytes) + private byte[] serialNo; + // 经度 + private double longitude = 0; + // 纬度 + private double latitude = 0; + // 海拔 (4 bytes) + private int altitude = 0; + // 备注信息长度, 0 表示没有存储信息 + private int descriptionLength = 0; + // 备注信息 (descriptionLength bytes) + private byte[] descriptionData; + // 红外数据的起始偏移地址 + private int irDataOffset; + // 文件末尾标识 (16 bytes) + private byte[] fileEndType = { + (byte) 0x37, (byte) 0x66, (byte) 0x07, (byte) 0x1a, + (byte) 0x12, (byte) 0x3a, (byte) 0x4c, (byte) 0x9f, + (byte) 0xa9, (byte) 0x5d, (byte) 0x21, (byte) 0xd2, + (byte) 0xda, (byte) 0x7d, (byte) 0x26, (byte) 0xbc + }; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bae10eaa..98888a22 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - port: 8080 + port: 9637 spring: application: