美文网首页LoRa相关LPWAN设计
从网关代码反推TTN网络服务

从网关代码反推TTN网络服务

作者: 小鱼儿他老汉 | 来源:发表于2018-09-24 07:45 被阅读38次

首先Github上找到PyCom的NanoGateway代码,改代码运行在PyCom的ESP32+SX127X的硬件上。自己拼凑一下也可运行。从改代码可以学到一些基本的IoT相关Python编程,包括驱动、线程管理、网络通讯、配置管理等,也可以反推出其与云端连接的接口。

TTN

TTN是The Things Network的缩写,虽然是一家设备云的供应商,却在LoRaWAN业务上下过苦功。也提供SX130X/SX127X的网关和设备。但很不幸,其官方接入的套接字服务器和接入路由服务器在国内都无法访问。所以,在国内测试需要自己搭建服务器。好在也不是什么难事。我把自己的EPIC IoTHub加个UDP端口,也就可以解决了。

接口代码

以下是部分代码。

PROTOCOL_VERSION = const(2)

PUSH_DATA = const(0)
PUSH_ACK = const(1)
PULL_DATA = const(2)
PULL_ACK = const(4)
PULL_RESP = const(3)

TX_ERR_NONE = "NONE"
TX_ERR_TOO_LATE = "TOO_LATE"
TX_ERR_TOO_EARLY = "TOO_EARLY"
TX_ERR_COLLISION_PACKET = "COLLISION_PACKET"
TX_ERR_COLLISION_BEACON = "COLLISION_BEACON"
TX_ERR_TX_FREQ = "TX_FREQ"
TX_ERR_TX_POWER = "TX_POWER"
TX_ERR_GPS_UNLOCKED = "GPS_UNLOCKED"

STAT_PK = {"stat": {"time": "", "lati": 0,
                   "long": 0, "alti": 0,
                   "rxnb": 0, "rxok": 0,
                   "rxfw": 0, "ackr": 100.0,
                   "dwnb": 0, "txnb": 0}}
RX_PK = {"rxpk": [{"time": "", "tmst": 0,
                  "chan": 0, "rfch": 0,
                  "freq": 868.1, "stat": 1,
                  "modu": "LORA", "datr": "SF7BW125",
                  "codr": "4/5", "rssi": 0,
                  "lsnr": 0, "size": 0,
                  "data": ""}]}
TX_ACK_PK = {"txpk_ack":{"error":""}}

上面代码是一些常量,用于UDP接口中。

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return json.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4], rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["datr"] = self._sf_to_dr(sf)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = float(snr)
        RX_PK["rxpk"][0]["data"] = binascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return json.dumps(RX_PK)

    def _push_data(self, data):
        token = os.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PUSH_DATA]) + binascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PUSH exception")
    
    def _pull_data(self):
        token = os.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_DATA]) + binascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PULL exception")

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = json.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_ACK]) + binascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PULL RSP ACK exception")

    def _lora_cb(self, lora):
        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            self._push_data(self._make_node_packet(rx_data, self.rtc.now(), stats.timestamp, stats.sf, stats.rssi, stats.snr))
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(mode=LoRa.LORA, frequency=self.frequency, bandwidth=LoRa.BW_125KHZ,
                     sf=self.sf, preamble=8, coding_rate=LoRa.CODING_4_5, tx_iq=True)

    def _send_down_link(self, data, tmst, datarate, frequency):
        self.lora.init(mode=LoRa.LORA, frequency=frequency, bandwidth=LoRa.BW_125KHZ,
                      sf=self._dr_to_sf(datarate), preamble=8, coding_rate=LoRa.CODING_4_5,
                      tx_iq=True)
        while time.ticks_us() < tmst:
            pass
        self.lora_sock.send(data)

    def _udp_thread(self):
        while True:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    print("Push ack")
                elif _type == PULL_ACK:
                    print("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = json.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - time.ticks_us() - 5000
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(handler=lambda x: self._send_down_link(binascii.a2b_base64(tx_pk["txpk"]["data"]),
                                                                                              tx_pk["txpk"]["tmst"] - 10, tx_pk["txpk"]["datr"],
                                                                                              int(tx_pk["txpk"]["freq"] * 1000000)), us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        print("Downlink timestamp error!, t_us:", t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    print("Pull rsp")
            except socket.timeout:
                pass
            except OSError as e:
                if e.errno == errno.EAGAIN:
                    pass
                else:
                    print("UDP recv OSError Exception")
            except Exception:
                print("UDP recv Exception")
            # Wait before trying to receive again
            time.sleep(0.025)

传输代码中,self.sock是TTN通讯,而self.lora_sock是与SX127X通讯,TTN库中也做为了一种套接字来通讯。不过我们暂时只需要了解前者。调用self.sock.sendto()的有三个方法:

  • _push_data()
  • _pull_data()
  • _ack_pull_rsp()

其中,_push_data()在初始化__init__()/Timer.Alarm()回调匿名函数/_lora_cb()等处被调用。_pull_data()Timer.Alarm()中调用。而_ack_pull_rsp()仅在_udp_threading中调用。

二级制协议

从代码中可以看出其UDP通讯协议如下:

packet = bytes([PROTOCOL_VERSION]) + token + bytes([PUSH_DATA]) + binascii.unhexlify(self.id) + data
packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_DATA]) + binascii.unhexlify(self.id)
packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_ACK]) + binascii.unhexlify(self.id) + resp
data, src = self.sock.recvfrom(1024)后续的代码解析        

