Compare commits

...

3 Commits

68 changed files with 1664 additions and 1044 deletions
Unified View
  1. +3
    -0
      .gitignore
  2. BIN
      lib/linux64/HCNetSDKCom/libAudioIntercom.so
  3. BIN
      lib/linux64/HCNetSDKCom/libAudioProcess.so
  4. BIN
      lib/linux64/HCNetSDKCom/libAudioRender.so
  5. BIN
      lib/linux64/HCNetSDKCom/libHCAlarm.so
  6. BIN
      lib/linux64/HCNetSDKCom/libHCCoreDevCfg.so
  7. BIN
      lib/linux64/HCNetSDKCom/libHCDisplay.so
  8. BIN
      lib/linux64/HCNetSDKCom/libHCGeneralCfgMgr.so
  9. BIN
      lib/linux64/HCNetSDKCom/libHCIndustry.so
  10. BIN
      lib/linux64/HCNetSDKCom/libHCPlayBack.so
  11. BIN
      lib/linux64/HCNetSDKCom/libHCPreview.so
  12. BIN
      lib/linux64/HCNetSDKCom/libHCVoiceTalk.so
  13. BIN
      lib/linux64/HCNetSDKCom/libStreamTransClient.so
  14. BIN
      lib/linux64/HCNetSDKCom/libSuperRender.so
  15. BIN
      lib/linux64/HCNetSDKCom/libSystemTransform.so
  16. BIN
      lib/linux64/HCNetSDKCom/libanalyzedata.so
  17. BIN
      lib/linux64/HCNetSDKCom/libiconv.so.2
  18. BIN
      lib/linux64/HCNetSDKCom/libopenal.so
  19. +12
    -0
      lib/linux64/HCNetSDK_Log_Switch.xml
  20. BIN
      lib/linux64/libAudioProcess.so
  21. BIN
      lib/linux64/libAudioRender.so
  22. BIN
      lib/linux64/libColorSpace.so
  23. BIN
      lib/linux64/libHCCore.so
  24. BIN
      lib/linux64/libHXVA.so
  25. BIN
      lib/linux64/libNPQos.so
  26. BIN
      lib/linux64/libPlayCtrl.so
  27. BIN
      lib/linux64/libSuperRender.so
  28. BIN
      lib/linux64/libcrypto.so.3
  29. BIN
      lib/linux64/libhcnetsdk.so
  30. BIN
      lib/linux64/libhpr.so
  31. BIN
      lib/linux64/libopenal.so.1
  32. BIN
      lib/linux64/libsafefunc.so
  33. BIN
      lib/linux64/libssl.so.3
  34. BIN
      lib/linux64/libz.so
  35. +0
    -4
      sdkLog/SdkLog_1_W.log
  36. +32
    -11
      src/main/java/com/inspect/nvr/config/HikLibConfig.java
  37. +27
    -8
      src/main/java/com/inspect/nvr/config/LincseekConfig.java
  38. +3
    -3
      src/main/java/com/inspect/nvr/config/SdkCleanup.java
  39. +4
    -2
      src/main/java/com/inspect/nvr/controller/CameraController.java
  40. +0
    -10
      src/main/java/com/inspect/nvr/controller/IvsCameraController.java
  41. +52
    -1
      src/main/java/com/inspect/nvr/domain/Infrared/Camera.java
  42. +2
    -4
      src/main/java/com/inspect/nvr/domain/Infrared/InfraredInfo.java
  43. +9
    -6
      src/main/java/com/inspect/nvr/enums/CameraEnum.java
  44. +9
    -0
      src/main/java/com/inspect/nvr/jna/lincseek/IRNetSDKConst.java
  45. +0
    -152
      src/main/java/com/inspect/nvr/service/DahuaCameraService.java
  46. +0
    -126
      src/main/java/com/inspect/nvr/service/DahuaLoginService.java
  47. +0
    -125
      src/main/java/com/inspect/nvr/service/HikLoginService.java
  48. +0
    -2
      src/main/java/com/inspect/nvr/service/IvsCameraService.java
  49. +0
    -186
      src/main/java/com/inspect/nvr/service/LincseekCameraService.java
  50. +0
    -123
      src/main/java/com/inspect/nvr/service/LincseekLoginService.java
  51. +186
    -0
      src/main/java/com/inspect/nvr/service/camera/AbstractCameraService.java
  52. +119
    -59
      src/main/java/com/inspect/nvr/service/camera/AbstractCameraSupport.java
  53. +47
    -0
      src/main/java/com/inspect/nvr/service/camera/BrandCameraService.java
  54. +54
    -0
      src/main/java/com/inspect/nvr/service/camera/CameraServiceFactory.java
  55. +204
    -0
      src/main/java/com/inspect/nvr/service/camera/DahuaCameraService.java
  56. +115
    -80
      src/main/java/com/inspect/nvr/service/camera/HikCameraService.java
  57. +78
    -0
      src/main/java/com/inspect/nvr/service/camera/LincHikCompositeCameraService.java
  58. +193
    -0
      src/main/java/com/inspect/nvr/service/camera/LincseekCameraService.java
  59. +114
    -0
      src/main/java/com/inspect/nvr/service/camera/login/AbstractLoginService.java
  60. +54
    -0
      src/main/java/com/inspect/nvr/service/camera/login/DahuaLoginService.java
  61. +47
    -0
      src/main/java/com/inspect/nvr/service/camera/login/HikLoginService.java
  62. +53
    -0
      src/main/java/com/inspect/nvr/service/camera/login/LincseekLoginService.java
  63. +3
    -13
      src/main/java/com/inspect/nvr/service/impl/DahuaServiceImpl.java
  64. +6
    -14
      src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java
  65. +43
    -113
      src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java
  66. +1
    -1
      src/main/java/com/inspect/nvr/task/ImageCleanupTask.java
  67. +193
    -0
      src/main/java/com/inspect/nvr/utils/DLT664Service.java
  68. +1
    -1
      src/main/resources/application.yml

