从零开始使用 Netty 接入领浩智能柜 实现设备通信


头像
𝒜𝓊𝓇𝑜𝓇𝒶
原创
发布时间: 2026-07-24 17:31:09 | 阅读数 0收藏数 0评论数 0
封面
本文记录领浩智能柜接入服务器的完整过程。简单来说,就是让智能柜能够和服务器建立稳定连接,并实现服务器对设备的控制。项目使用 Java Netty 搭建设备通信服务,通过分析设备提供的二进制通信协议,实现设备连接、心跳保活、设备编号获取、箱门开启、箱门状态查询,以及二维码、广告和背景图片等信息下发。文章从配置设备服务器地址开始,逐步介绍 Netty 服务端搭建、通信协议分析、编码器和解码器编写,以及命令码和业务处理器的实现。整个过程结合真实设备进行调试,从最开始的“设备连接中”,一步步实现到“设备已连接”,最终完成智能柜的具体控制功能。希望通过这篇文章,帮助大家了解如何使用 Netty 对接真实
1

连接设备

  1. 首先如图1所示进入设备的后台 设置你的服务器地址 这里我用的是内网穿透的地址
  2. 然后创建一个项目引入 netty的依赖 如图2所示
  3. 然后创建一个 server包 里面创建一个 NettyServer 代码如下
package com.gjq.server;


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyServer {

/**
* 启动 Netty 服务
*/
public void start() {

// Boss 线程组:负责接收客户端连接
// 这里只设置 1 个线程即可
EventLoopGroup boss = new NioEventLoopGroup(1);

// Worker 线程组:负责处理客户端的网络读写
EventLoopGroup worker = new NioEventLoopGroup();

try {
// 服务端启动器
ServerBootstrap bootstrap = new ServerBootstrap();

bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
// 设置客户端连接初始化器
.childHandler(
new ChannelInitializer<NioSocketChannel>() {
/**
* 每当有新的设备连接时,初始化该设备对应的 Channel
*/
@Override
protected void initChannel(NioSocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();


pipeline.addLast(new ChannelInboundHandlerAdapter() {

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("设备连接成功:" + ctx.channel().remoteAddress());
super.channelActive(ctx);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("设备断开连接:" + ctx.channel().remoteAddress());
super.channelInactive(ctx);
}
});

}
});

// 端口号
final int NETTY_PORT = 8800;

// 绑定端口
bootstrap.bind(NETTY_PORT).sync();

System.out.println("Netty服务启动成功,端口:" + NETTY_PORT);
} catch (Exception e) {
e.printStackTrace();
}
}
}


运行这个代码如图4所示我们和设备就连接上了

2

命令码

命令码

设备连接服务器以后,服务器首先下发获取设备编号命令,如果是内部设备,则会根据规则进行应答,服务器接收到设备编号进行匹配,确认是否建立网络通道


0x01:获取设备编号

0x02:打卡指定箱门

0x03:查询指定箱门状态

0x04:查询指定箱门卡号

0x05:打开指定箱门的消毒灯

0x06:关闭指定箱门的消毒灯

0x07:设置二维码

0x09:设置广告

0x0A:设置背景图片

0xFF:心跳回环


命令格式参考


帧头

长度

命令码

数据

帧尾

EF

05

01

保留

FF

应答格式参考


帧头

长度

命令码

数据

帧尾

EF

05

01

0000

FF



根据这个我们创建一个命令码 的常量类

package com.gjq.constants;

/**
* @version 1.0
* @author:gjq
* @description:柜机命令码常量
* @date:2026/07/06 16:47
*/
public class LockerCmdConstants {

/**
* 获取设备编号
*/
public static final byte GET_DEVICE_NO = 0x01;

/**
* 打开指定箱门
*/
public static final byte OPEN_DOOR = 0x02;

/**
* 查询箱门状态
*/
public static final byte GET_DOOR_STATUS = 0x03;

/**
* 设置二维码
*/
public static final byte SET_QR_CODE = 0x07;

/**
* 设置广告
*/
public static final byte SET_AD = 0x09;

/**
* 设置背景图片
*/
public static final byte SET_BG = 0x0A;

/**
* 心跳回环
*/
public static final byte HEART_BEAT = (byte) 0xFF;

}


协议的常量类


package com.gjq.constants;

