Skip to content

mqtt协议驱动

mqtt协议驱动是iboot平台默认的协议驱动, 开发者无需做任何配置即可使用mqtt协议驱动. mqtt broker需要自行搭建, 可以使用mosquitto、emqx等mqtt broker.

  1. 网络架构图mqtt协议驱动
  2. 配置流程

新增mqtt网关子设备产品 -> 配置产品物模型 -> 新增mqtt网关设备(mqtt连接) -> 新增mqtt网关子设备

  1. 添加网关子设备产品mqtt协议子产品

设备类型选择网关子设备, 父产品选择默认Mqtt网关产品(此网关产品已在系统中存在)

  1. 添加物模型
  • 物模型属性: 物模型属性需要先添加设备上报的字段, 然后新增一个json类型的属性将设备上报的字段绑定到json属性中 mqtt协议物模型属性
  • 事件模型: 协议指令选择订阅, 上行配置的主题需要以网关子设备产品编号开头 格式如下: 子产品编号/{deviceSn}/标识符, 其中{deviceSn}为设备编号的占位符, 下行配置无配置项。上报的负载必须通过输入@来提及json格式的物模型属性; 遗嘱选择否 mqtt协议物模型事件
  • 功能模型: 协议指令选择发布, 下行配置的主题没有做要求, 负载直接填写json格式的数据 mqtt协议功能模型
json
{"wendu": 30, "shidu": 30}
  1. 新增网关设备(配置菜单在: 设备管理 -> mqtt网关设备)

mqtt的网关设备其实就是一个mqtt客户端连接, 所以需要配置mqtt客户端的连接信息, 包括broker地址、端口、用户名、密码、客户端id等信息 mqtt协议网关设备

  • 所属产品:选择默认的Mqtt网关产品(此网关产品已在系统中存在)
  • 客户端id:整个mqtt broker中需要保证客户端id的唯一性, 否则可能出现客户端不断重连的情况
  • 主题:主题需要以网关子产品编号开头 格式如下: 网关子产品编号/#, 此主题写法的意思是此客户端将订阅此产品的所有主题
  1. 新增网关子设备

网关子设备的新增和直连设备的新增类似mqtt协议网关子设备

  • 所属产品:选择之前添加的网关子设备产品
  • 设备编号:此设备编号需要和事件模型上行配置中的{deviceSn}占位符一致, 比如设备发布的主题是: mqtt_default_demo/demo_001/wendu, 那么设备编号就需要填写demo_001, 否则子设备会一直处于离线状态

modbus协议驱动

modbus协议是一个国际标准的工业协议, 目前iboot平台已经开源了modbus协议驱动, 目前支持modbus rtu和modbus tcp两种实现, 开发者无需做任何开发即可使用modbus协议驱动。

  1. 网络架构图modbus协议驱动
  2. 配置流程

新增modbus网关子设备产品 -> 配置产品物模型 -> 新增modbus网关设备(串口或tcp连接) -> 新增modbus网关子设备

  1. 添加网关子设备产品

设备类型选择网关子设备, 父产品可以选择默认Modbus Rtu网关产品或Modbus Tcp网关产品(网关产品已在系统中存在)

  1. 添加物模型
  • 物模型属性: 物模型属性根据设备的文档来按需添加, 在读寄存器的时候一个点位地址需要配置一个属性
  • 事件模型: 协议指令选择读寄存器或读线圈, 下行配置的寄存器地址和数量需要根据设备的文档来配置, 上行配置需要输入@来提及物模型属性, 将采集的值和物模型属性进行绑定
  • 功能模型: 协议指令选择写寄存器或写线圈, 下行配置的寄存器地址和数量需要根据设备的文档来配置, 上行配置没有配置项
  • 注: 寄存器数量算法: 一个寄存器两个字节, 比如 readShort short类型占用两个字节所以寄存器数量填1
  1. 添加网关设备(配置菜单在: modbus网关设备)

添加modbus网关设备时 所属产品如果选择modbus rtu需要配置串口信息, 如果是modbus tcp需要配置modbus的tcp连接信息

  1. 添加网关子设备

