最近更新: 2021-09-12

MQTT用戶端入門 - 六、透過NB-IoT電信模組發送MQTT訊息,以Python和PHP為例

NB-IoT 或 LTE-M 都是 3GPP 電信組織針對 IoT 應用制訂的低功耗廣域無線電技術(LPWAN)。NB-IoT 資料透過 LTE/4G 電信網路傳遞。簡單說就是和 LTE/4G 共用基地台。你的手機收得到 LTE/4G 訊號的地方,你的 IoT 裝置就能使用 NB-IoT 電信模組發送資料。而 NB-IoT 模組也可以直接插一般 LTE/4G SIM 卡使用,只是資費比 NB-IoT 貴。

我在前公司接觸過兩家廠商的 NB-IoT 模組,一為 u-blox 的 SARA-N/SARA-R4系列,另一為 SIMCOM 的 SIM7000系列。因為是公司產品,不是我的著作權,所以不能公開程式碼。但可以說使用經驗,以及如何透過 NB-IoT 模組的 AT 指令發送原始的 MQTT 封包。

就價位上來說,SIMCOM SIM7000系列 最便宜。但功能和穩定度,還是 u-blox SARA-R4系列 比較好。一分錢一分貨。

設計程式時,我認為 u-blox 規劃的 AT 指令規律整齊, SIM7000 的 AT 指令就有些混亂了。

更進一步閱讀這兩家廠商的產品規格表,我們看到他們的產品價格是按功能豐富程度而定。最貴的產品提供 HTTP, MQTT 等協定的 AT 指令,我們可以用 AT 指令送出 HTTP 或 MQTT 資料。但最便宜的產品,只提供最基本的 TCP 支援,我們需要自己打包 HTTP 或 MQTT 資料。如何因應後者的狀況,就是本文的主要內容。

先看下圖的軟體技術堆疊。

網路傳輸的軟體技術堆疊

左側是我們在一般作業系統下的工作情形,作業系統的硬體驅動程式和 Socket API 提供一致的網路傳輸介面。程式設計者在這之上設計各種網路協定的函數庫。最後我們直接利用這些現成的函數庫傳送資料。

但在 NB-IoT 情境,我們的 IoT 裝置通常沒有作業系統,例如 Arduino 微控制器。就算有作業系統,例如 Raspberry Pi 跑 Rasbian 系統時,我們使用的 NB-IoT 模組也沒有驅動程式。沒有驅動程式,自然疊在它上面的 Socket API 和現成函數庫就通通不能用。所以在 NB-IoT 情境,我們只能走右側的方式,直接透過 NB-IoT 模組提供的 AT 指令發送資料封包。

只是各種模組的 AT 指令並不統一,這要自己看廠商提供的 AT 指令手冊。如果 NB-IoT 模組支援高階應用協定 (HTTP,MQTT) 的 AT 指令,那當然是直接用。如果不支援 HTTP, MQTT 這些應用協定,那就要先用 TCP 連接的 AT 指令,再用傳送資料的 AT 指令。

SARA-R4系列的 TCP/Internet 連接指令範例:


serial_port.write(b'AT+USOCR=6') # 6 = TCP

resp = serial_port.read()
# +USOCR: 2
# OK

# socket id is 2

serial_port.write(b'AT+USOCO=2,"203.75.129.103",1833')
resp = serial_port.read()
# OK

SIM7000系列的 TCP/Internet 連接指令範例:


serial_port.write(b'AT+CIPSTART="TCP","203.75.129.103",1883')
resp = serial_port.read()
# CONNECT OK

注意,有些 NB-IoT 模組不支援 DNS 查詢。此類模組指定目的地時,只能用 IP ,不能用網域名稱。

透過上述的 AT 連接指令,NB-IoT 模組就處於 TCP/Internet 可用狀態。接著我們再用 AT 傳送指令送出我們打包的資料。

SARA-R4系列的 TCP/Internet 資料封傳送指令範例:


# if socket_id is 2
serial_port.write(b'AT+USOWR=2,5,"Hello"')

resp = serial_port.read()
# +USOWR: 2,5
# OK

SIM7000系列的 TCP/Internet 資料封傳送指令範例:


serial_port.write(b'AT+CIPSEND=5')
resp = serial_port.read()
# SEND OK

serial_port.write(b'hello')
serial_port.write(b'\x1A') # Ctrl+Z to send

上述範例只是往遠端送出一個 ‘hello’ 字串。正式場合,我們要按網路協定規定的格式打包資料。

打包 MQTT publish 資料封包的範例,我已經放在 rocksources@github 。請看下列兩個連結的內容:

刪減過的 MQTT publish 封包範例如下。QoS 為 0 ,不含 MQTT 伺服器身份認證。


def mqtt_publish(conn, topic, message, client_id):
    def str_content(binstr):
        slen = len(binstr)
        msb = slen >> 8
        lsb = slen & 0xff
        return bytearray((msb, lsb)) + binstr

    # MQTT CONNECT
    variable_header = bytearray(b"\x00\x04MQTT\x04\xC2\x00\x0A")
    variable_header += str_content(client_id.encode())
    fixed_header = bytearray((0x10, len(variable_header)))

    conn.send(fixed_header + variable_header):

    # MQTT CONNACK
    rsp = conn.readline()

    # MQTT PUBLISH
    payload = bytearray(str_content(topic.encode())) + message.encode()

    payload_len = len(payload)
    digest = bytearray()
    while payload_len > 0:
        digit = payload_len % 128
        payload_len >>= 7
        if payload_len > 0:
            digit |= 0x80
        digest.append(digit)

    cmd = 0x30 # qos is 0
    fixed_header = bytearray(chr(cmd).encode()) + digest

    conn.send(fixed_header + payload):

MQTT用戶端入門系列文章