+ 3
- 0
.gitignore View File

@ -1,4 +1,5 @@
HELP.md HELP.md
AGENTS.md
target/ target/
!.mvn/wrapper/maven-wrapper.jar !.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/ !**/src/main/**/target/
@ -30,6 +31,8 @@ build/
!**/src/test/**/build/ !**/src/test/**/build/
/logs/ /logs/
/captures/
/sdkLog/
### VS Code ### ### VS Code ###
.vscode/ .vscode/

BIN
lib/linux64/HCNetSDKCom/libAudioIntercom.so View File


BIN
lib/linux64/HCNetSDKCom/libAudioProcess.so View File


BIN
lib/linux64/HCNetSDKCom/libAudioRender.so View File


BIN
lib/linux64/HCNetSDKCom/libHCAlarm.so View File


BIN
lib/linux64/HCNetSDKCom/libHCCoreDevCfg.so View File


BIN
lib/linux64/HCNetSDKCom/libHCDisplay.so View File


BIN
lib/linux64/HCNetSDKCom/libHCGeneralCfgMgr.so View File


BIN
lib/linux64/HCNetSDKCom/libHCIndustry.so View File


BIN
lib/linux64/HCNetSDKCom/libHCPlayBack.so View File


BIN
lib/linux64/HCNetSDKCom/libHCPreview.so View File


BIN
lib/linux64/HCNetSDKCom/libHCVoiceTalk.so View File


BIN
lib/linux64/HCNetSDKCom/libStreamTransClient.so View File


BIN
lib/linux64/HCNetSDKCom/libSuperRender.so View File


BIN
lib/linux64/HCNetSDKCom/libSystemTransform.so View File


BIN
lib/linux64/HCNetSDKCom/libanalyzedata.so View File


BIN
lib/linux64/HCNetSDKCom/libiconv.so.2 View File


BIN
lib/linux64/HCNetSDKCom/libopenal.so View File


+ 12
- 0
lib/linux64/HCNetSDK_Log_Switch.xml View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="GB2312"?>
<SdkLocal>
<SdkLog>
<logLevel>3</logLevel><!--req, 1-ERROR, 2-DEBUG, 3-INFO-->
<logDirectory>./SDKLOG/</logDirectory><!--the end of the string must be '/'-->
<autoDelete>true</autoDelete><!--true: There are less than 10 files in the directory, it will be auto deleted by sdk when the files are more than 10; false: No upper limit to the number of log files-->
</SdkLog>
<HeartbeatCfg>
<Interval>120</Interval> <!-- 心跳时间间隔,单位秒,等于0,使用默认值120s,取值范围为[30, 120] 小于30s,间隔为30s,大于120s,间隔为120s-->
<Count>1</Count> <!-- 触发异常回调需要心跳交互异常的次数,等于0,使用默认值1次-->
</HeartbeatCfg>
</SdkLocal>