在刚新增的网关设备那条记录的操作栏上面点击子设备按钮来新增子设备, 子设备的所属产品选择之前添加的网关子设备产品, 所属组必须指定, 因为在事件源配置采集器的数据需要选择哪些组的设备加入采集, 如果这里没有指定所属组则采集器无法采集到数据!

dtu协议驱动

基于DTU的modbus协议驱动是对modbus协议在网络架构上的扩展, 由于modbus rtu协议的网络架构时基于串口的, 串口的传输距离有限制, 所以需要将modbus rtu协议进行扩展那就需要一个串口服务器(dtu)做透传来将RS485的协议扩展到网络上(TCP), 目前iboot平台已经开源了dtu协议驱动, 开发者无需做任何开发即可使用dtu架构的modbus rtu协议驱动。此驱动的物模型配置和modbus协议驱动的物模型配置一致

  1. 网络架构图dtu协议驱动
  2. 配置流程(物模型配置和modbus协议一致)

新增DTU网关子设备产品 -> 配置产品物模型 -> 新增DTU网关设备 -> 新增网关子设备

  1. dtu是需要主动连接iboot平台的, 所以需要在iboot平台开放端口给dtu连接, 这个端口在平台的网络组件菜单上面进行配置dtu网络组件
  1. 注册码类型: 注册码类型一般为ascii和hex, 需要和dtu的配置一致
  2. 读空闲时长: 如果平台在此时间段内没有收到dtu的心跳包则会主动断开此dtu连接, 单位为秒, 如果设置为0则表示不进行心跳包检测, 如果配置大于0则需要同时配置dtu的心跳包时间, 还有此值需要大于(上图的小于是)dtu的心跳包时间
  1. dtu连接iboot平台的时候需要有一个编号让平台知道怎么区分对应的dtu, 所以在平台配置dtu网关设备的时候需要配置dtu的编号, 这个编号需要和dtu的注册包编号一致(dtu的注册包可以在dtu设备的配置界面进行获取). 以有人的dtu移动端配置为例有人dtu设备配置
  1. 注册包功能: 发送方式选择连接发送注册包, 注册包内容可以选择自定义, 注册包类型一般为ascii和hex两种按需选择就可以
  2. 心跳包功能: 心跳包功能可以让平台知道dtu是否在线, 心跳包和注册包必须在内容和注册码类型上都保持一致

dlt645协议驱动

编写中...

opcua协议驱动

编写中...

s7协议驱动

编写中...

项目介绍

  1. iotucy也是我们公司的一个开源项目, 此项目专注于网络底层, 是iboot项目协议驱动的基础 , 由于iboot的协议驱动基于iotucy框架开发所以如果要自定义驱动的用户需要对iotucy有个了解
  2. iotucy主要涉及并抽象了网络中的报文、协议、编解码器、同步异步、超时机制、断线重连机制等核心功能
  3. iotucy项目下的iot-test模块是以下教程的源码模块
  4. iotucy开源地址

快速开始

  1. 首先创建一个基于maven的springboot项目,然后导入iot对应的jar包
xml
<!-- 注意:3.9.0+版本需要先导入以下依赖 -->
<dependency>
      <groupId>com.iteaj</groupId>
      <version>${iot.version}</version>
      <artifactId>iot-boot-starter</artifactId>
</dependency>

以下包为可选依赖

code
<!-- 如果要开发自定义tcp、udp等服务端则加入依赖 -->
<dependency>
      <groupId>com.iteaj</groupId>
      <artifactId>iot-server</artifactId>
      <version>${iot.version}</version>
</dependency>
<!-- 如果要开发自定义tcp、udp等客户端则加入依赖 -->
<dependency>
      <groupId>com.iteaj</groupId>
      <artifactId>iot-client</artifactId>
      <version>${iot.version}</version>
</dependency>
  1. 编写自己的组件、报文、协议对象。服务端监听一个端口需要写至少两个类(注:以下代码均在iot-test模块里面):
  • 创建一个服务端的组件类, 以断路器为例
java
public class BreakerServerComponent extends LengthFieldBasedFrameDecoderServerComponent<BreakerServerMessage> {
    public BreakerServerComponent(ConnectProperties connectProperties) {
        super(connectProperties, ByteOrder.LITTLE_ENDIAN, 256, 0, 4, 0, 0, true);
    }
    // xxx 实现省略
}
  • 创建一个和服务段组件类相关联的服务端报文类
