雲中樹莓派(2):將傳感器數據上傳到AWS IoT 並利用Kibana進行展示
雲中樹莓派(3):通過 AWS IoT 控制樹莓派上的Led
1. Led 連接與測試
在某寶上買了幾樣配件,包括T型GPIO擴展板、40P排線、亞克力外殼、400孔面包板、若干杜邦線。現在我的樹莓派長得這個樣子了:

不由得感謝神奇的某寶,這些東西每一樣都不超過三四塊錢。
1.1 接線
以下幾個簡單步驟就完成了接線:
- 將排線一頭插在樹莓派的40個pin腳上,將另一頭插在擴展板上。要注意方向,多試幾次。還要注意在樹莓派關機時候再插入。
- 把擴展板插在面包板上。
- 把Led 的長腳(正極)插在面包板第6行的任何一個孔內(對應GPIO18),將其短腳(負極或接地)插在第7行的任何一個孔內(對應GND)。
簡單說下面包板。剛拿到手時還有點不知所措,稍微研究一下后就簡單了。面包板為長方形,長邊的兩邊是為了接電源的,每個長邊都是聯通的;中間區域內,每行內是聯通的。
1.2 簡單測試
下面的 python 能讓led 燈每兩秒鍾亮一次:
import RPi.GPIO as GPIO import time PIN_NO=18 GPIO.setmode(GPIO.BCM) GPIO.setup(PIN_NO, GPIO.OUT) loopCount = 0 for x in xrange(500): print("Loop " + str(loopCount)) GPIO.output(PIN_NO, GPIO.HIGH) time.sleep(2) GPIO.output(PIN_NO, GPIO.LOW) time.sleep(2) loopCount += 1 GPIO.cleanup()
也就是通過控制GPIO18的電壓為高還是低來控制Led 燈是亮還是滅。
2. AWS IoT Device Shadow
-
- UPDATE:如果設備的影子不存在,則創建一個該影子;如果存在,則使用請求中提供的數據更新設備的影子的內容。存儲數據時使用時間戳信息,以指明最新更新時間。向所有訂閱者發送消息,告知 desired 狀態與 reported 狀態之間的差異 (增量)。接收到消息的事物或應用程序可以根據 desired 狀態和 reported 狀態之間的差異執行操作。例如,設備可將其狀態更新為預期狀態,或者應用程序可以更新其 UI,以反映設備狀態的更改。
- GET:檢索設備的影子中存儲的最新狀態 (例如,在設備啟動期間,檢索配置和最新操作狀態)。此操作將返回整個 JSON 文檔,其中包括元數據。
- DELETE:刪除設備的影子,包括其所有內容。這將從數據存儲中刪除 JSON 文檔。您無法還原已刪除的設備的影子,但可以創建具有相同名稱的新影子。
也就是說,要通過 AWS IoT 來操作設備,需要通過設備影子進行。下圖是控制Led 燈泡的示意圖。外部應用和設備之間的交互通過設備影子進行。
Device Shadow 使用系統預留的 MQTT 主題來做應用程序和設備之間的通信:MQTT 主題 用途 $aws/things/myLightBulb/shadow/update/accepted
當設備的影子更新成功時,Device Shadow 服務將向此主題發送消息 $aws/things/myLightBulb/shadow/update/rejected
當設備的影子更新遭拒時,Device Shadow 服務將向此主題發送消息 $aws/things/myLightBulb/shadow/update/delta
當檢測到設備的影子的“reported”部分與“desired”部分之間存在差異時,Device Shadow 服務將向此主題發送消息。 $aws/things/myLightBulb/shadow/get/accepted
當獲取設備的影子的請求獲批時,Device Shadow 服務將向此主題發送消息。
$aws/things/myLightBulb/shadow/get/rejected
當獲取設備的影子的請求遭拒時,Device Shadow 服務將向此主題發送消息。
$aws/things/myLightBulb/shadow/delete/accepted
當設備的影子被刪除時,Device Shadow 服務將向此主題發送消息。
$aws/things/myLightBulb/shadow/delete/rejected
當刪除設備的影子的請求遭拒時,Device Shadow 服務將向此主題發送消息。
$aws/things/myLightBulb/shadow/update/documents
每次設備的影子更新成功執行時,Device Shadow 服務都會向此主題發布狀態文檔。
下圖顯示了控制流程:

