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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/766271.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python爬虫实战案例——王者荣耀皮肤抓取

大家好&#xff0c;我是你们的老朋友——南枫&#xff0c;今天我们一起来学习一下该如何抓取大家经常玩的游戏——王者荣耀里面的所有英雄的皮肤。 老规矩&#xff0c;直接上代码&#xff1a; 导入我们需要使用到的&#xff0c;也是唯一用到的库&#xff1a; 我们要抓取皮肤其…

【Linux】TCP协议【下二】{流量控制/滑动窗口/延迟应答/捎带应答/拥塞控制}

文章目录 1.流量控制--利用“窗口大小”字段协商数据量大小1. 1第一次的时候&#xff0c;怎么保证发送数据量是合理的1.2第三次握手ack的时候&#xff0c;可以携带数据&#xff01;1.3流量控制&#xff0c;属于可靠性还是属于效率&#xff1f; 2.滑动窗口--利用滑动窗口解决批量…

UE5 动画蓝图

文章目录 一、State Machines二、Blend Spaces三、Aim Offset四、Montage 初步介绍 Unreal Engine 5 Tutorial - Animation Blueprint Part 1: State Machines (youtube.com) Unreal Engine 5 Tutorial - Animation Blueprint Part 2: Blend Spaces (youtube.com) Unreal Engi…

读人工智能全传01图灵的电子大脑

1. 人工智能 1.1. 人类对人工智能的梦想&#xff0c;可以追溯到很久很久以前 1.1.1. 从古希腊开始&#xff0c;铁匠之神赫菲斯托斯(Hephaestus)拥有赋予金属物品生命的能力 1.1.2. 从16世纪的布拉格开始&#xff0c;传说中伟大的拉比在那里用黏土制作了一个傀儡魔像&#xf…

使用patch-package自动修改node_modules中的内容/打补丁

背景 在使用VuePress搭建个人博客的过程中&#xff0c;我需要使用到一个用来复制代码块的插件uepress-plugin-nuggets-style-copy。 问题&#xff1a;插件可以正常安装&#xff0c;但是启动会报错。通过查看错误信息&#xff0c;定位是插件中的copy.vue文件出现错误&#xff0c…

【实战场景】记一次UAT jvm故障排查经历

【实战场景】记一次UAT jvm故障排查经历 开篇词&#xff1a;干货篇&#xff1a;1.查看系统资源使用情况2.将十进制进程号转成十六进制3.使用jstack工具监视进程的垃圾回收情况4.输出指定线程的堆内存信息5.观察日志6.本地环境复现 总结篇&#xff1a;我是杰叔叔&#xff0c;一名…

仿论坛项目--初识Spring Boot

1. 技术准备 技术架构 • Spring Boot • Spring、Spring MVC、MyBatis • Redis、Kafka、Elasticsearch • Spring Security、Spring Actuator 开发环境 • 构建工具&#xff1a;Apache Maven • 集成开发工具&#xff1a;IntelliJ IDEA • 数据库&#xff1a;MySQL、Redi…

Docker拉取失败,利用 Git将 Docker镜像重新打 Tag 推送到阿里云等其他公有云镜像仓库里

目录 一、开通阿里云容器镜像服务 二、Git配置 三、去DockerHub找镜像 四、编写images.txt文件 ​五、演示 六、其他注意事项 最近一段时间 Docker 镜像一直是 Pull 不下来的状态&#xff0c;想直连 DockerHub 是几乎不可能的。更糟糕的是&#xff0c;很多原本可靠的国内…

Vue+ElementUi实现录音播放上传及处理getUserMedia报错问题

1.Vue安装插件 npm install --registryhttps://registry.npmmirror.com 2.Vue页面使用 <template><div class"app-container"><!-- header --><el-header class"procedureHeader" style"height: 20px;"><el-divid…

密码学及其应用 —— 密码学的经典问题