java
public class BreakerServerMessage extends ServerMessage {
     public BreakerServerMessage(byte[] message) {
         super(message);
     }
     //省略其他构造函数
     @Override
     protected MessageHead doBuild(byte[] message) {
         this.messageBody = MessageCreator.buildBreakerBody(message);
         return MessageCreator.buildBreakerHeader(message);
     }
}
  1. 创建协议对象,一个协议对象对应设备的协议文档里面的一个协议 比如:切换断路器的开合状态
java
public class SwitchStatusProtocol extends ServerInitiativeProtocol<BreakerServerMessage> {

    private String deviceSn;

    public SwitchStatusProtocol(String deviceSn) {
        this.deviceSn = deviceSn;
    }

    /**
     * 构建要发送给断路器的报文
     * @return
     * @throws IOException
     */
    @Override
    protected BreakerServerMessage doBuildRequestMessage() throws IOException {
        DefaultMessageHead messageHead = MessageCreator.buildBreakerHeader(Long.valueOf(this.deviceSn), 0, protocolType());
        return new BreakerServerMessage(messageHead);
    }

    @Override
    protected void doBuildResponseMessage(BreakerServerMessage message) {
        /*设备响应是否切换成功的处理*/
    }

    @Override
    public BreakerProtocolType protocolType() {
        return BreakerProtocolType.SwitchStatus;
    }
}

名词解释

  1. 报文对象(Message):报文是对在网络中进行传输的二进制数据的封装,也是二进制数据的载体,在一定程度上 报文 = 二进制数据
  2. 协议对象(Protocol):协议是报文的一个规范约束,比如报文内容是:0x04 AF CD EE 03,那怎么知道这一串表示的是什么呢,协议就是对这一串数据的声明, 比如第一个字节代表数据后面还有几个字节的长度, 第二个字节是电压,第三字节是电流,第四个字节是校验位
  3. 组件对象(FrameworkComponent):在服务端,组件用来管理一个端口所需要的各种接口;在客户端,组件用来管理连接同一个服务器的所有接口以及已经连接的所有客户端。比如服务端的设备管理器,报文需要用到的编解码器,同步异步处理都是由组件来管理
  4. 同步:在调用请求的时候,请求线程会加锁阻塞,直到接收到响应或者超时来解锁
  5. 异步:在调用请求的时候,请求线程在发送完报文后直接返回,不阻塞调用线程, 而是注册一个回调函数,等到对方响应或者超时的时候在做业务处理
  6. 编解码器:用来对网络上的二进制数据进行拆包和粘包的处理对象
  7. 协议工厂(ProtocolFactory):用来创建各个协议对象的地方(因为一个客户端可能包含多个功能(协议), 每个功能对应一个协议对象那就会有很多协议对象,协议工厂用来管理协议对象的创建)
  8. 协议处理器(ProtocolHandle): 用来对协议做业务处理的

下面将由一个例子来展开说明iot框架的使用 例子:比如服务端接受到客户端报文如下:0x01 11 12 13 05 06 EE FF 八个字节, 如果客户端发送完此报文之后没有连续发送, 服务端接受到的数据就是一包完整的报文, 我们可以很容易的读取缓存区的内容然后进行处理; 那如果第一包发送完之后服务端还在忙其他的时没有及时读取缓冲区内容这时候又接收到了客户端的第二个报文,这时候数据缓冲区的数据如下:0x01 11 12 13 05 06 EE FF 02 21 22 23 25 26 AA BB CC DD,这时候程序读取的缓冲区是两包完整的报文,这时候程序怎么将两包报文拆开处理呢?这时候就需要编解码器上场了! 如何处理上面报文粘包和拆包的情况呢?netty提供了一下几种常用的解码器