BIN
lib/linux64/libAudioProcess.so View File


BIN
lib/linux64/libAudioRender.so View File


BIN
lib/linux64/libColorSpace.so View File


BIN
lib/linux64/libHCCore.so View File


BIN
lib/linux64/libHXVA.so View File


BIN
lib/linux64/libNPQos.so View File


BIN
lib/linux64/libPlayCtrl.so View File


BIN
lib/linux64/libSuperRender.so View File


BIN
lib/linux64/libcrypto.so.3 View File


BIN
lib/linux64/libhcnetsdk.so View File


BIN
lib/linux64/libhpr.so View File


BIN
lib/linux64/libopenal.so.1 View File


BIN
lib/linux64/libsafefunc.so View File


BIN
lib/linux64/libssl.so.3 View File


BIN
lib/linux64/libz.so View File


+ 0
- 4
sdkLog/SdkLog_1_W.log View File

@ -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.

+ 32
- 11
src/main/java/com/inspect/nvr/config/HikLibConfig.java View File

@ -41,11 +41,25 @@ public class HikLibConfig {
//linux系统建议调用以下接口加载组件库 //linux系统建议调用以下接口加载组件库
if (osSelect.isLinux()) { if (osSelect.isLinux()) {
//Linux系统加载库路径
String arch = System.getProperty("os.arch").toLowerCase();
System.out.println("OS:" + arch);
String strPath1, strPath2, strPathCom;
if (arch.contains("amd64") || arch.contains("x86_64") || arch.equals("x86") || arch.startsWith("i386") || arch.startsWith("i686")) {
strPath1 = System.getProperty("user.dir") + "/lib/hik-linux64/libcrypto.so.3";
strPath2 = System.getProperty("user.dir") + "/lib/hik-linux64/libssl.so.3";
strPathCom = System.getProperty("user.dir") + "/lib/hik-linux64/";
System.out.println("x86 / x86_64 架构");
// } else if (arch.contains("aarch64") || arch.contains("arm64") || arch.startsWith("arm")) {
} else {
strPath1 = System.getProperty("user.dir") + "/lib/libcrypto.so.1.1";
strPath2 = System.getProperty("user.dir") + "/lib/libssl.so.1.1";
strPathCom = System.getProperty("user.dir") + "/lib/";
System.out.println("ARM / AArch64 架构");
}
HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256); HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256);
HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256); HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256);
//这里是库的绝对路径请根据实际情况修改注意改路径必须有访问权限
String strPath1 = System.getProperty("user.dir") + "/lib/libcrypto.so.1.1";
String strPath2 = System.getProperty("user.dir") + "/lib/libssl.so.1.1";
System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length()); System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length());
ptrByteArray1.write(); ptrByteArray1.write();
@ -55,7 +69,6 @@ public class HikLibConfig {
ptrByteArray2.write(); ptrByteArray2.write();
hcNetSDK.NET_DVR_SetSDKInitCfg(4, ptrByteArray2.getPointer()); hcNetSDK.NET_DVR_SetSDKInitCfg(4, ptrByteArray2.getPointer());
String strPathCom = System.getProperty("user.dir") + "/lib/";
HCNetSDK.NET_DVR_LOCAL_SDK_PATH struComPath = new HCNetSDK.NET_DVR_LOCAL_SDK_PATH(); HCNetSDK.NET_DVR_LOCAL_SDK_PATH struComPath = new HCNetSDK.NET_DVR_LOCAL_SDK_PATH();
System.arraycopy(strPathCom.getBytes(), 0, struComPath.sPath, 0, strPathCom.length()); System.arraycopy(strPathCom.getBytes(), 0, struComPath.sPath, 0, strPathCom.length());
struComPath.write(); struComPath.write();
@ -66,8 +79,7 @@ public class HikLibConfig {
boolean initSuc = hcNetSDK.NET_DVR_Init(); boolean initSuc = hcNetSDK.NET_DVR_Init();
//异常消息回调 //异常消息回调
if(fExceptionCallBack == null)
{
if (fExceptionCallBack == null) {
fExceptionCallBack = new HikFExceptionCallBack_Imp(); fExceptionCallBack = new HikFExceptionCallBack_Imp();
} }
@ -80,8 +92,8 @@ public class HikLibConfig {
//启动SDK写日志 //启动SDK写日志
hcNetSDK.NET_DVR_SetLogToFile(3, "./sdkLog", false); hcNetSDK.NET_DVR_SetLogToFile(3, "./sdkLog", false);
//设置连接时间与重连时间 //设置连接时间与重连时间
hcNetSDK.NET_DVR_SetConnectTime(200000,1);
hcNetSDK.NET_DVR_SetReconnect(100000,true);
hcNetSDK.NET_DVR_SetConnectTime(200000, 1);
hcNetSDK.NET_DVR_SetReconnect(100000, true);
//连接所有nvr //连接所有nvr
@ -97,16 +109,25 @@ public class HikLibConfig {
*/ */
private boolean createSDKInstance() { private boolean createSDKInstance() {
if (hcNetSDK == null) { if (hcNetSDK == null) {
String arch = System.getProperty("os.arch").toLowerCase();
System.out.println("OS:" + arch);
synchronized (HCNetSDK.class) { synchronized (HCNetSDK.class) {
String strDllPath = ""; String strDllPath = "";
try { try {
if (osSelect.isWindows()) if (osSelect.isWindows())
//win系统加载库路径 //win系统加载库路径
strDllPath = System.getProperty("user.dir") + "\\lib\\HCNetSDK.dll"; strDllPath = System.getProperty("user.dir") + "\\lib\\HCNetSDK.dll";
else if (osSelect.isLinux())
else if (osSelect.isLinux()) {
//Linux系统加载库路径 //Linux系统加载库路径
strDllPath = System.getProperty("user.dir") + "/lib/libhcnetsdk.so";
if (arch.contains("amd64") || arch.contains("x86_64") || arch.equals("x86") || arch.startsWith("i386") || arch.startsWith("i686")) {
strDllPath = System.getProperty("user.dir") + "/lib/hik-linux64/libhcnetsdk.so";
System.out.println("x86 / x86_64 架构");
} else if (arch.contains("aarch64") || arch.contains("arm64") || arch.startsWith("arm")) {
strDllPath = System.getProperty("user.dir") + "/lib/libhcnetsdk.so";
System.out.println("ARM / AArch64 架构");
}
}
hcNetSDK = (HCNetSDK) Native.loadLibrary(strDllPath, HCNetSDK.class); hcNetSDK = (HCNetSDK) Native.loadLibrary(strDllPath, HCNetSDK.class);
// HCNetSDK INSTANCE = (HCNetSDK) Native.load(LibraryLoad.getLoadLibrary("libhcnetsdk"), HCNetSDK.class); // HCNetSDK INSTANCE = (HCNetSDK) Native.load(LibraryLoad.getLoadLibrary("libhcnetsdk"), HCNetSDK.class);
} catch (Exception ex) { } catch (Exception ex) {


+ 27
- 8
src/main/java/com/inspect/nvr/config/LincseekConfig.java View File

@ -2,14 +2,7 @@ package com.inspect.nvr.config;
import com.inspect.nvr.jna.lincseek.IRNetSDK; import com.inspect.nvr.jna.lincseek.IRNetSDK;
import com.inspect.nvr.jna.lincseek.IRNetSDKStruct.IRNETHANDLE; import com.inspect.nvr.jna.lincseek.IRNetSDKStruct.IRNETHANDLE;
import com.sun.jna.DefaultTypeMapper;
import com.sun.jna.FromNativeContext;
import com.sun.jna.FromNativeConverter;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ToNativeContext;
import com.sun.jna.TypeConverter;
import com.sun.jna.*;
import com.sun.jna.win32.W32APIFunctionMapper; import com.sun.jna.win32.W32APIFunctionMapper;
import com.sun.jna.win32.W32APITypeMapper; import com.sun.jna.win32.W32APITypeMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -17,6 +10,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.lang.reflect.Proxy;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -30,6 +24,12 @@ public class LincseekConfig {
@Bean @Bean
public IRNetSDK initSDK() { public IRNetSDK initSDK() {
// 项目只有朗驰 Windows DLLLinux 容器不加载它避免影响海康和大华功能启动
if (!Platform.isWindows()) {
log.warn("[朗驰]当前系统非 Windows,已跳过仅支持 Windows 的朗驰 SDK 初始化");
return createUnsupportedSdk();
}
// 自定义 TypeMapper native Pointer 转换为 IRNETHANDLE // 自定义 TypeMapper native Pointer 转换为 IRNETHANDLE
DefaultTypeMapper mergedMapper = new DefaultTypeMapper() { DefaultTypeMapper mergedMapper = new DefaultTypeMapper() {
@Override @Override
@ -74,4 +74,23 @@ public class LincseekConfig {
return irNetSDK; return irNetSDK;
} }
/**
* 为不支持朗驰 DLL 的系统创建占位 Bean让其他厂商功能正常启动
*
* @return 清理调用可安全返回其他业务调用会抛出明确异常的 SDK 代理
*/
private IRNetSDK createUnsupportedSdk() {
// 使用 JDK 代理保持既有依赖注入结构不为单一平台限制改造多个业务服务
return (IRNetSDK) Proxy.newProxyInstance(
IRNetSDK.class.getClassLoader(),
new Class<?>[]{IRNetSDK.class},
(proxy, method, args) -> {
// 应用退出时没有实际朗驰资源按清理成功返回避免干扰其他 SDK 释放
if ("IRNET_ClientCleanup".equals(method.getName())) {
return true;
}
// 真正调用朗驰功能时直接说明平台限制不伪造 SDK 结果
throw new UnsupportedOperationException("朗驰 SDK 仅支持 Windows,当前运行环境无法调用");
});
}
} }

+ 3
- 3
src/main/java/com/inspect/nvr/config/SdkCleanup.java View File

@ -3,9 +3,9 @@ package com.inspect.nvr.config;
import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; import com.inspect.nvr.daHuaCarme.jna.NetSDKLib;
import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; import com.inspect.nvr.hikVision.utils.jna.HCNetSDK;
import com.inspect.nvr.jna.lincseek.IRNetSDK; 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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;


+ 4
- 2
src/main/java/com/inspect/nvr/controller/CameraController.java View File

@ -7,7 +7,7 @@ import com.inspect.nvr.hikVision.utils.AjaxResult;
import com.inspect.nvr.service.DahuaService; import com.inspect.nvr.service.DahuaService;
import com.inspect.nvr.service.HikVisionService; import com.inspect.nvr.service.HikVisionService;
import com.inspect.nvr.service.IvsCameraService; 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 com.inspect.nvr.utils.redis.RedisService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -52,7 +52,9 @@ public class CameraController {
if (CameraEnum.DAHUA.getCode() == camera.getCameraType()) { if (CameraEnum.DAHUA.getCode() == camera.getCameraType()) {
temperatureData = dahuaService.StartRemote(camera); temperatureData = dahuaService.StartRemote(camera);
} else if (CameraEnum.LINCSEEK.getCode() == camera.getCameraType()) { } 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 { } else {
temperatureData = executeWithTimeout(() -> cameraService.StartRemote(camera), 7, TimeUnit.SECONDS); temperatureData = executeWithTimeout(() -> cameraService.StartRemote(camera), 7, TimeUnit.SECONDS);
// temperatureData = cameraService.StartRemote(camera); // temperatureData = cameraService.StartRemote(camera);


+ 0
- 10
src/main/java/com/inspect/nvr/controller/IvsCameraController.java View File

@ -99,14 +99,4 @@ public class IvsCameraController {
ByteArrayInputStream byteArrayInputStream = ivsCameraService.capture(fileSessionId); ByteArrayInputStream byteArrayInputStream = ivsCameraService.capture(fileSessionId);
return ResponseEntity.ok().body(new InputStreamResource(byteArrayInputStream)); return ResponseEntity.ok().body(new InputStreamResource(byteArrayInputStream));
} }
@TimeTrace
@GetMapping("/digest")
public ResponseEntity<String> 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");
}
} }

+ 52
- 1
src/main/java/com/inspect/nvr/domain/Infrared/Camera.java View File

@ -50,4 +50,55 @@ public class Camera {
private int presetId; private int presetId;
private String address; 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/11viaNvr=true NVR 接入信息0/1/2/5/6
* pointNum/cameraType 两种模式共用3/4NVR 冗余字段不填充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;
}
}

+ 2
- 4
src/main/java/com/inspect/nvr/domain/Infrared/InfraredInfo.java View File

@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
public class InfraredInfo { public class InfraredInfo {
//文件版本 //文件版本
private String fileVersion;
private short fileVersion;
//矩阵宽度 //矩阵宽度
private short matrixWidth; private short matrixWidth;
//矩阵高度 //矩阵高度
@ -25,7 +25,7 @@ public class InfraredInfo {
//镜头度数 //镜头度数
private byte lensDegree; private byte lensDegree;
//拍摄距离 //拍摄距离
private long distance;
private long distance;
//相对湿度 //相对湿度
private byte humidity; private byte humidity;
//反射温度 //反射温度
@ -65,6 +65,4 @@ public class InfraredInfo {
//标注后图片路径 //标注后图片路径
private String outPath; private String outPath;
} }

+ 9
- 6
src/main/java/com/inspect/nvr/enums/CameraEnum.java View File

@ -8,18 +8,21 @@ import lombok.Getter;
@Getter @Getter
public enum CameraEnum { 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 int code;
final private String value;
final private String name;
CameraEnum(int code) {
CameraEnum(int code, String value, String name) {
this.code = code; this.code = code;
this.value = value;
this.name = name;
} }
} }

+ 9
- 0
src/main/java/com/inspect/nvr/jna/lincseek/IRNetSDKConst.java View File

@ -270,6 +270,15 @@ public interface IRNetSDKConst {
int EN_FT_SDK_CHANNEL_JPG = 1; // 通道JPG int EN_FT_SDK_CHANNEL_JPG = 1; // 通道JPG
int EN_FT_SDK_CHANNEL_BMP = 2; // 通道BMP 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;
// //
// 调色板模式 // 调色板模式
// //


+ 0
- 152
src/main/java/com/inspect/nvr/service/DahuaCameraService.java View File

@ -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://<host>/cgi-bin/snapshot.cgi?channel=<channel>&subtype=<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<String, CompletableFuture<byte[]>> 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<byte[]> 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<byte[]> 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);
}
}
}
}

+ 0
- 126
src/main/java/com/inspect/nvr/service/DahuaLoginService.java View File

@ -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<String, LLong> 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;
}
}

+ 0
- 125
src/main/java/com/inspect/nvr/service/HikLoginService.java View File

@ -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<String, Integer> 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;
}
}

+ 0
- 2
src/main/java/com/inspect/nvr/service/IvsCameraService.java View File

@ -20,6 +20,4 @@ public interface IvsCameraService {
ByteArrayInputStream downloadFile(String fileSessionId); ByteArrayInputStream downloadFile(String fileSessionId);
ByteArrayInputStream capture(String fileSessionId); ByteArrayInputStream capture(String fileSessionId);
byte[] captureDigest(String fileSessionId);
} }

+ 0
- 186
src/main/java/com/inspect/nvr/service/LincseekCameraService.java View File

@ -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<String, CompletableFuture<byte[]>> 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<byte[]> 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<byte[]> 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;
}
}

+ 0
- 123
src/main/java/com/inspect/nvr/service/LincseekLoginService.java View File

@ -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<String, IRNETHANDLE> 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;
}
}

+ 186
- 0
src/main/java/com/inspect/nvr/service/camera/AbstractCameraService.java View File

@ -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)));
}
}

