项目需求

调用捷宸EIO开发包DLL动态库控制应急门的开关停,现场应急门继电器连接的是6、7、8,分别对应的是开、停、关。0为打开,1为关闭。

java程序开发

在resource中新建lib将dll放入其中

image-20230206164856700

sdk工具开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.lyc.ocr.door;

import com.sun.jna.Library;
import com.sun.jna.Native;
public interface DoorCallSDK extends Library {

DoorCallSDK INSTANCE = (DoorCallSDK) Native.loadLibrary("IOSDK_x64", DoorCallSDK.class);

/**
* 创建设备
* </p>
* 以IP 地址为参数,创建一个设备
*
* @param strIp ansi 字符串,设备 IP 地址 如"192.168.1
* @return 0 表示创建设备失败,非 0 成功;表示"设备句柄"
*/
int Jc_CreateDev(String strIp);

/**
* 登录(解锁)设备
*
* @param lDev 设备句柄
* @param strPwd ansi 字符串 表示设备登录(解锁)密
* @return 成功:0,失败:非零
*/
int Jc_LoginDev(int lDev, String strPwd);

/**
* 删除(释放)设备
* </p>
* 释放不再需要使用的设备
*
* @param lDev 设备句柄
*/
void Jc_DelDev(int lDev);

/**
* 创建通讯设备(通过串口)
*
* @param btAddr modbus 地址
* @param btWorkType 工作模式 (0:TcpServer,1:TCPClient,2:UDP)
* @param btProtocol 协议类型 ( 0:Modbus_rtu , 1:modbus_Tcp
* @param uLocalPort 本地端口
* @param pDevIp 设备 ip 地址
* @param uDevPort 设备通讯端口
* @return 成功:非零值,失败:0
*/
int Jc_CreateNetComuDev(byte btAddr, byte btWorkType, byte btProtocol, short uLocalPort, String pDevIp, short uDevPort);

/**
* 判断是否已经连接到模块
*
* @param hDev 连接句柄(前面两种方式任意一种创建通讯设备)
* @return 成功:true,失败:false
*/
boolean Jc_IsDevConnected(int hDev);

/**
* 读取 IO 状态
*
* @param hDev 设备句柄
* @param strIo 16 个字节的缓冲区,每一个字节代表一个 io 状态 ‘0’:低 ‘1’:高
* @return 成功:0,失败:非零
*/
int Jc_GetIoState(int hDev, byte[] strIo);

/**
* 设置 IO 状态
*
* @param hDev 设备句柄
* @param strIo 16 个字节的缓冲区,每一个字节代表一个 io 状态 ‘0’:低 ‘1’:高
* @return 成功:0,失败:非零
*/
int Jc_SetIoState(int hDev, byte[] strIo);

/**
* 释放连接
*
* @param hDev 连接句柄
*/
void Jc_ReleaseComuDev(int hDev);
}

为保证达成jar包后能顺利读取到所需要的dll,将dll复制至C:\Windows\System32

提供接口供外部使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 设备通讯
*
* @return 是否成功
*/
@PostMapping("doorConn")
public boolean doorCommunication(@RequestBody DoorDTO doorDTO) {
try {
DoorCallSDK doorCallSDK = DoorCallSDK.INSTANCE;
// 连接到IO模块
int mhCon = doorCallSDK.Jc_CreateNetComuDev(doorDTO.getBtAddr(),
doorDTO.getBtWorkType(),
doorDTO.getBtProtocol(),
doorDTO.getLocalPort(),
doorDTO.getIp(),
doorDTO.getDevPort()
);
if (mhCon == 0) {
return false;
}
log.info("接口【/door/doorConn】什么什么门连接成功");
// 读取IO状态
sdk.Jc_GetIoState(mhCon, doorDTO.getMbtIoState());
// 修改某个IO口的状态
// doorDTO.setMbtIoState();
// 同步到设备中
sdk.Jc_SetIoState(mhCon, doorDTO.getMbtIoState());
// 释放连接
sdk.Jc_ReleaseComuDev(mhCon);
} catch (Exception e) {
return false;
} catch (UnsatisfiedLinkError error) {
log.error("接口设备什么什么门通信连接失败");
return false;
}
return true;
}

调用测试

image-20230206165210418

后续

捷宸反馈dll调用的文档过时了,重新发了一份IO连接demo过来,实现一个netty客户端达到应急门控制的目的

NettyClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package com.lyc.ocr.client;

