合宙Air780EP模組LuatOS指令碼開發MQTT應用示例

合宙LuatOS發表於2024-08-16

本文詳細講解了基於合宙Air780EP模組LuatOS開發的多個MQTT應用示例。

本文同樣適用於合宙的以下型號:
Air780EPA/Air780EPT/Air780EPS
Air780E/Air780EX/Air201…

一、相關準備工作

1.1 硬體準備

合宙EVB_Air780EP開發板一套,包括天線、SIM卡;

USB線

PC電腦

1.2 軟體準備

登入合宙官方IoT後臺:
https://iot.openluat.com/

LuatOS韌體下載:
https://gitee.com/openLuat/LuatOS/releases

DEMO原始碼下載:
https://gitee.com/openLuat/LuatOS/tree/master/demo/mqtt

注:進入LuatOS韌體頁面按下Ctrl+F,搜尋Air780EP即可找到對應的韌體。

推薦選用:相關型號韌體名稱後面數字版本號最高的最新relase版本進行除錯。

二、名詞解析及API說明

2.1 名詞解析

user_name使用者名稱
password密碼
client_id使用者標識

MQTT可以透過前兩個引數保證連線的安全,透過client_id確保裝置唯一性。

QOS訊息質量

分為0/1/2三個等級,分別表示只發一次,至少收到一次和只收到一次。透過QOS可以根據資料的重要性靈活,選擇傳送方式以節省頻寬和保證資料可靠。

關於publish時QOS值的說明:
QOS為0時,壓入底層TCP傳送堆疊,視為成功;

QOS為1時,收到伺服器回應PUBACK,視為成功;
QOS為2時,收到伺服器響應PUBREC,立即上行PUBCOMP壓入TCP傳送佇列,視為成功。

mqtt_host主機伺服器地址
mqtt_port埠

MQTT需要連線的伺服器引數。
mqtt_host可以說是ip或者域名。

topic主題

根據主題區別訊息型別和來源,主要用來分類資料。同時mqtt是釋出訂閱模型,topic是釋出和訂閱者通訊的重要通道。

payload訊息內容

釋出和訂閱的具體資料。

retain保留訊息

保留訊息是一條將保留標誌(retained flag)置為true的普通MQTT訊息。broker會針對主題依照QoS級別保留最後一條保留訊息,當訂閱者訂閱主題時會立即收到保留訊息。broker僅為每個主題保留一條保留訊息。

2.2 API說明

三、MQTT實現流程

3.1 建立一個TASK協程
介面:
sys.taskInit(func, arg1, arg2, argN)

引數:


返回值:

3.2 等待網路就緒

3.3 建立一個mqtt例項

介面:

mqtt.create(adapter,host,port,ssl,isipv6)

引數:

返回值:

3.4 連線伺服器

介面:
mqttc:connect()

引數:

返回值:

注意:本函式僅代表發起成功, 後續仍需根據ready函式判斷mqtt是否連線正常。

3.5 訂閱主題

介面:

mqttc:subscribe(topic, qos)

引數:

返回值:

3.6 釋出訊息

介面:

mqttc:publish(topic, data, qos, retain)

引數:

返回值:

3.7 接收訊息

3.8 斷開伺服器的連線

介面:mqttc:disconnect()

引數:無

返回值:

四、MQTT示例說明

本文以demo_lua\LuatOS\demo\mqtt這個demo為例作為演示,以MQTTX工具來測試MQTT執行狀況。

**4.1 在MQTTX軟體上建立連線 **

4.2 在MQTTX軟體上新增訂閱

注意:
訂閱主題格式要求預設為 /luatos/pub/ 加模組的IMEI號。
例如:/luatos/pub/868488076506128

4.3 設定MQTTX軟體上釋出訊息的主題

注意:
格式要求預設為 /luatos/sub/ 加模組的IMEI號。
例如:/luatos/sub/868488076506128

4.4 燒錄指令碼

上電開機後模組會自動向伺服器傳送訊息,這是程式碼預設設定的,若不需要可以刪掉。

4.5 模組向伺服器傳送訊息

透過Luatools軟體可以看到:

Luatools軟體下載:
https://wiki.luatos.com/pages/tools.html

五、MQTT單連結示例

5.1 修改指定引數

