NB-IoT 定位服務(wù)原型

傳統(tǒng)應(yīng)用

資產(chǎn)定位是非常成熟的物聯(lián)網(wǎng)應(yīng)用,歷史悠久。但是廣域數(shù)據(jù)服務(wù)最大的痛點(diǎn)在于功耗和待機(jī)。傳統(tǒng)上使用2G GSM/GPRS/CDMA作為首選技術(shù),因?yàn)檫@些技術(shù)相對(duì)網(wǎng)絡(luò)覆蓋率和發(fā)射電流要小于3G/4G技術(shù)。但是無(wú)論是使用數(shù)據(jù)服務(wù)還是短消息服務(wù),發(fā)射電流都比較大。相對(duì)來(lái)說,CDMA的耗電更小些,但是依然需要隔一段時(shí)間就充電一次。尤其是體積比較小的定位器,待機(jī)時(shí)間短、充電頻繁、電池壽命短就是一個(gè)最大痛點(diǎn)。

而NB-IoT的出現(xiàn)解決了這一個(gè)難題。雖然NB-IoT僅僅支持慢速移動(dòng)設(shè)備、不支持語(yǔ)音。但是可以替代大部分的GSM/CDMA定位設(shè)備如人員、自行車等。

NB-IoT 發(fā)展?fàn)顟B(tài)

目前NB-IoT的狀態(tài)是:

  1. NB-IoT模塊僅支持UDP以及部分CoAP協(xié)議;
  2. NB-IoT模塊不支持D-TLS協(xié)議;
  3. 中國(guó)電信運(yùn)營(yíng)商N(yùn)B-SIM卡采用定向綁定即白名單制度。

NB-IoT在澳大利亞等國(guó)也已經(jīng)落地,不知道他們的SIM卡是否會(huì)采用定向綁定。國(guó)內(nèi)外運(yùn)營(yíng)商的政策不知是否會(huì)有所不同?

這里,最讓人普通開發(fā)者撓頭的還是白名單制度。而且IP定向制度對(duì)于開發(fā)商和消費(fèi)者來(lái)說有一定的風(fēng)險(xiǎn)。

UDP協(xié)議

現(xiàn)有的服務(wù)器大多數(shù)都是TCP長(zhǎng)連接/HTTP短連接/MQTT等。針對(duì)UDP通訊,服務(wù)器端要做些修改,以支持UDP協(xié)議,同時(shí)增加對(duì)于CoAP/D-TLS協(xié)議的支持。由于UDP是大多數(shù)模塊的主要協(xié)議棧,而CoAP/D-TLS都是基于UDP的,理論上都是可以實(shí)現(xiàn)的。但是原來(lái)內(nèi)置的CoAP協(xié)議棧可能會(huì)被廢棄。同時(shí)D-TLS的版本比較多,可以先實(shí)現(xiàn)最簡(jiǎn)單的版本 RFC 6347。構(gòu)建私有的加密套件也是可行的。

NMEA報(bào)文轉(zhuǎn)發(fā)

UDP和TCP之間區(qū)別,決定了 ip:port 天生就不太穩(wěn)定,無(wú)法作為判斷依據(jù)。所以,GPS廣泛使用的NMEA可以通過一次格式轉(zhuǎn)換后轉(zhuǎn)發(fā)給服務(wù)器。不過要攜帶設(shè)備識(shí)別或Token,否則服務(wù)器無(wú)法判斷是來(lái)自哪臺(tái)設(shè)備的數(shù)據(jù)。

設(shè)備識(shí)別與授權(quán)

NB-IoT設(shè)備具備全球唯一的IMEI/IMSI號(hào)碼,都可以作為設(shè)備識(shí)別號(hào)。選擇哪一種直接和服務(wù)供應(yīng)商有關(guān)聯(lián),如果要占據(jù)SIM卡銷售,IMSI合理些,如果依賴于設(shè)備銷售,IMEI合理些。當(dāng)然也可以使用IMEI/IMSI的聯(lián)合識(shí)別。

當(dāng)設(shè)備上傳IMEI/IMSI號(hào)碼后,服務(wù)器可以下發(fā)Token,也可以在這個(gè)過程通過雙方的隨機(jī)數(shù)、登陸密鑰來(lái)產(chǎn)生AES密鑰。

通訊協(xié)議

