一、知识储备

先简单了解一些开发前要掌握的前期知识点。

1.1 什么是NB-IOT

NB-IoT全称窄带物联网(Narrow Band IOT),构建于蜂窝网络,只消耗大约180KHz的带宽,可直接部署于GSM网络、UMTS网络或LTE网络,以降低部署成本、实现平滑升级。

1.1.1 NB-IoT使用场景及特点

通常,我们把物联网设备分为三类:

①无需移动性,大数据量(上行),需较宽频段,比如城市监控摄像头。

②移动性强,需执行频繁切换,小数据量,比如车队追踪管理。

③无需移动性,小数据量,对时延不敏感,比如智能抄表。

NB-IoT正是为了无需移动性,小数据量,对时延不敏感的设备准备的,也就是第三类。

1.1.2 NB-IoT数据上传和网络组成

NB-IOT网络包括NB-IOT终端,NB-IOT基站,NB-IOT分组核心网,IOT衔接办理渠道,和职业使用效劳器。

NB-IoT基站通过以下两种协议来链接基站:

COAP协议:MCU(NB设备)—NB模块(UE)–eNode—核心网—IOT渠道—APP 效劳器—手机终端app

UDP协议:MCU(NB设备)—NB模块(UE)–eNode—核心网—UDP 效劳器—手机终端

1.1.3 CoAP协议

CoAP 是一种应用层协议,它运行于UDP 协议之上而不是像HTTP 那样运行于TCP 之上。CoAP 协议非常小巧,最小的数据包仅为4 字节。

在此模式下,用户的终端设备,可以通过本模块发送请求数据到指定的CoAP 服务器,然后模块接收来自CoAP 服务器的数据,对数据进行解析并将结果发至串口设备。用户不需要关注串口数据与网络数据包之间的数据转换过程,只需通过简单的参数设置,即可实现串口设备向CoAP 服务器的数据请求。

CoAP 一般用来接入一些物联网平台,比如:华为的物联网云平台,可以将数据发送到云平台后,通过云平台提供的接口用户自己开发自己的应用程序。

虽然支持双向数据透传,但是和传统2G 网络有所不同,为节省电量,模块随时可以向服务器发送数据,但是服务器并不能在任何时候将数据发往串口,这也是NB-IoT 网络的所具有的特点。

1.2 整体架构图

NB网络架构 nb-iot基本网络组成_物联网

1.3 消息处理流程

1.3.1 上行消息处理

NB网络架构 nb-iot基本网络组成_封装_02

1.3.2 下行消息处理

NB网络架构 nb-iot基本网络组成_NB网络架构_03

二、拼接指令帧

不同的终端厂商会自定义不同的应用层协议,并将编解码相关的能力烧入到终端设备的芯片中去,所以作为应用开发者,只需要详细研读相关文档,明确各个字段含义,以及加密解密的流程,封装好南向接口所需要的数据,其实整个流程还是很清爽的。

让我们来看下面的这个例子:

NB网络架构 nb-iot基本网络组成_数据_04


比如我们现在有这么一个帧结构,然后每个字段的含义分别是下面这些:

2.1 帧头(HEAD)

数值为68H,一帧的开始。

2.2 协议类型(T)

协议类型号。为后期扩展预留。本协议对应的类型号为0。

2.3 协议框架版本(V)

协议框架的版本号。为后期扩展预留。本协议对应的版本号为1。

2.4 帧长度(L)

整帧长度,包括帧头。

2.5 消息序号(MID)

下行报文和最近一次的上行报文一致。燃气表每次发送后数值自增1。

2.6 控制域(C)

NB网络架构 nb-iot基本网络组成_NB网络架构_05


Bit7:方向标识。0——上行报文,1——下行报文

Bit6::是否有后续帧。0——无后续报文,1——有后续报文

Bit0~Bit4:功能码。具体内容参考下表。

NB网络架构 nb-iot基本网络组成_封装_06

到这里,比如应用端的开发者想要下发一个响应帧或者要主动下发一个指令给终端(因为NB-LOT终端设备省电的机制,决定了一次通信的请求只能由终端来主动发起,服务器端只能做响应,或者将指令提前缓存起来,等待终端发起通信的时候来读取,这条原则见1.1.3,否则设备掉电速度会很快)他就可以先这么拼接:0x680001[L][MID][C]

其中L是全帧总长度,可以等到整帧组装完再计算,MID是上行报文的序列号(上下行报文的序列号需保持一致,不然无法找到响应,这个上行报文中肯定有携带,可以从中读取,比如第一条就是01H),C中定义了指令的功能,在应用端所做的消息流方向肯定是下行,所以Bit7 = 1,如果一次报文中就能把数据全部传完(即数据域的大小比较小),Bit6 = 0,Bit5作为保留字段不使用的话,Bit5 = 0,Bit4 - Bit0就按照想要实现的功能来封装,比如我现在想要使用功能码05H的功能,那么就是00005B,整个C就是85H(这个可以自己算一下进制之间的转换)。那么我们就可以得到:0x680001[L]0185