- 連接着的 Led 燈發送封裝在MQTT消息中的 reported 狀態 『off』 到 AWS IoT
- AWS IoT 通過 led 燈使用的證書來確定它所屬的虛擬事物
- AWS IoT 將 reported 狀態保存在設備影子 JSON 文件中
- 一條 AWS IoT rule 正監控着 led 的 off 狀態,它發送一個消息到某個 SNS topic
- 某 application 收到 SNS 消息。用戶將 led 期望狀態(desired state)設置為 on
- AWS IoT 收到該消息,它更新設備影子中的 desired state,同時發送包含期望狀態的 message 到某些topic。此時,reported 和 desired 狀態是 『Out of Sync』的
- led 收到 delta 消息,開始根據其中的 desired status 來設置其實際狀態
- led 將其狀態設置為 on,向 MQTT topic 發送新的 reported 狀態
- AWS IoT 更新設備影子,現在該設備的 reported 和 desired 狀態一致了
3. Python 代碼
代碼一共就兩個文件。文件 ledController.py 充當Led 燈的控制器,它定期向Led 發出『開』或『關』的指令,並定期獲取其狀態;文件 ledSwitch.py 充當 Led 等的操縱器,它通過調用樹莓派的 GPIO 接口來設置和獲取 led 燈的狀態,以及將其狀態上報給 IoT 服務。
AWS IoT 提供了 Device Shadow python SDK,因此不需要直接操作各種 MQTT 主題,而可以使用 get,update 和 delete 這種API。其地址在 https://github.com/aws/aws-iot-device-sdk-python/tree/master/AWSIoTPythonSDK/core/shadow。
3.1 ledController.py
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTShadowClient import logging import time import json import threading # Led shadow JSON Schema # # # Name: Led # { # "state: { # "desired": { # "light": <on|off> # } # } #} deviceShadowHandler = None def getDeviceStatus(): while True: print("Getting device status...\n") deviceShadowHandler.shadowGet(customShadowCallback_get, 50) time.sleep(60) def customShadowCallback_get(payload, responseStatus, token): if responseStatus == "timeout": print("Get request with token " + token + " time out!") if responseStatus == "accepted": print("========== Printing Device Current Status =========") print(payload) payloadDict = json.loads(payload) #{"state":{"desired":{"light":0},"reported":{"light":100} try: desired = payloadDict["state"]["desired"]["light"] desiredTime = payloadDict["metadata"]["desired"]["light"]["timestamp"] except Exception: print("Failed to get desired state and timestamp.") else: print("Desired status: " + str(desired) + " @ " + time.ctime(int(desiredTime))) try: reported = payloadDict["state"]["reported"]["light"] #"metadata":{"desired":{"light":{"timestamp":1533893848}},"reported":{"light":{"timestamp":1533893853}}} reportedTime = payloadDict["metadata"]["reported"]["light"]["timestamp"] except Exception: print("Failed to get reported time or timestamp") else: print("Reported status: " + str(reported) + " @ " + time.ctime(int(reportedTime))) print("=======================================\n\n") if responseStatus == "rejected": print("Get request with token " + token + " rejected!") def customShadowCallback_upate(payload, responseStatus, token): # payload is a JSON string which will be parsed by jason lib if responseStatus == "timeout": print("Update request with " + token + " time out!") if responseStatus == "accepted": playloadDict = json.loads(payload) print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") print("Update request with token: " + token + " accepted!") print("light: " + str(playloadDict["state"]["desired"]["light"])) print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n") if responseStatus == "rejected": print("Update request " + token + " rejected!") def customShadowCallback_delete(payload, responseStatus, token): if responseStatus == "timeout": print("Delete request " + token + " time out!") if responseStatus == "accepted": print("Delete request with token " + token + " accepted!") if responseStatus == "rejected": print("Delete request with token " + token + " rejected!") # Cofigure logging logger = logging.getLogger("AWSIoTPythonSDK.core") logger.setLevel(logging.ERROR) streamHandler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') streamHandler.setFormatter(formatter) logger.addHandler(streamHandler) # AWS IoT Core endpoint. Need change some values to yours. awsiotHost = "**********.iot.*********.amazonaws.com" awsiotPort = 8883; # AWS IoT Root Certificate. Needn't change. rootCAPath = "/home/pi/awsiot/VeriSign-Class3-Public-Primary-Certification-Authority-G5.pem" # Device private key. Need change to yours. privateKeyPath = "/home/pi/awsiot/aec2731afd-private.pem.key" # Device certificate. Need change to yours. certificatePath = "/home/pi/awsiot/aec2731afd-certificate.pem.crt" myAWSIoTMQTTShadowClient = None; myAWSIoTMQTTShadowClient = AWSIoTMQTTShadowClient("RaspberryLedController") myAWSIoTMQTTShadowClient.configureEndpoint(awsiotHost, awsiotPort) myAWSIoTMQTTShadowClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath) myAWSIoTMQTTShadowClient.configureAutoReconnectBackoffTime(1, 32, 20) myAWSIoTMQTTShadowClient.configureConnectDisconnectTimeout(60) # 10sec myAWSIoTMQTTShadowClient.configureMQTTOperationTimeout(50) #5sec #connect to AWS IoT myAWSIoTMQTTShadowClient.connect() #create a devcie Shadow with persistent subscription thingName = "homepi" deviceShadowHandler = myAWSIoTMQTTShadowClient.createShadowHandlerWithName(thingName, True) #Delete shadow JSON doc deviceShadowHandler.shadowDelete(customShadowCallback_delete, 50) #start a thread to get device status every 5 seconds statusLoopThread = threading.Thread(target=getDeviceStatus) statusLoopThread.start() #update shadow in a loop loopCount = 0 while True: desiredState = "off" if (loopCount % 2 == 0): desiredState = "on" print("To change Led desired status to \"" + desiredState + "\" ...\n") jsonPayload = '{"state":{"desired":{"light":"' + desiredState + '"}}}' print("payload is: " + jsonPayload + "\n") deviceShadowHandler.shadowUpdate(jsonPayload, customShadowCallback_upate, 60) loopCount += 1 time.sleep(60)
3.2 ledSwitch.py
import RPi.GPIO as GPIO from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTShadowClient import logging import time import json # Led shadow JSON Schema # # # Name: Led # { # "state: { # "desired": { # "light": <on|off> # } # } #} LED_PIN_NUM = 18 # GPIO Number of Led long pin. Change to yours. deviceShadowHandler = None #initialize GOPI GPIO.setmode(GPIO.BCM) GPIO.setup(LED_PIN_NUM, GPIO.OUT) def customShadowCallback_Delta(payload, responseStatus, token): # payload is a JSON string which will be parsed by jason lib print(responseStatus) print(payload) payloadDict = json.loads(payload) print("++++++++ Get DELTA data +++++++++++") desiredStatus = str(payloadDict["state"]["light"]) print("desired status: " + desiredStatus) print("version: " + str(payloadDict["version"])) #get device current status currentStatus = getDeviceStatus() print("Device current status is " + currentStatus) #udpate device current status if (currentStatus != desiredStatus): # update device status as desired updateDeviceStatus(desiredStatus) # send current status to IoT service sendCurrentState2AWSIoT() print("+++++++++++++++++++++++++++\n") def updateDeviceStatus(status): print("=============================") print("Set device status to " + status) if (status == "on"): turnLedOn(LED_PIN_NUM) else: turnLedOff(LED_PIN_NUM) print("=============================\n") def getDeviceStatus(): return getLedStatus(LED_PIN_NUM) def turnLedOn(gpionum): GPIO.output(gpionum, GPIO.HIGH) def turnLedOff(gpionum): GPIO.output(gpionum, GPIO.LOW) def getLedStatus(gpionum): outputFlag = GPIO.input(gpionum) print("outputFlag is " + str(outputFlag)) if outputFlag: return "on" else: return "off" def sendCurrentState2AWSIoT(): #check current status of device currentStatus = getDeviceStatus() print("Device current status is " + currentStatus) print("Sending reported status to MQTT...") jsonPayload = '{"state":{"reported":{"light":"' + currentStatus + '"}}}' print("Payload is: " + jsonPayload + "\n") deviceShadowHandler.shadowUpdate(jsonPayload, customShadowCallback_upate, 50) def customShadowCallback_upate(payload, responseStatus, token): # payload is a JSON string which will be parsed by jason lib if responseStatus == "timeout": print("Update request with " + token + " time out!") if responseStatus == "accepted": playloadDict = json.loads(payload) print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") print(payload) print("Update request with token: " + token + " accepted!") print("light: " + str(playloadDict["state"]["reported"]["light"])) print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n") if responseStatus == "rejected": print("Update request " + token + " rejected!") def customShadowCallback_Get(payload, responseStatus, token): print("responseStatus: " + responseStatus) print("payload: " + payload) payloadDict = json.loads(payload) # {"state":{"desired":{"light":37},"delta":{"light":37}},"metadata":{"desired":{"light":{"timestamp":1533888405}}},"version":54 stateStr = "" try: stateStr = stateStr + "Desired: " + str(payloadDict["state"]["desired"]["light"]) + ", " except Exception: print("No desired state") try: stateStr = stateStr + "Delta: " + str(payloadDict["state"]["delta"]["light"]) + ", " except Exception: print("No delta state") try: stateStr = stateStr + "Reported: " + str(payloadDict["state"]["reported"]["light"]) + ", " except Exception: print("No reported state") print(stateStr + ", Version: " + str(payloadDict["version"])) def printDeviceStatus(): print("=========================") status = getDeviceStatus() print(" Current status: " + str(status)) print("=========================\n\n") # Cofigure logging logger = logging.getLogger("AWSIoTPythonSDK.core") logger.setLevel(logging.DEBUG) streamHandler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') streamHandler.setFormatter(formatter) logger.addHandler(streamHandler) awsiotHost = "***********.iot.********.amazonaws.com" awsiotPort = 8883; rootCAPath = "/home/pi/awsiot/VeriSign-Class3-Public-Primary-Certification-Authority-G5.pem" privateKeyPath = "/home/pi/awsiot/aec2731afd-private.pem.key" certificatePath = "/home/pi/awsiot/aec2731afd-certificate.pem.crt" myAWSIoTMQTTShadowClient = None; myAWSIoTMQTTShadowClient = AWSIoTMQTTShadowClient("RaspberryLedSwitch") myAWSIoTMQTTShadowClient.configureEndpoint(awsiotHost, awsiotPort) myAWSIoTMQTTShadowClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath) myAWSIoTMQTTShadowClient.configureAutoReconnectBackoffTime(1, 32, 20) myAWSIoTMQTTShadowClient.configureConnectDisconnectTimeout(60) # 10sec myAWSIoTMQTTShadowClient.configureMQTTOperationTimeout(30) #5sec #connect to AWS IoT myAWSIoTMQTTShadowClient.connect() #create a devcie Shadow with persistent subscription thingName = "homepi" deviceShadowHandler = myAWSIoTMQTTShadowClient.createShadowHandlerWithName(thingName, True) #listen on deleta deviceShadowHandler.shadowRegisterDeltaCallback(customShadowCallback_Delta) #print the intital status printDeviceStatus() #send initial status to IoT service sendCurrentState2AWSIoT() #get the shadow after started deviceShadowHandler.shadowGet(customShadowCallback_Get, 60) #update shadow in a loop loopCount = 0 while True: time.sleep(1)
3.3 主要過程
(1)ledSwitch.py 在運行后,獲取led 的初始狀態,並將其發給 AWS IoT 服務:
========================= outputFlag is 0 Current status: off ========================= outputFlag is 0 Device current status is off Sending reported status to MQTT... Payload is: {"state":{"reported":{"light":"off"}}}
(2)ledController.py 開始運行后,它首先獲取led 的當前狀態,為 『off』
(3)Controller 將其 desired 狀態設置為 on
To change Led desired status to "on" ... payload is: {"state":{"desired":{"light":"on"}}}
(4)Switch 收到 DELTA 消息,調用 GPIO 接口設置其狀態,並向 IOT 服務報告其狀態
{"version":513,"timestamp":1533983956,"state":{"light":"on"},"metadata":{"light":{"timestamp":1533983956}},"clientToken":"93dfc84c-c9f9-49fb-b844-d55203991208"}
++++++++ Get DELTA data +++++++++++
desired status: on
version: 513
outputFlag is 0
Device current status is off
=============================
Set device status to on
=============================
outputFlag is 1
Device current status is on
Sending reported status to MQTT...
Payload is: {"state":{"reported":{"light":"on"}}}
(5)Controller 獲取最新狀態
{"state":{"desired":{"light":"on"},"reported":{"light":"on"}},"metadata":{"desired":{"light":{"timestamp":1533983956}},"reported":{"light":{"timestamp":1533983957}}},"version":514,"timestamp":1533983959,"clientToken":"f24bcbbb-4b24-4354-b1df-349afdf23422"}
Desired status: on @ Sat Aug 11 18:39:16 2018
Reported status: on @ Sat Aug 11 18:39:17 2018
(6)循環往復
歡迎大家關注我的個人公眾號:

參考鏈接:
- https://www.linkedin.com/pulse/understanding-internet-things-aws-iot-kay-lerch/
- https://iotbytes.wordpress.com/device-shadows-part-1-basics/
- https://github.com/pradeesi/AWS-IoT-Shadows
- https://github.com/aws/aws-iot-device-sdk-python/tree/master/samples/basicShadow
- https://raspi.tv/2013/rpi-gpio-basics-5-setting-up-and-using-outputs-with-rpi-gpio