import cn.hutool.core.convert.Convert;
import com.lyc.ocr.client.handler.NettyClientHandlerInitializer;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class NettyClient {

/**
* 重连频率,单位:秒
*/
private static final Integer RECONNECT_SECONDS = 20;

@Autowired
private NettyClientHandlerInitializer nettyClientHandlerInitializer;

private static int[] g_McRctable_16 =
{
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
};

/**
* 线程组,用于客户端对服务端的链接、数据读写
*/
private EventLoopGroup eventGroup = new NioEventLoopGroup();

/**
* 用于保存channel通道
*/
private static final Map<String, Channel> CHANNEL_MAP = new ConcurrentHashMap<>();

/**
* 保存用户需要重连的地址信息
*/
private static final Set<String> CONNECT_LIST = new HashSet<>();

public void reconnect() {
eventGroup.schedule(() -> {
log.info("[reconnect][开始重连]");

CONNECT_LIST.forEach((a) -> {
try {
String[] s = a.split(":");
start(s[0], Convert.toInt(s[1]), new byte[]{});
} catch (InterruptedException e) {
log.error("[reconnect][重连失败]", e);
}
});

}, RECONNECT_SECONDS, TimeUnit.SECONDS);
log.info("[reconnect][{} 秒后将发起重连]", RECONNECT_SECONDS);
}

/**
* 启动 Netty Client
*/
public void start(String host, Integer port, byte[] content) throws InterruptedException {
if (CHANNEL_MAP.containsKey(host + ":" + port) && CHANNEL_MAP.get(host + ":" + port) != null) {
return;
}
// 创建 Bootstrap 对象,用于 Netty Client 启动
Bootstrap bootstrap = new Bootstrap();
// 设置 Bootstrap 的各种属性。
bootstrap.group(eventGroup) // 设置一个 EventLoopGroup 对象
.channel(NioSocketChannel.class) // 指定 Channel 为客户端 NioSocketChannel
.remoteAddress(host, port) // 指定链接服务器的地址
.option(ChannelOption.SO_KEEPALIVE, true) // TCP Keepalive 机制,实现 TCP 层级的心跳保活功能
.option(ChannelOption.TCP_NODELAY, true) // 允许较小的数据包的发送,降低延迟
.handler(nettyClientHandlerInitializer);
// 链接服务器,并异步等待成功,即启动客户端
bootstrap.connect().addListener(new ChannelFutureListener() {

@Override
public void operationComplete(ChannelFuture future) throws Exception {
// 连接失败
if (!future.isSuccess()) {
log.error("[start][Netty Client 连接服务器({}:{}) 失败]", host, port);
reconnect();
return;
}
// 连接成功
CHANNEL_MAP.put(host + ":" + port, future.channel());
CONNECT_LIST.add(host + ":" + port);
// channel = future.channel();
log.info("[start][Netty Client 连接服务器({}:{}) 成功]", host, port);
if (content.length > 0) {
future.channel().writeAndFlush(content);
}
}

});
}

/**
* 关闭 Netty Server
*/
@PreDestroy
public void shutdown() {
// 关闭 Netty Client
CHANNEL_MAP.forEach((k, v) -> {
if (v != null) {
v.close();
}
});
// 优雅关闭一个 EventLoopGroup 对象
eventGroup.shutdownGracefully();
}

/**
* 发送消息
*
* @param content 消息体
*/
public void send(String host, Integer port, byte[] content) {
Channel channel = Optional.of(CHANNEL_MAP).map(a -> a.get(host + ":" + port)).orElse(null);
if (channel == null) {
log.error("[send][连接不存在]");
return;
}
if (!channel.isActive()) {
log.error("[send][连接({})未激活]", channel.id());
return;
}
// 发送消息
channel.writeAndFlush(content);
}

public void remove(ChannelId channelId) {
CHANNEL_MAP.forEach((k, v) -> {
if (channelId.equals(v.id())) {
CHANNEL_MAP.remove(k);
}
});
}

public int CRC_GetModbus16(byte[] pData, int nLength) {
int cRc_16 = 0xFFFF;
byte temp;

for (int i = 0; i < nLength; ++i) {
temp = (byte) (cRc_16 & 0xFF);
cRc_16 = (int) ((cRc_16 >> 8) ^ g_McRctable_16[(temp ^ pData[i]) & 0xFF]);
}


return cRc_16;
}

}

NettyClientHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.lyc.ocr.client.handler;

import com.lyc.ocr.client.NettyClient;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@ChannelHandler.Sharable
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

@Autowired
private NettyClient nettyClient;

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 发起重连
nettyClient.reconnect();
// 继续触发事件
super.channelInactive(ctx);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("[exceptionCaught][连接({}) 发生异常]", ctx.channel().id(), cause);
// 断开连接
nettyClient.remove(ctx.channel().id());
ctx.channel().close();
}