src/main/java/com/inspect/nvr/service/CommonCameraService.java → src/main/java/com/inspect/nvr/service/camera/AbstractCameraSupport.java View File


+ 47
- 0
src/main/java/com/inspect/nvr/service/camera/BrandCameraService.java View File

@ -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);
}

+ 54
- 0
src/main/java/com/inspect/nvr/service/camera/CameraServiceFactory.java View File

@ -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<Integer, BrandCameraService> registry = new HashMap<>();
private final HikCameraService defaultService;
public CameraServiceFactory(List<BrandCameraService> 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;
}
}

+ 204
- 0
src/main/java/com/inspect/nvr/service/camera/DahuaCameraService.java View File

@ -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://<host>/cgi-bin/snapshot.cgi?channel=<channel>&subtype=<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<String, CompletableFuture<byte[]>> 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<byte[]> 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<byte[]> 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);
}
}
}
}

src/main/java/com/inspect/nvr/service/HikCameraService.java → src/main/java/com/inspect/nvr/service/camera/HikCameraService.java View File


+ 78
- 0
src/main/java/com/inspect/nvr/service/camera/LincHikCompositeCameraService.java View File

@ -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+channel5s 到位等待与整对重试都在锁内
*/
@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;
}
}

+ 193
- 0
src/main/java/com/inspect/nvr/service/camera/LincseekCameraService.java View File