設(shè)備與服務(wù)器之間數(shù)據(jù)傳輸種類有:

  1. AUTH,登錄服務(wù)器、獲得Token、AES密鑰、初始矢量
  2. EVENT,異步事件推送到服務(wù)器
  3. STREAM,時(shí)序信號(hào)數(shù)據(jù)流推送到服務(wù)器(TCP常用)
  4. COMMAND,服務(wù)器異步下發(fā)指令
  5. CONFIG,服務(wù)器異步下發(fā)配置
  6. STATUS,服務(wù)器異步讀取設(shè)備配置

例如,GPS的每隔一段時(shí)間發(fā)送位置信息,或者移動(dòng)若干位置后推送信息,這可以使用EVENT推送。溫度、速度等時(shí)序信號(hào)可以通過STREAM推送到服務(wù)器。而推送時(shí)間配置等可以通過CONFIG下發(fā),通過STATUS讀取。

以下是我設(shè)計(jì)的一個(gè)簡(jiǎn)單的UDP測(cè)試服務(wù)器和客戶端,基于Twisted。

UDP Server

#!/usr/bin/env python
#coding: utf-8

from __future__ import print_function

import getopt
import os
import sys
import string
import struct
import binascii
import time
import uuid
import hashlib
import json

from twisted.internet.protocol import DatagramProtocol
from twisted.internet import defer
from twisted.internet import protocol, reactor
from twisted.python import log
from twisted.enterprise import adbapi


import txredisapi as redis

__version__ = "0.1"
__author__ = "allankliu@163.com"

'''
GpsPro

- [GPS - NMEA sentence information](http://aprs.gids.nl/nmea/)

   $GPBOD - Bearing, origin to destination
   $GPBWC - Bearing and distance to waypoint, great circle
   ** $GPGGA - Global Positioning System Fix Data
   $GPGLL - Geographic position, latitude / longitude
   $GPGSA - GPS DOP and active satellites 
   $GPGSV - GPS Satellites in view
   $GPHDT - Heading, True
   $GPR00 - List of waypoints in currently active route
   $GPRMA - Recommended minimum specific Loran-C data
   $GPRMB - Recommended minimum navigation info
   ** $GPRMC - Recommended minimum specific GPS/Transit data
   $GPRTE - Routes
   $GPTRF - Transit Fix Data
   $GPSTN - Multiple Data ID
   $GPVBW - Dual Ground / Water Speed
   $GPVTG - Track made good and ground speed
   $GPWPL - Waypoint location
   $GPXTE - Cross-track error, Measured
   $GPZDA - Date & Time

   $GPEX1 - Custom Extension 1
   $GPEX2 - Custom Extension 2

'''
class GpsPro(DatagramProtocol):

    lat = None
    lng = None
    alt = None
    heading = None
    speed = None
    ts = None
    imei = None
    imsi = None
    volt = None
    model = None
    snr = None
    token = None

    def datagramReceived(self, data, addr):
        print("[Peer]{}:{}".format(addr[0], addr[1]))
        if set(data).issubset(set(string.printable)):
            print("[Data][{}]{}".format(len(data), data))
        else:
            print("[Data][{}]{}".format(len(data), binascii.hexlify(data)))

        if data is None or len(data)==1:            
            resp = "NACK"
            self.transport.write(resp, addr)
            return

        if ',' not in data or '*' not in data:
            resp = "NACK:-1"
            self.transport.write(resp, addr)
            return

        if "AUTH" in data:
            resp = self.onAuth(data)
        elif "STAT" in data:
            resp = self.onStatus(data)
        elif "EVNT" in data:
            resp = self.onEvent(data)
        elif "STRM" in data:
            resp = self.onStream(data)
        elif "GPGGA" in data:
            resp = self.onGPGGA(data)
        elif "GPRMC" in data:
            resp = self.onGPRMC(data)
        else:
            resp = "NACK:-2"

        self.transport.write(resp, addr)


    def onHello(self, data):
        pass

    #AUTH,hhmmss.ss,hmac1,MyIMEI*hh
    #@defer.inlinecallbacks
    def onAuth(self, data):
        try:
            d,_ = data.split('*')
            _, ts, _id, _sign = d.split(',')
            self.token = uuid.uuid4()
            return "ACK:token,{}".format(self.token)
        except ValueError, e:
            return "NACK:{}".format(e)

    def onStatus(self, data):
        try:
            d,_ = data.split('*')
            _id, _hash, _sign = d.split(',')
            return "ACK"
        except ValueError, e:
            return "NACK:{}".format(e)

    #@defer.inlinecallbacks
    def onEvent(self, data):
        try:
            d,_ = data.split('*')
            _id, _volt, _endure = d.split(',')
            return "ACK"
        except ValueError, e:
            return "NACK:{}".format(e)

    #@defer.inlinecallbacks
    def onStream(self, data):
        try:
            d,_ = data.split('*')
            f = d.split(',')
            _id, _volt, _endure = d.split(',')
            return "ACK"
        except ValueError, e:
            return "NACK:{}".format(e)

    #$GPGGA,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh
    #@defer.inlinecallbacks
    def onGPGGA(self, data):
        try:
            d,_ = data.split('*')
            #_, ts, lat, ns, lng, ew, _,_,_,_,_,_,_,_,_._,_ = d.split(',')
            f = d.split(',')
            #print("field size:{}".format(len(f)))

            return "ACK"
        except ValueError, e:
            return "NACK:{}".format(e)

    #$GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62
    #@defer.inlinecallbacks
    def onGPRMC(self, data):
        try:
            d,_ = data.split('*')
            #ts, lat, ns, lng, ew, speed, degree, _,_,_ = d.split(',')
            f = d.split(',')
            #print("field size:{}".format(len(f)))

            return "ACK"
        except ValueError, e:
            return "NACK:{}".format(e)