/**
* @version 1.0
* @author:gjq
* @description:通信协议常量
* @date:2026/07/07 09:56
*/
public class ProtocolConstants {

/**
* 帧头
*/
public static final int FRAME_HEADER = 0xEF;


/**
* 帧尾
*/
public static final int FRAME_TAIL = 0xFF;
}


3

报文格式化处理



根据上面信息得知 我们发送的格式和下位机应答的格式都是固定的 所以我们写一个编码器和解码器对设备进行一个统一的处理


首先创建一个指令类 代码如下

package com.gjq.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @version 1.0
* @author:gjq
* @description: 柜机命令实体
* @date:2026/07/06 15:50
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LockerSocketDTO {

/**
* 命令码
*/
private byte cmd;

/**
* 数据
*/
private byte[] data;
}


  1. 创建一个 handler包 在这个包下创建 LockerDecoder 类 和LockerEncoder 类
  2. LockerDecoder 解码器代码如下 其实原理很简单就是挨个位置进行解析 再存放到实体类中
package com.gjq.handler;

import com.gjq.constants.ProtocolConstants;
import com.gjq.dto.LockerSocketDTO;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

/**
* @version 1.0
* @author:gjq
* @description:解码器
* @date:2026/07/06 15:54
*/
public class LockerDecoder extends ByteToMessageDecoder {

@Override
protected void decode(ChannelHandlerContext ctx,
ByteBuf in,
List<Object> out) {


// header + length + cmd+ data + tail 最低为5
if (in.readableBytes() < 5) {
in.discardReadBytes();
return;
}

in.markReaderIndex();

// 校验帧头
byte header = in.readByte();
if ((header & 0xFF) != ProtocolConstants.FRAME_HEADER) {
return;
}

// 读取长度
int length = in.readUnsignedByte();
// 判断是否收到完整数据
if (in.writerIndex() < length) {
in.resetReaderIndex();
return;
}

// 读取命令
byte cmd = in.readByte();

// data长度
int dataLength = in.readableBytes() - 1;
if (dataLength < 0) {
return;
}

// 读取数据
byte[] data = new byte[dataLength];
if (dataLength > 0) {
in.readBytes(data);
}

// 校验包尾
byte tail = in.readByte();
if ((tail & 0xFF) != ProtocolConstants.FRAME_TAIL) {
return;
}

System.out.printf(
"收到命令 header=%02X length=%02X cmd=%02X data=%s tail=%02X%n",
header & 0xFF,
length & 0xFF,
cmd & 0xFF,
ByteBufUtil.hexDump(data),
tail & 0xFF
);
out.add(new LockerSocketDTO(cmd, data));
}
}
  1. LockerEncoder 编码器代码如下 这个就是把数据 转换并存放到 ByteBuf中
package com.gjq.handler;

import com.gjq.constants.ProtocolConstants;
import com.gjq.dto.LockerSocketDTO;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

/**
* @version 1.0
* @author:gjq
* @description:编码器
* @date:2026/07/06 15:55
*/
public class LockerEncoder extends MessageToByteEncoder<LockerSocketDTO> {

@Override
protected void encode(ChannelHandlerContext ctx,
LockerSocketDTO msg,
ByteBuf out) {

byte[] data = msg.getData();

int length = 4 + (data == null ? 0 : data.length);
// 帧头
out.writeByte(ProtocolConstants.FRAME_HEADER);
// 长度
out.writeByte(length);
// 命令码
out.writeByte(msg.getCmd());

if (data != null) {
out.writeBytes(data);
} else {
out.writeByte(0x00);
}

out.writeByte(ProtocolConstants.FRAME_TAIL);

}
}
  1. 然后创建一个LockerServerHandler 类 这个里面存放我们与设备连接和命令操作的相关代码
package com.gjq.handler;


import com.gjq.constants.LockerCmdConstants;
import com.gjq.dto.LockerSocketDTO;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;