编解码器

  1. FixedLengthFrameDecoder: 固定长度解码器是最简单的一种方式,每个包的长度是固定的,比如每个包都是8个字节,那么程序就可以以8字节为单位进行拆包
  2. LineBasedFrameDecoder:换行符解码器是让每个报文都用换行符结尾,这时候程序可以循环读取每个字节判断,如果这个字节是换行符就说明已经读完了一包报文
  3. DelimiterBasedFrameDecoder:如果我们的数据里面刚好包含换行符这时候读取就会出错,这时候可以用自定义分隔符来拆分报文
  4. LengthFieldBasedFrameDecoder:不管是换行符解码还是自定义分隔符解码,都需要循环判断每个字节,如果在一包完整的报文很长的情况下性能会非常差,这时候有个非常好用且性能极高的解码器,长度字段解码,就是在报文里面加入一个长度字段用来标识整个报文的长度
  5. ByteToMessageDecoder:如果还有更好的解码方式可以使用自定义报文解码
  6. SimpleChannelInboundHandler:简单的解码器

在设备对接的时候厂家一般会提供协议文档,然后就需要我们来选择合适的解码器,当我们确认好了解码器之后就可以开始编码了,下面先开始服务端的对接教程

服务端教程

编写服务端网络程序时需要监听某个端口来给客户端连接, 当我们选择某个解码器之后就可以选择对应的服务端解码器组件来开启某个端口,iot框架适配了netty提供的几个常用的解码器

创建解码器组件
  1. FixedLengthFrameDecoderServerComponent 使用固定长度解码器的服务端组件
  2. LineBasedFrameDecoderServerComponent 使用换行符解码器的服务端组件
  3. DelimiterBasedFrameDecoderServerComponent 使用自定义分隔符解码器的服务端组件
  4. LengthFieldBasedFrameDecoderServerComponent 使用长度字段解码器的服务端组件
  5. ByteToMessageDecoderServerComponent 使用自定义解码器的服务端组件
  6. DatagramPacketDecoderServerComponent udp协议的服务端组件
  7. SimpleChannelDecoderComponent 简单自定义解码器对应的组件

以下是使用LengthFieldBasedFrameDecoderServerComponent示例

java
// 首先:必须先创建一个组件对象来继承LengthFieldBasedFrameDecoderServerComponent
// 以iot-test模块的断路器服务端模拟为例
public class BreakerServerComponent extends LengthFieldBasedFrameDecoderServerComponent<BreakerServerMessage> {

    public BreakerServerComponent(ConnectProperties connectProperties) {
        super(connectProperties, ByteOrder.LITTLE_ENDIAN, 256, 0
                , 4, 0, 0, true);
    }
    // xxx 实现省略
}
// 注:要求传入ConnectProperties对象作为构造参数, 此对象可以指定ip和端口

我们看到上面的组件需要一个泛型参数BreakerServerMessage, 此参数就是报文对象,上面我们说过报文对象是一个二进制数据载体,用于在iot框架各个对象中进行使用,下面我们来看看报文对象除了作为数据载体还有哪些扩展功能

创建报文对象
// 创建服务端报文对象必须继承ServerMessage类
public class BreakerServerMessage extends ServerMessage {

    public BreakerServerMessage(byte[] message) {
        super(message);
    }

    //省略其他构造函数

    @Override
    protected MessageHead doBuild(byte[] message) {
        this.messageBody = MessageCreator.buildBreakerBody(message);
        return MessageCreator.buildBreakerHeader(message);
    }
}
  1. 首先报文对象的构造函数BreakerServerMessage#BreakerServerMessage(byte[])必须存在
  2. 报文对象是连接组件和协议的桥梁,所以需要为每个服务端组件创建一个与之对应的报文对象
  3. 需要在BreakerServerMessage#doBuild(byte[])方法里初步解析出对应的equipCode、messageId、protocolType几个参数,这也是对客户端请求数据的初步解析

当我们创建了组件和报文类之后就可以启动应用了, 这时候日志里面会打印出组件配置的端口已经开启监听了。到了这里已经开了一个好头了算是成功了一半了,接下来就是创建协议对象了,从厂家那里拿到的协议文档至少包含一个协议, 一般我们建议为文档里面的每个协议创建一个对应的协议对象(Protocol)

创建协议对象

协议对象就是用来将接收到的二进制数据解析成和协议文档里对应的字段的;出于框架架构的需要我们将协议分成两种类型 如下:

  1. ClientInitiativeProtocol 声明此协议是客户端主动发起的协议 比如断路器主动上报当前的电流、电压、或报警
  2. ServerInitiativeProtocol 声明此协议是服务端主动发起的协议 比如服务端下发断开断路器的指令

