本章描述如何在Ubuntu上通過MQTT topic和通過物模型的編程方式,上報和接收業務報文。這個環節使用Ubuntu主機類比IoT裝置,讓使用者體驗裝置如何與阿里雲物聯網平台串連和互動,基於Link SDK3.0.1進行編寫。
安裝本地開發環境
通過Ubuntu系統進行開發
安裝Ubuntu16.04
本文編寫是對照的編譯環境是64位主機上的
Ubuntu16.04
,在其它Linux版本上尚未驗證過,推薦安裝與阿里一致的發行版以避免碰到相容性方面的問題。然後安裝64位的Desktop版本的
Ubuntu 16.04.x LTS
,下載地址:http://releases.ubuntu.com/16.04。如果您使用
Windows
作業系統,可以安裝虛擬機器軟體Virtualbox
獲得Linux開發環境,下載地址:https://www.virtualbox.org/wiki/Downloads。安裝必備軟體
本SDK的開發編譯環境使用如下軟體:
make-4.1
,git-2.7.4
,gcc-5.4.0
,gcov-5.4.0
,lcov-1.12
,bash-4.3.48
,tar-1.28
,mingw-5.3.1
可使用如下命令列安裝必要的軟體:
$ sudo apt-get install -y build-essential make git gcc
以MQTT Topic編程方式接入裝置
建立產品和裝置
請登入阿里雲IoT物聯網平台進行產品建立,登入時通過您的阿里雲帳號進行登入。因為是直接通過MQTT的Topic進行產品功能實現,所以在建立產品時選擇“基礎版”即可。
建立產品之後可以添加一個具體的裝置,阿里雲IoT物聯網平台會為裝置產生身份資訊。
產品功能實現
瞭解SDK根目錄結構
擷取Linkkit SDK後,頂層目錄結構如下:
$ ls certs config.bat external_libs extract.bat extract.sh LICENSE makefile make.settings model.json README.md src tools wrappers
配置SDK
SDK預設配置開啟了物模型選項,這裡僅示範基礎版的使用,先關閉物模型選項。
$ make menuconfig
填寫裝置認證到常式中
裝置開發人員需要實現SDK定義的相應HAL函數擷取裝置的身份資訊,由於本文使用Ubuntu來類比IoT裝置,在SDK版本v3.0.1中,開啟檔案
wrappers/os/ubuntu/HAL_OS_linux.c
,在v3.1.0/v3.2.0中,開啟檔案src/mqtt/examples/mqtt_example.c
,編輯如下程式碼片段,填入之前在物聯網平台建立產品和裝置後得到的裝置身份資訊:ProductKey:產品唯一標識
ProductSecret:產品金鑰
DeviceName:裝置唯一標識
DeviceSecret:裝置密鑰
#ifdef DYNAMIC_REGISTER ... ... #else #ifdef DEVICE_MODEL_ENABLED ... ... #else char _product_key[IOTX_PRODUCT_KEY_LEN + 1] = "xxxx"; /*使用實際的product key替換*/ char _product_secret[IOTX_PRODUCT_SECRET_LEN + 1] = "yyyy"; /*使用實際的product secret替換*/ char _device_name[IOTX_DEVICE_NAME_LEN + 1] = "zzzz"; /*使用實際的device name 替換*/ char _device_secret[IOTX_DEVICE_SECRET_LEN + 1] = "ssss"; /*使用device secret替換*/ #endif #endif
說明請在物聯網平台的管理主控台將topic
/${productKey}/${deviceName}/get
設定為“可訂閱可發布”許可權,下面的代碼中將會用到。初始化與建立串連
下面的程式碼片段來自MQTT上雲功能的常式
src/mqtt/examples/mqtt_example.c
, 它簡單描述了裝置的初始化以及串連過程。定製化MQTT參數。
iotx_mqtt_param_t mqtt_params; memset(&mqtt_params, 0x0, sizeof(mqtt_params)); /* mqtt_params.request_timeout_ms = 2000; */ /* mqtt_params.clean_session = 0; */ /* mqtt_params.keepalive_interval_ms = 60000; */ /* mqtt_params.write_buf_size = 1024; */ /* mqtt_params.read_buf_size = 1024; */ mqtt_params.handle_event.h_fp = example_event_handle;
說明上面的代碼中注釋掉的地方是mqtt相關配置的預設數值,使用者可以不用賦值,SDK會自動填寫預設值。如果使用者希望調整預設的串連參數,只需要去掉相應的注釋,並填入數值即可。
嘗試建立與伺服器的MQTT串連。
pclient = IOT_MQTT_Construct(&mqtt_params); if (NULL == pclient) { EXAMPLE_TRACE("MQTT construct failed"); return -1; }
說明將串連參數結構體傳參給
IOT_MQTT_Construct()
介面,即可觸發MQTT串連建立的動作成功返回非空值作為已建立串連的控制代碼,失敗則返回空。上報資料到雲端
在樣本檔案中定義了如下的topic。
/${productKey}/${deviceName}/get
下面的程式碼片段樣本了如何向這個Topic發送資料。
int example_publish(void *handle) { int res = 0; const char *fmt = "/%s/%s/get"; char *topic = NULL; int topic_len = 0; char *payload = "{\"message\":\"hello!\"}"; topic_len = strlen(fmt) + strlen(DEMO_PRODUCT_KEY) + strlen(DEMO_DEVICE_NAME) + 1; topic = HAL_Malloc(topic_len); if (topic == NULL) { EXAMPLE_TRACE("memory not enough"); return -1; } memset(topic, 0, topic_len); HAL_Snprintf(topic, topic_len, fmt, DEMO_PRODUCT_KEY, DEMO_DEVICE_NAME); res = IOT_MQTT_Publish_Simple(0, topic, IOTX_MQTT_QOS0, payload, strlen(payload));
說明其中,
IOT_MQTT_Publish_Simple()
的第1個參數可以填入之前調用IOT_MQTT_Construct()
得到的控制代碼傳回值,也可以直接填入0, 代表告訴SDK,使用當前已建立的唯一MQTT串連來發送訊息。從雲端訂閱並處理資料
說明樣本程式為了盡量簡單的示範發布/訂閱,代碼中對topic
/${productKey}/${deviceName}/get
進行了訂閱,意味著裝置發送給物聯網平台的資料將會被物聯網平台發送回裝置。下面的代碼訂閱指定的topic並指定接收到資料時的處理函數。
res = example_subscribe(pclient); if (res < 0) { IOT_MQTT_Destroy(&pclient); return -1; } ... ... int example_subscribe(void *handle) { ... res = IOT_MQTT_Subscribe(handle, topic, IOTX_MQTT_QOS0, example_message_arrive, NULL); ...
說明其中,
IOT_MQTT_Subscribe()
的第1個參數可以填入之前調用IOT_MQTT_Construct()
得到的控制代碼傳回值,也可以直接填入0, 代表告訴SDK,使用當前已建立的唯一MQTT串連來訂閱Topic。樣本程式中收到來自雲端訊息,在回呼函數中處理時只是把訊息列印出來。
void example_message_arrive(void *pcontext, void *pclient, iotx_mqtt_event_msg_pt msg) { iotx_mqtt_topic_info_t *topic_info = (iotx_mqtt_topic_info_pt) msg->msg; switch (msg->event_type) { case IOTX_MQTT_EVENT_PUBLISH_RECEIVED: /* print topic name and topic message */ EXAMPLE_TRACE("Message Arrived:"); EXAMPLE_TRACE("Topic : %.*s", topic_info->topic_len, topic_info->ptopic); EXAMPLE_TRACE("Payload: %.*s", topic_info->payload_len, topic_info->payload); EXAMPLE_TRACE("\n"); break; default: break; } }
範例程式碼向該Topic周期性的發送資料,使用者在實現自己的產品邏輯時不需要周期的發送資料,只是有需要上報的時候再發送資料。
while (1) { if (0 == loop_cnt % 20) { example_publish(pclient); } IOT_MQTT_Yield(pclient, 200); loop_cnt += 1; }
編譯例子程式
在SDK頂層目錄運行如下命令:
make distclean make
註:每次在根目錄執行完make會自動產生代碼在output/中,使用者如已修改output/中代碼需儲存,請自行備份。
編譯成功完成後,產生的範例程式在當前路徑的
output/release/bin
目錄下:$ tree output/release output/release/ +-- bin ... ... | +-- mqtt-example ... ...
觀察資料
執行如下命令:
$ ./output/release/bin/mqtt-example
在Linux的console裡面也可以看見樣本程式列印的來自雲端的資料:
example_message_arrive|031 :: Message Arrived: example_message_arrive|032 :: Topic : /a1MZxO*****/test_01/get example_message_arrive|033 :: Payload: {"message":"hello!"} example_message_arrive|034 ::
以物模型編程方式接入裝置
建立產品和裝置
可以在阿里雲IoT物聯網平台以及其上承載的多個行業服務中進行產品的建立。
若產品需要在生活物聯網平台(註:基於阿里雲IoT物聯網平台建立的針對生活情境的行業服務)進行建立,可以登入生活物聯網平台建立產品。
本樣本產品的物模型描述檔案
model_for_examples.JSON
存放在./src/dev_model/examples/
目錄下,為了簡化使用者在物聯網平台控制台上的操作,使用者可以在控制台建立自己的產品後,將該檔案中的productkey
替換為自己建立產品的productKey
,然後在 產品詳情 - 功能定義 頁面單擊 匯入物模型 按鈕將該JSON檔案匯入到自己建立的產品中,這樣使用者的產品將具備樣本產品的全部物模型定義。產品功能實現
填寫裝置身份資訊到常式中。
裝置的身份資訊通過HAL調用返回給SDK,由於本體驗基於Linux,因此相關的HAL實現位於
wrappers/os/ubuntu/HAL_OS_linux.c
,使用者需要把檔案中的以下裝置身份資訊替換成自己建立的裝置的身份資訊。#ifdef DEVICE_MODEL_ENABLED char _product_key[IOTX_PRODUCT_KEY_LEN + 1] = "a1RIsMLz2BJ"; char _product_secret[IOTX_PRODUCT_SECRET_LEN + 1] = "fSAF0hle6xL0oRWd"; char _device_name[IOTX_DEVICE_NAME_LEN + 1] = "example1"; char _device_secret[IOTX_DEVICE_SECRET_LEN + 1] = "RDXf67itLqZCwdMCRrw0N5FHbv5D7jrE";
說明使用者也可以不用修改這些全域變數,而是直接修改
HAL_GetProductKey()
等函數返回裝置身份資訊。編譯與運行程式。
在SDK頂層目錄執行如下命令。
$ make distclean $ make
編譯成功完成後,產生的進階版例子程式在當前路徑的
output/release/bin
目錄下,名為linkkit-example-solo
。在SDK頂層目錄執行如下命令。
$ ./output/release/bin/linkkit-example-solo
觀察資料
樣本程式會定期將
Counter
屬性的數值上報雲端,因此可以在雲端查看收到的屬性。使用者可以將該屬性配置為可讀寫屬性,並且可以在雲端對該屬性進行設定,然後再次查看從裝置端上報的Counter
值。屬性上報
樣本中使用
__user_post_property__
作為上報屬性的例子。該樣本會迴圈上報各種情況的payload,使用者可觀察在上報錯誤payload時返回的提示資訊。代碼中上報屬性的程式碼片段如下。
/* Post Property Example */ if (time_now_sec % 11 == 0 && user_master_dev_available()) { user_post_property(); }
觀察屬性上報樣本函數。
void user_post_property(void) { static int example_index = 0; int res = 0; user_example_ctx_t *user_example_ctx = user_example_get_ctx(); char *property_payload = "NULL"; if (example_index == 0) {
正常上報屬性的情況。
void user_post_property(void) { static int cnt = 0; int res = 0; char property_payload[30] = {0}; HAL_Snprintf(property_payload, sizeof(property_payload), "{\"Counter\": %d}", cnt++); res = IOT_Linkkit_Report(EXAMPLE_MASTER_DEVID, ITM_MSG_POST_PROPERTY, (unsigned char *)property_payload, strlen(property_payload)); EXAMPLE_TRACE("Post Property Message ID: %d", res); }
下面是上報正常屬性時的日誌。
[inf] dm_msg_request(205): DM Send Message, URI: /sys/a1X2bEnP82z/test_06/thing/event/property/post, Payload: {"id":"2","version":"1.0","params":{"LightSwitch":1},"method":"thing.event.property.post"} [inf] MQTTPublish(2546): Upstream Topic: '/sys/a1X2bEnP82z/test_06/thing/event/property/post'
這裡是發送給雲端的訊息。
> { > "id": "2", > "version": "1.0", > "params": { > "Counter": 1 > }, > "method": "thing.event.property.post" > }
收到的雲端應答。
< { < "code": 200, < "data": { < }, < "id": "1", < "message": "success", < "method": "thing.event.property.post", < "version": "1.0" < }
使用者回呼函數的日誌。
user_report_reply_event_handler.314: Message Post Reply Received, Devid: 0, Message ID: 2, Code: 200, Reply: {}
屬性設定處理
收到屬性set請求時,會進入如下回呼函數。
static int user_property_set_event_handler(const int devid, const char *request, const int request_len) { int res = 0; user_example_ctx_t *user_example_ctx = user_example_get_ctx(); EXAMPLE_TRACE("Property Set Received, Devid: %d, Request: %s", devid, request);
將屬性設定的執行結果發回雲端,更新雲端裝置屬性。
res = IOT_Linkkit_Report(user_example_ctx->master_devid, ITM_MSG_POST_PROPERTY, (unsigned char *)request, request_len); EXAMPLE_TRACE("Post Property Message ID: %d", res); return 0; }
日誌中可以看到從服務端下來的屬性設定訊息。
[dbg] iotx_mc_cycle(1774): PUBLISH [inf] iotx_mc_handle_recv_PUBLISH(1549): Downstream Topic: '/sys/a1csED27mp7/AdvExample1/thing/service/property/set' [inf] iotx_mc_handle_recv_PUBLISH(1550): Downstream Payload:
從雲端收到的屬性設定報文內容。
< { < "method": "thing.service.property.set", < "id": "161430786", < "params": { < "LightSwitch": 1 < }, < "version": "1.0.0" < }
發送回雲端的應答訊息。
> { > "id": "161430786", > "code": 200, > "data": { > } > } [inf] dm_client_publish(106): Publish Result: 0 [inf] _iotx_linkkit_event_callback(219): Receive Message Type: 15 [inf] _iotx_linkkit_event_callback(221): Receive Message: {"devid":0,"payload":{"LightSwitch":1}} [dbg] _iotx_linkkit_event_callback(339): Current Devid: 0 [dbg] _iotx_linkkit_event_callback(340): Current Payload: {"LightSwitch":1}
user_property_set_event_handler()
樣本回呼函數中收到屬性設定的日誌。user_property_set_event_handler.160: Property Set Received, Devid: 0, Request: {"LightSwitch":1}
這樣,一條從服務端設定屬性的命令就到達裝置端並執行完畢了。
最後收到的對屬性上報的應答。
< { < "code": 200, < "data": { < }, < "id": "2", < "message": "success", < "method": "thing.event.property.post", < "version": "1.0" < } [dbg] iotx_mc_handle_recv_PUBLISH(1555): Packet Ident : 00000000 [dbg] iotx_mc_handle_recv_PUBLISH(1556): Topic Length : 60 [dbg] iotx_mc_handle_recv_PUBLISH(1560): Topic Name : /sys/a1csED27mp7/AdvExample1/thing/event/property/post_reply [dbg] iotx_mc_handle_recv_PUBLISH(1563): Payload Len/Room : 104 / 4935 [dbg] iotx_mc_handle_recv_PUBLISH(1564): Receive Buflen : 5000 [dbg] iotx_mc_handle_recv_PUBLISH(1575): delivering msg ... [dbg] iotx_mc_deliver_message(1291): topic be matched [inf] dm_msg_proc_thing_event_post_reply(258): Event Id: property [dbg] dm_msg_response_parse(167): Current Request Message ID: 2 [dbg] dm_msg_response_parse(168): Current Request Message Code: 200 [dbg] dm_msg_response_parse(169): Current Request Message Data: {} [dbg] dm_msg_response_parse(174): Current Request Message Desc: success [dbg] dm_ipc_msg_insert(87): dm msg list size: 0, max size: 50 [dbg] dm_msg_cache_remove(142): Remove Message ID: 2 [inf] _iotx_linkkit_event_callback(219): Receive Message Type: 30 [inf] _iotx_linkkit_event_callback(221): Receive Message: {"id":2,"code":200,"devid":0,"payload":{}} [dbg] _iotx_linkkit_event_callback(476): Current Id: 2 [dbg] _iotx_linkkit_event_callback(477): Current Code: 200 [dbg] _iotx_linkkit_event_callback(478): Current Devid: 0 user_report_reply_event_handler.300: Message Post Reply Received, Devid: 0, Message ID: 2, Code: 200, Reply: {}
說明實際的產品收到屬性設定時,應該解析屬性並進行相應處理而不是僅僅將數值發送回雲端。
事件上報
樣本中使用
IOT_Linkkit_TriggerEvent
上報屬性。該樣本會迴圈上報各種情況的payload,使用者可觀察在上報錯誤payload時返回的提示資訊。正常上報事件的情況。
void user_post_event(void) { int res = 0; char *event_id = "HardwareError"; char *event_payload = "{\"ErrorCode\": 0}"; res = IOT_Linkkit_TriggerEvent(EXAMPLE_MASTER_DEVID, event_id, strlen(event_id), event_payload, strlen(event_payload)); EXAMPLE_TRACE("Post Event Message ID: %d", res); }
樣本程式中
Error
事件(Event)是約每10s上報一次,在以上各種情況中迴圈。其中正常上報的日誌如下。[inf] dm_msg_request(218): DM Send Message, URI: /sys/a1csED27mp7/AdvExample1/thing/event/HardwareError/post, Payload: {"id":"1","version":"1.0","params":{"ErrorCode":0},"method":"thing.event.HardwareError.post"} [dbg] MQTTPublish(319): ALLOC: (136) / [200] @ 0x1195150 [inf] MQTTPublish(378): Upstream Topic: '/sys/a1csED27mp7/AdvExample1/thing/event/HardwareError/post' [inf] MQTTPublish(379): Upstream Payload:
向雲端上報的事件訊息內容及日誌。
> { > "id": "1", > "version": "1.0", > "params": { > "ErrorCode": 0 > }, > "method": "thing.event.HardwareError.post" > } [inf] dm_client_publish(106): Publish Result: 0 [dbg] alcs_observe_notify(105): payload:{"id":"1","version":"1.0","params":{"ErrorCode":0},"method":"thing.event.Error.post"} [inf] dm_server_send(76): Send Observe Notify Result 0 [dbg] dm_msg_cache_insert(79): dmc list size: 0 user_post_event.470: Post Event Message ID: 1 [dbg] iotx_mc_cycle(1774): PUBLISH [inf] iotx_mc_handle_recv_PUBLISH(1549): Downstream Topic: '/sys/a1csED27mp7/AdvExample1/thing/event/HardwareError/post_reply' [inf] iotx_mc_handle_recv_PUBLISH(1550): Downstream Payload:
從雲端收到的應答訊息內容及日誌。
< { < "code": 200, < "data": { < }, < "id": "1", < "message": "success", < "method": "thing.event.HardwareError.post", < "version": "1.0" < } [dbg] iotx_mc_handle_recv_PUBLISH(1555): Packet Ident : 00000000 [dbg] iotx_mc_handle_recv_PUBLISH(1556): Topic Length : 57 [dbg] iotx_mc_handle_recv_PUBLISH(1560): Topic Name : /sys/a1csED27mp7/AdvExample1/thing/event/Error/post_reply [dbg] iotx_mc_handle_recv_PUBLISH(1563): Payload Len/Room : 101 / 4938 [dbg] iotx_mc_handle_recv_PUBLISH(1564): Receive Buflen : 5000 [dbg] iotx_mc_handle_recv_PUBLISH(1575): delivering msg ... [dbg] iotx_mc_deliver_message(1291): topic be matched [inf] dm_msg_proc_thing_event_post_reply(258): Event Id: Error [dbg] dm_msg_response_parse(167): Current Request Message ID: 1 [dbg] dm_msg_response_parse(168): Current Request Message Code: 200 [dbg] dm_msg_response_parse(169): Current Request Message Data: {} [dbg] dm_msg_response_parse(174): Current Request Message Desc: success [dbg] dm_ipc_msg_insert(87): dm msg list size: 0, max size: 50 [dbg] dm_msg_cache_remove(142): Remove Message ID: 1 [inf] _iotx_linkkit_event_callback(219): Receive Message Type: 31 [inf] _iotx_linkkit_event_callback(221): Receive Message: {"id":1,"code":200,"devid":0,"eventid":"Error","payload":"success"} [dbg] _iotx_linkkit_event_callback(513): Current Id: 1 [dbg] _iotx_linkkit_event_callback(514): Current Code: 200 [dbg] _iotx_linkkit_event_callback(515): Current Devid: 0 [dbg] _iotx_linkkit_event_callback(516): Current EventID: Error [dbg] _iotx_linkkit_event_callback(517): Current Message: success
使用者回呼函數
user_trigger_event_reply_event_handler()
中的日誌。user_trigger_event_reply_event_handler.310: Trigger Event Reply Received, Devid: 0, Message ID: 1, Code: 200, EventID: Error, Message: success
服務調用
註冊服務訊息的處理函數。
IOT_RegisterCallback(ITE_SERVICE_REQUEST, user_service_request_event_handler);
收到服務要求訊息時,會進入下面的回呼函數。裝置端示範了一個簡單的加法運算服務,入參為
NumberA
和NumberB
,出參為Result
,常式中使用cJSON
解析屬性的值。static int user_service_request_event_handler(const int devid, const char *serviceid, const int serviceid_len, const char *request, const int request_len, char **response, int *response_len) { int add_result = 0; cJSON *root = NULL, *item_number_a = NULL, *item_number_b = NULL; const char *response_fmt = "{\"Result\": %d}"; EXAMPLE_TRACE("Service Request Received, Service ID: %.*s, Payload: %s", serviceid_len, serviceid, request); /* Parse Root */ root = cJSON_Parse(request); if (root == NULL || !cJSON_IsObject(root)) { EXAMPLE_TRACE("JSON Parse Error"); return -1; } if (strlen("Operation_Service") == serviceid_len && memcmp("Operation_Service", serviceid, serviceid_len) == 0) { /* Parse NumberA */ item_number_a = cJSON_GetObjectItem(root, "NumberA"); if (item_number_a == NULL || !cJSON_IsNumber(item_number_a)) { cJSON_Delete(root); return -1; } EXAMPLE_TRACE("NumberA = %d", item_number_a->valueint); /* Parse NumberB */ item_number_b = cJSON_GetObjectItem(root, "NumberB"); if (item_number_b == NULL || !cJSON_IsNumber(item_number_b)) { cJSON_Delete(root); return -1; } EXAMPLE_TRACE("NumberB = %d", item_number_b->valueint); add_result = item_number_a->valueint + item_number_b->valueint; /* Send Service Response To Cloud */ *response_len = strlen(response_fmt) + 10 + 1; *response = (char *)HAL_Malloc(*response_len); if (*response == NULL) { EXAMPLE_TRACE("Memory Not Enough"); return -1; } memset(*response, 0, *response_len); HAL_Snprintf(*response, *response_len, response_fmt, add_result); *response_len = strlen(*response); } cJSON_Delete(root); return 0; }
此時在裝置端可以看到如下日誌。
收到的雲端的服務調用,輸入參數為
NumberA
(值為1),NumberB
(值為2)。< { < "method": "thing.service.Operation_Service", < "id": "280532170", < "params": { < "NumberB": 2, < "NumberA": 1 < }, < "version": "1.0.0" < }
在回呼函數中將
NumberA
和NumberB
的值相加後賦值給Result
後,上報到雲端。> { > "id": "280532170", > "code": 200, > "data": { > "Result": 3 > } > }
關於進階版單品常式中服務、屬性、事件的說明就此結束。