2.7 数据对象ID (DID)

NB网络架构 nb-iot基本网络组成_NB网络架构_07


比如阀控状态,那么DID就是0001H

2.8 数据域 (D)

在数据域这一块需要用到加密,不同的功能可能对应不同的加密方式,比如我们用的是AES128 ECB加密算法,并要求数据补齐。

128位,即16字节,所以补齐要补齐到16个字节。补齐算法我们可以使用PKCS5PaddingPKSCS7Padding算法,PKSCS7Padding算法填充的原则是,如果需要N字节补齐,报文长度少于N个字节,需要补满N个字节,补(N-len)个(N-len)。如果报文长度正好时N字节的整数倍,则需要补N个十进制N。

比如关阀指令,原数据域的内容是01H,通过PKSCS7Padding算法补齐后就是0x010F..(此处省略13个0F)0F,然后使用加密算法加密即可。

知识补充AES128算法 advanced encryption standard 128 algorithm,是一种标准的分组加密对称密钥加密算法,所以在使用的时候首先要先获取到密钥。密钥的生成方式多种多样,比如有的协议的做法就是先给一把主密钥,然后通过这把主密钥对设备上报报文中的某字段进行HMAC-SHA256计算,取前16位(这里要注意位数和AES128算法保持一致),然后HMAC计算的结果去对数据域进行一个加密,终端在接收到加密报文后会受芯片的控制做一个对应的解密处理,就可以获取到指令进行对应的动作了。

2.9 校验域 (CRC)

终于对数据域加完密了,不要忘记通信还需要CRC校验,来确保丢失了数据后还能够自动修正。CRC算法有很多种,一般校验的范围是从”消息序号“到加密后的”数据域“,不同的CRC算法对相同数据执行,结果也是不一样的,并要注意CRC是有大小端格式的区别的(高低位),一般默认大端格式,这个网上有很多demo,自己写一个工具轮子也可。

如果用的CRC是:CRC-16/XMODEM
C语言版本就是下面这样,项目中用的Java对应着改写即可。

unsigned short CRC16(unsigned char *puchMsg, unsigned int usDataLen)  
{  
  unsigned short wCRCin = 0x0000;  
  unsigned short wCPoly = 0x1021;  
  unsigned char wChar = 0;  

  while (usDataLen--)     
  {  
        wChar = *(puchMsg++);  
        wCRCin ^= (wChar << 8);  
        for(int i = 0;i < 8;i++)  
        {  
          if(wCRCin & 0x8000)  
            wCRCin = (wCRCin << 1) ^ wCPoly;  
          else  
            wCRCin = wCRCin << 1;  
        }  
  }  
  return (wCRCin) ;  
}

java版:

/**
 * CRC16-XMODEM算法(四字节)
 * @param bytes
 * @param offset
 * @param count
 * @return
 */
public static int crc16_ccitt_xmodem(byte[] bytes,int offset,int count) {
    int crc = 0x0000; // initial value
    int polynomial = 0x1021; // poly value
    for (int index = offset; index < count; index++) {
        byte b = bytes[index];
        for (int i = 0; i < 8; i++) {
            boolean bit = ((b >> (7 - i) & 1) == 1);
            boolean c15 = ((crc >> 15 & 1) == 1);
            crc <<= 1;
            if (c15 ^ bit)
                crc ^= polynomial;
        }
    }
    crc &= 0xffff;
    return crc;
}

/**
 * CRC16-XMODEM算法(两字节)
 * @param bytes
 * @param offset
 * @param count
 * @return
 */
public static short crc16_ccitt_xmodem_short(byte[] bytes,int offset,int count) {
    return (short)crc16_ccitt_xmodem(bytes,offset,count);
}
2.10 帧尾(TAIL)

最后就是帧尾,数值为16H,一帧的结尾,到此为止帧的封装就结束啦!

三、下发指令

整个拼接帧的过程,可以封装成一个Encoder(编码器),放到IOT平台上,让下发终端的通信在平台处自动编码。也可以把整个流程封装成一个功能,每次终端的请求上来时,在应用侧调用功能做一个编码的工作,这样可能更加灵活。

现在我们下发了一个指令:680001003C02850001005c0550077b9a67edd1f68392e59b236c1ce090af3127fa53c398184dd6bbd2b2a9140a45534922264634120868846236a216,身边的终端传来"啪嗒"一声,真是天籁之音!