說明:protocol.js 不是類,而是導出一組常量和兩個派生名映射;deviceProtocol.js 里的 DeviceProtocol 類 才是運行時的協(xié)議協(xié)調層。下面按「在插件里各自干什么」說明,并配上對應代碼。
1. protocol.js:協(xié)議「字典層」(靜態(tài)數據)
作用:把業(yè)務上用的屬性名/動作名和 MIoT 的 siid/piid(讀屬性)或 siid/aiid(發(fā)動作) 綁在一起。插件里所有和云端、設備打交道的名,都應以這里為準,避免在業(yè)務代碼里寫裸數字。
-
PROP_LIST(讀/寫/訂閱的屬性)
每個 key 是你在 UI/邏輯里用的名字;value 是數組:[siid, piid, 默認value, 是否參與云端拉取]。注釋寫明與DeviceProtocol里初始化一致。
/**
* deviceProtocol類中初始化用到的數據
* 與PROP_LIST_FORMAT相對應
* value為Array,第一項為SIID、第二項為PIID,第三項為默認值,第四項為是否加入請求列表獲取數據
*/
export const PROP_LIST = {
'deviceStatus': [2, 1, 400],
'errorCode': [2, 2, 0],
'workMode': [2, 3, 0],
// ... 略 ...
'stopMessage': [2, 31, '']
}
-
EVENT_LIST(要下發(fā)的動作/事件)
每項是[siid, aiid],供寫action時用。
/**
* 定義的ACTION,第一項為SIID,第二項為AIID
*/
export const EVENT_LIST = {
powerSwitch: [2, 1], //開關
startWork: [2, 2], //啟動程序
workControl: [2, 3], //工作啟停 0暫停 1運行 2放棄當前任務
// ...
historyControl: [2, 11],
}
-
PROP_LIST_NAME/EVENT_LIST_NAME
把PROP_LIST/EVENT_LIST的值改成和 key 相同的字符串,方便在「按名字發(fā)指令」時做校驗、避免拼錯業(yè)務名(注釋里點到了sendSetProperty/sendAction的 name 規(guī)范)。
/**
* 根據PROP_LIST處理后的prop字典
* 用于sendSetProperty時,name數值的規(guī)范問題
*/
export const PROP_LIST_NAME = { ...PROP_LIST }
Object.keys(PROP_LIST_NAME).map(key => {
PROP_LIST_NAME[key] = key
})
/**
* 根據EVENT_LIST處理后的event字典
* 用于sendAction時,name數值的規(guī)范問題
*/
export const EVENT_LIST_NAME = { ...EVENT_LIST }
Object.keys(EVENT_LIST_NAME).map(key => {
EVENT_LIST_NAME[key] = key
})
在插件流程中的位置:protocol.js 本身不跑邏輯;它是單一數據源——DeviceProtocol 的映射表、訂閱列表、拉屬性列表、發(fā) action / set_property 的編碼,都從這里取數。
2. DeviceProtocol 類:運行時「協(xié)議與狀態(tài)橋梁」
作用:在插件進程里做一個單例協(xié)調器——用 protocol.js 建「云端 key ? 業(yè)務字段名」的 Map;訂閱設備/云端推送、從云端拉全量屬性;用 mitt 把結果寫成 setState,并響應寫屬性 / 發(fā)動作的請求(通常由 useProtocol 一類 hook 通過 emit 調用)。
2.1 單例 + 與外部 store 的綁定
構造時把調用方傳進來的 store(常見是帶 setState 的協(xié)議狀態(tài)或頁面狀態(tài))掛到 this.deviceState;若已有單例則直接復用。進入后臺再回前臺時,若設備在線會防抖再拉一次云端全量,避免斷線或狀態(tài)陳舊。
constructor(store) {
console.log('onNewDeviceProtocol')
this.deviceState = store
// 單例模式
if (DeviceProtocol.instance) {
// this.debounceInitProtocolState()
return DeviceProtocol.instance
} else {
this.initDeviceProtocol()
DeviceProtocol.instance = this
let isPrevStateBackground = false
this.appStateListener = AppState.addEventListener('change', (appState) => {
console.log('appStateListenerChange', appState)
// 首次進入跳過
if (appState === 'background') {
isPrevStateBackground = true
}
if (appState === 'active' && isPrevStateBackground) {
// this.deviceState.setState({ loadingVisible: true })
Device.isOnline && this.debounceInitProtocolState()
}
})
}
}
initDeviceProtocol() {
this.protocolMap = this.initProtocolMap()
this.createEventEmitter()
this.subscribeMessage()
// 注釋防止離線模式數據重置
Device.isOnline && this.debounceInitProtocolState()
// this.debounceInitProtocolState()
}
2.2 protocolMap:統(tǒng)一鍵名
把 prop.siid.piid / event.siid.aiid 映射成 PROP_LIST / EVENT_LIST 的 key,供推送和云端返回時反查業(yè)務字段名。
/**
* 映射字段與siid、piid、aiid。
* 格式為 prop|event.siid.piid|aiid=>定義的字段名
*/
initProtocolMap() {
const pMap = new Map()
Object.keys(PROP_LIST).map(item => {
pMap.set(`prop.${PROP_LIST[item][0]}.${PROP_LIST[item][1]}`, item)
})
// 只需要映射屬性字段,不需要映射事件
Object.keys(EVENT_LIST).map(item => {
pMap.set(`event.${EVENT_LIST[item][0]}.${EVENT_LIST[item][1]}`, item)
})
console.log(pMap)
return pMap
}
2.3 訂閱:設備側推送 → 合并后 setState
createSubscribeData 用 PROP_LIST/EVENT_LIST 拼出要訂閱的字符串;subscribeMessage 里一邊 DeviceEvent.deviceReceivedMessages 收推送,一邊 subscribeMessages 向設備聲明訂閱。收到的 messages 里若 key 在 protocolMap 里,就聚成 toRefreshData,再 emit('setState', ...)。
createSubscribeData() {
const subscribeData = []
Object.keys(PROP_LIST).map(item => {
subscribeData.push(`prop.${PROP_LIST[item][0]}.${PROP_LIST[item][1]}`)
})
Object.keys(EVENT_LIST).map(item => {
subscribeData.push(`event.${EVENT_LIST[item][0]}.${EVENT_LIST[item][1]}`)
})
return subscribeData
}
subscribeMessage() {
const { remove } = DeviceEvent.deviceReceivedMessages.addListener(
(device, messages = new Map()) => {
// ...
const toRefreshData = {}
messages.forEach((value, key) => {
if (this.protocolMap.has(key)) {
console.log(`subscribeMessage---${this.protocolMap.get(key)}=>${value}`)
toRefreshData[this.protocolMap.get(key)] = value
}
})
if (Object.keys(toRefreshData).length > 0) {
this.deviceProtocolEmitter.emit('setState', toRefreshData)
}
},
)
this.removeDeviceReceivedMessages = remove
Device.getDeviceWifi().subscribeMessages(this.createSubscribeData())
// ...
}
2.4 全量拉?。?code>get_properties 分段 + 再 setState
initProtocolStateFromCloud 按 PROP_LIST 里「長度≤3 或第 4 項為 true」的項組請求參數,分成多批(每批最多 30 個)callMethodFromCloud('get_properties', ...),合并結果后用 protocolMap 轉成業(yè)務字段,然后 deviceProtocolEmitter.emit('setState', resultList),并關 loading。失敗時發(fā)錯誤態(tài)、deviceStatus: 400 等,并和防抖邏輯配合。
async initProtocolStateFromCloud() {
console.log('initProtocolStateFromCloud')
const paramsList = Object.values(PROP_LIST).filter(item => item.length <= 3 || item[3] === true).map(item => {
return {
siid: item[0],
piid: item[1],
}
})
// ... 分片 Promise.all ...
result.map(item => {
resultList[this.protocolMap.get(`prop.${item.siid}.${item.piid}`)] = item.value
})
this.deviceState.setState({
loadingVisible: false
})
this.deviceProtocolEmitter.emit('setState', resultList)
// catch 中 emit deviceStatus: 400 等
}
// initProtocolStateFromCloud的防抖形式
debounceInitProtocolState = _.debounce(this.initProtocolStateFromCloud, 15000, { leading: true })
2.5 事件總線:對接 UI/Hook 的「寫」和「合狀態(tài)」
createEventEmitter 里訂閱三類事件:
-
setState→ 調this.deviceState.setState(state)(把協(xié)議數據寫進你傳入的 store); -
protocolAction→ 用EVENT_LIST組action調云端; -
protocolSetProperty→ 用PROP_LIST組set_properties。
createEventEmitter() {
// ...
const onSetState = (state) => {
console.log('listened setState', state)
this.deviceState.setState(state)
}
const onProtocolAction = (payload = { name: '', value: '', ... }) => {
// ...
if (!EVENT_LIST.hasOwnProperty(name)) {
throw new Error(`event '${name}' is not defined in EVENT_LIST`)
}
const actionParams = {
did: Device.deviceID,
siid: EVENT_LIST[name][0],
aiid: EVENT_LIST[name][1],
in: [{ piid: EVENT_LIST[name][1], value: value }]
}
Device.getDeviceWifi()
.callMethodFromCloud('action', actionParams)
// ...
}
const onProtocolSetProperty = (payload) => {
if (!PROP_LIST.hasOwnProperty(name)) {
throw new Error(`prop '${name}' is not defined in PROP_LIST`)
}
const setPropertyParams = [{
did: Device.deviceID,
siid: PROP_LIST[name][0],
piid: PROP_LIST[name][1],
value: value
}]
Device.getDeviceWifi()
.callMethodFromCloud('set_properties', setPropertyParams)
// ...
}
this.deviceProtocolEmitter.on('protocolAction', onProtocolAction.bind(this))
this.deviceProtocolEmitter.on('protocolSetProperty', onProtocolSetProperty.bind(this))
this.deviceProtocolEmitter.on('setState', onSetState.bind(this))
}
2.6 卸載時取消訂閱、清總線、拆單例
clearAllListener() {
this.cancelSubscribeMessage()
this.removeDeviceReceivedListener()
this.clearEventEmitter()
DeviceProtocol.instance = null
this.appStateListener.remove()
}
3. 兩者在插件里如何「串起來」(邏輯小結)
| 角色 | 說明 |
|---|---|
protocol.js |
定義本設備在 MIoT 上的全部屬性/動作與 siid/piid/aiid 的對應關系及默認、拉取規(guī)則。改協(xié)議或加字段主要改這里。 |
DeviceProtocol |
單例:用上述定義建 protocolMap;進插件時訂閱推送 + 拉全量;運行中把推送/拉取結果通過 setState 寫進你傳入的 store;用戶操作時由上層 emit('protocolAction' | 'protocolSetProperty') 經此類轉成 callMethodFromCloud。 |
若你項目里還有 useProtocol.js,一般會 new DeviceProtocol(某 setState) 并 deviceProtocolEmitter.emit(...),與上面 createEventEmitter 的三種事件一一對應;你當前 deviceProtocol.js 里 constructor 接收的 store 需帶 setState 方法,與 onSetState 的調用方式一致。