在程式碼開頭,可根據自己的伺服器修改指定的引數。
注意:
user_name、password在有些伺服器上是可以不傳入的,或者是對傳入的值沒有要求限制;要根據實際伺服器要求來填寫。

--根據自己的伺服器修改以下引數
local mqtt_host = "lbsmqtt.airm2m.com"
local mqtt_port = 1884
local mqtt_isssl = false
local client_id = "abc"
local user_name = "user"
local password = "password"
local pub_topic = "/luatos/pub/" .. (mcu.unique_id():toHex())
-- 該變數在下方程式碼有重新賦值,根據實際應用場景,可自行修改指令碼邏輯
local sub_topic = "/luatos/sub/" ..  (mcu.unique_id():toHex())
-- 該變數在下方程式碼有重新賦值,根據實際應用場景,可自行修改指令碼邏輯

5.2 連線/訂閱/釋出流程

此task實現的是MQTT的連線、訂閱訊息、釋出訊息的流程。
注意:
要先等待網路就緒之後,才可進行MQTT後續操作。
待網路就緒之後,根據程式碼編寫情況此時client_id、pub_topic和sub_topic會發生變化,會覆蓋掉程式碼開頭部分時的配置,這點需要注意;device_id為模組的IMEI號。
示例原始碼下載:
https://gitee.com/openLuat/LuatOS/tree/master/demo/mqtt

sys.taskInit(function()
    -- 等待聯網
    local ret, device_id = sys.waitUntil("net_ready")
    -- 下面的是mqtt的引數均可自行修改
    -- client_id = device_id
    pub_topic = "/luatos/pub/" .. device_id
    sub_topic = "/luatos/sub/" .. device_id

    -- 列印一下上報(pub)和下發(sub)的topic名稱
    -- 上報: 裝置 ---> 伺服器
    -- 下發: 裝置 <--- 伺服器
    -- 可使用mqtt.x等客戶端進行除錯
    log.info("mqtt", "pub", pub_topic)
    log.info("mqtt", "sub", sub_topic)

    -- 列印一下支援的加密套件, 通常來說, 韌體已包含常見的99%的加密套件
    -- if crypto.cipher_suites then
    --     log.info("cipher", "suites", json.encode(crypto.cipher_suites()))
    -- end
    if mqtt == nil then
        while 1 do
            sys.wait(1000)
            log.info("bsp", "本bsp未適配mqtt庫, 請查證")
        end
    end

    -------------------------------------
    -------- MQTT 演示程式碼 --------------
    -------------------------------------

    mqttc = mqtt.create(nil, mqtt_host, mqtt_port, mqtt_isssl, ca_file)

    mqttc:auth(client_id,user_name,password) -- client_id必填,其餘選填
    -- mqttc:keepalive(240) -- 預設值240s
    mqttc:autoreconn(true, 3000) -- 自動重連機制

    mqttc:on(function(mqtt_client, event, data, payload)
        -- 使用者自定義程式碼
        log.info("mqtt", "event", event, mqtt_client, data, payload)
        if event == "conack" then
            -- 聯上了
            sys.publish("mqtt_conack")
            mqtt_client:subscribe(sub_topic)--單主題訂閱
            -- mqtt_client:subscribe({[topic1]=1,[topic2]=1,[topic3]=1})--多主題訂閱
        elseif event == "recv" then
            log.info("mqtt", "downlink", "topic", data, "payload", payload)
            sys.publish("mqtt_payload", data, payload)
        elseif event == "sent" then
            -- log.info("mqtt", "sent", "pkgid", data)
        -- elseif event == "disconnect" then
            -- 非自動重連時,按需重啟mqttc
            -- mqtt_client:connect()
        end
    end)

    -- mqttc自動處理重連, 除非自行關閉
    mqttc:connect()
	sys.waitUntil("mqtt_conack")
    while true do
        -- 演示等待其他task傳送過來的上報資訊
        local ret, topic, data, qos = sys.waitUntil("mqtt_pub", 300000)
        if ret then
            -- 提供關閉本while迴圈的途徑, 不需要可以註釋掉
            if topic == "close" then break end
            mqttc:publish(topic, data, qos)
        end
        -- 如果沒有其他task上報, 可以寫個空等待
        --sys.wait(60000000)
    end
    mqttc:close()
    mqttc = nil
end)

