應用場景說明:
在經(jīng)濟高速發(fā)展的今天乒融,現(xiàn)代人對自己的生活的要求越來越高粤铭,家電設備也迅猛增加,但是日常生活中,人們不擅長對于家電的管理邮偎,而造成了大量的不必要的能耗損失蔚叨。在這樣的一種情況下棺克,如果有一套智能家居系統(tǒng)能夠管理家庭電器的狀態(tài)症概,我們也可以隨時的控制家電,這樣我們的生活效率將會有很大程度上的提高但荤。
在這樣的一個需求的大背景下罗岖,我們又對設備和設備之間,人和設備之間腹躁,進行了一番詳細的分析桑包。首先我們來看設備和設備之間的需求。
1.假設在N市的A設備的狀態(tài)發(fā)生改變纺非,我們需要遠在M市的B設備的狀態(tài)也發(fā)生改變哑了,這種需求我們稱為不同網(wǎng)段的設備間的聯(lián)動需求。
2.假設在N市的A設備的狀態(tài)發(fā)生改變烧颖,我們需要當前統(tǒng)一網(wǎng)段下的B設備也發(fā)生改變弱左,這種需求我們稱為同一網(wǎng)段的設備間的聯(lián)動需求。
3.假設主人遠行炕淮,而忘記了自己家有沒有鎖門拆火,有沒有關燈,有沒有不該開啟的電器設備關閉涂圆,這是我們需要得知家中設備運行的狀態(tài)们镜,并調(diào)控到最佳狀態(tài)。我們稱之為人機遠程調(diào)控需求润歉。
4.假設在家庭與廣域網(wǎng)斷網(wǎng)的情況下模狭,我們還可以得知和控制家庭的設備,而不是失去對于家庭設備間的控制踩衩。我們稱之為遠程失聯(lián)需求嚼鹉。
基于國內(nèi)市場的大需求和我們自行分析的小需求下,我們設計了一套滿足于以上四點控制剛性需求的智能家居控制系統(tǒng)驱富。
系統(tǒng)結構說明
我們現(xiàn)在有了上面的需求分析锚赤,這時我們就可以對系統(tǒng)進行選型和架構了,我們對于設備間的聯(lián)動需求進行分析后選擇了一種物聯(lián)網(wǎng)廣泛使用的推送消息的協(xié)議機制(mqtt),然后對它進行二次開發(fā)和封裝萌朱。下面我們就來看看系統(tǒng)的設計結構:
結構:
1.節(jié)點事件上報(publish nodeid event),這個場景用于當人在現(xiàn)場對設備的狀態(tài)進行了改變,這時策菜,該設備應該向主服務器進行通報晶疼,事件的發(fā)生酒贬,以及當前的狀態(tài),還有為了實現(xiàn)設備的熱插拔翠霍,當設備連上這套系統(tǒng)后锭吨,它便會廣播上線通知,當設備異常斷開系統(tǒng)后寒匙,會發(fā)送遺言離線通知零如,方便我們對節(jié)點事件異常進行及時的處理。
2.節(jié)點屬性上報(publish nodeid property),當人為的改變了設備后锄弱,主服務器和在線的控制端將會受到該設備節(jié)點的屬性上報通知考蕾,這個行為主要是及時的獲取設備點的狀態(tài)信息。當設備剛上線是也會進行屬性播報会宪,以便控制端熱加載設備肖卧。
3.節(jié)點方法被調(diào)用(subscribe nodeid call | publish 0 ack),當N市的A設備狀態(tài)發(fā)生改變,M市的B設備也要發(fā)生狀態(tài)的改變掸鹅,就會直接讓A去控制B設備塞帐,這時我們成A為控制器,B為執(zhí)行器巍沙。那么A就會調(diào)用控制遠程設備命令葵姥,B就會收到call命令之后執(zhí)行命令并返回一個ack以確認信息的無誤性。
4.系統(tǒng)廣播事件(subscribe 0 system),當所有的設備同時接受統(tǒng)一命令的調(diào)控時,我們?yōu)榱颂岣咝畔⑻幚淼男适褂孟到y(tǒng)廣播事件來統(tǒng)一調(diào)度句携。
在這五個控制總命令下榔幸,我們還將設計針對每種設備的控制子命令格式。從而達到既從屬分布式控制有歸屬于集中式控制系統(tǒng)务甥。
智能家居互聯(lián)的通訊協(xié)議:
1.角色定義:?
節(jié)點牡辽,設備,控制器敞临,服務器
2.主題結構:
?yqmiot/<accountid>/<receiver>/<sender>/<command>
3.消息結構:
{
receiver:?,???#?接受者nodeid
sender:?,???????#?發(fā)送者nodeid
name:?,????????#?主命令(名字有帶商定)
action:?,???????#?子命令(可為null)
callseq:?,?????#?調(diào)用序號(多次調(diào)用時確定回包對應的請求)?(非call和ack命令可以為null)
params:?,???????#?命令參數(shù)
#?seq:?,???????????#?包序號(用戶篩選重復數(shù)據(jù)包)?暫未使用
}
備注:receiver,sender,name?未來這三者在發(fā)送數(shù)據(jù)包中可能被省略态辛,因主題中已經(jīng)存在。
屬性上報(property)
-command: "property"
-params:?設備屬性?({"name":?"hello",?"status":?"正忙呢",?"yqmiot.property.nodeid":?27888})
事件上報(event)
-command: "event"
-action:?事件名?("yqmiot.event.online",?"yqmiot.event.offline")
-params:?事件參數(shù)
方法調(diào)用(call)
-command: "call"
-action:?方法名?("yqmiot.method.ping",?"yqmiot.method.test")
-callseq:?調(diào)用序號(每次調(diào)用都必須唯一)
-params:?方法參數(shù)
調(diào)用響應(ack)
-command: "ack"
-action:?call包中的action
-callseq:?call包中的seq
-params:?回應參數(shù)
其他(暫未使用)
服務器 nodeid: 0
全頻道廣播?nodeid:?0xffffffff
全服廣播?accountid:?0,?nodeid:?0
我們把通信協(xié)議搭建好了之后挺尿,就來開始構建整個系統(tǒng)奏黑,接下來就是要使用編程語言進行編程實現(xiàn)。從協(xié)議開始编矾,一步一步構建 整套系統(tǒng)的通訊層和應用層熟史,以及控制端。
系統(tǒng)的構建:
1.設備控制端的構建:
我們是基于可以運行嵌入式linux系統(tǒng)的設備窄俏,對節(jié)點進行控制蹂匹。由于linux系統(tǒng)的便利性。我們使用了python這種腳本對設備客戶端進行了編程處理凹蜈,接下來我們一步步的看限寞,被控器的客戶端構建忍啸。
1.1.引入依賴包和常用參數(shù).
# -*- encoding: utf-8 -*-
importlogging
importtime
importsys
importgetopt
importjson
frompaho.mqtt.clientimportClientasMqtt
VERSION?="1.0.1"
"""
每個設備都擁有三類特性:屬性,事件履植,方法计雌。
屬性表示設備的當前狀態(tài),比如:電力狀態(tài)玫霎,照明開關等凿滤。每當屬性發(fā)生改變就會立即上報。
事件表示設備當前發(fā)生了什么庶近,按下按鈕翁脆,電力不足警告等。
方法則是設備對外提供的操作接口拦盹,通過它可以對設備進行控制鹃祖。比如:重啟,打開照明普舆,關機等恬口。
"""
YQMIOT_OK?=0
YQMIOT_TIMEOUT?=1
YQMIOT_BROADCAST_RECEIVER?=0#?廣播接受者id
#?系統(tǒng)命令
YQMIOT_COMMAND_PROPERTY?="property"#?屬性上報
YQMIOT_COMMAND_EVENT?="event"#?事件上報
YQMIOT_COMMAND_CALL?="call"#?方法調(diào)用
YQMIOT_COMMAND_ACK?="ack"#?方法響應
#?系統(tǒng)事件
YQMIOT_EVENT_ONLINE?="yqmiot.event.online"#?上線通知
YQMIOT_EVENT_OFFLINE?="yqmiot.event.offline"#?下線通知
YQMIOT_EVENT_TEST?="yqmiot.event.test"#?按下測試按鈕
#?系統(tǒng)屬性
YQMIOT_PROPERTY_NODEID?="yqmiot.property.nodeid"#?節(jié)點id號
YQMIOT_PROPERTY_ACCOUNTID?="yqmiot.property.accountid"#?節(jié)點所在賬號id(頻道id)頻道隔離
YQMIOT_PROPERTY_MODEL?="yqmiot.property.model"#?設備所屬類型
YQMIOT_PROPERTY_VERSION?="yqmiot.property.version"#?設備所屬固件版本號
#?系統(tǒng)方法
YQMIOT_METHOD_PING?="yqmiot.method.ping"#?ping連通測試
YQMIOT_METHOD_TEST?="yqmiot.method.test"#?方法調(diào)用測試
logging.basicConfig(level=logging.DEBUG,
format='[%(asctime)s]?%(levelname)s?%(message)s',
datefmt='%Y-%m-%d?%H:%M:%S')
root?=?logging.getLogger()
root.setLevel(logging.NOTSET)
1.2.mqtt通訊層的基本封裝
classMqttClient(object):
"""Mqtt通訊封裝"""
def__init__(self,address):
ifnotisinstance(address,tuple)?orlen(address)?!=2:
raiseValueError("Invalid?address.")
defon_connect(client,userdata,flags,rc):
self.handleConnected()
defon_message(client,userdata,msg):
self.handleMessage(msg.topic,?msg.payload)
self.client?=?Mqtt()
self.address?=?address
self.client.on_connect?=?on_connect
self.client.on_message?=?on_message
defhandleConnected(self):
pass
defhandleMessage(self,topic,payload):
pass
defpublish(self,topic,payload=None,qos=0,retain=False):
self.client.publish(topic,?payload,?qos,?retain)
defsubscribe(self,topic,qos=0):
self.client.subscribe(topic,?qos)
defstart(self):
self.client.connect_async(self.address[0],self.address[1])
self.client.loop_start()
defstop(self):
self.client.loop_stop()
defusername_pw_set(self,username,password=None):
self.client.username_pw_set(username,?password)
defwill_set(self,topic,payload=None,qos=0,retain=False):
self.client.will_set(topic,?payload,?qos,?retain)
1.3.家居互聯(lián)通訊層封裝
classYqmiotBase(MqttClient):
"""月球貓互聯(lián)通訊基類"""
def__init__(self,address,accountid,nodeid,authkey=None,username=None,password=None):
"""username和password是mqtt賬號密碼。"""
super(YqmiotBase,self).__init__(address)
self.username?=?username
self.password?=?password
self.accountid?=?accountid
self.nodeid?=?nodeid
self.authkey?=?authkey#TODO
self.callMethodInfo?=?{}#
self.callMethodTimeout?=10*1000#?方法調(diào)用超時時間TODO處理多線程問題沼侣。調(diào)用超時
self.callseq?=0
ifself.accountid?<=0orself.nodeid?<=0:
raiseValueError("Invalid?accountid?or?nodeid.")
defhandleConnected(self):
super(YqmiotBase,self).handleConnected()
#?偵聽發(fā)送給自己的消息
topic?="yqmiot/{self.accountid}/{self.nodeid}/#".format(self=self)
self.subscribe(topic)
defhandleMessage(self,topic,payload):
super(YqmiotBase,self).handleMessage(topic,?payload)
try:
prefix,?account,?receiver,?sender,?command?=?topic.split("/")
account?=int(account)
receiver?=int(receiver)
sender?=int(sender)
except:
logging.error("Invalid?topic.?{}".format(topic))
return
#?if?prefix?!=?"yqmiot"?\
#?????or?account?!=?self.accountid?\
#?????or?receiver?!=?self.nodeid:?#TODO處理廣播
#?????logging.error("It's?not?my?topic.?{}".format(topic))
#?????return
try:
payload?=?json.loads(payload)
except:
logging.error("Invalid?payload.?{}".format(payload))
return
cmd?=?Command(
name=?command,
action=?payload.get("action"),
receiver=?receiver,
sender=?sender,
callseq=?payload.get("callseq"),
params=?payload.get("params"))
try:
self.handleCommand(cmd)
except:
logging.error("Error?processing?command.?{}".format(topic))
return
defsendCommand(self,cmd):
ifcmd:
try:
accountid?=self.accountid
receiver?=?cmd.receiverifcmd.receiver?!=NoneelseYQMIOT_BROADCAST_RECEIVER#?默認接受者是服務器
sender?=self.nodeid
name?=?cmd.name
action?=?cmd.action
callseq?=?cmd.callseq
params?=?cmd.paramsifcmd.params?!=Noneelse{}
topic?="yqmiot/{}/{}/{}/{}".format(accountid,?receiver,?sender,?name)
payload?=?{"action":?cmd.action,"callseq":?callseq,"params":?params}
self.publish(topic,?json.dumps(payload))
exceptException,?e:
logging.error("Error?sending?command."+str(e))
else:
logging.error("Invalid?cmd.")
defhandleCommand(self,cmd):
ifcmd.name?==?YQMIOT_COMMAND_CALL:
self.handleCommandCall(cmd)
elifcmd.name?==?YQMIOT_COMMAND_ACK:
callseq?=?cmd.callseq
ifcallseq?inself.callMethodInfo:
info?=self.callMethodInfo.pop(callseq)
cmd.action?=?info["action"]
cmd.time?=?millis()?-?info["time"]
self.handleCommandAck(cmd)
else:
logging.error("Drop?unknown?command.")
else:
logging.error("Command?not?supported.")
defhandleCommandCall(self,cmd):
ifcmd.action?==?YQMIOT_METHOD_PING:
self.handleCommandCallPing(cmd)
else:
logging.warn("Could?not?find?method.")
defhandleCommandAck(self,cmd):
ifcmd.action?==?YQMIOT_METHOD_PING:
self.handleCommandCallPingAck(cmd)
defcallMethod(self,receiver,action,params=None):
ifreceiver?and?receiver?!=?YQMIOT_BROADCAST_RECEIVER?and?action:
try:
self.callseq?+=1
cmd?=?Command(
name=?YQMIOT_COMMAND_CALL,
action=?action,
receiver=?receiver,
callseq=self.callseq,
params=?params)
self.callMethodInfo[cmd.callseq]?=?{"action":?action,"callseq":?cmd.callseq,"time":?millis()}
self.sendCommand(cmd)
except:
logging.error("Error?calling?remote?action.")
else:
logging.error("Remote?action?parameter?is?incorrect.")
defcallMethodPing(self,receiver):
self.callMethod(receiver,?YQMIOT_METHOD_PING)
defhandleCommandCallPing(self,cmd):
self.sendCommand(cmd.reply())
defhandleCommandCallPingAck(self,cmd):
pass
1.4.互聯(lián)客戶端封裝
classYqmiotClient(YqmiotBase):
"""月球貓互聯(lián)客戶端
屬性定時上報
屬性變更上報
事件上報
處理方法調(diào)用祖能,并回包"""
defstart(self):
#?離線通知
topic?="yqmiot/{}/{}/{}/{}".format(self.accountid,?YQMIOT_BROADCAST_RECEIVER,self.nodeid,?YQMIOT_COMMAND_EVENT)
payload?=?{"action":?YQMIOT_EVENT_OFFLINE}
self.will_set(topic,?json.dumps(payload))
super(YqmiotClient,self).start()
defhandleConnected(self):
super(YqmiotClient,self).handleConnected()
logging.info("Connect?server?successfully.")
#?上線通知
self.reportEvent(YQMIOT_EVENT_ONLINE)
#TODO推送下線遺言
defreportProperty(self,params):
"""屬性上報
params(dict)?設備屬性集"""
ifisinstance(params,dict):
try:
cmd?=?Command(
name=?YQMIOT_COMMAND_PROPERTY,
receiver=?YQMIOT_BROADCAST_RECEIVER,
params=?params)
self.sendCommand(cmd)
except:
logging.error("An?error?occurred?while?reporting?the?property.")
else:
raiseTypeError("Incorrect?params?type.")
defreportEvent(self,action,params=None):
"""事件上報
action?事件名
params?參數(shù)"""
ifaction:
try:
cmd?=?Command(
name=?YQMIOT_COMMAND_EVENT,
action=?action,
receiver=?YQMIOT_BROADCAST_RECEIVER,
params=?params)
self.sendCommand(cmd)
except:
logging.error("An?error?occurred?while?reporting?the?event.")
else:
raiseTypeError("Incorrect?action?type.")
1.5.家居系統(tǒng)互聯(lián)控制器封裝
classYqmiotController(YqmiotBase):
"""
月球貓互聯(lián)控制器
"""
#?訂閱廣播消息
defhandleConnected(self):
super(YqmiotController,self).handleConnected()
logging.info("Connect?server?successfully.")
#?偵聽設備上報
topic?="yqmiot/{self.accountid}/0/#".format(self=self)
self.subscribe(topic)
defhandleCommand(self,cmd):
ifcmd.name?==?YQMIOT_COMMAND_PROPERTY:
self.handleCommandProperty(cmd)
elifcmd.name?==?YQMIOT_COMMAND_EVENT:
self.handleCommandEvent(cmd)
else:
super(YqmiotController,self).handleCommand(cmd)
defhandleCommandProperty(self,cmd):
print"設備?{}?上報屬性:{}".format(cmd.sender,?cmd.params)
defhandleCommandEvent(self,cmd):
print"設備?{}?上報事件:{}?參數(shù):{}".format(cmd.sender,?cmd.action,?cmd.params)
到這里為止,我們的控制系統(tǒng)的客戶端已經(jīng)封裝完畢蛾洛,但是這才剛剛起步养铸,我們有了客戶端,那我們還需要遠程控制器轧膘,我們?yōu)榱撕啽闫鹨娛褂昧藈eb終端的方案钞螟。來進行對設備客戶端的控制,由于代碼量很大我這里就簡要的介紹一下谎碍。
在控制端中主要使用的mqtt推送協(xié)議鳞滨,然后轉(zhuǎn)換成socket以便實時控制。因為我們的技術棧使用的是vuejs蟆淀,大家如果不了解可以先去了解了解拯啦,這是一種以數(shù)據(jù)為驅(qū)動的web解決方案,告別了傳統(tǒng)的dom節(jié)點控制熔任。使得運行速度和性能得到了很大的提升褒链。我們在控制得到socket數(shù)據(jù)后,然后進行分發(fā)進入各種控制器疑苔,分別管理不同數(shù)據(jù)和業(yè)務邏輯的實現(xiàn)以及數(shù)據(jù)的調(diào)配甫匹。
實踐效果:
好下面我們就來看看最后達到的控制效果吧!