Semtech原版协议

在Github上可以找到Semtech原版协议

      ______                              _
     / _____)             _              | |    
    ( (____  _____ ____ _| |_ _____  ____| |__  
     \____ \| ___ |    (_   _) ___ |/ ___)  _ \ 
     _____) ) ____| | | || |_| ____( (___| | | |
    (______/|_____)_|_|_| \__)_____)\____)_| |_|
      (C)2013 Semtech-Cycleo

Basic communication protocol between Lora gateway and server
=============================================================


1. Introduction
----------------

The protocol between the gateway and the server is purposefully very basic and 
for demonstration purpose only, or for use on private and reliable networks.

There is no authentication of the gateway or the server, and the acknowledges 
are only used for network quality assessment, not to correct UDP datagrams 
losses (no retries).


2. System schematic and definitions
------------------------------------

     ((( Y )))
         |
         |
    + - -|- - - - - - - - - - - - - +        xxxxxxxxxxxx          +--------+
    | +--+-----------+     +------+ |       xx x  x     xxx        |        |
    | |              |     |      | |      xx  Internet  xx        |        |
    | | Concentrator |<--->| Host |<-------xx     or    xx-------->|        |
    | |              | SPI |      | |      xx  Intranet  xx        | Server |
    | +--------------+     +------+ |       xxxx   x   xxxx        |        |
    |    ^                     ^    |           xxxxxxxx           |        |
    |    | PPS +-------+ NMEA  |    |                              |        |
    |    +-----|  GPS  |-------+    |                              +--------+
    |          | (opt) |            |
    |          +-------+            |
    |                               |
    |             Gateway           |
    +- - - - - - - - - - - - - - - -+

__Concentrator__: radio RX/TX board, based on Semtech multichannel modems 
(SX130x), transceivers (SX135x) and/or low-power stand-alone modems (SX127x). 

__Host__: embedded computer on which the packet forwarder is run. Drives the 
concentrator through a SPI link.

__GPS__: GNSS (GPS, Galileo, GLONASS, etc) receiver with a "1 Pulse Per Second"
output and a serial link to the host to send NMEA frames containing time and
geographical coordinates data. Optional.

__Gateway__: a device composed of at least one radio concentrator, a host, some 
network connection to the internet or a private network (Ethernet, 3G, Wifi, 
microwave link), and optionally a GPS receiver for synchronization. 

__Server__: an abstract computer that will process the RF packets received and 
forwarded by the gateway, and issue RF packets in response that the gateway 
will have to emit.

It is assumed that the gateway can be behind a NAT or a firewall stopping any 
incoming connection.
It is assumed that the server has an static IP address (or an address solvable 
through a DNS service) and is able to receive incoming connections on a 
specific port.