首先我们先看一下ClientInitiativeProtocol方法声明, 还是以断路器为例

java
// 用来接收断路器主动上报的电压电流等数据
public class DataAcceptProtocol extends ClientInitiativeProtocol<BreakerServerMessage> {

    private double v; // 电压
    private double i; // 电流
    private double power1; // 有功功率
    private double power2; // 无功功率
    private double py; // 功率因素

    public DataAcceptProtocol(BreakerServerMessage requestMessage) {
        super(requestMessage);
    }

    @Override
    protected void doBuildRequestMessage(BreakerServerMessage requestMessage) {
        byte[] message = requestMessage.getBody().getMessage();
        this.v = ByteUtil.bytesToInt(message, 0) / 100.0;
        this.i = ByteUtil.bytesToInt(message, 4) / 100.0;
        this.power1 = ByteUtil.bytesToInt(message, 8) / 100.0;
        this.power2 = ByteUtil.bytesToInt(message, 12) / 100.0;
        this.py = ByteUtil.bytesToShort(message, 16) / 100.0;
    }

    // 响应断路器的请求
    @Override
    protected BreakerServerMessage doBuildResponseMessage() {
        Message.MessageHead head = requestMessage().getHead();
        return new BreakerServerMessage(MessageCreator.buildBreakerHeader(head
                .getEquipCode(), head.getMessageId(), 4, head.getType()),
                MessageCreator.buildBreakerBody(StatusCode.Success));
    }

    // 省略其他
}

平台已经可以接收设备主动上报的数据了,那平台要怎么主动给设备发送数据呢?ServerInitiativeProtocol协议就是用来声明一个协议是平台主动发给客户端的,下面以平台下发给断路器切换开关为例

java
/**
 * 切换断路器的开闭状态
 */
public class SwitchStatusProtocol extends ServerInitiativeProtocol<BreakerServerMessage> {

    private String deviceSn;

    public SwitchStatusProtocol(String deviceSn) {
        this.deviceSn = deviceSn;
    }

    /**
     * 构建要发送给断路器的报文
     */
    @Override
    protected BreakerServerMessage doBuildRequestMessage() throws IOException {
        DefaultMessageHead messageHead = MessageCreator.buildBreakerHeader(Long.valueOf(this.deviceSn), 0, protocolType());
        return new BreakerServerMessage(messageHead);
    }

    /**
     * 处理断路器对此处请求的响应
     */
    @Override
    protected void doBuildResponseMessage(BreakerServerMessage message) {
        /*设备响应是否切换成功的处理*/
    }

    @Override
    public BreakerProtocolType protocolType() {
        return BreakerProtocolType.SwitchStatus;
    }
}
// 然后在业务代码里面调用请求方法:new SwitchStatusProtocol(deviceSn).request(); 这样就可以向指定的设备发起请求了
创建报文头对象

看到现在我们已经可以接收到设备的请求和主动发起请求给设备了,但是有一个问题,一般一台设备会包含很多功能(协议),因为每个功能都会创建一个协议对象那么问题来了,当设备发给平台的时候平台怎么知道需要交给哪个协议处理呢? 一般来说如果有多个协议那么协议文档一般会有一个字段用来区分的, 比如报文:0x01 11 12 13 05 06 EE FF的第二个字节 0x11作为协议类型字段,比如01代表切换开关,02代表上报温湿度,03代表锁断路器;所以我们需要先解析出协议类型(如果有的话) 我们在看一下报文对象:需要在doBuild方法里面先解析出报文头(MessageHead),报文头里面需要包含设备编号和协议类型等信息

java
// 先看一下报文头的接口声明,其中type就是用来声明一个客户端的协议类型的
interface MessageHead {

    /**
     * 设备编号
     * @return
     */
    String getEquipCode();

    /**
     * 报文的唯一编号
     * @return
     */
    String getMessageId();

    /**
     * 获取交易类型
     * @return
     */
    <T> T getType();

    byte[] getMessage();

    default int getLength() {
        return getMessage().length;
    }
}
// 然后在报文对象里面解析出报文头
public class BreakerServerMessage extends ServerMessage {
    //省略其他构造函数
    @Override
    protected MessageHead doBuild(byte[] message) {
        this.messageBody = MessageCreator.buildBreakerBody(message);
        return MessageCreator.buildBreakerHeader(message);
    }
}
获取协议对象