@Slf4j
@RequiredArgsConstructor
public class LockerServerHandler extends SimpleChannelInboundHandler<LockerSocketDTO> {

/**
* 设备连接成功
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("设备连接成功:" + ctx.channel().remoteAddress());
}

@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("设备断开连接:" + ctx.channel().remoteAddress());
}

/**
* 接收设备消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, LockerSocketDTO msg) {

}



/**
* Netty 异常处理回调
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
  1. 修改server 把刚刚写好的解码器 编码器和命令实现 添加到 ChannelPipeline中
package com.gjq.server;


import com.gjq.handler.LockerServerHandler;
import com.gjq.handler.LockerDecoder;
import com.gjq.handler.LockerEncoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyServer {

/**
* 启动 Netty 服务
*/
public void start() {

// Boss 线程组:负责接收客户端连接
// 这里只设置 1 个线程即可
EventLoopGroup boss = new NioEventLoopGroup(1);

// Worker 线程组:负责处理客户端的网络读写
EventLoopGroup worker = new NioEventLoopGroup();

try {
// 服务端启动器
ServerBootstrap bootstrap = new ServerBootstrap();

bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
// 设置客户端连接初始化器
.childHandler(
new ChannelInitializer<NioSocketChannel>() {

/**
* 每当有新的设备连接时,初始化该设备对应的 Channel
*/
@Override
protected void initChannel(NioSocketChannel ch) {


ChannelPipeline pipeline = ch.pipeline();
// 解码
pipeline.addLast(new LockerDecoder());
// 编码
pipeline.addLast(new LockerEncoder());
// 业务处理
pipeline.addLast(new LockerServerHandler());


}
});

// 端口号
final int NETTY_PORT = 8800;

// 绑定端口
bootstrap.bind(NETTY_PORT).sync();

System.out.println("Netty服务启动成功,端口:" + NETTY_PORT);
} catch (Exception e) {
e.printStackTrace();
}
}
}

重新运行效果如图2所示

4

心跳互连

如图所示 我们设备上写的是连接中 我们需要与设备进行心跳交互才能变成已连接


设备连接到服务器以后需要主动发送一次报文

下位机接收到报文后15秒发送一次心跳,服务器接收到设备心跳,相同的心跳命令再 推送回来,设备收到以后会刷新发送间隔,15秒后再次发送心跳。

如果超过35秒未收到服务器下发心跳,则认为服务器宕机,执行断线重连。


上位机发送:


帧头

长度

命令码

数据

帧尾

EF

05

FF

00

FF

下位机应答:


帧头

长度

命令码

数据

帧尾

EF

05

FF

00

FF



修改处理代码如下

其实就是设备连接时候发送一个心跳 这时候设备延迟15s 会再给你发送 然后你再发送一个 这样设备每15s都会给你发送一个 如图3 设备已经变成了已连接

package com.gjq.handler;


import com.gjq.constants.LockerCmdConstants;
import com.gjq.dto.LockerSocketDTO;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;

@RequiredArgsConstructor
public class LockerServerHandler extends SimpleChannelInboundHandler<LockerSocketDTO> {

/**
* 设备连接成功
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("设备连接成功:" + ctx.channel().remoteAddress());
// 发送一个心跳
LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.HEART_BEAT, new byte[]{0x00});
ctx.writeAndFlush(response);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("设备断开连接:" + ctx.channel().remoteAddress());
}

/**
* 接收设备消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, LockerSocketDTO msg) {
switch (msg.getCmd()) {
case LockerCmdConstants.HEART_BEAT:
handleHeartBeat(ctx, msg);
break;
case LockerCmdConstants.GET_DEVICE_NO:
break;
case LockerCmdConstants.OPEN_DOOR:
break;
case LockerCmdConstants.GET_DOOR_STATUS:
break;
case LockerCmdConstants.SET_AD, LockerCmdConstants.SET_BG, LockerCmdConstants.SET_QR_CODE:
break;
}
}


/**
* 心跳处理
*/
private void handleHeartBeat(ChannelHandlerContext ctx, LockerSocketDTO msg) {

System.out.printf("收到心跳:%s%n", ctx.channel().remoteAddress());
// 原样返回
LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.HEART_BEAT, new byte[]{0x00});

ctx.writeAndFlush(response);
}