3. Upstream protocol
---------------------

### 3.1. Sequence diagram ###

    +---------+                                                    +---------+
    | Gateway |                                                    | Server  |
    +---------+                                                    +---------+
         | -----------------------------------\                         |
         |-| When 1-N RF packets are received |                         |
         | ------------------------------------                         |
         |                                                              |
         | PUSH_DATA (token X, GW MAC, JSON payload)                    |
         |------------------------------------------------------------->|
         |                                                              |
         |                                           PUSH_ACK (token X) |
         |<-------------------------------------------------------------|
         |                              ------------------------------\ |
         |                              | process packets *after* ack |-|
         |                              ------------------------------- |
         |                                                              |

### 3.2. PUSH_DATA packet ###

That packet type is used by the gateway mainly to forward the RF packets 
received, and associated metadata, to the server.

 Bytes  | Function
:------:|---------------------------------------------------------------------
 0      | protocol version = 2
 1-2    | random token
 3      | PUSH_DATA identifier 0x00
 4-11   | Gateway unique identifier (MAC address)
 12-end | JSON object, starting with {, ending with }, see section 4

### 3.3. PUSH_ACK packet ###

That packet type is used by the server to acknowledge immediately all the 
PUSH_DATA packets received.

 Bytes  | Function
:------:|---------------------------------------------------------------------
 0      | protocol version = 2
 1-2    | same token as the PUSH_DATA packet to acknowledge
 3      | PUSH_ACK identifier 0x01


4. Upstream JSON data structure
--------------------------------

The root object can contain an array named "rxpk":