上面已经解析出了对应的协议类型了,下一步就需要创建协议对象来解析对应的报文了。上面说过组件对象用来管理整个框架中的各个接口, 首先组件对象实现了协议工厂接口 如下:

java
// 协议工厂接口
public interface IotProtocolFactory<T extends SocketMessage> extends StorageManager<String, Protocol> {}
// 组件对象实现了协议工厂接口 如下
public abstract class TcpDecoderServerComponent<M extends ServerMessage> extends TcpServerComponent<M> implements IotSocketServer, IotProtocolFactory<M> {}

所以我们可以在组件里面来管理各个协议对象的创建,以断路器组件为例:

java
public class BreakerServerComponent extends LengthFieldBasedFrameDecoderServerComponent<BreakerServerMessage> {
    @Override
    public AbstractProtocol getProtocol(BreakerServerMessage message) {
        // 因为我们已经解析出了协议类型了,所以可以通过协议类型创建对应的协议
        BreakerProtocolType type = message.getHead().getType();

        // 断路器主动推送电压和电流值
        if(type == BreakerProtocolType.PushData) {
             return new DataAcceptProtocol(message);
        } else { // 移除平台主动请求的协议
            return remove(message.getHead().getMessageId());
        }
    }
}
业务处理

现在我们已经通过协议解析出了客户端给我们的数据了,接下去就是对这些数据进行处理, 比如数据入库,实时批处理,缓存等等操作,iot框架提供了一个专门的接口处理业务,先看协议处理器的声明 如下

java
public interface ProtocolHandle<T extends Protocol> {

    Method method = ReflectionUtils.findMethod(ProtocolHandle.class, "handle", Protocol.class);

    /**
     * 协议的业务处理
     * @param protocol
     */
    Object handle(T protocol);
}

只有一个方法handle,注意泛型参数<T extends Protocol>,此参数用来声明此处理器是用于处理哪个协议的,所以理论上一个协议对象有一个与之对应的处理器(不需要业务处理的协议就不需要创建),那问题来了,程序是怎么根据协议对象获取对应的处理器的呢?这时候需要将协议处理器对象注入到spring容器,将此对象交由spring容器管理,所以我们需要一个处理器工厂用来可以方便的通过协议对象获取对应的协议处理器对象 声明如下

java
public abstract class BusinessFactory<T extends ProtocolHandle> implements InitializingBean, BeanFactoryAware {

    private ListableBeanFactory beanFactory;
    private HashMap<Class<? extends Protocol>, T> mapper;

    // 通过协议类型对象获取对应的协议处理器对象
    public T getProtocolHandle(Class<? extends Protocol> protocolClazz){
        return mapper.get(protocolClazz);
    }
    // 省略其他接口
}

下面我们看一下断路器的协议处理器对象:

java
// 注入到spring容器
@Component
public class DataAcceptHandle implements ServerProtocolHandle<DataAcceptProtocol> {
    // 比如做数据入库、发送到mqtt网关、缓存到redis、提交到
    @Override
    public Object handle(DataAcceptProtocol protocol) {
        final int i = RandomUtil.randomInt(1, 9);
        if(i % 2 == 0) { // 测试自动创建数据表
            TaosBreakerUsingStable entity = new TaosBreakerUsingStable(protocol.getEquipCode());
            entity.setI(protocol.getI());
            entity.setV(protocol.getV());
            entity.setPy(protocol.getPy());
            entity.setSn(protocol.getEquipCode());
            entity.setPower1(protocol.getPower1());
            entity.setPower2(protocol.getPower2());

            return entity;
        } else { // 测试插入数据表
            TaosBreakerDataTable dataTable = new TaosBreakerDataTable();
            dataTable.setI(protocol.getI());
            dataTable.setV(protocol.getV());
            dataTable.setPy(protocol.getPy());
            dataTable.setTs(new Date());
            dataTable.setSn(protocol.getEquipCode());
            dataTable.setPower1(protocol.getPower1());
            dataTable.setPower2(protocol.getPower2());
            return dataTable;
        }
    }     
}
响应客户端请求