5.3 傳送資料

此task的功能為模組每3秒向伺服器傳送一次資料。

-- 這裡演示在另一個task裡上報資料, 會定時上報資料,不需要就註釋掉
sys.taskInit(function()
    sys.wait(3000)
	local data = "123,"
	local qos = 1 -- QOS0不帶puback, QOS1是帶puback的
    while true do
        sys.wait(3000)
        if mqttc and mqttc:ready() then
            local pkgid = mqttc:publish(pub_topic, data .. os.date(), qos)
            -- local pkgid = mqttc:publish(topic2, data, qos)
            -- local pkgid = mqttc:publish(topic3, data, qos)
        end
    end
end)

5.4 mqtt-uart透傳

此程式碼可實現mqtt-uart透傳,利用串列埠工具給伺服器發訊息或者接收來著伺服器的訊息。
注意:
要使用串列埠1,且波特率為9600。

-- 以下是演示與uart結合, 簡單的mqtt-uart透傳實現,不需要就註釋掉
local uart_id = 1
uart.setup(uart_id, 9600)
uart.on(uart_id, "receive", function(id, len)
    local data = ""
    while 1 do
        local tmp = uart.read(uart_id)
        if not tmp or #tmp == 0 then
            break
        end
        data = data .. tmp
    end
    log.info("uart", "uart收到資料長度", #data)
    sys.publish("mqtt_pub", pub_topic, data)
end)
sys.subscribe("mqtt_payload", function(topic, payload)
    log.info("uart", "uart傳送資料長度", #payload)
    uart.write(1, payload)
end)

六、MQTT多連結示例

多連結的核心,就是要建立兩個mqtt客戶端的物件,透過不同的變數控制,程式碼部分如下。

示例原始碼下載:
https://gitee.com/openLuat/LuatOS/tree/master/demo/mqtt

    --------------------第一個mqtt客戶端--------------------
    mqttc1 = mqtt.create(nil, mqtt_host, mqtt_port, mqtt_isssl, ca_file)    -- 建立的第一個mqtt物件
    mqttc1:auth(client1_id,user_name,password) -- client_id必填,其餘選填
    -- mqttc1:keepalive(240) -- 預設值240s
    mqttc1:autoreconn(true, 3000) -- 自動重連機制
    mqttc1:on(function(mqtt_client, event, data, payload)
        -- 使用者自定義程式碼
        log.info("mqtt", "event", event, mqtt_client, data, payload)
        if event == "conack" then
            -- 聯上了
            sys.publish("mqtt_conack")
            mqtt_client:subscribe(sub_topic_client)--單主題訂閱
            -- mqtt_client:subscribe({[topic1]=1,[topic2]=1,[topic3]=1})--多主題訂閱
        elseif event == "recv" then
            -- 客戶端1 接收資料
            log.info("mqtt", "downlink", "topic", data, "payload", payload)
            sys.publish("mqtt_payload", data, payload)
        elseif event == "sent" then
            -- log.info("mqtt", "sent", "pkgid", data)
        -- elseif event == "disconnect" then
            -- 非自動重連時,按需重啟mqttc
            -- mqtt_client:connect()
        end~
    end)

    --------------------第二個mqtt客戶端--------------------
    mqttc2 = mqtt.create(nil, mqtt_host, mqtt_port, mqtt_isssl, ca_file)    -- 建立的第二個mqtt物件
    mqttc2:auth(client2_id,user_name,password) -- client_id必填,其餘選填
    -- mqttc2:keepalive(240) -- 預設值240s
    mqttc2:autoreconn(true, 3000) -- 自動重連機制
    mqttc2:on(function(mqtt_client, event, data, payload)
        -- 使用者自定義程式碼
        log.info("mqtt", "event", event, mqtt_client, data, payload)
        if event == "conack" then
            -- 聯上了
            sys.publish("mqtt_conack")
            
            mqtt_client:subscribe(sub_topic_client)     -- 主題訂閱 -> 訂閱主題可以額外自定義
            -- mqtt_client:subscribe({[topic1]=1,[topic2]=1,[topic3]=1})--多主題訂閱
        elseif event == "recv" then
            -- 客戶端2 接收資料
            log.info("mqtt", "downlink", "topic", data, "payload", payload)
            sys.publish("mqtt_payload", data, payload)
        elseif event == "sent" then
            -- log.info("mqtt", "sent", "pkgid", data)
        -- elseif event == "disconnect" then
            -- 非自動重連時,按需重啟mqttc
            -- mqtt_client:connect()
        end
    end)

    -- 客戶端1 傳送資料
    mqttc1:publish(topic, data, qos)    -- 釋出topic主題可以自定義,可以不相同
    -- 客戶端2 傳送資料
    mqttc2:publish(topic, data, qos)    -- 釋出topic主題可以自定義,可以不相同

七、MQTT SSL不帶證書連線示例

示例原始碼下載:

https://gitee.com/openLuat/LuatOS/tree/master/demo/mqtt

       local mqtt_host = "broker.emqx.io"
    local mqtt_port = 8883
    local mqtt_isssl = true -- 是否使用ssl> false 不加密 | true 無證書加密 | table 有證書加密
    local client_id = "abc"
    local user_name = "user"
    local password = "password"

    local pub_topic = "/luatos/pub/"
    local sub_topic = "/luatos/sub/"

    -- 統一聯網函式
    sys.taskInit(function()
        local device_id = mcu.unique_id():toHex()
        -----------------------------
        -- 統一聯網函式, 可自行刪減
        ----------------------------
        if wlan and wlan.connect then
            -- wifi 聯網, ESP32系列均支援
            local ssid = "luatos1234"
            local password = "12341234"
            log.info("wifi", ssid, password)
            -- TODO 改成自動配網
            -- LED = gpio.setup(12, 0, gpio.PULLUP)
            wlan.init()
            wlan.setMode(wlan.STATION) -- 預設也是這個模式,不呼叫也可以
            device_id = wlan.getMac()
            wlan.connect(ssid, password, 1)
        elseif mobile then
            -- Air780E/Air600E系列
            --mobile.simid(2) -- 自動切換SIM卡
            -- LED = gpio.setup(27, 0, gpio.PULLUP)
            device_id = mobile.imei()
        elseif w5500 then
            -- w5500 乙太網, 當前僅Air105支援
            w5500.init(spi.HSPI_0, 24000000, pin.PC14, pin.PC01, pin.PC00)
            w5500.config() --預設是DHCP模式
            w5500.bind(socket.ETH0)
            -- LED = gpio.setup(62, 0, gpio.PULLUP)
        elseif socket or mqtt then
            -- 適配的socket庫也OK
            -- 沒有其他操作, 單純給個註釋說明
        else
            -- 其他不認識的bsp, 迴圈提示一下吧
            while 1 do
                sys.wait(1000)
                log.info("bsp", "本bsp可能未適配網路層, 請查證")
            end
        end
        -- 預設都等到聯網成功
        sys.waitUntil("IP_READY")
        sys.publish("net_ready", device_id)
    end)

    sys.taskInit(function()
        -- 等待聯網
        local ret, device_id = sys.waitUntil("net_ready")
        -- 下面的是mqtt的引數均可自行修改
        client_id = device_id
        pub_topic = "/luatos/pub/" .. device_id
        sub_topic = "/luatos/sub/" .. device_id

        -- 列印一下上報(pub)和下發(sub)的topic名稱
        -- 上報: 裝置 ---> 伺服器
        -- 下發: 裝置 <--- 伺服器
        -- 可使用mqtt.x等客戶端進行除錯
        log.info("mqtt", "pub", pub_topic)
        log.info("mqtt", "sub", sub_topic)

        if mqtt == nil then
            while 1 do
                sys.wait(1000)
                log.info("bsp", "本bsp未適配mqtt庫, 請查證")
            end
        end

        -------------------------------------
        -------- MQTT 演示程式碼 --------------
        -------------------------------------
        mqttc = mqtt.create(nil, mqtt_host, mqtt_port, mqtt_isssl, ca_file)

        mqttc:auth(client_id,user_name,password) -- client_id必填,其餘選填
        -- mqttc:keepalive(240) -- 預設值240s
        mqttc:autoreconn(true, 3000) -- 自動重連機制

        mqttc:on(function(mqtt_client, event, data, payload)
            -- 使用者自定義程式碼
            log.info("mqtt", "event", event, mqtt_client, data, payload)
            if event == "conack" then
                -- 聯上了
                sys.publish("mqtt_conack")
                mqtt_client:subscribe(sub_topic)--單主題訂閱
                -- mqtt_client:subscribe({[topic1]=1,[topic2]=1,[topic3]=1})--多主題訂閱
            elseif event == "recv" then
                log.info("mqtt", "downlink", "topic", data, "payload", payload)
                sys.publish("mqtt_payload", data, payload)
            elseif event == "sent" then
                -- log.info("mqtt", "sent", "pkgid", data)
            -- elseif event == "disconnect" then
                -- 非自動重連時,按需重啟mqttc
                -- mqtt_client:connect()
            end
        end)

        -- mqttc自動處理重連, 除非自行關閉
        mqttc:connect()
        sys.waitUntil("mqtt_conack")
        while true do
            -- 演示等待其他task傳送過來的上報資訊
            local ret, topic, data, qos = sys.waitUntil("mqtt_pub", 300000)
            if ret then
                -- 提供關閉本while迴圈的途徑, 不需要可以註釋掉
                if topic == "close" then break end
                mqttc:publish(topic, data, qos)
            end
            -- 如果沒有其他task上報, 可以寫個空等待
            --sys.wait(60000000)
        end
        mqttc:close()
        mqttc = nil
    end)

八、MQTT SSL帶證書連線示例

示例原始碼下載:

https://gitee.com/openLuat/LuatOS/tree/master/demo/mqtt

    local mqtt_host = "broker.emqx.io"
    local mqtt_port = 8883
    local mqtt_isssl = {    -- 是否使用ssl> false 不加密 | true 無證書加密 | table 有證書加密
        server_cert = io.readFile("/luadb/broker.emqx.io-ca.crt"),  -- 把證書檔案作為指令碼檔案一起燒錄到模組內,就可以用/luadb/路徑直接讀取
        client_cert=nil,
        client_key=nil,
        client_password=nil,
    }
    
    local client_id = "abc"
    local user_name = "user"
    local password = "password"

    local pub_topic = "/luatos/pub/"
    local sub_topic = "/luatos/sub/"

    -- 統一聯網函式
    sys.taskInit(function()
        local device_id = mcu.unique_id():toHex()
        -----------------------------
        -- 統一聯網函式, 可自行刪減
        ----------------------------
        if wlan and wlan.connect then
            -- wifi 聯網, ESP32系列均支援
            local ssid = "luatos1234"
            local password = "12341234"
            log.info("wifi", ssid, password)
            -- TODO 改成自動配網
            -- LED = gpio.setup(12, 0, gpio.PULLUP)
            wlan.init()
            wlan.setMode(wlan.STATION) -- 預設也是這個模式,不呼叫也可以
            device_id = wlan.getMac()
            wlan.connect(ssid, password, 1)
        elseif mobile then
            -- Air780E/Air600E系列
            --mobile.simid(2) -- 自動切換SIM卡
            -- LED = gpio.setup(27, 0, gpio.PULLUP)
            device_id = mobile.imei()
        elseif w5500 then
            -- w5500 乙太網, 當前僅Air105支援
            w5500.init(spi.HSPI_0, 24000000, pin.PC14, pin.PC01, pin.PC00)
            w5500.config() --預設是DHCP模式
            w5500.bind(socket.ETH0)
            -- LED = gpio.setup(62, 0, gpio.PULLUP)
        elseif socket or mqtt then
            -- 適配的socket庫也OK
            -- 沒有其他操作, 單純給個註釋說明
        else
            -- 其他不認識的bsp, 迴圈提示一下吧
            while 1 do
                sys.wait(1000)
                log.info("bsp", "本bsp可能未適配網路層, 請查證")
            end
        end
        -- 預設都等到聯網成功
        sys.waitUntil("IP_READY")
        sys.publish("net_ready", device_id)
    end)

    sys.taskInit(function()
        -- 等待聯網
        local ret, device_id = sys.waitUntil("net_ready")
        -- 下面的是mqtt的引數均可自行修改
        client_id = device_id
        pub_topic = "/luatos/pub/" .. device_id
        sub_topic = "/luatos/sub/" .. device_id

        -- 列印一下上報(pub)和下發(sub)的topic名稱
        -- 上報: 裝置 ---> 伺服器
        -- 下發: 裝置 <--- 伺服器
        -- 可使用mqtt.x等客戶端進行除錯
        log.info("mqtt", "pub", pub_topic)
        log.info("mqtt", "sub", sub_topic)

        if mqtt == nil then
            while 1 do
                sys.wait(1000)
                log.info("bsp", "本bsp未適配mqtt庫, 請查證")
            end
        end

        -------------------------------------
        -------- MQTT 演示程式碼 --------------
        -------------------------------------
        mqttc = mqtt.create(nil, mqtt_host, mqtt_port, mqtt_isssl, ca_file)

        mqttc:auth(client_id,user_name,password) -- client_id必填,其餘選填
        -- mqttc:keepalive(240) -- 預設值240s
        mqttc:autoreconn(true, 3000) -- 自動重連機制

        mqttc:on(function(mqtt_client, event, data, payload)
            -- 使用者自定義程式碼
            log.info("mqtt", "event", event, mqtt_client, data, payload)
            if event == "conack" then
                -- 聯上了
                sys.publish("mqtt_conack")
                mqtt_client:subscribe(sub_topic)--單主題訂閱
                -- mqtt_client:subscribe({[topic1]=1,[topic2]=1,[topic3]=1})--多主題訂閱
            elseif event == "recv" then
                log.info("mqtt", "downlink", "topic", data, "payload", payload)
                sys.publish("mqtt_payload", data, payload)
            elseif event == "sent" then
                -- log.info("mqtt", "sent", "pkgid", data)
            -- elseif event == "disconnect" then
                -- 非自動重連時,按需重啟mqttc
                -- mqtt_client:connect()
            end
        end)

        -- mqttc自動處理重連, 除非自行關閉
        mqttc:connect()
        sys.waitUntil("mqtt_conack")
        while true do
            -- 演示等待其他task傳送過來的上報資訊
            local ret, topic, data, qos = sys.waitUntil("mqtt_pub", 300000)
            if ret then
                -- 提供關閉本while迴圈的途徑, 不需要可以註釋掉
                if topic == "close" then break end
                mqttc:publish(topic, data, qos)
            end
            -- 如果沒有其他task上報, 可以寫個空等待
            --sys.wait(60000000)
        end
        mqttc:close()
        mqttc = nil
    end)

九、常見問題Q&A

以下針對客戶朋友們實際應用中的反饋,整理了MQTT應用中的常見問題:

  1. 模組支援MQTT最新的版本是多少?
    支援MQTT_V3.1、MQTT_V3.1.1版本。

  2. 模組最多支援幾路連結?
    MQTT/TCP/UDP的連結公用8路通道。

  3. client id是否允許重複?
    client id不允許重複,要保證唯一性,如果在已經有一個該id的連結情況下,另外一個裝置也用了相同的clientid接入,會把另一個在連連結擠掉。

  4. Qos0、Qos1、Qos2如何選擇?
    應用允許的情況下,建議使用Qos0;Qos1和Qos2會加重網路負擔,4G網路還好,特別是2G網路,在網路擁堵和較差的情況下,資料傳輸的次數越多,掉線的機率就會越高。

  5. 為什麼mqtt ssl訪問失敗?
    檢查伺服器是否支援模組支援的加密套件,用模組程式中的三元組和證書,在mqttx或者mqttfx客戶端工具上嘗試是否可以成功連結。

  6. 排查是否裝置單體問題:
    如果同一地點,某些裝置正常,某些裝置異常,按照如下幾種情況分析。
    01)分析正常裝置和異常裝置的使用環境是否相同:
    如果不同,例如異常裝置固定在鋼製牆壁上,正常裝置放置在桌子上,鋼製牆壁可能對天線射頻有干擾,將異常裝置和正常裝置放置在同樣的使用環境中,再對比測試。
    如果相同,參考下方第2)步。
    2)分析正常和異常的裝置,駐留的小區是否相同:
    如果相同,重點排查異常裝置的天線射頻部分。
    如同不同,多測試幾次,確認下,是不是在異常小區內很容易出問題,如果異常小區很容易出問題,可能就是小區擁堵造成的。

相關文章