@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object event) throws Exception {
// 空闲时,向服务端发起一次心跳,通过指定命令获取当前服务端应急门状态,使服务端发送应急们状态给当前客户端
if (event instanceof IdleStateEvent) {
byte[] sendData = new byte[8];
sendData[0] = 0x01; //地址码,根据设备地址填写
sendData[1] = 0x03; //功能码,表示读取
sendData[2] = 0x00; //起始地址高位
sendData[3] = 0x01; //起始地址低位
sendData[4] = 0x00; //要读取的寄存器数量的高位
sendData[5] = 0x01; //要读取的寄存器数量的低位


int uCrc = nettyClient.CRC_GetModbus16(sendData, 6);

sendData[6] = (byte) (uCrc); //crc16校验高位
sendData[7] = (byte) (uCrc >> 8); //crc16校验低位
ctx.writeAndFlush(sendData)
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
super.userEventTriggered(ctx, event);
}
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 读取服务端信息,防止连接服务端出现ReadTimeOutException
log.info("Client,接收到服务端发来的消息:" + msg);
}
}

NettyClientHandlerInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.lyc.ocr.client.handler;

import com.lyc.ocr.client.codec.InvocationEncoder;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class NettyClientHandlerInitializer extends ChannelInitializer<Channel> {

/**
* 心跳超时时间
*/
private static final Integer READ_TIMEOUT_SECONDS = 60;

@Autowired
private NettyClientHandler nettyClientHandler;

@Override
protected void initChannel(Channel ch) {
ch.pipeline()
// 空闲检测
.addLast(new IdleStateHandler(READ_TIMEOUT_SECONDS, 0, 0))
.addLast(new ReadTimeoutHandler(3 * READ_TIMEOUT_SECONDS))
// 编码器
.addLast(new InvocationEncoder())
// 客户端处理器
.addLast(nettyClientHandler)
;
}

}

InvocationEncoder

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.lyc.ocr.client.codec;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class InvocationEncoder extends MessageToByteEncoder<byte[]> {
@Override
protected void encode(ChannelHandlerContext ctx, byte[] content, ByteBuf out) {
// 写入内容
out.writeBytes(content);
}
}

上面的controller修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.lyc.ocr.controller;

import com.lyc.ocr.client.NettyClient;
import com.lyc.ocr.door.DoorDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Slf4j
@RequestMapping("/door")
@RestController
public class DoorController {

@Resource
private NettyClient nettyClient;

/**
* 设备通讯
*
* @return 是否成功
*/
@PostMapping("doorConn")
public boolean doorCommunication(@RequestBody DoorDTO doorDTO) {
// 发送指令
String strSet;
strSet = doorDTO.getMbtIoState().replace(" ", "");

//置高寄存器值
int uSetH = 0;
//置低寄存器值
int uSetL = 0;

for (int i = 0; i < 16; i++) {
String[] ss = strSet.split("");
if ("1".equals(ss[i])) {
//把置高寄存器相应的位置成1
uSetH |= 1 << i;
//把置低寄存器相应的位置成0
uSetL &= ~(1 << i);
} else if ("0".equals(ss[i])) {
//把置高寄存器相应的位置成0
uSetH &= ~(1 << i);
//把置低寄存器相应的位置成1
uSetL |= 1 << i;
}
}

byte[] sendData = new byte[13];
sendData[0] = 0x01; //地址码,根据设备地址填写
sendData[1] = 0x10; //功能码,表示要写多个寄存器
sendData[2] = 0x00; //起始地址高位
sendData[3] = 0x02; //起始地址低位
sendData[4] = 0x00; //寄存器数量高位
sendData[5] = 0x02; //寄存器数量低位
sendData[6] = 0x04; //要写的字节数
sendData[7] = (byte) (uSetH >> 8); //
sendData[8] = (byte) (uSetH); //
sendData[9] = (byte) (uSetL >> 8); //
sendData[10] = (byte) (uSetL); //

int uCrc = nettyClient.CRC_GetModbus16(sendData, 11);

//crc16校验低位
sendData[11] = (byte) (uCrc);
//crc16校验高位
sendData[12] = (byte) (uCrc >> 8);
try {
// 如果传了指定ip和端口则重置netty链接
if (StringUtils.hasText(doorDTO.getIp()) && doorDTO.getDevPort() != null) {
nettyClient.start(doorDTO.getIp(), doorDTO.getDevPort(), sendData);
}
nettyClient.send(doorDTO.getIp(), doorDTO.getDevPort(), sendData);
} catch (Exception e) {
return false;
}
return true;
}
}

image-20230220133750591