class GpsProFactory(protocol.Factory):
    def buildProtocol(self, addr):
        return GpsPro()

def main():
    log.startLogging(sys.stdout)
    reactor.listenUDP(5863, GpsPro())
    reactor.run()

if __name__ == "__main__":
    main()

Twisted UDP Server和TCP Server最大區(qū)別之一:沒有Factory,沒有Connection。因?yàn)閁DP的特性,許多協(xié)議相關(guān)的流控等都需要自己實(shí)現(xiàn)。UDP連接不穩(wěn)定,NB-IoT設(shè)備只能夠保證20秒內(nèi) ip:port 是有效的返回路徑。這都需要額外的設(shè)計(jì)。

就慢速報(bào)文來(lái)說,大多數(shù)情況都是GPS上報(bào)報(bào)文,丟失也無(wú)所謂,也可以無(wú)需數(shù)據(jù)應(yīng)答。但是Auth/Command/Config/Status卻都需要應(yīng)答和重傳機(jī)制。

該代碼還需要對(duì)接MySQL數(shù)據(jù)庫(kù)和Redis隊(duì)列服務(wù),才能夠構(gòu)成一個(gè)實(shí)際應(yīng)用。

此外,由于NB-IoT Modem接口特性,串口間收發(fā)的已經(jīng)采用Hex字符串了。例如:

“Hello” -> “48656C6C6F”
“0A0B0C0D” -> “3041304230433044”

相比之下,用Hex字符串傳輸比較浪費(fèi)資源,等于是被轉(zhuǎn)換了兩次。所以二進(jìn)制數(shù)據(jù)可以直接轉(zhuǎn)發(fā):

"\x0A\x0B\x0C\x0D" -> "0A0B0C0D"

使用二進(jìn)制協(xié)議,在接收端使用struct分解參數(shù)也很容易。

UDP Client

#!/usr/bin/env python

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

from __future__ import print_function

import time
import hashlib
import uuid
from datetime import datetime

from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor

class EchoClientDatagramProtocol(DatagramProtocol):
    ts = str(int(time.time()))
    imei = "353070060339633" # My GSM phone IMEI as a demo
    md5 = hashlib.md5()
    md5.update(ts)
    md5.update(imei)
    sign = md5.hexdigest().upper()
    auth = "AUTH,{},{},{}*hh".format(ts,imei,sign)

    strings = [
        auth,
        "$GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62",
        "$GPGGA,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh"
    ]
    
    def startProtocol(self):
        #self.transport.connect('127.0.0.1', 8000)
        self.transport.connect('127.0.0.1', 5863)
        self.sendDatagram()
    
    def sendDatagram(self):
        if len(self.strings):
            datagram = self.strings.pop(0)
            self.transport.write(datagram)
        else:
            reactor.stop()

    def datagramReceived(self, datagram, host):
        print('Datagram received: ', repr(datagram))
        self.sendDatagram()

    def onCommand(self, data):
        pass

    def onConfig(self, data):
        pass

def main():
    protocol = EchoClientDatagramProtocol()
    t = reactor.listenUDP(0, protocol)
    reactor.run()

if __name__ == '__main__':
    main()

UDP客戶端和UDP服務(wù)器幾乎是對(duì)稱的設(shè)計(jì)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容