/**
* Netty 异常处理回调
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}


5

获取设备编号

设备连接到服务器以后,服务器首先下发获取设备编号命令,如果是内部设备,则会根据规则进行应答如下:

(下位机应答长度不准确 疑似去掉了帧头帧尾)

上位机发送:


帧头

长度

命令码

数据

帧尾

EF

05

01

00

FF

下位机应答:


帧头

长度

命令码

数据

帧尾

EF

14

01

33 31 30 32 35 31 31 32 34 30 30 33

FF

设备编号为纯数字组合,例如:310251124003,ASCII码编译后为33 31 30 32 35 31 31 32 34 30 30 33。


我在设备连接时候发起了一个发送设备编号的请求 代码如下 效果如图

package com.gjq.handler;


import com.gjq.constants.LockerCmdConstants;
import com.gjq.dto.LockerSocketDTO;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;

@RequiredArgsConstructor
public class LockerServerHandler extends SimpleChannelInboundHandler<LockerSocketDTO> {

/**
* 设备连接成功
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("设备连接成功:" + ctx.channel().remoteAddress());
// 发送一个心跳
LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.HEART_BEAT, new byte[]{0x00});
ctx.writeAndFlush(response);

// 获取设备编号
LockerSocketDTO lockerSocketDTO = new LockerSocketDTO(LockerCmdConstants.GET_DEVICE_NO, new byte[]{0x00});
ctx.writeAndFlush(lockerSocketDTO);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("设备断开连接:" + ctx.channel().remoteAddress());
}

/**
* 接收设备消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, LockerSocketDTO msg) {
switch (msg.getCmd()) {
case LockerCmdConstants.HEART_BEAT:
handleHeartBeat(ctx, msg);
break;
case LockerCmdConstants.GET_DEVICE_NO:
handleDeviceNo(ctx, msg);
break;
case LockerCmdConstants.OPEN_DOOR:
break;
case LockerCmdConstants.GET_DOOR_STATUS:
break;
case LockerCmdConstants.SET_AD, LockerCmdConstants.SET_BG, LockerCmdConstants.SET_QR_CODE:
break;
}
}

/**
* 获取设备编号处理
*/
private void handleDeviceNo(ChannelHandlerContext ctx, LockerSocketDTO msg) {
// 获取设备id
String deviceNo = new String(msg.getData(), StandardCharsets.US_ASCII);
System.out.println("设备上线 -----> deviceNo=" + deviceNo);
}


/**
* 心跳处理
*/
private void handleHeartBeat(ChannelHandlerContext ctx, LockerSocketDTO msg) {

System.out.printf("收到心跳:%s%n", ctx.channel().remoteAddress());
// 原样返回
LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.HEART_BEAT, new byte[]{0x00});

ctx.writeAndFlush(response);
}


/**
* Netty 异常处理回调
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}


6

打开指定箱门

上位机发送:


帧头

长度

命令码

数据

帧尾

EF

05

02

01

FF

数据位说明:16进制,01至FF最多255个箱门

下位机应答:


帧头

长度

命令码

数据

帧尾

EF

05

02

00(开门成功)

02(无此箱门)

20(通讯失败)

FF

说明:

02:如果硬件端总共10个箱门,发送开11号箱门命令,则认为无此箱门号。

20:如果485通讯异常(级联线接触不良,锁板故障等)都会返回20


我依旧在连接时候发请求进行测试 效果看视频

这个代码有一个前提就是需要一个响应的常量类 代码如下

package com.gjq.constants;

/**
* @version 1.0
* @author:gjq
* @description: 柜机响应数据常量
* @date:2026/07/08 15:37
*/
public class LockerResponseConstants {

/**
* 成功
*/
public static final byte SUCCESS = 0x00;

/**
* 失败
*/
public static final byte FAILED = 0x01;

/**
* 门已打开
*/
public static final byte DOOR_OPEN = 0x00;

/**
* 门已关闭
*/
public static final byte DOOR_CLOSE = 0x11;

/**
* 无此箱门
*/
public static final byte DOOR_NOT_EXIST = 0x02;

/**
* 通讯失败
*/
public static final byte COMMUNICATION_FAILED = 0x20;

}


代码如下

package com.gjq.handler;


import com.gjq.constants.LockerCmdConstants;
import com.gjq.constants.LockerResponseConstants;
import com.gjq.dto.LockerSocketDTO;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;