1. 古典密码学问题 1.1 问题1&#xff1a;破解凯撒密码 1.1.1 问题 凯撒密码是最简单的单字母替换加密方案。这是一种通过将字母表中的字母固定向右移动几位来实现的加密方法。解密下面的文本&#xff0c;该文本通过对一个去除了空格的法语文本应用凯撒密码获得&#xff1a; …

layui-按钮

1.用法 使用 用button标签 type"button" class"layui-button" 效果&#xff1a; 2.主题设置 前面都要加上layui-bin 3.尺寸设置 可以叠加使用&#xff01; 4.圆角设置 加一个layui-bin-radius 5.按钮图标设置 里面加一个i标签 加class"layui-…

借教室(题解)

P1083 [NOIP2012 提高组] 借教室 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 思路&#xff1a;二分前缀和 我们将和质检员那题差不多&#xff0c;只需要将候选人二分即可 #include<bits/stdc.h> using namespace std; #define int long long int n,m; int r[100000…

【操作与配置】VSCode配置Python

Python环境配置 可以参见&#xff1a;【操作与配置】Python&#xff1a;CondaPycharm_pycharmconda-CSDN博客 官网下载Python&#xff1a;http://www.python.org/download/官网下载Conda&#xff1a;Miniconda — Anaconda documentation VSCode插件安装 插件安装后需重启V…

disql使用

进入bin目录&#xff1a;cd /opt/dmdbms/bin 启动disql&#xff1a;./disql&#xff0c;然后输入用户名、密码 sh文件直接使用disql&#xff1a; 临时添加路径到PATH环境变量&#xff1a;在当前会话中临时使用disql命令而无需每次都写完整路径&#xff0c;可以在执行脚本之前…

Eclipse + GDB + J-Link 的单片机程序调试实践

Eclipse GDB J-Link 的调试实践 本文介绍如何创建Eclipse的调试配置&#xff0c;如何控制调试过程&#xff0c;如何查看修改各种变量。 对 Eclipse 的要求 所用 Eclipse 应当安装了 Eclipse Embedded CDT 插件。从 https://www.eclipse.org/downloads/packages/ 下载 Ecli…

20240628模拟赛总结

cf好了 让我们开始 T1 Two Regular Polygons 判断能不能构造出题中要求的正多边形 关键是n%m0 Two Regular Polygons #include<bits/stdc.h> using namespace std; int t; int n,m; int main() {cin>>t;for(int i1;i<t;i){cin>>n>>m;if(n%m0)co…

MySQL 代理层:ProxySQL

文章目录 说明安装部署1.1 yum 安装1.2 启停管理1.3 查询版本1.4 Admin 管理接口 入门体验功能介绍3.1 多层次配置系统 读写分离将实例接入到代理服务定义主机组之间的复制关系配置路由规则事务读的配置延迟阈值和请求转发 ProxySQL 核心表mysql_usersmysql_serversmysql_repli…

【C++】相机标定源码笔记- 标定工具库测试

标定工具库测试 一、计算相机内参&#xff1a;对两个相机进行内参标定&#xff0c;并将标定结果保存到指定的文件中 采集图像&#xff1a;相机1-16张 相机2-17张 定义保存相机1/2内参的文件(.yml)路径。 定义相机1/2采集的图片文件夹路径。定义相机1/2存储文件名的向量获取文件…

作为图形渲染API,OpenGL和Direct3D的全方位对比。

当你在网页看到很多美轮美奂的图形效果&#xff0c;3D交互效果&#xff0c;你知道是如何实现的吗&#xff1f;当然是借助图形渲染API了&#xff0c;说起这个不就不得说两大阵营&#xff0c;OpenGL和Direct3D&#xff0c;贝格前端工场在本文对二者做个详细对比。 一、什么是图形…

26.5 Django模板层

1. 模版介绍 在Django中, 模板(Templates)主要用于动态地生成HTML页面. 当需要基于某些数据(如用户信息, 数据库查询结果等)来动态地渲染HTML页面时, 就会使用到模板.以下是模板在Django中使用的几个关键场景: * 1. 动态内容生成: 当需要根据数据库中的数据或其他动态数据来生…