``` json
{
    "rxpk":[ {...}, ...]
}

That array contains at least one JSON object, each object contain a RF packet
and associated metadata with the following fields:

Name Type Function
time string UTC time of pkt RX, us precision, ISO 8601 'compact' format
tmst number Internal timestamp of "RX finished" event (32b unsigned)
freq number RX central frequency in MHz (unsigned float, Hz precision)
chan number Concentrator "IF" channel used for RX (unsigned integer)
rfch number Concentrator "RF chain" used for RX (unsigned integer)
stat number CRC status: 1 = OK, -1 = fail, 0 = no CRC
modu string Modulation identifier "LORA" or "FSK"
datr string LoRa datarate identifier (eg. SF12BW500)
datr number FSK datarate (unsigned, in bits per second)
codr string LoRa ECC coding rate identifier
rssi number RSSI in dBm (signed integer, 1 dB precision)
lsnr number Lora SNR ratio in dB (signed float, 0.1 dB precision)
size number RF packet payload size in bytes (unsigned integer)
data string Base64 encoded RF packet payload, padded

Example (white-spaces, indentation and newlines added for readability):

{"rxpk":[
    {
        "time":"2013-03-31T16:21:17.528002Z",
        "tmst":3512348611,
        "chan":2,
        "rfch":0,
        "freq":866.349812,
        "stat":1,
        "modu":"LORA",
        "datr":"SF7BW125",
        "codr":"4/6",
        "rssi":-35,
        "lsnr":5.1,
        "size":32,
        "data":"-DS4CGaDCdG+48eJNM3Vai-zDpsR71Pn9CPA9uCON84"
    },{
        "time":"2013-03-31T16:21:17.530974Z",
        "tmst":3512348514,
        "chan":9,
        "rfch":1,
        "freq":869.1,
        "stat":1,
        "modu":"FSK",
        "datr":50000,
        "rssi":-75,
        "size":16,
        "data":"VEVTVF9QQUNLRVRfMTIzNA=="
    },{
        "time":"2013-03-31T16:21:17.532038Z",
        "tmst":3316387610,
        "chan":0,
        "rfch":0,
        "freq":863.00981,
        "stat":1,
        "modu":"LORA",
        "datr":"SF10BW125",
        "codr":"4/7",
        "rssi":-38,
        "lsnr":5.5,
        "size":32,
        "data":"ysgRl452xNLep9S1NTIg2lomKDxUgn3DJ7DE+b00Ass"
    }
]}

The root object can also contain an object named "stat" :

{
    "rxpk":[ {...}, ...],
    "stat":{...}
}

It is possible for a packet to contain no "rxpk" array but a "stat" object.

{
    "stat":{...}
}

That object contains the status of the gateway, with the following fields:

Name Type Function
time string UTC 'system' time of the gateway, ISO 8601 'expanded' format
lati number GPS latitude of the gateway in degree (float, N is +)
long number GPS latitude of the gateway in degree (float, E is +)
alti number GPS altitude of the gateway in meter RX (integer)
rxnb number Number of radio packets received (unsigned integer)
rxok number Number of radio packets received with a valid PHY CRC
rxfw number Number of radio packets forwarded (unsigned integer)
ackr number Percentage of upstream datagrams that were acknowledged
dwnb number Number of downlink datagrams received (unsigned integer)
txnb number Number of packets emitted (unsigned integer)

Example (white-spaces, indentation and newlines added for readability):

{"stat":{
    "time":"2014-01-12 08:59:28 GMT",
    "lati":46.24000,
    "long":3.25230,
    "alti":145,
    "rxnb":2,
    "rxok":2,
    "rxfw":2,
    "ackr":100.0,
    "dwnb":2,
    "txnb":2
}}
  1. Downstream protocol

5.1. Sequence diagram

+---------+                                                    +---------+
| Gateway |                                                    | Server  |
+---------+                                                    +---------+
     | -----------------------------------\                         |
     |-| Every N seconds (keepalive time) |                         |
     | ------------------------------------                         |
     |                                                              |
     | PULL_DATA (token Y, MAC@)                                    |
     |------------------------------------------------------------->|
     |                                                              |
     |                                           PULL_ACK (token Y) |
     |<-------------------------------------------------------------|
     |                                                              |

+---------+                                                    +---------+
| Gateway |                                                    | Server  |
+---------+                                                    +---------+
     |      ------------------------------------------------------\ |
     |      | Anytime after first PULL_DATA for each packet to TX |-|
     |      ------------------------------------------------------- |
     |                                                              |
     |                            PULL_RESP (token Z, JSON payload) |
     |<-------------------------------------------------------------|
     |                                                              |
     | TX_ACK (token Z, JSON payload)                               |
     |------------------------------------------------------------->|

5.2. PULL_DATA packet

That packet type is used by the gateway to poll data from the server.

This data exchange is initialized by the gateway because it might be
impossible for the server to send packets to the gateway if the gateway is
behind a NAT.

When the gateway initialize the exchange, the network route towards the
server will open and will allow for packets to flow both directions.
The gateway must periodically send PULL_DATA packets to be sure the network
route stays open for the server to be used at any time.

Bytes Function
0 protocol version = 2
1-2 random token
3 PULL_DATA identifier 0x02
4-11 Gateway unique identifier (MAC address)

5.3. PULL_ACK packet

That packet type is used by the server to confirm that the network route is
open and that the server can send PULL_RESP packets at any time.

Bytes Function
0 protocol version = 2
1-2 same token as the PULL_DATA packet to acknowledge
3 PULL_ACK identifier 0x04

5.4. PULL_RESP packet

That packet type is used by the server to send RF packets and associated
metadata that will have to be emitted by the gateway.

Bytes Function
0 protocol version = 2
1-2 random token
3 PULL_RESP identifier 0x03
4-end JSON object, starting with {, ending with }, see section 6

5.5. TX_ACK packet

That packet type is used by the gateway to send a feedback to the server
to inform if a downlink request has been accepted or rejected by the gateway.
The datagram may optionnaly contain a JSON string to give more details on
acknoledge. If no JSON is present (empty string), this means than no error
occured.

Bytes Function
0 protocol version = 2
1-2 same token as the PULL_RESP packet to acknowledge
3 TX_ACK identifier 0x05
4-end [optional] JSON object, starting with {, ending with }, see section 6
  1. Downstream JSON data structure

The root object of PULL_RESP packet must contain an object named "txpk":

{
    "txpk": {...}
}

That object contain a RF packet to be emitted and associated metadata with the following fields:

Name Type Function
imme bool Send packet immediately (will ignore tmst & time)
tmst number Send packet on a certain timestamp value (will ignore time)
time string Send packet at a certain time (GPS synchronization required)
freq number TX central frequency in MHz (unsigned float, Hz precision)
rfch number Concentrator "RF chain" used for TX (unsigned integer)
powe number TX output power in dBm (unsigned integer, dBm precision)
modu string Modulation identifier "LORA" or "FSK"
datr string LoRa datarate identifier (eg. SF12BW500)
datr number FSK datarate (unsigned, in bits per second)
codr string LoRa ECC coding rate identifier
fdev number FSK frequency deviation (unsigned integer, in Hz)
ipol bool Lora modulation polarization inversion
prea number RF preamble size (unsigned integer)
size number RF packet payload size in bytes (unsigned integer)
data string Base64 encoded RF packet payload, padding optional
ncrc bool If true, disable the CRC of the physical layer (optional)

Most fields are optional.
If a field is omitted, default parameters will be used.

Examples (white-spaces, indentation and newlines added for readability):

{"txpk":{
    "imme":true,
    "freq":864.123456,
    "rfch":0,
    "powe":14,
    "modu":"LORA",
    "datr":"SF11BW125",
    "codr":"4/6",
    "ipol":false,
    "size":32,
    "data":"H3P3N2i9qc4yt7rK7ldqoeCVJGBybzPY5h1Dd7P7p8v"
}}
{"txpk":{
    "imme":true,
    "freq":861.3,
    "rfch":0,
    "powe":12,
    "modu":"FSK",
    "datr":50000,
    "fdev":3000,
    "size":32,
    "data":"H3P3N2i9qc4yt7rK7ldqoeCVJGBybzPY5h1Dd7P7p8v"
}}

The root object of TX_ACK packet must contain an object named "txpk_ack":

{
    "txpk_ack": {...}
}

That object contain status information concerning the associated PULL_RESP packet.

Name Type Function
error string Indication about success or type of failure that occured for downlink request.

The possible values of "error" field are:

Value Definition
NONE Packet has been programmed for downlink
TOO_LATE Rejected because it was already too late to program this packet for downlink
TOO_EARLY Rejected because downlink packet timestamp is too much in advance
COLLISION_PACKET Rejected because there was already a packet programmed in requested timeframe
COLLISION_BEACON Rejected because there was already a beacon planned in requested timeframe
TX_FREQ Rejected because requested frequency is not supported by TX RF chain
TX_POWER Rejected because requested power is not supported by gateway
GPS_UNLOCKED Rejected because GPS is unlocked, so GPS timestamp cannot be used

Examples (white-spaces, indentation and newlines added for readability):

{"txpk_ack":{
    "error":"COLLISION_PACKET"
}}
  1. Revisions

v1.3

  • Added downlink feedback from gateway to server (PULL_RESP -> TX_ACK)

v1.2

  • Added value of FSK bitrate for upstream.
  • Added parameters for FSK bitrate and frequency deviation for downstream.

v1.1

  • Added syntax for status report JSON object on upstream.

v1.0

  • Initial version.

## 总结

Semtech的packet forwarder是一个简单的报文转发协议,协议公开,也有许多参考代码。可以基于Node.js/Python/Java开发相应的套接字服务器。但是这种方式有些老旧,有若干缺点:

- UDP连接方式,下行控制较为复杂;
- 缺乏安全连接方式,如TLS;
- 服务器以明文ID方式来识别设备,很容易出现冒充的设备;
- 服务器每次收到ID和报文,都需要检查一次数据库来判断,增加了数据库压力。TCP长连接至少不需要反复认证。

建议改用长连接+AES方式,或者直接使用MQTT+TLS方式最直接。

最后,PyCom的代码中使用了三个线程:两个定时器,一个UDP接收线程。这在单通道服务器中还相对简单。如果是8通道则会比较复杂,至少UDP发送处的线程锁需要管理好。也可以通过一个通道一个进程的方式进行处理。

相关文章

网友评论

    本文标题:从网关代码反推TTN网络服务

    本文链接:https://www.haomeiwen.com/subject/wxdcoftx.html