@RequiredArgsConstructor
public class LockerServerHandler extends SimpleChannelInboundHandler<LockerSocketDTO> {

/**
* 设备连接成功
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("设备连接成功:" + ctx.channel().remoteAddress());
// 发送一个心跳
LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.OPEN_DOOR, new byte[]{0x32});
ctx.writeAndFlush(response);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("设备断开连接:" + ctx.channel().remoteAddress());
}

/**
* 接收设备消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, LockerSocketDTO msg) {
switch (msg.getCmd()) {
case LockerCmdConstants.HEART_BEAT:
handleHeartBeat(ctx, msg);
break;
case LockerCmdConstants.GET_DEVICE_NO:
handleDeviceNo(ctx, msg);
break;
case LockerCmdConstants.OPEN_DOOR:
handleOpenDoor(ctx, msg);
break;
case LockerCmdConstants.GET_DOOR_STATUS:
break;
case LockerCmdConstants.SET_AD, LockerCmdConstants.SET_BG, LockerCmdConstants.SET_QR_CODE:
break;
}
}

/**
* 获取设备编号处理
*/
private void handleDeviceNo(ChannelHandlerContext ctx, LockerSocketDTO msg) {
// 获取设备id
String deviceNo = new String(msg.getData(), StandardCharsets.US_ASCII);
System.out.println("设备上线 -----> deviceNo=" + deviceNo);
}


/**
* 心跳处理
*/
private void handleHeartBeat(ChannelHandlerContext ctx, LockerSocketDTO msg) {

System.out.printf("收到心跳:%s%n", ctx.channel().remoteAddress());
// 原样返回
LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.HEART_BEAT, new byte[]{0x00});

ctx.writeAndFlush(response);
}

/**
* 打开格口的处理
*
* @param ctx
* @param msg
*/
private void handleOpenDoor(ChannelHandlerContext ctx, LockerSocketDTO msg) {
// 处理返回数据
switch (msg.getData()[0]) {
// 格口不存在
case LockerResponseConstants.DOOR_NOT_EXIST -> System.err.println("柜口开启失败,当前设备无此格口");
// 通讯失败
case LockerResponseConstants.COMMUNICATION_FAILED -> System.err.println("通讯失败,请检查设备连接是否正常");
// 开门成功
case LockerResponseConstants.SUCCESS -> System.out.println("开门成功");

}
}


/**
* Netty 异常处理回调
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}


7

查询指定箱门状态

上位机发送:


帧头

长度

命令码

数据

帧尾

EF

05

03

01

FF

数据位说明:16进制,01至FF最多255个箱门

下位机应答:


帧头

长度

命令码

数据

帧尾

EF

05

03

00(打开)

11(关闭)

02(无此箱门)

20(通讯失败)

FF

数据位说明:

00/11:根据锁的类型(开门通/闭门通)刚好相反。

开门通:00打开,11关闭

闭门通:11打开,00关闭

02:如果硬件端总共10个箱门,发送开11号箱门命令,则认为无此箱门号。

20:如果485通讯异常(级联线接触不良,锁板故障等)都会返回20


代码如下 效果如图


package com.gjq.handler;


import com.gjq.constants.LockerCmdConstants;
import com.gjq.constants.LockerResponseConstants;
import com.gjq.dto.LockerSocketDTO;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;

@RequiredArgsConstructor
public class LockerServerHandler extends SimpleChannelInboundHandler<LockerSocketDTO> {

/**
* 设备连接成功
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("设备连接成功:" + ctx.channel().remoteAddress());

LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.GET_DOOR_STATUS, new byte[]{0x01});
ctx.writeAndFlush(response);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("设备断开连接:" + ctx.channel().remoteAddress());
}

/**
* 接收设备消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, LockerSocketDTO msg) {
switch (msg.getCmd()) {
case LockerCmdConstants.HEART_BEAT:
handleHeartBeat(ctx, msg);
break;
case LockerCmdConstants.GET_DEVICE_NO:
handleDeviceNo(ctx, msg);
break;
case LockerCmdConstants.OPEN_DOOR:
handleOpenDoor(ctx, msg);
break;
case LockerCmdConstants.GET_DOOR_STATUS:
handleGetDoorStatus(ctx,msg);
break;
case LockerCmdConstants.SET_AD, LockerCmdConstants.SET_BG, LockerCmdConstants.SET_QR_CODE:
break;
}
}

/**
* 获取设备编号处理
*/
private void handleDeviceNo(ChannelHandlerContext ctx, LockerSocketDTO msg) {
// 获取设备id
String deviceNo = new String(msg.getData(), StandardCharsets.US_ASCII);
System.out.println("设备上线 -----> deviceNo=" + deviceNo);
}


/**
* 心跳处理
*/
private void handleHeartBeat(ChannelHandlerContext ctx, LockerSocketDTO msg) {
System.out.printf("收到心跳:%s%n", ctx.channel().remoteAddress());
// 原样返回
LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.HEART_BEAT, new byte[]{0x00});
ctx.writeAndFlush(response);
}

