记一次大华门禁JAVA-linux-SDK的接入经历
接入背景
2020年杭州对交通安全教育及其重视,于是各个街道社区都兴建起了交通治理工作站(交治站),一方面是针对交通违规驾驶员的教育工作,另一方面也是新增一个安全教育的宣传渠道.
我们公司是做VR驾驶的,也有一些设备部署在交治站,因为我们的设备相对是独立的,所以特别开了个小门,并且安装了大华的门禁.想通过二维码扫码,然后服务端远程开门的方式做到24小时开放体验.跟大华的技术咨询后决定以主动注册的方式实现我们的需求.
下面我就说下我的对接历程.
对接历程
注意:接入之前一定要确认自己的设备是否支持二维码事件,如果不支持,是不能使用二维码透传的
1. 接入环境
linux,java,springboot
2.sdk环境搭建
a.首先是需要去官网下载sdk:
https://www.dahuatech.com/service/downloadlists/836.html
我用的是这个sdk,不同环境的不能混用,虽然sdk大部分代码差不多,但也是存在一定差异的
b.包的配置
当前版本的linux64文件夹
以前版本的linux64
libs文件夹里的jar包按照自己项目的导包方式引入,我的是springboot maven的项目,所以直接上传到了私库里.libs下面的linux64里的一堆依赖文件在linux下是不能直接加载到的,默认是需要放在程序的同级目录的,我试过,并不能加载到,可能是我放的位置有问题.在以前版本的sdk-linux64文件夹下还是有一个说明文件的,我把说明内容贴在下面.
【说明】
Demo以Qt开发,在CentOS上进行编译,以下以CentOS系统为例,说明的Demo使用方式,其他Linux版本参考CentOS系统。
如果SDK各动态库文件和可执行文件在同一级目录下,则使用同级目录下的库文件;此时也可能报找不到库文件的错误,需要显示设置一下环境变量:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
如果不在同一级目录下,则需要将以上文件的目录加载到动态库搜索路径中,设置的方式有以下几种:
一. 将网络SDK各动态库路径加入到LD_LIBRARY_PATH环境变量
1.在终端输入:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/XXX
只在当前终端起作用
2. 修改~/.bashrc~/.bash_profile
,最后一行添加export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/XXX
,保存之后,使用source .bashrc执行该文件 ,当前用户生效
3. 修改/etc/profile,添加内容如第2条,同样保存之后使用source执行该文件 所有用户生效
二.在/etc/ld.so.conf文件结尾添加网络sdk库的路径,如/XXX,保存之后,然后执行ldconfig
三.可以将网络sdk各依赖库放入到/lib64、/lib或usr/lib64、usr/lib下
四.可以在Makefile中使用-Wl,-rpath来指定动态路径,直接将dhnetsdk库以–l方式显示加载进来
比如:-Wl,-rpath=/XXX -lhdhnetsdk
推荐使用一或二的方式,但要注意优先使用的是同级目录下的库文件。
整完以后记得给这些文件读的权限.
3.业务流程
在sdk下的doc里有主动注册的接入文档,另外我还有门禁的配置说明书.
a.设备配置
首先肯定得有门禁设备,按照说明书来配置门禁
门禁说明书
a1.WiFi配置,选择“通讯设置 > 网络设置 > Wi-Fi”,具有 Wi-Fi 模块的设备具有此功能,具体以实物为主。
a2.配置 IP,选择“通讯设置 > 网络设置 > IP 设置”。我这里直接选择启用DHCP自动获取IP地址
a3.主动注册,主动注册功能可以使设备对接管理平台,通过管理平台管理设备,选择“通讯设置 > 网络设置 > 主动注册”。
只需配置ip端口即可,记得点击启用.
b.代码实现
目前主要有两种二维码开门的方式:
1、二维码包含卡号信息,刷二维码相当于刷卡开门;
2、刷二维码时,设备上报二维码信息给上层,有上层自己校验二维码信息,再决定是否开门。
接口流程
1、二维码包含卡号信息
1)客户发加密方式和秘钥给设备;
CLIENT_OperateAccessControlManager(NET_EM_ACCESS_CTL_SET_QRCODEDECODE_INFO)
2)客户发卡给设备;
CLIENT_ControlDevice(DH_CTRL_RECORDSET_INSERTEX)
3)客户根据秘钥和卡号生成二维码字符串;
CLIENT_EncryptString
4)客户通过公有的二维码生成方法把二维码字符串转化为二维码;
5)在设备上刷这个二维码的时候,就相当于刷卡,与刷卡开门方式等同。
二维码的秘钥是上层平台传给设备的。生成二维码字符串的时候,就用相同的秘钥去生成。
前三步参考接口如下:
接口:CLIENT_OperateAccessControlManager
对应枚举:
public static final int NET_EM_ACCESS_CTL_SET_QRCODEDECODE_INFO = 12;
// 设置二维码的解码信息, 对应结构体
pstInparam = NET_IN_SET_QRCODE_DECODE_INFO,
pstOutParam = NET_OUT_SET_QRCODE_DECODE_INFO
// 设置二维码的解码信息入参
public static class NET_IN_SET_QRCODE_DECODE_INFO extends Structure{
public int dwSize;
public int emCipher; // 加密方式, 参考枚举 NET_ENUM_QRCODE_CIPHER
public byte[] szKey = new byte[33]; // 秘钥
public byte[] byReserved = new byte[3]; // 字节对齐
public NET_IN_SET_QRCODE_DECODE_INFO() {
this.dwSize = this.size();
}
}
二维码字符串生成接口:CLIENT_EncryptString
传入卡号、设备的秘钥,生成字符串,这个字符串可以通过公有的二维码生成算法生成二维码。
卡号是平台端向设备发卡,接口CLIENT_ControlDevice
对应控制枚举:
DH_CTRL_RECORDSET_INSERTEX, // 添加指纹记录,获得记录集编号(对应 NET_CTRL_RECORDSET_INSERT_PARAM)
2、二维码上报事件方式
报警订阅接口流程:
CLIENT_Init/CLIENT_Cleanup ---- 初始化/清理库 应用程序开启及关闭时调用一次即可。
CLIENT_LoginEx2/CLIENT_Logout ----- 登陆/登出设备,CLIENT_Login可以返回一个登陆会话句柄
CLIENT_SetDVRMessCallBack-----设置报警消息回调函数
CLIENT_StartListenEx/CLIENT_StopListen 开始/停止订阅报警
CLIENT_SetDVRMessCallBack参数里有个回调函数,有报警触发的时候会收到回调消息
在回调函数中检测对应的事件
#define DH_ALARM_QR_CODE_CHECK 0x335a // 二维码上报事件(对应结构体 ALARM_QR_CODE_CHECK_INFO)
二维码上报事件信息( DH_ALARM_QR_CODE_CHECK )
typedef struct tagALARM_QR_CODE_CHECK_INFO{
int nEventID; // 事件ID
NET_TIME_EX UTC; // 事件发生的时间
double dbPTS; // 时间戳(单位是毫秒)
char szQRCode[256]; // 二维码字符串
BYTE byReserved[1024]; // 预留字节
} ALARM_QR_CODE_CHECK_INFO;
客户从ALARM_QR_CODE_CHECK_INFO .szQRCode
中获取到二维码信息后,自己去判断是否需要开门,如果需要开门,可以使用以下方式来开门:
接口:CLIENT_ControlDevice
枚举:
DH_CTRL_ACCESS_OPEN, // 门禁控制-开门(对应结构体 NET_CTRL_ACCESS_OPEN)
我选择的是第二种方式,设备连接到服务端-用户扫码-扫码上传到服务端-服务端处理业务-下发开门控制
基本流程可参考主动注册使用说明书,我这里只把几个流程的代码贴一下
流程说明
步骤1 调用 CLIENT_Init 初始化接口,完成 SDK 初始化。
步骤2 调用 CLIENT_ListenServer 接口开始监听。CLIENT_ListenServer 接口需要传入主动注
册监听回调函数 fServiceCallBack,通过该回调函数可以获取到注册到本服务平台的设
备的 IP、ID 和端口等信息。监听状态下,回调函数返回设备主动注册传上来的 ID、IP
及端口等信息,用户在回调中获取到信息后可以登录需要的设备并执行其他业务操作。
步骤3 调用 CLIENT_StopListenServer 接口结束监听。
步骤4 调用 CLIENT_Cleanup 接口清理释放 SDK 资源。
注意事项
CLIENT_Init 和 CLIENT_Cleanup 接口需成对调用,支持单线程多次成对调用,但我们建议
全局均只调用一次,勿反复初始化/清理资源。
请确保设备主动注册配置中的目标 IP 与端口和服务器 IP 与端口完全匹配。
服务器只有处于监听状态下时才能向主动注册的设备发起通信,因此所有需要服务器向设备
下发命令的操作都必须在侦听状态下完成,不能中途停止监听。
如果需要在回调函数中调用 SDK 的接口,请另起一个线程执行,否则容易引起程序崩溃。
步骤1
public class InitModule {
public static NetSDKLib netsdk = NetSDKLib.NETSDK_INSTANCE;
private static boolean init = false;
private static boolean bLogopen = false;
public static boolean init(NetSDKLib.fDisConnect disConnect) {
init = netsdk.CLIENT_Init(disConnect, null);
if (!init) {
//初始化失败
System.out.println("初始化失败");
return false;
}
//打开日志,可选
NetSDKLib.LOG_SET_PRINT_INFO setLog = new NetSDKLib.LOG_SET_PRINT_INFO();
File path = new File("./sdklog/");
if (!path.exists()) {
path.mkdir();
}
String logPath = path.getAbsoluteFile().getParent() + "\\sdklog\\" + ToolKits.getDate() + ".log";
setLog.nPrintStrategy = 0;
setLog.bSetFilePath = 1;
System.arraycopy(logPath.getBytes(), 0, setLog.szLogFilePath, 0, logPath.getBytes().length);
System.out.println(logPath);
setLog.bSetPrintStrategy = 1;
bLogopen = netsdk.CLIENT_LogOpen(setLog);
if(!bLogopen ) {
System.err.println("Failed to open NetSDK log");
}
netsdk.CLIENT_SetAutoReconnect(HaveReConnectCallBack.getInstance(), null);
// 设置更多网络参数,NET_PARAM的nWaittime,nConnectTryNum成员与CLIENT_SetConnectTime
// 接口设置的登录设备超时时间和尝试次数意义相同,可选
NetSDKLib.NET_PARAM netParam = new NetSDKLib.NET_PARAM();
netParam.nConnectTime = 10000; // 登录时尝试建立链接的超时时间
netParam.nGetConnInfoTime = 3000; // 设置子连接的超时时间
netsdk.CLIENT_SetNetworkParam(netParam);
System.out.println("初始化成功");
return true;
}
public static void cleanup() {
if (bLogopen) {
netsdk.CLIENT_LogClose();
}
if (init) {
netsdk.CLIENT_Cleanup();
}
}
}
打开日志这一步其实还是很必要的,纠结了我两天的问题就是通过这个日志解决的.
整个程序结束的时候一定要调用CLIENT_Cleanup 来释放资源.
步骤2
开启监听
public class ApiModule {
public static NetSDKLib netsdk = NetSDKLib.NETSDK_INSTANCE;
// 设备信息
public static NetSDKLib.NET_DEVICEINFO_Ex m_stDeviceInfo = new NetSDKLib.NET_DEVICEINFO_Ex();
// 登陆句柄
public static LLong m_hLoginHandle = new LLong(0);
private static LLong mServerHandler = new LLong(0);
//开启监听
public static LLong startServer(String ip) {
mServerHandler = netsdk.CLIENT_ListenServer(ip, 9500, 1000, ServiceCallBack.getInstance(), null);
if (0 == mServerHandler.longValue()) {
System.err.println("Failed to start server." + ToolKits.getErrorCodePrint());
} else {
System.out.printf("Start server, [Server address %s][Server port %d]\n", ip, 9500);
}
return mServerHandler;
}
}
1.我这里只是个demo,设置的固定端口;
2.需要说明的是,这里开启监听的ip一定要内网ip,不能是公网ip,也不能是127.0.0.1或localhost,在这里也浪费了我好多时间,我以为是我端口的问题,也推荐使用tcpdump工具抓包,用来排查下协议是否接受成功,具体的使用方法可自行百度;
3.CLIENT_ListenServer设置的回调函数,有设备上线时,会回调到那个函数里
监听回调
public class ServiceCallBack implements NetSDKLib.fServiceCallBack {
private static DelayQueueMsg delayQueueMsg = SpringContext.getBean(DelayQueueMsg.class);
private ServiceCallBack() {
System.out.println("监听回调函数初始化");
}
private static class CallBackHolder {
private static ServiceCallBack instance = new ServiceCallBack();
}
public static ServiceCallBack getInstance() {
return ServiceCallBack.CallBackHolder.instance;
}
@Override
public int invoke(NetSDKLib.LLong lHandle, String pIp, int wPort, int lCommand, Pointer pParam, int dwParamLen, Pointer dwUserData) {
System.out.println("进来啦");
System.out.println("服务监听回调:login=" + lHandle + ",ip=" + pIp + ",port=" + wPort);
// 将 pParam 转化为序列号
byte[] buffer = new byte[dwParamLen];
pParam.read(0, buffer, 0, dwParamLen);
String deviceId = "";
try {
deviceId = new String(buffer, "GBK").trim();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.printf("Register Device Info [Device address %s][port %s][DeviceID %s] \n", pIp, wPort, deviceId);
if (ApiModule.m_hLoginHandle.longValue() == 0) {
System.out.println("当前线程id:" + Thread.currentThread().getId());
//登录
JSONObject object = new JSONObject();
object.put("pIp", pIp);
object.put("wPort", wPort);
object.put("deviceId", deviceId);
delayQueueMsg.put(new SocketMsgDelay(lHandle.intValue(), object.toJSONString()));
}
return 0;
}
}
1.设备上线,服务端接受回调,需要在这个回调里登录设备
2.我这里使用的是DelayQueue延时队列,异步去处理登录,之所以使用异步,是因为我直接登录的时候会报网络异常,连接失败.大华的技术人员告诉我说是不能在一个线程里调用多个sdk,其实在文档里也是这样建议的,虽然我做的异步,但是还是会报错,具体怎么解决的,我放在下一说明里
不建议使用这个延时队列来做异步,有时候会失效,并不能消费队列,目前我还没有找到问题所在,建议还是使用消息中间件来做队列吧,
登录处理
public static boolean login(String m_strIp, int m_nPort, String m_strUser, String m_strPassword, String deviceIds) {
Pointer deviceId = ToolKits.GetGBKStringToPointer(deviceIds);
//入参
NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam = new NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstInParam.nPort = m_nPort;
pstInParam.szIP = m_strIp.getBytes();
pstInParam.szPassword = m_strPassword.getBytes();
pstInParam.szUserName = m_strUser.getBytes();
pstInParam.emSpecCap = 2;//这一步
pstInParam.pCapParam = deviceId;这一步至关重要
//出参
NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam = new NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstOutParam.stuDeviceInfo = m_stDeviceInfo;
m_hLoginHandle = netsdk.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);
if (m_hLoginHandle.longValue() == 0) {
System.err.printf("登录设备[%s] 端口[%d]失败. %s\n", m_strIp, m_nPort, ToolKits.getErrorCodePrint());
} else {
System.out.println("登录成功 [ " + m_strIp + " ]");
}
return m_hLoginHandle.longValue() == 0 ? false : true;
}public static boolean login(String m_strIp, int m_nPort, String m_strUser, String m_strPassword, String deviceIds) {
Pointer deviceId = ToolKits.GetGBKStringToPointer(deviceIds);
// IntByReference nError = new IntByReference(0);
//入参
NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam = new NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstInParam.nPort = m_nPort;
pstInParam.szIP = m_strIp.getBytes();
pstInParam.szPassword = m_strPassword.getBytes();
pstInParam.szUserName = m_strUser.getBytes();
pstInParam.emSpecCap = 2;
pstInParam.pCapParam = deviceId;
//出参
NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam = new NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstOutParam.stuDeviceInfo = m_stDeviceInfo;
// m_hLoginHandle = netsdk.CLIENT_LoginEx2(m_strIp, m_nPort, m_strUser, m_strPassword, 2, null, m_stDeviceInfo, nError);
m_hLoginHandle = netsdk.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);
if (m_hLoginHandle.longValue() == 0) {
System.err.printf("登录设备[%s] 端口[%d]失败. %s\n", m_strIp, m_nPort, ToolKits.getErrorCodePrint());
} else {
System.out.println("登录成功 [ " + m_strIp + " ]");
}
return m_hLoginHandle.longValue() == 0 ? false : true;
}
pstInParam.emSpecCap = 2; pstInParam.pCapParam = deviceId;
这俩参数至关重要,我也在这里卡了很久,尤其是deviceId,我也是设置了这个才解决了上面那个登录网络异常的问题的
步骤3
这里插播一条,流程图里没有写的,就是监听二维码扫码事件,根据自己的需求处理二维码
/**
* 订阅报警信息
*
* @return
*/
public static void startListen() {
// 设置报警回调函数
netsdk.CLIENT_SetDVRMessCallBack(fAlarmAccessDataCB.getInstance(),
null);
// 订阅报警
boolean bRet = netsdk.CLIENT_StartListenEx(m_hLoginHandle);
if (!bRet) {
System.err.println("订阅报警失败! LastError = 0x%x\n"
+ netsdk.CLIENT_GetLastError());
} else {
System.out.println("订阅报警成功.");
}
}
/*
* 报警事件回调 -----门禁事件(对应结构体 ALARM_ACCESS_CTL_EVENT_INFO)
*/
private static class fAlarmAccessDataCB implements NetSDKLib.fMessCallBack {
private fAlarmAccessDataCB() {
}
private static class fAlarmDataCBHolder {
private static fAlarmAccessDataCB instance = new fAlarmAccessDataCB();
}
public static fAlarmAccessDataCB getInstance() {
return fAlarmAccessDataCB.fAlarmDataCBHolder.instance;
}
public boolean invoke(int lCommand, LLong lLoginID, Pointer pStuEvent,
int dwBufLen, String strDeviceIP, NativeLong nDevicePort,
Pointer dwUser) {
System.out.print("command = " + lCommand);
switch (lCommand) {
case NetSDKLib.DH_ALARM_QR_CODE_CHECK: // 设备请求对方发起对讲事件
{
NetSDKLib.ALARM_ACCESS_CTL_EVENT_INFO msg = new NetSDKLib.ALARM_ACCESS_CTL_EVENT_INFO();
ToolKits.GetPointerData(pStuEvent, msg);
System.out.println(msg);
//开门或其他业务
openDoor();
break;
}
}
return true;
}
}
public static void openDoor() {
int emType = NetSDKLib.CtrlType.CTRLTYPE_CTRL_ACCESS_OPEN;
NetSDKLib.NET_CTRL_ACCESS_OPEN accessOpen = new NetSDKLib.NET_CTRL_ACCESS_OPEN();
accessOpen.emOpenDoorType = NetSDKLib.EM_OPEN_DOOR_TYPE.EM_OPEN_DOOR_TYPE_REMOTE;
boolean bRet = netsdk.CLIENT_ControlDevice(m_hLoginHandle, emType,
accessOpen.getPointer(), 3000);
}
最后,程序结束时,结束监听,释放资源,大功告成
我这里都只是个demo,所以我只管开,不管关闭与释放资源,大家做的时候,切记都要做的
更多推荐
所有评论(0)