如果我们需要确认客户端的请求, 比如断路器要同步平台的时间, 这时候平台接收到请求之后需要将时间按照协议格式发给客户端这时候要怎么将数据给客户端?还是以断路器为例:

java
// 比如当我们收到断路器上报的电压电流值之后需要跟断路器说一声我已经收到你发送的值了,可以在 #doBuildResponseMessage()方法里面根据协议文档的要求构建要发送给断路器的报文, 平台会主动响应给断路器
public class DataAcceptProtocol extends ClientInitiativeProtocol<BreakerServerMessage> {
    // 确认断路器请求
    @Override
    protected BreakerServerMessage doBuildResponseMessage() {
        Message.MessageHead head = requestMessage().getHead();
        return new BreakerServerMessage(MessageCreator.buildBreakerHeader(head
                .getEquipCode(), head.getMessageId(), 4, head.getType()),
                MessageCreator.buildBreakerBody(StatusCode.Success));
    }
    // 省略其他
}

到了这里这个iot框架已经实现了闭环了,接收客户端数据 -> 数据解析 -> 业务处理 -> 响应给客户端 或者 平台请求客户端 -> 客户端反馈处理结果 -> 平台接收结果 -> 业务处理。但是还有很多的细节需要处理,比如协议的同步和异步处理,设备的注册等等。接着往下看!

协议异步请求

异步请求指的是不阻塞调用请求的线程(不等待设备响应只管发送),这里面会碰到的问题:

  1. 不知道设备有没有接收到请求: 写出去之后可能因为网络等原因导致设备没收到请求或者执行了请求指令应答的时候出现了问题,平台没有收到应答 解决方案:协议在请求之后保存此协议对象,如果因为网络等原因设备没有应答标记此协议状态为超时,平台可以在超时的时候下发查询指令来查询设备的状态
  2. 设备有没有成功执行此请求:设备收到了请求,但是设备执行请求失败了 解决方案:由于保存了协议,这时候再设备应答的时候可以取出对应的请求协议,然后解析响应报文就可以知道设备对请求指令的执行状态了
协议同步请求

我们知道tcp和http不一样并不是基于同步请求响应模式,也就是说平台给设备发送请求完之后请求线程不会阻塞等待设备的响应,当然也不建议阻塞等待因为太耗费性能,这也是物联网一般不会使用http的原因;因为物联网设备太多如果都使用同步会太耗费服务器资源,但是iot框架提供了一个同步机制,原理是调用请求方法时在写出报文之后加锁阻塞此请求然后等设备响应或者响应超时后释放锁。虽然同步会阻塞请求线程,但是在一些需要实时知道请求结果得场景下(比如前端发起请求之后需要知道执行状态)可以简化是流程

报文标识

不管是同步请求还是异步请求,这里都会有个问题就是设备在应答的时候怎么知道此应答对应哪一条请求指令(协议),解决的方法也很简单给每条下发指令的报文做一个标记(MessageId),设备在应答的时候把请求报文的MessageId原封不动的发上来,然后只需要通过此messageId获取对应的请求协议对象(保存协议对象时用map存储, messageId作为key, Protocol作为value),那我们在什么时候解析此messageId呢? 如下:

java
// 1. 在报文里面解析报文头得时候一起解析出messageId
public class BreakerServerMessage extends ServerMessage {
    //省略其他构造函数
    @Override
    protected MessageHead doBuild(byte[] message) {
        this.messageBody = MessageCreator.buildBreakerBody(message);
        return MessageCreator.buildBreakerHeader(message);
    }
}
// 2. 在对应得组件里面移除掉对应的协议并且返回
public class BreakerServerComponent extends LengthFieldBasedFrameDecoderServerComponent<BreakerServerMessage> {
    @Override
    public AbstractProtocol getProtocol(BreakerServerMessage message) {
        // 因为我们已经解析出了协议类型了,所以可以通过协议类型创建对应的协议
        BreakerProtocolType type = message.getHead().getType();

        // 断路器主动推送电压和电流值
        if(type == BreakerProtocolType.PushData) {
            return new DataAcceptProtocol(message);
        } else { // 移除平台主动请求的协议
            return remove(message.getHead().getMessageId());
        }
    }
    // 省略其他方法
}
interface MessageHead {
    /**
     * 报文的唯一编号
     * @return
     */
    String getMessageId();
}