/**
* 打开格口的处理
*
* @param ctx
* @param msg
*/
private void handleOpenDoor(ChannelHandlerContext ctx, LockerSocketDTO msg) {
// 处理返回数据
switch (msg.getData()[0]) {
// 格口不存在
case LockerResponseConstants.DOOR_NOT_EXIST -> System.err.println("柜口开启失败,当前设备无此格口");
// 通讯失败
case LockerResponseConstants.COMMUNICATION_FAILED -> System.err.println("通讯失败,请检查设备连接是否正常");
// 开门成功
case LockerResponseConstants.SUCCESS -> System.out.println("开门成功");

}
}

/**
* 获取格口状态
*
* @param ctx
* @param msg
*/
private void handleGetDoorStatus(ChannelHandlerContext ctx, LockerSocketDTO msg) {
switch (msg.getData()[0]) {
// 格口打开
case LockerResponseConstants.DOOR_OPEN -> System.out.println("格口打开");
// 格口关闭
case LockerResponseConstants.DOOR_CLOSE ->System.out.println("格口关闭");
// 无格口
case LockerResponseConstants.DOOR_NOT_EXIST -> System.err.println("当前设备无此格口");
// 通讯失败
case LockerResponseConstants.COMMUNICATION_FAILED ->
System.err.println("通信失败请检查设备连接是否正常");
}

}


/**
* Netty 异常处理回调
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}


8

设置信息

设置设备二维码【服务器->设备】

设备连接到服务器以后,服务器发送【获取设备编号】命令后,下发【设置二维码命令】,设备接收到命令后,立即更新显示的二维码内容。

(数据内容为二维码的访问地址,并非二维码的图片地址)

例1:

上位机发送:


帧头

长度

命令码

数据

帧尾

EF

2B

07

68 74 74 70 73 3A 2F 2F 77 77 77 2E 62 61 69 64 75 2E 63 6F 6D 2F 6F 70 65 6E 2F 33 31 30 32 35 31 31 32 34 30 30 33

FF

数据位说明:如:

https://www.baidu.com/open/310251124003

ASCII码编译后为:

68 74 74 70 73 3A 2F 2F 77 77 77 2E 62 61 69 64 75 2E 63 6F 6D 2F 6F 70 65 6E 2F 33 31 30 32 35 31 31 32 34 30 30 33

下位机应答:


帧头

长度

命令码

数据

帧尾

EF

05

07

00(设置成功)

01(设置失败)

FF


设置广告【服务器->设备】

服务器根据业务逻辑,需要更新广告的时候下发指令,设备主动下载视频文件,并保存至本地。

例1:

上位机发送:


帧头

长度

命令码

息屏时间

数据

帧尾

EF

23

09

0A

68 74 74 70 73 3A 2F 2F 78 69 6E 6C 69 61 6E 73 6D 61 72 74 2E 63 6F 6D 2F 72 65 6E 74 61 6C 31 2E 6D 70 34

FF

( 可以理解为数据位的第0位为熄屏时间 byte[0] )

息屏时间说明:单位秒:78即120秒屏幕无触摸,则自动播放广告;

熄屏时间设置为0 即为永不熄屏(视频地址需要传输正常的)

例如 EF 06 09 00 68 74 74 70 73 3A 2F 2F 78 69 6E 6C 69 61 6E 73 6D 61 72 74 2E 63 6F 6D 2F 72 65 6E 74 61 6C 31 2E 6D 70 34 FF

数据位说明:如:

https://xinliansmart.com/rental1.mp4

ASCII码编译后为:

68 74 74 70 73 3A 2F 2F 78 69 6E 6C 69 61 6E 73 6D 61 72 74 2E 63 6F 6D 2F 72 65 6E 74 61 6C 31 2E 6D 70 34

下位机应答:


帧头

长度

命令码

数据

帧尾

EF

05

09

00(设置成功)

01(设置失败)

FF

设置背景图片【服务器->设备】

服务器根据业务逻辑,需要更换背景的时候下发指令,设备主动下载图片文件,并保存至本地。

例1:

上位机发送:


帧头

长度

命令码

数据

帧尾

EF

23

0A

68 74 74 70 73 3A 2F 2F 78 69 6E 6C 69 61 6E 73 6D 61 72 74 2E 63 6F 6D 2F 62 67 32 2E 70 6E 67


FF

数据位说明:如:

https://xinliansmart.com/bg2.png

ASCII码编译后为:

68 74 74 70 73 3A 2F 2F 78 69 6E 6C 69 61 6E 73 6D 61 72 74 2E 63 6F 6D 2F 62 67 32 2E 70 6E 67

下位机应答:


帧头

长度

命令码

数据

帧尾

EF

05

0A

00(设置成功)

01(设置失败)

FF

完整代码如下

package com.gjq.handler;


import com.gjq.constants.LockerCmdConstants;
import com.gjq.constants.LockerResponseConstants;
import com.gjq.dto.LockerSocketDTO;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;

@RequiredArgsConstructor
public class LockerServerHandler extends SimpleChannelInboundHandler<LockerSocketDTO> {

/**
* 设备连接成功
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("设备连接成功:" + ctx.channel().remoteAddress());

LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.GET_DOOR_STATUS, new byte[]{0x01});
ctx.writeAndFlush(response);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("设备断开连接:" + ctx.channel().remoteAddress());
}

/**
* 接收设备消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, LockerSocketDTO msg) {
switch (msg.getCmd()) {
case LockerCmdConstants.HEART_BEAT:
handleHeartBeat(ctx, msg);
break;
case LockerCmdConstants.GET_DEVICE_NO:
handleDeviceNo(ctx, msg);
break;
case LockerCmdConstants.OPEN_DOOR:
handleOpenDoor(ctx, msg);
break;
case LockerCmdConstants.GET_DOOR_STATUS:
handleGetDoorStatus(ctx,msg);
break;
case LockerCmdConstants.SET_AD, LockerCmdConstants.SET_BG, LockerCmdConstants.SET_QR_CODE:
handleSetInfo(ctx,msg);
break;
}
}

/**
* 获取设备编号处理
*/
private void handleDeviceNo(ChannelHandlerContext ctx, LockerSocketDTO msg) {
// 获取设备id
String deviceNo = new String(msg.getData(), StandardCharsets.US_ASCII);
System.out.println("设备上线 -----> deviceNo=" + deviceNo);
}