@ -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<String, CompletableFuture<byte[]>> 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<byte[]> 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<byte[]> 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;
}
}

+ 114
- 0
src/main/java/com/inspect/nvr/service/camera/login/AbstractLoginService.java View File

@ -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<H> {
/**
* 会话缓存10分钟未被访问即过期仅过期/容量淘汰时回调 SDK 登出
*/
private final Cache<String, H> 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;
}
}

+ 54
- 0
src/main/java/com/inspect/nvr/service/camera/login/DahuaLoginService.java View File

@ -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<LLong> {
@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();
}
}

+ 47
- 0
src/main/java/com/inspect/nvr/service/camera/login/HikLoginService.java View File

@ -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 返回的 userIDint
*/
@Slf4j
@Service
public class HikLoginService extends AbstractLoginService<Integer> {
@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();
}
}

+ 53
- 0
src/main/java/com/inspect/nvr/service/camera/login/LincseekLoginService.java View File

@ -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<IRNETHANDLE> {
@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;
}
}

+ 3
- 13
src/main/java/com/inspect/nvr/service/impl/DahuaServiceImpl.java View File

@ -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.Camera;
import com.inspect.nvr.domain.Infrared.NvrInfo; import com.inspect.nvr.domain.Infrared.NvrInfo;
import com.inspect.nvr.domain.Infrared.TemperatureData; 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.service.DahuaService;
import com.inspect.nvr.utils.StringUtils; import com.inspect.nvr.utils.StringUtils;
import com.inspect.nvr.utils.redis.RedisService; import com.inspect.nvr.utils.redis.RedisService;
@ -106,12 +106,7 @@ public class DahuaServiceImpl implements DahuaService {
// // 3. 安全转换并创建LLong对象 // // 3. 安全转换并创建LLong对象
// if (ObjectUtil.isEmpty(m_hLoginHandle)) { // if (ObjectUtil.isEmpty(m_hLoginHandle)) {
log.info("大华摄像机登录 ip:{}", camera.getIp()); 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")); // m_hLoginHandle = new NetSDKLib.LLong((Long) redisService.redisTemplate.opsForValue().get(camera.getIp() + "_m_hLoginHandle"));
if (StringUtils.isNull(m_hLoginHandle)) { if (StringUtils.isNull(m_hLoginHandle)) {
return null; return null;
@ -200,12 +195,7 @@ public class DahuaServiceImpl implements DahuaService {
LLong m_hLoginHandle = null; LLong m_hLoginHandle = null;
// } // }
// if (ObjectUtil.isEmpty(m_hLoginHandle)) { // 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); log.info("相机:m_hLoginHandle" + camera.getIp() + "m_hLoginHandle" + m_hLoginHandle);
// } // }
String ctr = "GOTO_PRESET"; String ctr = "GOTO_PRESET";


+ 6
- 14
src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java View File

@ -17,9 +17,9 @@ import com.inspect.nvr.hikVision.utils.AjaxResult;
import com.inspect.nvr.hikVision.utils.StringUtils; import com.inspect.nvr.hikVision.utils.StringUtils;
import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; import com.inspect.nvr.hikVision.utils.jna.HCNetSDK;
import com.inspect.nvr.hikVision.utils.jna.HikVisionUtils; 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.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.service.HikVisionService;
import com.inspect.nvr.tempCount.TempCount; import com.inspect.nvr.tempCount.TempCount;
import com.inspect.nvr.utils.DateUtils; import com.inspect.nvr.utils.DateUtils;
@ -226,13 +226,9 @@ public class HikVisionServiceImpl implements HikVisionService {
* 海康实时测温接口 * 海康实时测温接口
* 优化并发场景 * 优化并发场景
*/ */
@Override
public TemperatureData StartRemote(Camera camera) { 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; return data;
} }
@ -249,12 +245,7 @@ public class HikVisionServiceImpl implements HikVisionService {
log.info("实时测温入口================================================="); log.info("实时测温入口=================================================");
if (camera.getLUserID() != 0) { 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); camera.setLUserID(userId);
} }
@ -451,6 +442,7 @@ public class HikVisionServiceImpl implements HikVisionService {
} }
//红外摄像头任务流程 //红外摄像头任务流程
@Override
public InfraredInfo cameraCalculatePicture(Camera camera) { public InfraredInfo cameraCalculatePicture(Camera camera) {
log.info("进入摄像头红外图片接口!"); log.info("进入摄像头红外图片接口!");
picPath = "/2/"; picPath = "/2/";


+ 43
- 113
src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java View File

@ -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.HCNetSDK;
import com.inspect.nvr.hikVision.utils.jna.HikVisionUtils; import com.inspect.nvr.hikVision.utils.jna.HikVisionUtils;
import com.inspect.nvr.service.*; 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.StringHexConverter;
import com.inspect.nvr.utils.redis.RedisService; import com.inspect.nvr.utils.redis.RedisService;
import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.IntByReference;
@ -63,6 +65,8 @@ public class IvsCameraServiceImpl implements IvsCameraService {
private HikLoginService hikLoginService; private HikLoginService hikLoginService;
@Resource @Resource
private LincseekCameraService lincseekCameraService; private LincseekCameraService lincseekCameraService;
@Resource
private CameraServiceFactory cameraServiceFactory;
@Override @Override
public IvsPresetListView ptzPresetList(String cameraCode, String domainCode) { public IvsPresetListView ptzPresetList(String cameraCode, String domainCode) {
@ -86,58 +90,37 @@ public class IvsCameraServiceImpl implements IvsCameraService {
//跳转预置位 //跳转预置位
@Override @Override
public PtzControlResult ptzControl(PtzControlParam param) { 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 { 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) { } 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) { public PtzControlResult ptzDetailControl(Camera camera, int cameraType) {
//大华预置位跳转 //大华预置位跳转
PtzControlResult ptzControlResult = null; PtzControlResult ptzControlResult = null;
@ -146,15 +129,10 @@ public class IvsCameraServiceImpl implements IvsCameraService {
log.info("开始登录大华NVR 进行跳转预置位"); log.info("开始登录大华NVR 进行跳转预置位");
dahuaService.cameraControl(camera, 0, 0, 0); dahuaService.cameraControl(camera, 0, 0, 0);
} else if (CameraEnum.LINCSEEK.getCode() == cameraType) { } else if (CameraEnum.LINCSEEK.getCode() == cameraType) {
lincseekCameraService.ptzPreset(camera);
lincseekCameraService.gotoPreset(camera);
} else { } 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()); boolean gotoPreset = hcNetSDK.NET_DVR_PTZPreset_Other(userId, camera.getChannel(), HCNetSDK.GOTO_PRESET, camera.getPointNum());
if (!gotoPreset) { if (!gotoPreset) {
@ -385,67 +363,19 @@ public class IvsCameraServiceImpl implements IvsCameraService {
public ByteArrayInputStream capture(String fileSessionId) { public ByteArrayInputStream capture(String fileSessionId) {
final String rawString = StringHexConverter.fromHex(fileSessionId); final String rawString = StringHexConverter.fromHex(fileSessionId);
log.info("downloadImage hexString: {}, rawString: {}", fileSessionId, rawString); 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);
} }
} }

+ 1
- 1
src/main/java/com/inspect/nvr/task/ImageCleanupTask.java View File

@ -14,7 +14,7 @@ import java.time.format.DateTimeParseException;
import java.util.Comparator; import java.util.Comparator;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.inspect.nvr.service.CommonCameraService.FILE_DIR;
import static com.inspect.nvr.service.camera.AbstractCameraSupport.FILE_DIR;
/** /**
* 定期清理抓拍图片 * 定期清理抓拍图片


+ 193
- 0
src/main/java/com/inspect/nvr/utils/DLT664Service.java View File

@ -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 6642016 带电设备红外诊断应用规范
*/
@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
};
}
}

+ 1
- 1
src/main/resources/application.yml View File

@ -1,5 +1,5 @@
server: server:
port: 8080
port: 9637
spring: spring:
application: application:


Loading…
Cancel
Save