Bootstrap

仰邦BX.K协议对接

背景

使用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
私有项目地址,如有需要,请在评论区@我

有问题还请指出,用于及时修改文章

;