背景
使用BX 6K控制卡控制诱导屏显示剩余车位数,由于控制卡和服务端不在一个局域网内,所以不能使用官网提供的案例,官网提供的案例为控制卡为TCP Server,服务端为TCP Client,因此需要开发此程序,服务端左右TCP Server,控制卡为TCP Client。
项目创建
在start.spring.io创建spring boot项目,应用webflux包,或者直接应用netty也可以
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.23</version>
</dependency>
启动TCPServer
由于仰邦的协议接口不是固定的,所以不能使用工具拆包粘包,需自行处理,虽然文档说帧结构为:
但是心跳包固定为:0x61 0x63 0x6B,启动链接的包为:tel+16位自定义字符串+16个字节加密字符,都不是标准格式,不太好处理,如果有大神指导如何处理请在评论区告知一声
package com.fyqj.guidingServer;
import com.fyqj.guidingServer.codec.GuidingDecoder;
import com.fyqj.guidingServer.codec.GuidingEncoder;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import reactor.core.publisher.Flux;
import reactor.netty.tcp.TcpServer;
@SpringBootApplication
public class GuidingDisplay {
public static void main(String[] args) {
SpringApplication.run(GuidingDisplay.class, args);
}
@Bean
CommandLineRunner commandLineRunner() {
return string -> {
createTcpServer();
};
}
private void createTcpServer() {
TcpServer.create()
.host("0.0.0.0").handle((in,out) -> {
in.receive()
.asByteArray()
.subscribe();
return Flux.never();
})
.doOnConnection(c -> c
.addHandler("decoder" , new GuidingDecoder())
.addHandler("encoder" , new GuidingEncoder())
)
.port(8306).bindNow();
}
}
decoder,根据首帧截取11位自定义字符,将channel存入内存,后续用于交互。控制卡上报的帧没有什么具体的意义,所以本项目并没有解析控制卡上报的帧,如需解析,请自行拆包粘包
package com.fyqj.guidingServer.codec;
import com.fyqj.guidingServer.utils.ScreenUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j
public class GuidingDecoder extends ByteToMessageDecoder{
private Boolean firstFrame = true;
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelRegistered");
ctx.fireChannelRegistered();
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelUnregistered");
ctx.fireChannelUnregistered();
String code = ScreenUtil.CONTEXTS_REVERSE.get(ctx);
ScreenUtil.CONTEXTS.remove(code);
ScreenUtil.CONTEXTS_REVERSE.remove(ctx);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) {
if(firstFrame) {
ByteBuf outByteBuf = buffer.readRetainedSlice(buffer.readableBytes());
byte[] data = new byte[outByteBuf.readableBytes()];
outByteBuf.readBytes(data);
String code = new String(data);
code = code.substring(3, 14);
firstFrame = false;
ScreenUtil.CONTEXTS.put(code, ctx);
ScreenUtil.CONTEXTS_REVERSE.put(ctx, code);
out.add(outByteBuf);
}
buffer.readRetainedSlice(buffer.readableBytes());
}
}
消息体encode可以使用官网提供的例子,无需修改
package com.fyqj.guidingServer.codec;
import com.fyqj.guidingServer.protocol.BxDataPack;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.springframework.stereotype.Component;
public class GuidingEncoder extends MessageToByteEncoder<BxDataPack> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, BxDataPack cmd, ByteBuf out) throws Exception {
cmd.pack(out);
}
}
package com.fyqj.guidingServer.protocol;
import com.fyqj.guidingServer.utils.BxUtils;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
/**
*
*/
@Data
@AllArgsConstructor
public class BxDataPack {
private static final int WRAP_A5_NUM = 8;
private static final int WRAP_5A_NUM = 1;
// 目标地址
private short dstAddr = (short) 0xfffe;
//
// 源地址
private short srcAddr = (short) 0x8000;
//
// 保留字
private byte r0 = 0x00;
private byte r1 = 0x00;
private byte r2 = 0x00;
//
// option
// 不发送 barcode
private byte option = 0x00;
private String barCode;
//
// crc 模式
// 默认无校验
private byte crcMode = 0x00;
//
// 显示模式
private byte dispMode = 0x00;
//
// 设备类型
private byte deviceType = (byte) 0xfe;
//
// 协议版本号
private byte version = 0x02;
//
// 数据域长度
private short dataLen;
//
// 数据
private byte[] data;
//
// crc
private short crc;
private BxDataPack() {}
public BxDataPack(byte[] data) {
this.data = data;
this.dataLen = (short) data.length;
}
public BxDataPack(BxCmd cmd) {
this.data = cmd.build();
this.dataLen = (short) data.length;
}
/**
* 对数据进行转义
* @param src
* @return
*/
private static byte[] wrap(byte[] src) {
int len = 0;
len = src.length;
for(byte d : src) {
if((d == (byte)0xa5) || (d == (byte)0x5a) || (d == (byte)0xa6) || (d == (byte)0x5b)) {
len++;
}
}
//
// 加上帧头和帧尾的A5,5A
//len += 2;
len += WRAP_5A_NUM;
len += WRAP_A5_NUM;
//
// 开始转义
byte[] dst;
dst = new byte[len];
int offset = 0;
//
// 帧头
for(int i=0; i<WRAP_A5_NUM; i++){
dst[offset++] = (byte) 0xa5;
}
for(byte data : src) {
if(data == (byte)0xa5) {
dst[offset++] = (byte) 0xa6;
dst[offset++] = 0x02;
}
else if(data == (byte)0xa6) {
dst[offset++] = (byte) 0xa6;
dst[offset++] = 0x01;
}
else if(data == 0x5a) {
dst[offset++] = 0x5b;
dst[offset++] = 0x02;
}
else if(data == 0x5b) {
dst[offset++] = 0x5b;
dst[offset++] = 0x01;
}
else{
dst[offset++] = data;
}
}
// 帧尾
for(int i=0; i<WRAP_5A_NUM; i++){
dst[offset++] = 0x5a;
}
//
return dst;
}
/**
* 对数据进行封装,生成字节流
*/
public void pack(ByteBuf out) {
BxByteArray bytes = new BxByteArray();
//
// 目标地址
bytes.add(dstAddr, BxByteArray.Endian.LITTLE);
//
// 源地址
bytes.add(srcAddr, BxByteArray.Endian.LITTLE);
//
// 保留字
bytes.add(r0);
bytes.add(r1);
bytes.add(r2);
//
// option
bytes.add(option);
//
// crc mode
bytes.add(crcMode);
//
bytes.add(dispMode);
//
bytes.add(deviceType);
//
bytes.add(version);
//
bytes.add(dataLen);
//
// 数据域
bytes.add(data);
//
// add crc
crc = 0x0;
bytes.add(crc);
//
byte[] origin = bytes.build();
int originLen = origin.length;
crc = BxUtils.CRC16(origin, 0, originLen-2);
origin[originLen-2] = (byte)(crc & 0xff);
origin[originLen-1] = (byte)(crc>>8);
//
// 进行转义
byte[] result = wrap(origin);
out.writeBytes(result);
}
/**
* 将BYTE数组解析成 bx.k.BxDataPack
* @param src
* @return
*/
public static BxDataPack parse(byte[] src, int length) {
//
// 反转义
byte[] dst = unwrap(src, length);
if(dst == null) {
return null;
}
else {
//
// check crc
//if(bx.k.BxUtils.CRC16())
short crcCalculated = BxUtils.CRC16(dst, 0, dst.length-2);
short crcGot = BxUtils.bytesToShort(dst, dst.length-2, BxUtils.ENDIAN.LITTLE);
if(crcCalculated != crcGot)
return null;
BxDataPack pack = new BxDataPack();
int offset = 0;
//
// 目标地址
pack.dstAddr = BxUtils.bytesToShort(dst, offset, BxUtils.ENDIAN.LITTLE);
offset += 2;
//
// 源地址
pack.srcAddr = BxUtils.bytesToShort(dst, offset, BxUtils.ENDIAN.LITTLE);
offset += 2;
//
// 保留字 r0, r1, r2
pack.r0 = dst[offset++];
pack.r1 = dst[offset++];
pack.r2 = dst[offset++];
//
// option
pack.option = dst[offset++];
if(pack.option == 0X01) {
byte[] code = Arrays.copyOfRange(dst, offset, offset+16);
offset = offset+16;
pack.barCode= new String(code);
}
//
// 校验模式
pack.crcMode = dst[offset++];
//
// 显示模式
pack.dispMode = dst[offset++];
//
// 设备类型
pack.deviceType = dst[offset++];
//
// 协议版本
pack.version = dst[offset++];
//
// 数据域长度
pack.dataLen = BxUtils.bytesToShort(dst, offset, BxUtils.ENDIAN.LITTLE);
offset += 2;
//
// 数据
//pack.data = new byte[pack.dataLen];
pack.data = Arrays.copyOfRange(dst, offset, offset+pack.dataLen);
offset += pack.dataLen;
//
// crc
pack.crc = BxUtils.bytesToShort(dst, offset, BxUtils.ENDIAN.LITTLE);
//
return pack;
}
}
/**
* 去除数据转义
* @param src
* @param length
* @return
*/
private static byte[] unwrap(byte[] src, int length) {
int len = 0;
if(length == 0)
len = 0;
if(src[0] != (byte)0xa5)
len = 0;
if(src[length-1] != (byte)0x5a)
len = 0;
len = length;
for(byte d : src) {
if((d == (byte)0xa5) || (d == (byte)0x5a) || (d == (byte)0xa6) || (d == (byte)0x5b)) {
len--;
}
}
byte[] dst;
//
// 如果计算的帧长度为0,说明数据不正确
if(len == 0)
return null;
dst = new byte[len];
int offset = 0;
for(int i=0; i<length; ) {
if((src[i] == (byte)0xa5) || (src[i] == 0x5a)) {
i++;
} else if(src[i] == (byte)0xa6) {
if(src[i+1] == 0x01) {
dst[offset++] = (byte)0xa6;
i = i+2;
}
else if(src[i+1] == 0x02) {
dst[offset++] = (byte)0xa5;
i = i+2;
}
else
return null;
} else if(src[i] == 0x5b) {
if(src[i+1] == 0x01) {
dst[offset++] = (byte)0x5b;
i = i+2;
}
else if(src[i+1] == 0x02) {
dst[offset++] = (byte)0x5a;
i = i+2;
}
else
return null;
}
else {
dst[offset++] = src[i++];
}
}
return dst;
}
}
源码地址:https://gitee.com/pengchao0903/city-guiding-display-server.git
私有项目地址,如有需要,请在评论区@我