/**
* 心跳处理
*/
private void handleHeartBeat(ChannelHandlerContext ctx, LockerSocketDTO msg) {
System.out.printf("收到心跳:%s%n", ctx.channel().remoteAddress());
// 原样返回
LockerSocketDTO response = new LockerSocketDTO(LockerCmdConstants.HEART_BEAT, new byte[]{0x00});
ctx.writeAndFlush(response);
}

/**
* 打开格口的处理
*
* @param ctx
* @param msg
*/
private void handleOpenDoor(ChannelHandlerContext ctx, LockerSocketDTO msg) {
// 处理返回数据
switch (msg.getData()[0]) {
// 格口不存在
case LockerResponseConstants.DOOR_NOT_EXIST -> System.err.println("柜口开启失败,当前设备无此格口");
// 通讯失败
case LockerResponseConstants.COMMUNICATION_FAILED -> System.err.println("通讯失败,请检查设备连接是否正常");
// 开门成功
case LockerResponseConstants.SUCCESS -> System.out.println("开门成功");

}
}

/**
* 设置柜机信息处理
*
* @param ctx
* @param msg
*/
private void handleSetInfo(ChannelHandlerContext ctx, LockerSocketDTO msg) {
byte datum = msg.getData()[0];
if (datum == LockerResponseConstants.FAILED) {
System.err.println("柜机信息设置失败");
}
}

/**
* 获取格口状态
*
* @param ctx
* @param msg
*/
private void handleGetDoorStatus(ChannelHandlerContext ctx, LockerSocketDTO msg) {
switch (msg.getData()[0]) {
// 格口打开
case LockerResponseConstants.DOOR_OPEN -> System.out.println("格口打开");
// 格口关闭
case LockerResponseConstants.DOOR_CLOSE ->System.out.println("格口关闭");
// 无格口
case LockerResponseConstants.DOOR_NOT_EXIST -> System.err.println("当前设备无此格口");
// 通讯失败
case LockerResponseConstants.COMMUNICATION_FAILED ->
System.err.println("通信失败请检查设备连接是否正常");
}

}


/**
* Netty 异常处理回调
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}


阅读记录0
点赞0
收藏0
禁止 本文未经作者允许授权,禁止转载
猜你喜欢
评论/提问(已发布 0 条)
头像
评论 评论
收藏 收藏
分享 分享
pdf下载 下载
pdf下载 举报